Skip to main content

Envelope Structure

Source: src/envelope.rs:9-24 The Envelope type encapsulates all metadata and ciphertext required for decryption:
pub struct Envelope {
    pub version: u8,
    pub algorithm: Algorithm,
    pub key_id: Uuid,
    pub nonce: Vec<u8>,
    pub ciphertext: Vec<u8>,
    pub tag: Vec<u8>,
}

Field Descriptions

FieldTypePurpose
versionu8Envelope format version (currently 1)
algorithmAlgorithmEncryption algorithm identifier (AES-256-GCM or ChaCha20-Poly1305)
key_idUuidIdentifier of the key used for encryption
nonceVec<u8>Random nonce (12 bytes for AEAD)
ciphertextVec<u8>Encrypted payload (variable length)
tagVec<u8>Authentication tag (16 bytes for AEAD)
The key_id field enables key rotation: envelopes are self-describing and can be decrypted with the correct historical key.

Binary Serialization

Source: src/envelope.rs:27-52

Format Layout

The deterministic binary format uses big-endian byte order:
┌──────────────┬─────────────┬─────────────┬────────────────┬────────────┬─────────────────┬────────────┬──────────────┬────────────┐
│ version (1B) │ algorithm   │ key_id      │ nonce_len (2B) │ nonce      │ ciphertext_len  │ ciphertext │ tag_len (1B) │ tag        │
│              │ (1B)        │ (16B)       │                │ (variable) │ (4B)            │ (variable) │              │ (variable) │
└──────────────┴─────────────┴─────────────┴────────────────┴────────────┴─────────────────┴────────────┴──────────────┴────────────┘

Serialization Example

let binary = envelope.serialize_binary()?;
// Returns Vec<u8> with deterministic byte layout
Implementation (src/envelope.rs:28-52):
pub fn serialize_binary(&self) -> Result<Vec<u8>> {
    if self.version != 1 {
        return Err(QimemError::UnsupportedVersion(self.version));
    }
    let nonce_len = u16::try_from(self.nonce.len())
        .map_err(|_| QimemError::InvalidEnvelope("nonce too long"))?;
    let ct_len = u32::try_from(self.ciphertext.len())
        .map_err(|_| QimemError::InvalidEnvelope("ciphertext too long"))?;
    let tag_len = u8::try_from(self.tag.len())
        .map_err(|_| QimemError::InvalidEnvelope("tag too long"))?;

    let mut out = Vec::with_capacity(
        1 + 1 + 16 + 2 + self.nonce.len() + 4 + self.ciphertext.len() + 1 + self.tag.len(),
    );
    out.push(self.version);
    out.push(self.algorithm.id());
    out.extend_from_slice(self.key_id.as_bytes());
    out.extend_from_slice(&nonce_len.to_be_bytes());
    out.extend_from_slice(&self.nonce);
    out.extend_from_slice(&ct_len.to_be_bytes());
    out.extend_from_slice(&self.ciphertext);
    out.push(tag_len);
    out.extend_from_slice(&self.tag);
    Ok(out)
}

Constraints

  • Nonce: Maximum 65,535 bytes (u16)
  • Ciphertext: Maximum 4,294,967,295 bytes (u32)
  • Tag: Maximum 255 bytes (u8)
In practice, nonces are 12 bytes and tags are 16 bytes for AEAD algorithms.

Binary Deserialization

Source: src/envelope.rs:54-130

Parsing Rules

pub fn deserialize_binary(input: &[u8]) -> Result<Self> {
    // 1. Parse version byte
    let version = *input.get(0).ok_or(QimemError::InvalidEnvelope("missing version"))?;
    if version != 1 {
        return Err(QimemError::UnsupportedVersion(version));
    }

    // 2. Parse algorithm ID
    let alg_id = *input.get(1).ok_or(QimemError::InvalidEnvelope("missing algorithm"))?;
    let algorithm = Algorithm::from_id(alg_id)?;

    // 3. Parse UUID key_id (16 bytes)
    let key_bytes = input.get(2..18).ok_or(QimemError::InvalidEnvelope("missing key id"))?;
    let key_id = Uuid::from_slice(key_bytes)
        .map_err(|_| QimemError::InvalidEnvelope("invalid key id"))?;

    // 4. Parse nonce length (u16 big-endian) and nonce bytes
    // 5. Parse ciphertext length (u32 big-endian) and ciphertext bytes
    // 6. Parse tag length (u8) and tag bytes
    // 7. Validate no trailing bytes

    Ok(Self { version, algorithm, key_id, nonce, ciphertext, tag })
}

