{"record":{"id":"7b9cb9624bf6ae35","repo":"tinyhumansai/openhuman","slug":"aes-gcm-decrypt-failed-e","errorCode":null,"errorMessage":"AES-GCM decrypt failed: {e}","messagePattern":"AES-GCM decrypt failed: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/api/rest.rs","lineNumber":1162,"sourceCode":"    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.\n/// 3. Standard base64 with padding (legacy backend format).\n/// 4. Standard base64 without padding.\n/// 5. A raw 32-byte ASCII key (len == 32, used as-is).\n///","sourceCodeStart":1144,"sourceCodeEnd":1180,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/a221052e0df5b1f7598fceba7329fd1af95d6699/src/api/rest.rs#L1144-L1180","documentation":"Thrown when the AES-256-GCM cipher rejects the decrypted blob: aes-gcm's decrypt returns aead::Error when authentication fails, meaning the tag does not verify for this key, IV, and ciphertext. The message is generic on purpose because GCM cannot distinguish wrong key from tampered ciphertext from wrong IV/tag ordering.","triggerScenarios":"Decrypting a handoff blob with a key that is not the one that encrypted it (e.g. local core pointed at a different backend environment), a payload that was truncated or byte-altered after encryption, or a blob produced by a sender that orders the parts differently than IV(16) + tag(16) + ciphertext.","commonSituations":"Environment mismatch: the shared handoff secret configured locally differs from the backend's (staging vs production secret, rotated key, key from another deployment). Also corrupted payloads from copy/paste or transport, and version skew after the backend changes its encryption format.","solutions":["Verify the shared encryption key matches the backend environment the payload came from (same env/deployment, post-rotation).","Obtain a fresh handoff payload and retry decryption immediately.","Confirm the payload format is exactly IV 16 + tag 16 + ciphertext, base64 (the backend encryptMessageFromString layout).","Reproduce with a known-good round-trip (encrypt with the same key, decrypt) to isolate whether key or payload is at fault."],"exampleFix":"// before\nlet plain = cipher.decrypt(nonce, ct_with_tag.as_ref()).map_err(|e| anyhow::anyhow!(\"AES-GCM decrypt failed: {e}\"))?;\n\n// after\nlet plain = cipher.decrypt(nonce, ct_with_tag.as_ref()).map_err(|_| {\n    anyhow::anyhow!(\"AES-GCM decrypt failed: wrong key or corrupted handoff payload; re-check the shared secret and re-fetch the blob\")\n})?;","handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"match decrypt_handoff_blob(&blob, &key) {\n    Ok(plain) => { /* proceed */ }\n    Err(e) if e.to_string().contains(\"AES-GCM decrypt failed\") => {\n        // wrong key or corrupted blob: re-fetch a fresh handoff payload once,\n        // and surface a clear 'credentials handoff failed, re-connect' error to the user\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Pin the shared encryption key to the same backend environment that issued the handoff blob.","Treat key rotation as a coordinated change: deploy the new secret on both sides before sending new blobs.","After any backend crypto format change, verify with a round-trip test before shipping the client."],"tags":["rust","crypto","aes-gcm","auth-failure","key-mismatch"],"backgroundTag":null,"analyzedSha":"a221052e0df5b1f7598fceba7329fd1af95d6699","analyzedAt":"2026-08-16T12:47:06.542Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}