{"record":{"id":"c0f366832fee8fc9","repo":"FiloSottile/age","slug":"non-zero-padding","errorCode":null,"errorMessage":"non-zero padding","messagePattern":"non-zero padding","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/bech32/bech32.go","lineNumber":102,"sourceCode":"\tfor idx, value := range data {\n\t\tif value>>frombits != 0 {\n\t\t\treturn nil, fmt.Errorf(\"invalid data range: data[%d]=%d (frombits=%d)\", idx, value, frombits)\n\t\t}\n\t\tacc = acc<<frombits | uint32(value)\n\t\tbits += frombits\n\t\tfor bits >= tobits {\n\t\t\tbits -= tobits\n\t\t\tret = append(ret, byte(acc>>bits)&maxv)\n\t\t}\n\t}\n\tif pad {\n\t\tif bits > 0 {\n\t\t\tret = append(ret, byte(acc<<(tobits-bits))&maxv)\n\t\t}\n\t} else if bits >= frombits {\n\t\treturn nil, fmt.Errorf(\"illegal zero padding\")\n\t} else if byte(acc<<(tobits-bits))&maxv != 0 {\n\t\treturn nil, fmt.Errorf(\"non-zero padding\")\n\t}\n\treturn ret, nil\n}\n\n// Encode encodes the HRP and a bytes slice to Bech32. If the HRP is uppercase,\n// the output will be uppercase.\nfunc Encode(hrp string, data []byte) (string, error) {\n\tvalues, err := convertBits(data, 8, 5, true)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif len(hrp) < 1 {\n\t\treturn \"\", fmt.Errorf(\"invalid HRP: %q\", hrp)\n\t}\n\tfor p, c := range hrp {\n\t\tif c < 33 || c > 126 {\n\t\t\treturn \"\", fmt.Errorf(\"invalid HRP character: hrp[%d]=%d\", p, c)\n\t\t}","sourceCodeStart":84,"sourceCodeEnd":120,"githubUrl":"https://github.com/FiloSottile/age/blob/b74dce4cdbe35b5e5f66c06d9612b72f89028758/internal/bech32/bech32.go#L84-L120","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"func isCanonicalBech32(s string) bool {\n\tif strings.ToLower(s) != s && strings.ToUpper(s) != s { return false }\n\thrp, data, err := bech32.Decode(s)\n\treturn err == nil && hrp != \"\" && len(data) > 0\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["bech32","decoding","padding","internal"],"backgroundTag":"bech32-nonzero-padding","analyzedSha":"b74dce4cdbe35b5e5f66c06d9612b72f89028758","analyzedAt":"2026-08-31T23:59:31.627Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}