tinyhumansai/openhuman · error
encryption key must decode to 32 raw bytes (raw, base64, or
Error message
encryption key must decode to 32 raw bytes (raw, base64, or base64url accepted; got len={}) What it means
Thrown by key_bytes_from_string (src/api/rest.rs:1186) when no accepted decoding yields exactly 32 bytes. The function tries the raw string itself, then URL_SAFE_NO_PAD, URL_SAFE, STANDARD, and STANDARD_NO_PAD in order; each successful decode must be exactly 32 bytes. The bail reports the trimmed input length to hint at which format was expected.
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 a221052e0d)
Solutions
- Generate a proper 32-byte key and pass it base64url-no-pad (43 chars) — the backend's current format — or as a raw 32-character string.
- If the key is hex (64 chars), convert it to 32 raw bytes and re-encode as base64url before passing.
- Check the decoded length in a scratch script: every engine that decodes must land on exactly 32 bytes.
- Match the format the other side (backend encryptMessageFromString) expects; do not invent a passphrase.
Example fix
# before (passphrase / hex key) OPENHUMAN_HANDOFF_KEY="my-secret-password" OPENHUMAN_HANDOFF_KEY="a1b2..." # 64 hex chars -> decodes to 48 bytes, fails # after (32 random bytes, base64url no padding -> 43 chars) openssl rand -raw 32 | basenc --base64url -w0 | tr -d '='
Defensive patterns
Strategy: validation
Validate before calling
use base64::engine::general_purpose::{STANDARD, URL_SAFE, URL_SAFE_NO_PAD, STANDARD_NO_PAD};
use base64::engine::Engine;
fn key_decodes_to_32_bytes(key: &str) -> bool {
let k = key.trim();
k.len() == 32
|| [URL_SAFE_NO_PAD.decode(k), URL_SAFE.decode(k), STANDARD.decode(k), STANDARD_NO_PAD.decode(k)]
.iter()
.any(|r| r.as_ref().map(|b| b.len() == 32).unwrap_or(false))
}
if !key_decodes_to_32_bytes(&configured_key) {
anyhow::bail!("handoff key misconfigured: must be raw 32 chars or base64 of 32 bytes");
} Try / catch
match key_bytes_from_string(key_str) {
Ok(bytes) => bytes,
Err(e) => {
log::warn!("[handoff] rejecting configured encryption key: {e}");
return Err(e.context("configure a 32-byte key (base64url-no-pad, 43 chars, or raw 32 chars)"));
}
} Prevention
- Generate keys with `openssl rand -raw 32` and encode base64url without padding (43 chars).
- Validate the key format at startup, not at first handoff, so misconfiguration fails fast with a clear message.
- Never substitute a passphrase; the field is raw key material, not a secret string.
When it happens
Trigger: Passing a human-rememberable passphrase instead of a real key; a hex-encoded 64-char key (valid base64 characters but decodes to 48 bytes, so every engine fails the 32-byte check); a base64 key for a non-256-bit cipher (16 or 24 bytes after decode); a key with a stray character or wrong length; an empty/blank string.
Common situations: An operator puts a password into the handoff-encryption env var rather than the generated 32-byte secret; the backend sends the key in a format this decoder predates; whitespace is trimmed automatically, so the usual culprit is genuinely wrong key material, not formatting.
Related errors
- encrypted payload too short
- invalid AES key: {e}
- AES-GCM decrypt failed: {e}
- Invalid ${paramName}: '${value}'. Must be a valid integer ID
- Invalid ${paramName}: ${String(value)}. Type must be an inte
AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16).
Data as JSON: /api/errors/6ba54be704342363.
Report an issue: GitHub.