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 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
96pub 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
113pub 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}