{"record":{"id":"f55ed1b15d229b3f","repo":"tinyhumansai/openhuman","slug":"invalid-aes-key-e","errorCode":null,"errorMessage":"invalid AES key: {e}","messagePattern":"invalid AES key: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/api/rest.rs","lineNumber":1158,"sourceCode":"        anyhow::bail!(\"encrypted payload too short\");\n    }\n    let iv = &combined[0..16];\n    let tag = &combined[16..32];\n    let ciphertext = &combined[32..];\n\n    // aes-gcm expects ciphertext || tag\n    let mut ct_with_tag = Vec::with_capacity(ciphertext.len() + tag.len());\n    ct_with_tag.extend_from_slice(ciphertext);\n    ct_with_tag.extend_from_slice(tag);\n\n    use aes_gcm::aead::generic_array::typenum::U16;\n    use aes_gcm::aead::{Aead, KeyInit};\n    use aes_gcm::aes::Aes256;\n    use aes_gcm::AesGcm;\n    type Aes256Gcm16 = AesGcm<Aes256, U16>;\n\n    let cipher =\n        Aes256Gcm16::new_from_slice(&key).map_err(|e| anyhow::anyhow!(\"invalid AES key: {e}\"))?;\n    let nonce = aes_gcm::aead::generic_array::GenericArray::from_slice(iv);\n    let plain = cipher\n        .decrypt(nonce, ct_with_tag.as_ref())\n        .map_err(|e| anyhow::anyhow!(\"AES-GCM decrypt failed: {e}\"))?;\n\n    String::from_utf8(plain).context(\"handoff plaintext is not UTF-8\")\n}\n\n/// Decode the shared encryption key into 32 raw AES bytes.\n///\n/// Accepts, in order of preference:\n/// 1. base64url without padding — the current backend format (e.g.\n///    a 43-char alphanumeric string using `-` / `_`). This must be tried\n///    BEFORE standard base64 because `-`/`_` are invalid in the standard\n///    alphabet and would fail cleanly, whereas a standard-base64 string\n///    never contains `-`/`_` so base64url_no_pad will still decode it\n///    correctly as long as there's no padding.\n/// 2. base64url with padding.","sourceCodeStart":1140,"sourceCodeEnd":1176,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/a221052e0df5b1f7598fceba7329fd1af95d6699/src/api/rest.rs#L1140-L1176","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nlet cipher = Aes256Gcm16::new_from_slice(&key).map_err(|e| anyhow::anyhow!(\"invalid AES key: {e}\"))?;\n\n// after\nanyhow::ensure!(key.len() == 32, \"AES-256 key must be exactly 32 bytes, got {}\", key.len());\nlet cipher = Aes256Gcm16::new_from_slice(&key).map_err(|e| anyhow::anyhow!(\"invalid AES key: {e}\"))?","handlingStrategy":"validation","validationCode":"// key_bytes_from_string already guarantees 32 bytes; if you hold a raw key, assert first:\nanyhow::ensure!(key.len() == 32, \"AES-256 key must be 32 raw bytes, got {}\", key.len());\nlet cipher = Aes256Gcm16::new_from_slice(&key)?;","typeGuard":null,"tryCatchPattern":"match Aes256Gcm16::new_from_slice(&key) {\n    Ok(cipher) => cipher,\n    Err(_) => return Err(anyhow::anyhow!(\"handoff key is not 32 bytes; check the shared secret format\")),\n}","preventionTips":["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."],"tags":["rust","crypto","aes-gcm","key-length"],"backgroundTag":null,"analyzedSha":"a221052e0df5b1f7598fceba7329fd1af95d6699","analyzedAt":"2026-08-16T12:47:06.542Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}