{"record":{"id":"6d572a1d96fbb558","repo":"tinyhumansai/openhuman","slug":"encrypted-payload-too-short","errorCode":null,"errorMessage":"encrypted payload too short","messagePattern":"encrypted payload too short","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/api/rest.rs","lineNumber":1140,"sourceCode":"        self.authed_json(\n            bearer_jwt,\n            Method::DELETE,\n            &format!(\"auth/integrations/{id}\"),\n            None,\n        )\n        .await?;\n        Ok(())\n    }\n}\n\n/// AES-256-GCM decrypt compatible with backend `encryptMessageFromString` (IV 16 + tag 16 + ciphertext, base64).\npub fn decrypt_handoff_blob(b64_ciphertext: &str, key_str: &str) -> Result<String> {\n    let key = key_bytes_from_string(key_str)?;\n    let combined = base64::engine::general_purpose::STANDARD\n        .decode(b64_ciphertext.trim())\n        .context(\"base64-decode encrypted payload\")?;\n    if combined.len() < 32 {\n        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}\"))?;","sourceCodeStart":1122,"sourceCodeEnd":1158,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/a221052e0df5b1f7598fceba7329fd1af95d6699/src/api/rest.rs#L1122-L1158","documentation":"Thrown by decrypt_handoff_blob in src/api/rest.rs when a base64-decoded handoff payload is shorter than 32 bytes. The backend's encryptMessageFromString produces IV(16) + GCM tag(16) + ciphertext, so anything under 32 bytes cannot even contain the IV and tag, and decryption is refused before AES is touched. It means the input is not a payload produced by that encrypt function.","triggerScenarios":"Calling decrypt_handoff_blob with a truncated or mangled base64 string (e.g. a deep-link/OAuth handoff blob cut off in a shell or URL, percent-decoded twice, or pasted with characters lost). Also hit when the plaintext is passed instead of the encrypted blob, an empty string is passed, or a backend version with a different payload layout sends data this decoder does not understand.","commonSituations":"Backend OAuth integration-token handoff (the IntegrationTokensHandoff path around src/api/rest.rs:897) where the blob crossed a boundary that altered it: URL percent-encoding applied/stripped incorrectly, newline inserted by a terminal wrap, base64 variant mismatch (base64url vs standard), or a test using a hand-made payload instead of one from the real encryptMessageFromString.","solutions":["Re-fetch or re-copy the encrypted payload from the backend and confirm it is the exact base64 string that was sent, unmodified.","Check for transport mangling: percent-decoding applied twice, whitespace/newlines stripped, or padding '=' characters lost; re-encode/normalize before calling.","Pre-validate: base64-decode the payload and assert len >= 32 (and ideally len > 32, i.e. non-empty ciphertext) before invoking decrypt_handoff_blob.","If the payload is genuinely short, verify the sender actually used encryptMessageFromString (IV 16 + tag 16 + ciphertext, base64) and not a different format such as hex or raw concatenation."],"exampleFix":"// before\nlet plaintext = decrypt_handoff_blob(&blob, &key)?; // panics downstream of 'encrypted payload too short'\n\n// after\nuse base64::engine::general_purpose::STANDARD as B64;\nlet decoded = B64.decode(blob.trim()).context(\"base64-decode encrypted payload\")?;\nanyhow::ensure!(decoded.len() >= 32 && decoded.len() > 32, \"encrypted payload too short\");\nlet plaintext = decrypt_handoff_blob(&blob, &key)?;","handlingStrategy":"validation","validationCode":"use base64::engine::general_purpose::STANDARD as B64;\nuse base64::engine::Engine;\n\nfn handoff_blob_is_wellformed(b64: &str) -> bool {\n    match B64.decode(b64.trim()) {\n        Ok(decoded) => decoded.len() > 32, // IV(16) + tag(16) + non-empty ciphertext\n        Err(_) => false,\n    }\n}\n\n// before decrypting:\nif !handoff_blob_is_wellformed(&blob) {\n    anyhow::bail!(\"handoff payload malformed: expected base64 of IV+tag+ciphertext (>32 bytes)\");\n}","typeGuard":null,"tryCatchPattern":"match decrypt_handoff_blob(&blob, &key) {\n    Ok(plain) => { /* use plain */ }\n    Err(e) if e.to_string().contains(\"encrypted payload too short\") => {\n        // payload truncated/mangled before it reached us: re-fetch, do not retry as-is\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Pass handoff blobs through verbatim; never re-encode or strip characters between receiving and decrypting.","Assert decoded length >= 32 (ideally > 32) before calling decrypt_handoff_blob.","In tests, generate payloads with the same IV+tag+ciphertext layout rather than arbitrary base64 strings."],"tags":["rust","crypto","aes-gcm","base64","auth-handoff"],"backgroundTag":null,"analyzedSha":"a221052e0df5b1f7598fceba7329fd1af95d6699","analyzedAt":"2026-08-16T12:47:06.542Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}