Skip to main content

void_crypto/
envelope.rs

1//! Per-commit derived key encryption with envelope format.
2//!
3//! Each commit uses a unique derived key. The envelope format includes a magic
4//! header and key nonce, enabling automatic detection during decryption.
5//!
6//! Envelope format: `[magic: 4 bytes "VD01"][key_nonce: 16 bytes][encrypted_body: N bytes]`
7//!
8//! The key nonce derives a commit-specific key from the root key using
9//! HKDF with scope `commit:<hex(nonce)>`.
10
11use crate::aead::{decrypt, encrypt};
12use crate::kdf::{derive_scoped_key, KeyNonce};
13use crate::{CryptoError, CryptoResult};
14
15/// Magic bytes identifying envelope format version 1.
16pub const MAGIC_V1: &[u8; 4] = b"VD01";
17
18/// Total envelope header size (magic + 16-byte nonce).
19const ENVELOPE_HEADER_SIZE: usize = 4 + KeyNonce::SIZE;
20
21/// Generates a cryptographically secure random key nonce.
22pub fn generate_key_nonce() -> KeyNonce {
23    KeyNonce::generate()
24}
25
26/// Encrypts plaintext using a derived key and wraps it in an envelope.
27///
28/// # Security
29///
30/// A unique encryption key is derived from the root key using the provided nonce:
31/// `derived_key = derive_scoped_key(root_key, "commit:<hex(nonce)>")`
32///
33/// Each commit gets a unique encryption key. The nonce is embedded in the
34/// envelope header so decryption can re-derive the same key.
35pub fn encrypt_with_envelope(
36    root_key: &[u8; 32],
37    nonce: &KeyNonce,
38    plaintext: &[u8],
39    aad: &[u8],
40) -> CryptoResult<Vec<u8>> {
41    let scope = format!("commit:{}", hex::encode(nonce.as_bytes()));
42    let derived_key = derive_scoped_key(root_key, &scope)?;
43
44    let encrypted = encrypt(&derived_key, plaintext, aad)?;
45
46    let mut envelope = Vec::with_capacity(ENVELOPE_HEADER_SIZE + encrypted.len());
47    envelope.extend_from_slice(MAGIC_V1);
48    envelope.extend_from_slice(nonce.as_bytes());
49    envelope.extend_from_slice(&encrypted);
50
51    Ok(envelope)
52}
53
54/// Decrypts a blob with envelope format (VD01 header required).
55///
56/// Returns `(plaintext, nonce)` used for key derivation.
57/// Errors if blob does not have VD01 envelope header.
58pub fn decrypt_envelope(
59    root_key: &[u8; 32],
60    blob: &[u8],
61    aad: &[u8],
62) -> CryptoResult<(Vec<u8>, KeyNonce)> {
63    if blob.len() <= ENVELOPE_HEADER_SIZE || !blob.starts_with(MAGIC_V1) {
64        return Err(CryptoError::Decryption(
65            "missing VD01 envelope header".into(),
66        ));
67    }
68
69    let nonce = KeyNonce::from_bytes(&blob[4..ENVELOPE_HEADER_SIZE])
70        .ok_or_else(|| CryptoError::Decryption("invalid envelope nonce length".into()))?;
71
72    let scope = format!("commit:{}", hex::encode(nonce.as_bytes()));
73    let derived_key = derive_scoped_key(root_key, &scope)?;
74
75    let plaintext = decrypt(&derived_key, &blob[ENVELOPE_HEADER_SIZE..], aad)?;
76
77    Ok((plaintext, nonce))
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use crate::aead::AAD_COMMIT;
84
85    const TEST_AAD: &[u8] = b"void:envelope:test";
86
87    #[test]
88    fn envelope_roundtrip() {
89        let root_key = [0x42u8; 32];
90        let nonce = generate_key_nonce();
91        let plaintext = b"hello, envelope!";
92
93        let envelope = encrypt_with_envelope(&root_key, &nonce, plaintext, TEST_AAD).unwrap();
94
95        assert!(envelope.starts_with(MAGIC_V1));
96        assert_eq!(&envelope[4..20], nonce.as_bytes());
97
98        let (decrypted, returned_nonce) = decrypt_envelope(&root_key, &envelope, TEST_AAD).unwrap();
99        assert_eq!(decrypted, plaintext);
100        assert_eq!(returned_nonce, nonce);
101    }
102
103    #[test]
104    fn wrong_key_fails() {
105        let root_key = [0x42u8; 32];
106        let wrong_key = [0x43u8; 32];
107        let nonce = generate_key_nonce();
108        let plaintext = b"secret data";
109
110        let envelope = encrypt_with_envelope(&root_key, &nonce, plaintext, TEST_AAD).unwrap();
111        let result = decrypt_envelope(&wrong_key, &envelope, TEST_AAD);
112        assert!(result.is_err());
113    }
114
115    #[test]
116    fn envelope_with_aad_commit() {
117        let root_key = [0x42u8; 32];
118        let nonce = generate_key_nonce();
119        let plaintext = b"commit data with proper AAD";
120
121        let envelope = encrypt_with_envelope(&root_key, &nonce, plaintext, AAD_COMMIT).unwrap();
122
123        let (decrypted, _) = decrypt_envelope(&root_key, &envelope, AAD_COMMIT).unwrap();
124        assert_eq!(decrypted, plaintext);
125
126        let result = decrypt_envelope(&root_key, &envelope, b"wrong:aad");
127        assert!(result.is_err());
128    }
129
130    #[test]
131    fn generate_key_nonce_unique() {
132        let nonce1 = generate_key_nonce();
133        let nonce2 = generate_key_nonce();
134        assert_ne!(nonce1, nonce2);
135    }
136}