tinyhumansai/openhuman · error
encrypted payload too short
Error message
encrypted payload too short
What it means
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.
Source
Thrown at src/api/rest.rs:1140
self.authed_json(
bearer_jwt,
Method::DELETE,
&format!("auth/integrations/{id}"),
None,
)
.await?;
Ok(())
}
}
/// AES-256-GCM decrypt compatible with backend `encryptMessageFromString` (IV 16 + tag 16 + ciphertext, base64).
pub fn decrypt_handoff_blob(b64_ciphertext: &str, key_str: &str) -> Result<String> {
let key = key_bytes_from_string(key_str)?;
let combined = base64::engine::general_purpose::STANDARD
.decode(b64_ciphertext.trim())
.context("base64-decode encrypted payload")?;
if combined.len() < 32 {
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}"))?;View on GitHub (pinned to a221052e0d)
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.
Example fix
// before
let plaintext = decrypt_handoff_blob(&blob, &key)?; // panics downstream of 'encrypted payload too short'
// after
use base64::engine::general_purpose::STANDARD as B64;
let decoded = B64.decode(blob.trim()).context("base64-decode encrypted payload")?;
anyhow::ensure!(decoded.len() >= 32 && decoded.len() > 32, "encrypted payload too short");
let plaintext = decrypt_handoff_blob(&blob, &key)?; Defensive patterns
Strategy: validation
Validate before calling
use base64::engine::general_purpose::STANDARD as B64;
use base64::engine::Engine;
fn handoff_blob_is_wellformed(b64: &str) -> bool {
match B64.decode(b64.trim()) {
Ok(decoded) => decoded.len() > 32, // IV(16) + tag(16) + non-empty ciphertext
Err(_) => false,
}
}
// before decrypting:
if !handoff_blob_is_wellformed(&blob) {
anyhow::bail!("handoff payload malformed: expected base64 of IV+tag+ciphertext (>32 bytes)");
} Try / catch
match decrypt_handoff_blob(&blob, &key) {
Ok(plain) => { /* use plain */ }
Err(e) if e.to_string().contains("encrypted payload too short") => {
// payload truncated/mangled before it reached us: re-fetch, do not retry as-is
}
Err(e) => return Err(e),
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- invalid AES key: {e}
- 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/6d572a1d96fbb558.
Report an issue: GitHub.