tinyhumansai/openhuman · error
encrypted payload too short
Error message
encrypted payload too short
What it means
decrypt_handoff_blob base64-decodes the payload and requires at least 32 bytes (16-byte IV + 16-byte GCM tag) before any ciphertext can exist; anything shorter bails with 'encrypted payload too short'. The blob is produced by the backend's encryptMessageFromString (IV || tag || ciphertext, standard base64), so a short payload means truncation or wrong encoding - it is never a key problem (key length is checked separately).
Solutions
- Verify the payload is complete, standard base64 and decodes to at least 32 bytes.
- Re-issue the handoff from the source system rather than hand-repairing the string.
- Confirm the backend still uses the IV16 + tag16 + ciphertext layout of encryptMessageFromString.
- Do not strip characters beyond the leading and trailing whitespace the function already trims.
Defensive patterns
Strategy: validation
Validate before calling
fn handoff_blob_decodes_long_enough(b64: &str) -> bool {
use base64::Engine;
base64::engine::general_purpose::STANDARD
.decode(b64.trim())
.map(|raw| raw.len() >= 32)
.unwrap_or(false)
} Prevention
- Validate blob length before decryption so users get a 'truncated link' message instead of a crypto error.
- Pass handoff blobs through channels that do not truncate (POST bodies or full URLs), not clipped log lines.
- Pin the wire format (IV16 + tag16 + ciphertext, standard base64) in tests against the backend's encryptMessageFromString.
When it happens
Trigger: The handoff blob was truncated in transit: cut off in a URL query param, clipboard copy, or log line; the payload was re-encoded (hex, base64url, double-encoded) instead of standard base64; an empty or placeholder string was passed.
Common situations: Deep-link or query-param handoff where the blob got trimmed by length limits; pasting blobs through tools that strip characters; a backend change to the wire format.
Related errors
- encryption key must decode to 32 raw bytes
- Ed25519 derivation path must be fully hardened
- Failed to derive private key for path
- [tunnel] no device keypair
- [tunnel] no session cipher — handshake incomplete
AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17).
Data as JSON: /api/errors/6d572a1d96fbb558.
Report an issue: GitHub.
Appendix: 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 7491200858)