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/// Derive a 32-byte auth token from a master key using HKDF-SHA256.
83///
84/// The token is derived with a distinct info string (`"server-auth-token"`),
85/// making it cryptographically independent of the encryption key derived by
86/// [`ChaChaEngine::new`]. The server stores `BLAKE3(token)` and never learns
87/// the encryption key.
88pub fn derive_auth_token(master_key: &[u8]) -> FsResult<[u8; 32]> {
89    let hk = Hkdf::<Sha256>::new(Some(b"doublecrypt-v1"), master_key);
90    let mut token = [0u8; 32];
91    hk.expand(b"server-auth-token", &mut token)
92        .map_err(|e| FsError::Encryption(format!("HKDF auth-token derivation failed: {e}")))?;
93    Ok(token)
94}
95
96/// Encrypt a logical object payload into an EncryptedObject envelope.
97pub fn encrypt_object(
98    engine: &dyn CryptoEngine,
99    kind: crate::model::ObjectKind,
100    plaintext: &[u8],
101) -> FsResult<EncryptedObject> {
102    let (nonce_vec, ciphertext) = engine.encrypt(plaintext)?;
103    let mut nonce = [0u8; 12];
104    nonce.copy_from_slice(&nonce_vec);
105    Ok(EncryptedObject {
106        kind,
107        version: 1,
108        nonce,
109        ciphertext,
110    })
111}
112
113/// Decrypt an EncryptedObject envelope back to plaintext bytes.
114pub fn decrypt_object(engine: &dyn CryptoEngine, obj: &EncryptedObject) -> FsResult<Vec<u8>> {
115    engine.decrypt(&obj.nonce, &obj.ciphertext)
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use crate::model::ObjectKind;
122
123    #[test]
124    fn test_encrypt_decrypt_roundtrip() {
125        let engine = ChaChaEngine::generate().unwrap();
126        let plaintext = b"hello, doublecrypt!";
127        let enc = encrypt_object(&engine, ObjectKind::FileDataChunk, plaintext).unwrap();
128        let dec = decrypt_object(&engine, &enc).unwrap();
129        assert_eq!(&dec, plaintext);
130    }
131
132    #[test]
133    fn test_wrong_key_fails() {
134        let engine1 = ChaChaEngine::generate().unwrap();
135        let engine2 = ChaChaEngine::generate().unwrap();
136        let plaintext = b"secret data";
137        let enc = encrypt_object(&engine1, ObjectKind::FileDataChunk, plaintext).unwrap();
138        assert!(decrypt_object(&engine2, &enc).is_err());
139    }
140}