grafos_securestore/
crypto.rs

1//! Crypto backend trait and mock implementation.
2//!
3//! Real AEAD backends are available behind feature flags:
4//! - `crypto-aes-gcm` — AES-256-GCM via the `aes-gcm` crate
5//! - `crypto-chacha20poly1305` — ChaCha20-Poly1305 via the `chacha20poly1305` crate
6
7use alloc::string::String;
8use alloc::vec::Vec;
9
10/// Error from a cryptographic operation.
11#[derive(Debug, PartialEq)]
12pub struct CryptoError(pub String);
13
14/// Trait abstracting symmetric encryption operations.
15///
16/// Implementors provide authenticated encryption with associated data (AEAD).
17/// The base crate ships a [`MockCryptoBackend`] for testing. Real
18/// implementations (e.g. AES-256-GCM) can be provided behind feature flags.
19pub trait CryptoBackend {
20    /// Encrypt `plaintext` using `key`, `nonce`, and `aad`.
21    fn encrypt(
22        &self,
23        key: &[u8],
24        nonce: &[u8],
25        aad: &[u8],
26        plaintext: &[u8],
27    ) -> Result<Vec<u8>, CryptoError>;
28
29    /// Decrypt `ciphertext` using `key`, `nonce`, and `aad`.
30    fn decrypt(
31        &self,
32        key: &[u8],
33        nonce: &[u8],
34        aad: &[u8],
35        ciphertext: &[u8],
36    ) -> Result<Vec<u8>, CryptoError>;
37
38    /// Generate a fresh encryption key.
39    fn generate_key(&self) -> Vec<u8>;
40
41    /// Generate a fresh nonce.
42    fn generate_nonce(&self) -> Vec<u8>;
43}
44
45/// XOR-based mock crypto backend for testing.
46///
47/// **NOT SECURE.** This backend XORs plaintext with the key (cycling the key
48/// bytes). It exists solely for unit tests where real crypto is unnecessary.
49pub struct MockCryptoBackend {
50    key_counter: core::cell::Cell<u64>,
51    nonce_counter: core::cell::Cell<u64>,
52}
53
54impl MockCryptoBackend {
55    pub fn new() -> Self {
56        Self {
57            key_counter: core::cell::Cell::new(1),
58            nonce_counter: core::cell::Cell::new(1),
59        }
60    }
61}
62
63impl Default for MockCryptoBackend {
64    fn default() -> Self {
65        Self::new()
66    }
67}
68
69impl CryptoBackend for MockCryptoBackend {
70    fn encrypt(
71        &self,
72        key: &[u8],
73        _nonce: &[u8],
74        _aad: &[u8],
75        plaintext: &[u8],
76    ) -> Result<Vec<u8>, CryptoError> {
77        if key.is_empty() {
78            return Err(CryptoError("empty key".into()));
79        }
80        let ciphertext: Vec<u8> = plaintext
81            .iter()
82            .enumerate()
83            .map(|(i, &b)| b ^ key[i % key.len()])
84            .collect();
85        Ok(ciphertext)
86    }
87
88    fn decrypt(
89        &self,
90        key: &[u8],
91        _nonce: &[u8],
92        _aad: &[u8],
93        ciphertext: &[u8],
94    ) -> Result<Vec<u8>, CryptoError> {
95        if key.is_empty() {
96            return Err(CryptoError("empty key".into()));
97        }
98        // XOR is its own inverse
99        let plaintext: Vec<u8> = ciphertext
100            .iter()
101            .enumerate()
102            .map(|(i, &b)| b ^ key[i % key.len()])
103            .collect();
104        Ok(plaintext)
105    }
106
107    fn generate_key(&self) -> Vec<u8> {
108        let counter = self.key_counter.get();
109        self.key_counter.set(counter + 1);
110        let mut key = Vec::with_capacity(32);
111        for i in 0..32u8 {
112            key.push(i.wrapping_add(counter as u8));
113        }
114        key
115    }
116
117    fn generate_nonce(&self) -> Vec<u8> {
118        let counter = self.nonce_counter.get();
119        self.nonce_counter.set(counter + 1);
120        let mut nonce = Vec::with_capacity(12);
121        for i in 0..12u8 {
122            nonce.push(i.wrapping_add(counter as u8));
123        }
124        nonce
125    }
126}
127
128// ---------------------------------------------------------------------------
129// AES-256-GCM backend (feature-gated)
130// ---------------------------------------------------------------------------
131
132#[cfg(feature = "crypto-aes-gcm")]
133mod aes_gcm_backend {
134    use super::{CryptoBackend, CryptoError};
135    use aes_gcm::aead::{Aead, KeyInit, Payload};
136    use aes_gcm::{Aes256Gcm, Nonce};
137    use alloc::string::ToString;
138    use alloc::vec::Vec;
139
140    /// AES-256-GCM AEAD backend.
141    ///
142    /// Keys must be exactly 32 bytes; nonces must be exactly 12 bytes.
143    /// Uses OS randomness via `getrandom` for key/nonce generation.
144    pub struct AesGcmBackend;
145
146    impl AesGcmBackend {
147        pub fn new() -> Self {
148            Self
149        }
150    }
151
152    impl Default for AesGcmBackend {
153        fn default() -> Self {
154            Self::new()
155        }
156    }
157
158    impl CryptoBackend for AesGcmBackend {
159        fn encrypt(
160            &self,
161            key: &[u8],
162            nonce: &[u8],
163            aad: &[u8],
164            plaintext: &[u8],
165        ) -> Result<Vec<u8>, CryptoError> {
166            if key.len() != 32 {
167                return Err(CryptoError("AES-256-GCM requires a 32-byte key".into()));
168            }
169            if nonce.len() != 12 {
170                return Err(CryptoError("AES-256-GCM requires a 12-byte nonce".into()));
171            }
172            let cipher = Aes256Gcm::new_from_slice(key).map_err(|e| CryptoError(e.to_string()))?;
173            let nonce = Nonce::from_slice(nonce);
174            let payload = Payload {
175                msg: plaintext,
176                aad,
177            };
178            cipher
179                .encrypt(nonce, payload)
180                .map_err(|e| CryptoError(e.to_string()))
181        }
182
183        fn decrypt(
184            &self,
185            key: &[u8],
186            nonce: &[u8],
187            aad: &[u8],
188            ciphertext: &[u8],
189        ) -> Result<Vec<u8>, CryptoError> {
190            if key.len() != 32 {
191                return Err(CryptoError("AES-256-GCM requires a 32-byte key".into()));
192            }
193            if nonce.len() != 12 {
194                return Err(CryptoError("AES-256-GCM requires a 12-byte nonce".into()));
195            }
196            let cipher = Aes256Gcm::new_from_slice(key).map_err(|e| CryptoError(e.to_string()))?;
197            let nonce = Nonce::from_slice(nonce);
198            let payload = Payload {
199                msg: ciphertext,
200                aad,
201            };
202            cipher
203                .decrypt(nonce, payload)
204                .map_err(|e| CryptoError(e.to_string()))
205        }
206
207        fn generate_key(&self) -> Vec<u8> {
208            let mut key = alloc::vec![0u8; 32];
209            getrandom::getrandom(&mut key).expect("getrandom failed for key generation");
210            key
211        }
212
213        fn generate_nonce(&self) -> Vec<u8> {
214            let mut nonce = alloc::vec![0u8; 12];
215            getrandom::getrandom(&mut nonce).expect("getrandom failed for nonce generation");
216            nonce
217        }
218    }
219}
220
221#[cfg(feature = "crypto-aes-gcm")]
222pub use aes_gcm_backend::AesGcmBackend;
223
224// ---------------------------------------------------------------------------
225// ChaCha20-Poly1305 backend (feature-gated)
226// ---------------------------------------------------------------------------
227
228#[cfg(feature = "crypto-chacha20poly1305")]
229mod chacha_backend {
230    use super::{CryptoBackend, CryptoError};
231    use alloc::string::ToString;
232    use alloc::vec::Vec;
233    use chacha20poly1305::aead::{Aead, KeyInit, Payload};
234    use chacha20poly1305::{ChaCha20Poly1305, Nonce};
235
236    /// ChaCha20-Poly1305 AEAD backend.
237    ///
238    /// Keys must be exactly 32 bytes; nonces must be exactly 12 bytes.
239    /// Uses OS randomness via `getrandom` for key/nonce generation.
240    pub struct ChaChaBackend;
241
242    impl ChaChaBackend {
243        pub fn new() -> Self {
244            Self
245        }
246    }
247
248    impl Default for ChaChaBackend {
249        fn default() -> Self {
250            Self::new()
251        }
252    }
253
254    impl CryptoBackend for ChaChaBackend {
255        fn encrypt(
256            &self,
257            key: &[u8],
258            nonce: &[u8],
259            aad: &[u8],
260            plaintext: &[u8],
261        ) -> Result<Vec<u8>, CryptoError> {
262            if key.len() != 32 {
263                return Err(CryptoError(
264                    "ChaCha20-Poly1305 requires a 32-byte key".into(),
265                ));
266            }
267            if nonce.len() != 12 {
268                return Err(CryptoError(
269                    "ChaCha20-Poly1305 requires a 12-byte nonce".into(),
270                ));
271            }
272            let cipher =
273                ChaCha20Poly1305::new_from_slice(key).map_err(|e| CryptoError(e.to_string()))?;
274            let nonce = Nonce::from_slice(nonce);
275            let payload = Payload {
276                msg: plaintext,
277                aad,
278            };
279            cipher
280                .encrypt(nonce, payload)
281                .map_err(|e| CryptoError(e.to_string()))
282        }
283
284        fn decrypt(
285            &self,
286            key: &[u8],
287            nonce: &[u8],
288            aad: &[u8],
289            ciphertext: &[u8],
290        ) -> Result<Vec<u8>, CryptoError> {
291            if key.len() != 32 {
292                return Err(CryptoError(
293                    "ChaCha20-Poly1305 requires a 32-byte key".into(),
294                ));
295            }
296            if nonce.len() != 12 {
297                return Err(CryptoError(
298                    "ChaCha20-Poly1305 requires a 12-byte nonce".into(),
299                ));
300            }
301            let cipher =
302                ChaCha20Poly1305::new_from_slice(key).map_err(|e| CryptoError(e.to_string()))?;
303            let nonce = Nonce::from_slice(nonce);
304            let payload = Payload {
305                msg: ciphertext,
306                aad,
307            };
308            cipher
309                .decrypt(nonce, payload)
310                .map_err(|e| CryptoError(e.to_string()))
311        }
312
313        fn generate_key(&self) -> Vec<u8> {
314            let mut key = alloc::vec![0u8; 32];
315            getrandom::getrandom(&mut key).expect("getrandom failed for key generation");
316            key
317        }
318
319        fn generate_nonce(&self) -> Vec<u8> {
320            let mut nonce = alloc::vec![0u8; 12];
321            getrandom::getrandom(&mut nonce).expect("getrandom failed for nonce generation");
322            nonce
323        }
324    }
325}
326
327#[cfg(feature = "crypto-chacha20poly1305")]
328pub use chacha_backend::ChaChaBackend;