Skip to main content

hashtree_core/
crypto.rs

1//! Content Hash Key (CHK) encryption for HashTree
2//!
3//! **⚠️ EXPERIMENTAL: Encryption API is unstable and may change.**
4//!
5//! Uses convergent encryption where the key is derived from the content itself.
6//! This enables deduplication: same content → same ciphertext.
7//!
8//! Algorithm:
9//! 1. content_hash = SHA256(plaintext)
10//! 2. key = HKDF-SHA256(content_hash, salt="hashtree-chk", info="encryption-key")
11//! 3. ciphertext = AES-256-GCM(key, zero_nonce, plaintext)
12//!
13//! Zero nonce is safe because CHK guarantees same key = same content.
14//!
15//! Format: [ciphertext][16-byte auth tag]
16//!
17//! The content_hash acts as the "decryption key" - store it securely.
18
19use aes_gcm::{
20    aead::{Aead, KeyInit},
21    Aes256Gcm, Nonce,
22};
23use hkdf::Hkdf;
24use rand::RngCore;
25use sha2::{Digest, Sha256};
26
27/// 32-byte encryption key (256 bits) - this is the content hash
28pub type EncryptionKey = [u8; 32];
29
30/// Nonce size for AES-GCM (96 bits)
31const NONCE_SIZE: usize = 12;
32
33/// Auth tag size for AES-GCM
34const TAG_SIZE: usize = 16;
35
36/// HKDF salt for CHK derivation
37const CHK_SALT: &[u8] = b"hashtree-chk";
38
39/// Encryption error
40#[derive(Debug, thiserror::Error)]
41pub enum CryptoError {
42    #[error("Encryption failed: {0}")]
43    EncryptionFailed(String),
44    #[error("Decryption failed: {0}")]
45    DecryptionFailed(String),
46    #[error("Encrypted data too short")]
47    DataTooShort,
48    #[error("Invalid key length")]
49    InvalidKeyLength,
50    #[error("Unsupported encryption suite: {0}")]
51    UnsupportedSuite(u8),
52    #[error("Key derivation failed")]
53    KeyDerivationFailed,
54}
55
56/// Derive encryption key from content hash using HKDF
57fn derive_key(content_hash: &[u8; 32]) -> Result<[u8; 32], CryptoError> {
58    let hk = Hkdf::<Sha256>::new(Some(CHK_SALT), content_hash);
59
60    let mut key = [0u8; 32];
61    hk.expand(b"encryption-key", &mut key)
62        .map_err(|_| CryptoError::KeyDerivationFailed)?;
63
64    Ok(key)
65}
66
67/// Generate a random 32-byte key (for non-CHK encryption)
68pub fn generate_key() -> EncryptionKey {
69    let mut key = [0u8; 32];
70    rand::thread_rng().fill_bytes(&mut key);
71    key
72}
73
74/// Compute content hash (SHA256) - this becomes the decryption key for CHK
75pub fn content_hash(data: &[u8]) -> EncryptionKey {
76    let hash = Sha256::digest(data);
77    let mut result = [0u8; 32];
78    result.copy_from_slice(&hash);
79    result
80}
81
82/// CHK encrypt: derive key from content, encrypt with zero nonce
83///
84/// Returns: (ciphertext with auth tag, content_hash as decryption key)
85///
86/// Zero nonce is safe because CHK guarantees: same key = same content.
87/// We never encrypt different content with the same key.
88///
89/// The content_hash is both:
90/// - The decryption key (store securely, share with authorized users)
91/// - Enables dedup: same content → same ciphertext
92pub fn encrypt_chk(plaintext: &[u8]) -> Result<(Vec<u8>, EncryptionKey), CryptoError> {
93    let chash = content_hash(plaintext);
94    let key = derive_key(&chash)?;
95    let zero_nonce = [0u8; NONCE_SIZE];
96
97    let cipher = Aes256Gcm::new_from_slice(&key)
98        .map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?;
99
100    let ciphertext = cipher
101        .encrypt(&Nonce::from(zero_nonce), plaintext)
102        .map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?;
103
104    Ok((ciphertext, chash))
105}
106
107/// CHK decrypt: derive key from content_hash, decrypt with zero nonce
108///
109/// The key parameter is the content_hash returned from encrypt_chk
110pub fn decrypt_chk(ciphertext: &[u8], key: &EncryptionKey) -> Result<Vec<u8>, CryptoError> {
111    if ciphertext.len() < TAG_SIZE {
112        return Err(CryptoError::DataTooShort);
113    }
114
115    let enc_key = derive_key(key)?;
116    let zero_nonce = [0u8; NONCE_SIZE];
117
118    let cipher = Aes256Gcm::new_from_slice(&enc_key)
119        .map_err(|e| CryptoError::DecryptionFailed(e.to_string()))?;
120
121    let plaintext = cipher
122        .decrypt(&Nonce::from(zero_nonce), ciphertext)
123        .map_err(|e| CryptoError::DecryptionFailed(e.to_string()))?;
124    if content_hash(&plaintext) != *key {
125        return Err(CryptoError::DecryptionFailed(
126            "CHK plaintext hash mismatch".into(),
127        ));
128    }
129    Ok(plaintext)
130}
131
132/// Encrypt with a provided key (non-CHK, random nonce)
133///
134/// Returns: `[12-byte nonce][ciphertext][16-byte auth tag]`
135pub fn encrypt(plaintext: &[u8], key: &EncryptionKey) -> Result<Vec<u8>, CryptoError> {
136    let cipher =
137        Aes256Gcm::new_from_slice(key).map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?;
138
139    let mut nonce_bytes = [0u8; NONCE_SIZE];
140    rand::thread_rng().fill_bytes(&mut nonce_bytes);
141    let nonce = Nonce::from(nonce_bytes);
142
143    let ciphertext = cipher
144        .encrypt(&nonce, plaintext)
145        .map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?;
146
147    let mut result = Vec::with_capacity(NONCE_SIZE + ciphertext.len());
148    result.extend_from_slice(&nonce_bytes);
149    result.extend_from_slice(&ciphertext);
150
151    Ok(result)
152}
153
154/// Decrypt with a provided key (non-CHK)
155///
156/// Input: `[12-byte nonce][ciphertext][auth tag]`
157pub fn decrypt(encrypted: &[u8], key: &EncryptionKey) -> Result<Vec<u8>, CryptoError> {
158    if encrypted.len() < NONCE_SIZE + TAG_SIZE {
159        return Err(CryptoError::DataTooShort);
160    }
161
162    let cipher =
163        Aes256Gcm::new_from_slice(key).map_err(|e| CryptoError::DecryptionFailed(e.to_string()))?;
164
165    let nonce = Nonce::try_from(&encrypted[..NONCE_SIZE]).map_err(|_| CryptoError::DataTooShort)?;
166    let ciphertext = &encrypted[NONCE_SIZE..];
167
168    cipher
169        .decrypt(&nonce, ciphertext)
170        .map_err(|e| CryptoError::DecryptionFailed(e.to_string()))
171}
172
173/// Check if data could be encrypted (based on minimum size for non-CHK)
174pub fn could_be_encrypted(data: &[u8]) -> bool {
175    data.len() >= NONCE_SIZE + TAG_SIZE
176}
177
178/// Calculate encrypted size for given plaintext size (non-CHK with nonce prefix)
179pub fn encrypted_size(plaintext_size: usize) -> usize {
180    NONCE_SIZE + plaintext_size + TAG_SIZE
181}
182
183/// Calculate encrypted size for CHK (no nonce prefix)
184pub fn encrypted_size_chk(plaintext_size: usize) -> usize {
185    plaintext_size + TAG_SIZE
186}
187
188/// Calculate plaintext size from encrypted size (non-CHK)
189pub fn plaintext_size(encrypted_size: usize) -> usize {
190    encrypted_size.saturating_sub(NONCE_SIZE + TAG_SIZE)
191}
192
193/// Convert key to hex string
194pub fn key_to_hex(key: &EncryptionKey) -> String {
195    hex::encode(key)
196}
197
198/// Convert hex string to key
199pub fn key_from_hex(hex_str: &str) -> Result<EncryptionKey, CryptoError> {
200    let bytes = hex::decode(hex_str).map_err(|_| CryptoError::InvalidKeyLength)?;
201    if bytes.len() != 32 {
202        return Err(CryptoError::InvalidKeyLength);
203    }
204    let mut key = [0u8; 32];
205    key.copy_from_slice(&bytes);
206    Ok(key)
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn test_chk_encrypt_decrypt() {
215        let plaintext = b"Hello, World!";
216
217        let (ciphertext, key) = encrypt_chk(plaintext).unwrap();
218        let decrypted = decrypt_chk(&ciphertext, &key).unwrap();
219
220        assert_eq!(decrypted, plaintext);
221    }
222
223    #[test]
224    fn test_chk_deterministic() {
225        let plaintext = b"Same content produces same ciphertext";
226
227        let (ciphertext1, key1) = encrypt_chk(plaintext).unwrap();
228        let (ciphertext2, key2) = encrypt_chk(plaintext).unwrap();
229
230        // Same content = same key = same ciphertext (dedup works!)
231        assert_eq!(key1, key2);
232        assert_eq!(ciphertext1, ciphertext2);
233    }
234
235    #[test]
236    fn test_chk_different_content() {
237        let (ciphertext1, key1) = encrypt_chk(b"Content A").unwrap();
238        let (ciphertext2, key2) = encrypt_chk(b"Content B").unwrap();
239
240        // Different content = different everything
241        assert_ne!(key1, key2);
242        assert_ne!(ciphertext1, ciphertext2);
243    }
244
245    #[test]
246    fn test_chk_wrong_key_fails() {
247        let (ciphertext, _key) = encrypt_chk(b"Secret data").unwrap();
248        let wrong_key = generate_key();
249
250        let result = decrypt_chk(&ciphertext, &wrong_key);
251        assert!(result.is_err());
252    }
253
254    #[test]
255    fn test_non_chk_encrypt_decrypt() {
256        let key = generate_key();
257        let plaintext = b"Hello, World!";
258
259        let encrypted = encrypt(plaintext, &key).unwrap();
260        let decrypted = decrypt(&encrypted, &key).unwrap();
261
262        assert_eq!(decrypted, plaintext);
263    }
264
265    #[test]
266    fn test_non_chk_random_nonce() {
267        let key = generate_key();
268        let plaintext = b"Same content";
269
270        let encrypted1 = encrypt(plaintext, &key).unwrap();
271        let encrypted2 = encrypt(plaintext, &key).unwrap();
272
273        // Random nonce = different ciphertext
274        assert_ne!(encrypted1, encrypted2);
275
276        // But both decrypt correctly
277        assert_eq!(decrypt(&encrypted1, &key).unwrap(), plaintext);
278        assert_eq!(decrypt(&encrypted2, &key).unwrap(), plaintext);
279    }
280
281    #[test]
282    fn test_empty_data() {
283        let (ciphertext, key) = encrypt_chk(b"").unwrap();
284        let decrypted = decrypt_chk(&ciphertext, &key).unwrap();
285        assert_eq!(decrypted, b"");
286    }
287
288    #[test]
289    fn test_large_data() {
290        let plaintext = vec![0u8; 1024 * 1024]; // 1MB
291
292        let (ciphertext, key) = encrypt_chk(&plaintext).unwrap();
293        let decrypted = decrypt_chk(&ciphertext, &key).unwrap();
294
295        assert_eq!(decrypted, plaintext);
296    }
297
298    #[test]
299    fn test_key_hex_roundtrip() {
300        let key = generate_key();
301        let hex_str = key_to_hex(&key);
302        let key2 = key_from_hex(&hex_str).unwrap();
303        assert_eq!(key, key2);
304    }
305
306    #[test]
307    fn test_encrypted_size_chk() {
308        let plaintext = b"Test data";
309        let (ciphertext, _) = encrypt_chk(plaintext).unwrap();
310        assert_eq!(ciphertext.len(), encrypted_size_chk(plaintext.len()));
311    }
312
313    #[test]
314    fn test_tampered_data_fails() {
315        let (mut ciphertext, key) = encrypt_chk(b"Important data").unwrap();
316
317        // Tamper with ciphertext
318        if let Some(byte) = ciphertext.last_mut() {
319            *byte ^= 0xFF;
320        }
321
322        let result = decrypt_chk(&ciphertext, &key);
323        assert!(result.is_err());
324    }
325}