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
| Field | Type | Purpose |
|---|
version | u8 | Envelope format version (currently 1) |
algorithm | Algorithm | Encryption algorithm identifier (AES-256-GCM or ChaCha20-Poly1305) |
key_id | Uuid | Identifier of the key used for encryption |
nonce | Vec<u8> | Random nonce (12 bytes for AEAD) |
ciphertext | Vec<u8> | Encrypted payload (variable length) |
tag | Vec<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
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
- Version Enforcement: Only version
1 is accepted (src/envelope.rs:62)
- Algorithm Validity: Unknown algorithm IDs return
UnsupportedAlgorithm error
- Length Consistency: Declared lengths must match actual remaining bytes
- 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
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
| Feature | Binary | JSON |
|---|
| Size | Compact (minimal overhead) | Verbose (Base64 + JSON overhead) |
| Human-readable | No | Yes |
| Deterministic | Yes | Yes (with stable field order) |
| Use Case | Production storage, network transmission | Debugging, logging, API responses |
| Performance | Faster (no Base64 encoding) | Slower (JSON parsing + Base64) |
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.
- Version must be 1: Rejects unknown formats
- Algorithm ID must be recognized: Prevents processing of unsupported ciphers
- key_id must be valid UUID: 16-byte RFC 4122 format
- Lengths must be consistent: Declared lengths match actual data
- No trailing bytes: Input must be fully consumed
- 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