最後の文字にはデータに使わないビットがあります

RFC 4648 §3.5は適合するエンコーダーにパッドビットを0にするよう求めます。入力が1バイトなら2番目のBase64文字の上位2ビットだけがデータで、残る4ビットはパッドビットです。gの値32は100000、hの値33は100001です。fのバイトは01100110で、両表現のデータ部分は一致します。Zg==は正規の出力、Zh==には0でないパッドビットがあります。末尾の=と捨てるビットを混同しないでください。

Zg==とZh==の末尾4ビットは異なりますが、同じfのバイトを復元します
自作のビット図です。青はデータ、灰色は捨てるパッドビットです。01100110は公開例fのバイトです。

RFC 4648 §3.5, §4: Canonical Encoding / Base64

実際のデコードと再エンコードを確認

WHATWG Infraの寛容なBase64デコードは、残った12ビットの末尾4ビット、18ビットの末尾2ビットを捨てます。HTMLのatobはこの手順を使います。Moyoutilはatobでバイトを復元しUTF-8テキストとして解釈します。実行例ではZg==とZh==はどちらもf、Zm8=とZm9=はどちらもfoでした。再エンコードするとそれぞれZg==とZm8=になります。下のコードは任意のバイナリをUTF-8にせず、バイトを直接再エンコードします。

for (const input of ['Zg==', 'Zh==', 'Zm8=', 'Zm9=']) {
  const bytes = atob(input);
  const canonical = btoa(bytes);
  console.log(input, bytes, canonical, input === canonical);
}
// Zg== f Zg== true
// Zh== f Zg== false
// Zm8= fo Zm8= true
// Zm9= fo Zm8= false

同じバイトと同じ元の文字列は別の条件です。キャッシュキーや比較で必要な一致を先に決めてください。受信側のプロトコルが正準表現を要求するなら、その規則を検証します。署名検証前に文字列を勝手に変更する方法は勧めません。この再エンコード比較はパディング付き標準Base64だけを対象とし、全プロトコルの検証器ではありません。

WHATWG Infra: Forgiving base64 decode

WHATWG HTML: atob

よくある質問

すべてのデコーダーがZh==を許可しますか?

いいえ。RFC 4648は0でないパッドビットの拒否を許し、参照する規格が動作を定める場合があります。確認結果はHTML atobと現在のMoyoutil実装のものです。