Some bits in the final character carry no data
RFC 4648 §3.5 requires conforming encoders to set pad bits to zero. With one input byte, only the top two bits of the second Base64 character carry data; the other four are pad bits. The character g has index 32, or 100000; h has index 33, or 100001. The byte for f is 01100110, and the data bits match in both representations. Zg== is canonical output; Zh== has nonzero pad bits. Do not confuse trailing = characters with the bits that are discarded.
Check decoding and re-encoding with the actual tools
WHATWG Infra forgiving Base64 decoding discards four trailing bits from a remaining 12-bit group, or two from an 18-bit group. HTML atob uses that algorithm. Moyoutil restores bytes with atob and interprets them as UTF-8 text. In our executed examples, Zg== and Zh== both returned f; Zm8= and Zm9= both returned fo. Re-encoding produced Zg== and Zm8=, respectively. The code below re-encodes bytes directly rather than converting arbitrary binary data to UTF-8 text.
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= falseEqual bytes and equal original strings are different conditions. Decide which equality your cache key or comparison needs. If the receiving protocol requires canonical input, validate its rules. We do not recommend changing signed strings before verification. This re-encoding comparison is limited to padded standard Base64 and is not a validator for every protocol.
Frequently Asked Questions
Does every decoder accept Zh==?
No. RFC 4648 allows rejection of nonzero pad bits, and a referring specification can define the behavior. These results describe HTML atob and the current Moyoutil implementation.