doublecrypt_core/
crypto.rs1use 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
10pub trait CryptoEngine: Send + Sync {
12 fn encrypt(&self, plaintext: &[u8]) -> FsResult<(Vec<u8>, Vec<u8>)>;
14
15 fn decrypt(&self, nonce: &[u8], ciphertext: &[u8]) -> FsResult<Vec<u8>>;
17}
18
19pub struct ChaChaEngine {
22 key: [u8; 32],
24}
25
26impl ChaChaEngine {
27 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 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
82pub 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
99pub 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}