tinyhumansai/openhuman · error
invalid AES key: {e}
Error message
invalid AES key: {e} What it means
Thrown when Aes256Gcm16::new_from_slice rejects the key slice while building the cipher inside decrypt_handoff_blob. For Aes256 the aes-gcm crate requires exactly 32 key bytes; any other length is an InvalidLength error surfaced as 'invalid AES key'. In this code path key_bytes_from_string has already guaranteed 32 bytes, so in practice this arm is a defensive guard that only fires if that upstream contract is broken.
Source
Thrown at src/api/rest.rs:1158
anyhow::bail!("encrypted payload too short");
}
let iv = &combined[0..16];
let tag = &combined[16..32];
let ciphertext = &combined[32..];
// aes-gcm expects ciphertext || tag
let mut ct_with_tag = Vec::with_capacity(ciphertext.len() + tag.len());
ct_with_tag.extend_from_slice(ciphertext);
ct_with_tag.extend_from_slice(tag);
use aes_gcm::aead::generic_array::typenum::U16;
use aes_gcm::aead::{Aead, KeyInit};
use aes_gcm::aes::Aes256;
use aes_gcm::AesGcm;
type Aes256Gcm16 = AesGcm<Aes256, U16>;
let cipher =
Aes256Gcm16::new_from_slice(&key).map_err(|e| anyhow::anyhow!("invalid AES key: {e}"))?;
let nonce = aes_gcm::aead::generic_array::GenericArray::from_slice(iv);
let plain = cipher
.decrypt(nonce, ct_with_tag.as_ref())
.map_err(|e| anyhow::anyhow!("AES-GCM decrypt failed: {e}"))?;
String::from_utf8(plain).context("handoff plaintext is not UTF-8")
}
/// Decode the shared encryption key into 32 raw AES bytes.
///
/// Accepts, in order of preference:
/// 1. base64url without padding — the current backend format (e.g.
/// a 43-char alphanumeric string using `-` / `_`). This must be tried
/// BEFORE standard base64 because `-`/`_` are invalid in the standard
/// alphabet and would fail cleanly, whereas a standard-base64 string
/// never contains `-`/`_` so base64url_no_pad will still decode it
/// correctly as long as there's no padding.
/// 2. base64url with padding.View on GitHub (pinned to a221052e0d)
Solutions
- Route every key through key_bytes_from_string (base64url-no-pad, base64 variants, or raw 32 chars) so the 32-byte invariant holds.
- If calling the cipher directly, assert key.len() == 32 before new_from_slice and fail with a clear message about the expected formats.
- Keep the rest_tests.rs key-format tests green when touching key decoding.
Example fix
// before
let cipher = Aes256Gcm16::new_from_slice(&key).map_err(|e| anyhow::anyhow!("invalid AES key: {e}"))?;
// after
anyhow::ensure!(key.len() == 32, "AES-256 key must be exactly 32 bytes, got {}", key.len());
let cipher = Aes256Gcm16::new_from_slice(&key).map_err(|e| anyhow::anyhow!("invalid AES key: {e}"))? Defensive patterns
Strategy: validation
Validate before calling
// key_bytes_from_string already guarantees 32 bytes; if you hold a raw key, assert first:
anyhow::ensure!(key.len() == 32, "AES-256 key must be 32 raw bytes, got {}", key.len());
let cipher = Aes256Gcm16::new_from_slice(&key)?; Try / catch
match Aes256Gcm16::new_from_slice(&key) {
Ok(cipher) => cipher,
Err(_) => return Err(anyhow::anyhow!("handoff key is not 32 bytes; check the shared secret format")),
} Prevention
- Always source the key via key_bytes_from_string instead of hand-decoding it.
- Keep the key-format round-trip tests (rest_tests.rs) green when refactoring.
- Never feed a passphrase or hex string straight into the cipher constructor.
When it happens
Trigger: Constructing the cipher with a key whose byte length is not exactly 32. In the live path this requires key_bytes_from_string to return a non-32-byte vector, which its own bail at src/api/rest.rs:1213 prevents; it becomes reachable if someone bypasses key_bytes_from_string or edits its 32-byte assertion.
Common situations: Refactoring decrypt_handoff_blob to accept a raw key parameter directly, changing key_bytes_from_string to tolerate other lengths, or unit-testing the cipher step in isolation with an arbitrary-length key string.
Related errors
- encrypted payload too short
- AES-GCM decrypt failed: {e}
- encryption key must decode to 32 raw bytes (raw, base64, or
- [tunnel] no device keypair
- [tunnel] no session cipher — handshake incomplete
AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16).
Data as JSON: /api/errors/f55ed1b15d229b3f.
Report an issue: GitHub.