FiloSottile/age · error
non-zero padding
Error message
non-zero padding
What it means
This is the companion to "illegal zero padding": in convertBits with pad=false (decode, 5→8 direction), leftover bits below frombits are acceptable padding only if they are all zero. If the leftover bits form a non-zero value, the payload encodes more information than a byte-aligned representation allows, and bech32.Decode rejects the string.
Source
Thrown at internal/bech32/bech32.go:102
for idx, value := range data {
if value>>frombits != 0 {
return nil, fmt.Errorf("invalid data range: data[%d]=%d (frombits=%d)", idx, value, frombits)
}
acc = acc<<frombits | uint32(value)
bits += frombits
for bits >= tobits {
bits -= tobits
ret = append(ret, byte(acc>>bits)&maxv)
}
}
if pad {
if bits > 0 {
ret = append(ret, byte(acc<<(tobits-bits))&maxv)
}
} else if bits >= frombits {
return nil, fmt.Errorf("illegal zero padding")
} else if byte(acc<<(tobits-bits))&maxv != 0 {
return nil, fmt.Errorf("non-zero padding")
}
return ret, nil
}
// Encode encodes the HRP and a bytes slice to Bech32. If the HRP is uppercase,
// the output will be uppercase.
func Encode(hrp string, data []byte) (string, error) {
values, err := convertBits(data, 8, 5, true)
if err != nil {
return "", err
}
if len(hrp) < 1 {
return "", fmt.Errorf("invalid HRP: %q", hrp)
}
for p, c := range hrp {
if c < 33 || c > 126 {
return "", fmt.Errorf("invalid HRP character: hrp[%d]=%d", p, c)
}View on GitHub (pinned to b74dce4cdb)
Solutions
- Reject the string as non-canonical bech32; it cannot be trusted even if the checksum passes — obtain the original string from a trusted source
- If you generated it with your own encoder, fix the encoder to zero-pad the final group (append byte(acc<<(tobits-bits))&maxv with zeroed bits)
- For age usage, re-derive the recipient/identity string with bech32.Encode from the raw key bytes rather than editing the string
Defensive patterns
Strategy: validation
Validate before calling
func isCanonicalBech32(s string) bool {
if strings.ToLower(s) != s && strings.ToUpper(s) != s { return false }
hrp, data, err := bech32.Decode(s)
return err == nil && hrp != "" && len(data) > 0
} Prevention
- Reject non-canonical strings outright; do not accept strings that merely pass checksums
- Re-derive bech32 strings from raw key bytes with bech32.Encode instead of editing string fragments
- Treat this error on age keys as a tampering signal and request a fresh key from the owner
- Round-trip test: Decode then Encode must reproduce the original string byte-for-byte
When it happens
Trigger: Calling bech32.Decode on a bech32 string whose final partial byte's padding bits are non-zero — the data part is byte-length-consistent but the discarded bits are not zero, indicating a deliberately or accidentally crafted non-canonical string.
Common situations: Fuzzed or adversarial bech32 inputs (this check exists to enforce canonical encoding); strings produced by non-conformant third-party encoders that leave garbage in padding bits; tampering with age recipient/identity strings where an attacker flips low-order bits that fall in padding.
Related errors
- illegal zero padding
- invalid data range: data[%d]=%d (frombits=%d)
- invalid HRP: %q
- invalid HRP character: hrp[%d]=%d
- mixed case HRP: %q
AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31).
Data as JSON: /api/errors/c0f366832fee8fc9.
Report an issue: GitHub.