Skip to main content

doublecrypt_core/
crypto.rs

1use crate::error::{FsError, FsResult};
2use crate::model::EncryptedObject;
3use chacha20poly1305::aead::{Aead, KeyInit};
4use chacha20poly1305::{ChaCha20Poly1305, Nonce};
5use hkdf::Hkdf;
6use rand::RngCore;
7use sha2::Sha256;
8use zeroize::Zeroize;
9
10/// Trait for encrypting and decrypting logical object payloads.
11pub trait CryptoEngine: Send + Sync {
12    /// Encrypt plaintext into ciphertext with a nonce. Returns (nonce, ciphertext).
13    fn encrypt(&self, plaintext: &[u8]) -> FsResult<(Vec<u8>, Vec<u8>)>;
14
15    /// Decrypt ciphertext with the given nonce. Returns plaintext.
16    fn decrypt(&self, nonce: &[u8], ciphertext: &[u8]) -> FsResult<Vec<u8>>;
17}
18
19/// ChaCha20-Poly1305 based crypto engine.
20/// Derives the actual encryption key from a master key using HKDF.
21pub struct ChaChaEngine {
22    /// Derived 256-bit encryption key.
23    key: [u8; 32],
24}
25
26impl ChaChaEngine {
27    /// Create from a raw master key. The encryption key is derived via HKDF-SHA256.
28    pub fn new(master_key: &[u8]) -> FsResult<Self> {
29        let hk = Hkdf::<Sha256>::new(Some(b"doublecrypt-v1"), master_key);
30        let mut key = [0u8; 32];
31        hk.expand(b"block-encryption", &mut key)
32            .map_err(|e| FsError::Encryption(format!("HKDF expand failed: {e}")))?;
33        Ok(Self { key })
34    }
35
36    /// Convenience: create with a randomly generated master key (for testing / new FS).
37    pub fn generate() -> FsResult<Self> {
38        let mut master = [0u8; 32];
39        rand::thread_rng().fill_bytes(&mut master);
40        let engine = Self::new(&master)?;
41        master.zeroize();
42        Ok(engine)
43    }
44}
45
46impl CryptoEngine for ChaChaEngine {
47    fn encrypt(&self, plaintext: &[u8]) -> FsResult<(Vec<u8>, Vec<u8>)> {
48        let cipher = ChaCha20Poly1305::new((&self.key).into());
49        let mut nonce_bytes = [0u8; 12];
50        rand::thread_rng().fill_bytes(&mut nonce_bytes);
51        let nonce = Nonce::from_slice(&nonce_bytes);
52
53        let ciphertext = cipher
54            .encrypt(nonce, plaintext)
55            .map_err(|e| FsError::Encryption(format!("AEAD encrypt failed: {e}")))?;
56
57        Ok((nonce_bytes.to_vec(), ciphertext))
58    }
59
60    fn decrypt(&self, nonce: &[u8], ciphertext: &[u8]) -> FsResult<Vec<u8>> {
61        if nonce.len() != 12 {
62            return Err(FsError::Decryption(format!(
63                "invalid nonce length: {}",
64                nonce.len()
65            )));
66        }
67        let cipher = ChaCha20Poly1305::new((&self.key).into());
68        let nonce = Nonce::from_slice(nonce);
69
70        cipher
71            .decrypt(nonce, ciphertext)
72            .map_err(|e| FsError::Decryption(format!("AEAD decrypt failed: {e}")))
73    }
74}
75
76impl Drop for ChaChaEngine {
77    fn drop(&mut self) {
78        self.key.zeroize();
79    }
80}
81
82/// Encrypt a logical object payload into an EncryptedObject envelope.
83pub fn encrypt_object(
84    engine: &dyn CryptoEngine,
85    kind: crate::model::ObjectKind,
86    plaintext: &[u8],
87) -> FsResult<EncryptedObject> {
88    let (nonce_vec, ciphertext) = engine.encrypt(plaintext)?;
89    let mut nonce = [0u8; 12];
90    nonce.copy_from_slice(&nonce_vec);
91    Ok(EncryptedObject {
92        kind,
93        version: 1,
94        nonce,
95        ciphertext,
96    })
97}
98
99/// Decrypt an EncryptedObject envelope back to plaintext bytes.
100pub fn decrypt_object(
101    engine: &dyn CryptoEngine,
102    obj: &EncryptedObject,
103) -> FsResult<Vec<u8>> {
104    engine.decrypt(&obj.nonce, &obj.ciphertext)
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use crate::model::ObjectKind;
111
112    #[test]
113    fn test_encrypt_decrypt_roundtrip() {
114        let engine = ChaChaEngine::generate().unwrap();
115        let plaintext = b"hello, doublecrypt!";
116        let enc = encrypt_object(&engine, ObjectKind::FileDataChunk, plaintext).unwrap();
117        let dec = decrypt_object(&engine, &enc).unwrap();
118        assert_eq!(&dec, plaintext);
119    }
120
121    #[test]
122    fn test_wrong_key_fails() {
123        let engine1 = ChaChaEngine::generate().unwrap();
124        let engine2 = ChaChaEngine::generate().unwrap();
125        let plaintext = b"secret data";
126        let enc = encrypt_object(&engine1, ObjectKind::FileDataChunk, plaintext).unwrap();
127        assert!(decrypt_object(&engine2, &enc).is_err());
128    }
129}