tinyhumansai/openhuman · error

encryption key must decode to 32 raw bytes

Error message

encryption key must decode to 32 raw bytes (raw, base64, or base64url accepted; got len={})

What it means

key_bytes_from_string accepts a 32-byte AES-256 key either as raw text or as base64 (URL_SAFE_NO_PAD, URL_SAFE, STANDARD, and STANDARD_NO_PAD are all tried in turn); if none of the decodings yields exactly 32 bytes it bails reporting the raw input length. A wrong-size key (16 or 24 bytes), a corrupted base64 string, or an unsupported encoding like hex fails here - before any decryption is attempted.

Solutions

  1. Generate exactly 32 bytes, e.g. `openssl rand -base64 32`, and store the resulting base64 string.
  2. Strip surrounding whitespace and newlines from the stored secret, then verify it decodes to 32 bytes.
  3. If using the raw form, ensure the string is exactly 32 characters.
  4. Do not hex-encode the key - that encoding is not tried.

Example fix

# before (16-byte key, hex key, or wrapped string)
export HANDOFF_KEY='1c2e...64-hex-chars'

# after
export HANDOFF_KEY="$(openssl rand -base64 32)"   # decodes to exactly 32 bytes
Defensive patterns

Strategy: validation

Validate before calling

fn key_decodes_to_32_bytes(k: &str) -> bool {
    use base64::Engine;
    let t = k.trim();
    base64::engine::general_purpose::STANDARD.decode(t)
        .or_else(|_| base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(t))
        .map(|d| d.len() == 32)
        .unwrap_or(t.len() == 32)
}

Prevention

When it happens

Trigger: The configured key was generated for AES-128 (16 bytes); a hex-encoded key (64 hex chars) was supplied since hex decoding is not attempted; the base64 string lost a character or gained a newline; a 44-char standard-base64 key is fine but a truncated 43-char unpadded copy is not.

Common situations: Ops generates the wrong key size; the key is pasted from a secret manager with embedded line breaks; a team assumes hex or base32 encoding is supported.

Related errors


AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17). Data as JSON: /api/errors/6ba54be704342363. Report an issue: GitHub.

Appendix: source

Thrown at src/api/rest.rs:1213

    // `base64::Engine` has generic methods and therefore isn't
    // dyn-compatible, so we unroll the attempts instead of looping over
    // a slice of trait objects.
    macro_rules! try_decode {
        ($engine:expr) => {
            if let Ok(decoded) = $engine.decode(trimmed) {
                if decoded.len() == 32 {
                    return Ok(decoded);
                }
            }
        };
    }
    try_decode!(URL_SAFE_NO_PAD);
    try_decode!(URL_SAFE);
    try_decode!(STANDARD);
    try_decode!(STANDARD_NO_PAD);

    anyhow::bail!(
        "encryption key must decode to 32 raw bytes (raw, base64, or base64url accepted; got len={})",
        trimmed.len()
    );
}

#[cfg(test)]
#[path = "rest_tests.rs"]
mod key_bytes_from_string_tests;

View on GitHub (pinned to 7491200858)