Validation Checks

  1. Version Enforcement: Only version 1 is accepted (src/envelope.rs:62)
  2. Algorithm Validity: Unknown algorithm IDs return UnsupportedAlgorithm error
  3. Length Consistency: Declared lengths must match actual remaining bytes
  4. No Trailing Data: Input must be fully consumed (src/envelope.rs:118-120)
    if idx != input.len() {
        return Err(QimemError::InvalidEnvelope("trailing bytes"));
    }
    
Envelopes with trailing bytes are rejected to prevent confusion attacks where additional data is appended.

JSON Serialization

Source: src/envelope.rs:132-146

JSON Format

The Envelope struct derives Serialize and Deserialize from serde:
{
  "version": 1,
  "algorithm": "aes_256_gcm",
  "key_id": "550e8400-e29b-41d4-a716-446655440000",
  "nonce": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
  "ciphertext": [65, 66, 67, ...],
  "tag": [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160]
}

API Usage

let json_bytes = envelope.serialize_json()?;
// Returns Vec<u8> containing UTF-8 JSON

let envelope = Envelope::deserialize_json(&json_bytes)?;
// Parses JSON and validates version
Implementation (src/envelope.rs:132-145):
pub fn serialize_json(&self) -> Result<Vec<u8>> {
    serde_json::to_vec(self).map_err(|err| QimemError::Serialization(err.to_string()))
}

pub fn deserialize_json(input: &[u8]) -> Result<Self> {
    let envelope: Self = serde_json::from_slice(input)
        .map_err(|err| QimemError::Serialization(err.to_string()))?;
    if envelope.version != 1 {
        return Err(QimemError::UnsupportedVersion(envelope.version));
    }
    Ok(envelope)
}
Algorithm is serialized with snake_case naming via #[serde(rename_all = "snake_case")].

Tamper Detection

The envelope format provides multiple layers of integrity protection:

AEAD Tag Verification

The 16-byte authentication tag in the envelope is verified during decryption:
let plaintext = engine.decrypt(&key, &envelope)?;
// Returns Err(QimemError::Decryption) if tag is invalid
Protects Against:
  • Ciphertext modification
  • Nonce substitution
  • Tag forgery

Algorithm Binding

The CryptoEngine validates that the envelope’s algorithm matches the engine’s configuration (src/crypto.rs:96-98):
if self.algorithm != envelope.algorithm {
    return Err(QimemError::InvalidEnvelope("algorithm mismatch"));
}
Protects Against:
  • Algorithm downgrade attacks
  • Cross-algorithm confusion

Version Enforcement

Both serialization methods reject unsupported versions:
if self.version != 1 {
    return Err(QimemError::UnsupportedVersion(self.version));
}
Protects Against:
  • Forward compatibility issues
  • Version rollback attempts

Serialization Format Comparison

FeatureBinaryJSON
SizeCompact (minimal overhead)Verbose (Base64 + JSON overhead)
Human-readableNoYes
DeterministicYesYes (with stable field order)
Use CaseProduction storage, network transmissionDebugging, logging, API responses
PerformanceFaster (no Base64 encoding)Slower (JSON parsing + Base64)

HTTP API Format

Source: src/platform_api.rs:123-135, 147-161 The platform API uses Base64-encoded binary envelopes for optimal size: Encryption Response:
{
  "envelope": "AQFQToQA4ptBD6cWRGZVQQAACg4DBA..."
}
Decryption Request:
{
  "input": "AQFQToQA4ptBD6cWRGZVQQAACg4DBA..."
}

Validation Rules Summary

All validation rules are enforced during deserialization. Violations result in structured errors.
  1. Version must be 1: Rejects unknown formats
  2. Algorithm ID must be recognized: Prevents processing of unsupported ciphers
  3. key_id must be valid UUID: 16-byte RFC 4122 format
  4. Lengths must be consistent: Declared lengths match actual data
  5. No trailing bytes: Input must be fully consumed
  6. Tag verification during decryption: AEAD guarantee

Example: Round-Trip Serialization

use qimem::{CryptoEngine, Algorithm, Envelope};

// Encrypt
let envelope = engine.encrypt(&key, b"secret")?;

// Serialize to binary
let binary = envelope.serialize_binary()?;

// Deserialize from binary
let recovered = Envelope::deserialize_binary(&binary)?;

// Decrypt
let plaintext = engine.decrypt(&key, &recovered)?;
assert_eq!(plaintext, b"secret");

Next Steps

Encryption

Learn how envelopes are created during encryption

Key Rotation

Understand how key_id enables decryption with rotated keys

Build docs developers (and LLMs) love