FiloSottile/age · error
mixed case
Error message
mixed case
What it means
bech32.Decode requires the input string to be uniformly cased: if the string is neither all-lowercase nor all-uppercase, decoding is ambiguous because the bech32 checksum is case-dependent. Decode rejects mixed-case strings with this error. age's ParseRecipient/ParseIdentity rely on this validation.
Source
Thrown at internal/bech32/bech32.go:145
var ret strings.Builder
ret.WriteString(hrp)
ret.WriteString("1")
for _, p := range values {
ret.WriteByte(charset[p])
}
for _, p := range createChecksum(hrp, values) {
ret.WriteByte(charset[p])
}
if lower {
return ret.String(), nil
}
return strings.ToUpper(ret.String()), nil
}
// Decode decodes a Bech32 string. If the string is uppercase, the HRP will be uppercase.
func Decode(s string) (hrp string, data []byte, err error) {
if strings.ToLower(s) != s && strings.ToUpper(s) != s {
return "", nil, fmt.Errorf("mixed case")
}
pos := strings.LastIndex(s, "1")
if pos < 1 || pos+7 > len(s) {
return "", nil, fmt.Errorf("separator '1' at invalid position: pos=%d, len=%d", pos, len(s))
}
hrp = s[:pos]
for p, c := range hrp {
if c < 33 || c > 126 {
return "", nil, fmt.Errorf("invalid character human-readable part: s[%d]=%d", p, c)
}
}
for p, c := range s[pos+1:] {
// Fold ASCII explicitly. Unicode case folding can turn a non-ASCII
// rune into a shorter valid charset member.
if c >= 'A' && c <= 'Z' {
c += 'a' - 'A'
}
d := strings.IndexRune(charset, c)View on GitHub (pinned to b74dce4cdb)
Solutions
- Normalize the input before decoding: if the string contains letters, convert entirely with strings.ToLower(s) (or ToUpper) and pass that to Decode
- Re-copy the key from its original source (age-keygen output, config file) without retyping, avoiding auto-capitalizing editors
- Disable auto-capitalization/formatting in UIs or shells where keys are entered, and never store title-cased display forms as the canonical value
Example fix
// before id, err := agessh.ParseIdentity(userInput) // "Age1..." fails: "mixed case" // after canonical := strings.ToLower(strings.TrimSpace(userInput)) id, err := agessh.ParseIdentity(canonical)
Defensive patterns
Strategy: validation
Validate before calling
func isUniformCaseBech32(s string) bool {
if strings.ToLower(s) != s && strings.ToUpper(s) != s {
return false // would trigger "mixed case"
}
return strings.Contains(s, "1")
} Prevention
- Normalize input with strings.TrimSpace + strings.ToLower before parsing any bech32 string
- Never retype keys manually; always copy from the source file verbatim
- Disable auto-capitalization in UIs, terminals, and note apps where keys are handled
- Store canonical lowercase forms; apply case changes only at display time
When it happens
Trigger: Calling bech32.Decode (or agessh.ParseRecipient/ParseIdentity) with a string like "Age1abcDEF" — typically from manual typing, copy/paste that preserved inconsistent case, or display code that title-cased the key for presentation and it got fed back as input.
Common situations: Users re-typing an age key with mixed case; documents/UI that auto-capitalize (phone autocorrect, title-case formatting); logs showing prettified keys pasted back into tools; concatenating case-normalized and raw fragments of a key.
Related errors
- separator '1' at invalid position: pos=%d, len=%d
- invalid identity encoding: %v
- illegal zero padding
- non-zero padding
- mixed case HRP: %q
AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31).
Data as JSON: /api/errors/fdec72dcd97220d4.
Report an issue: GitHub.