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

  1. Re-fetch or re-copy the encrypted payload from the backend and confirm it is the exact base64 string that was sent, unmodified.
  2. Check for transport mangling: percent-decoding applied twice, whitespace/newlines stripped, or padding '=' characters lost; re-encode/normalize before calling.
  3. Pre-validate: base64-decode the payload and assert len >= 32 (and ideally len > 32, i.e. non-empty ciphertext) before invoking decrypt_handoff_blob.
  4. 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

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


AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16). Data as JSON: /api/errors/6d572a1d96fbb558. Report an issue: GitHub.