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

  1. Normalize the input before decoding: if the string contains letters, convert entirely with strings.ToLower(s) (or ToUpper) and pass that to Decode
  2. Re-copy the key from its original source (age-keygen output, config file) without retyping, avoiding auto-capitalizing editors
  3. 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

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


AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31). Data as JSON: /api/errors/fdec72dcd97220d4. Report an issue: GitHub.