common/crypto/
secret.rs

1//! Content encryption using ChaCha20-Poly1305
2//!
3//! This module provides symmetric encryption for bucket data. Each encrypted item
4//! (nodes, files) has its own unique `Secret` key, providing:
5//! - **Content-addressed storage**: Encrypted data can be hashed deterministically
6//! - **Per-item encryption**: Compromising one key doesn't affect other items
7//! - **Efficient key rotation**: Can re-encrypt specific items without touching others
8
9use std::io::Read;
10use std::ops::Deref;
11
12use chacha20poly1305::Key;
13use chacha20poly1305::{
14    aead::{Aead, KeyInit},
15    ChaCha20Poly1305, Nonce,
16};
17use serde::{Deserialize, Serialize};
18
19/// Size of ChaCha20-Poly1305 nonce in bytes
20pub const NONCE_SIZE: usize = 12;
21/// Size of ChaCha20-Poly1305 key in bytes (256 bits)
22pub const SECRET_SIZE: usize = 32;
23/// Default chunk size for streaming operations
24#[allow(dead_code)]
25pub const CHUNK_SIZE: usize = 4096;
26
27/// Errors that can occur during encryption/decryption
28#[derive(Debug, thiserror::Error)]
29pub enum SecretError {
30    #[error("secret error: {0}")]
31    Default(#[from] anyhow::Error),
32    #[error("IO error: {0}")]
33    Io(#[from] std::io::Error),
34}
35
36/// A 256-bit symmetric encryption key for content encryption
37///
38/// Each `Secret` is used to encrypt a single item (node or data blob) using ChaCha20-Poly1305 AEAD.
39/// The encrypted format is: `nonce (12 bytes) || ciphertext (variable) || tag (16 bytes)`.
40///
41/// # Examples
42///
43/// ```ignore
44/// // Generate a new random secret
45/// let secret = Secret::generate();
46///
47/// // Encrypt data
48/// let plaintext = b"sensitive data";
49/// let ciphertext = secret.encrypt(plaintext)?;
50///
51/// // Decrypt data
52/// let recovered = secret.decrypt(&ciphertext)?;
53/// assert_eq!(plaintext, &recovered[..]);
54/// ```
55#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
56pub struct Secret([u8; SECRET_SIZE]);
57
58impl Default for Secret {
59    fn default() -> Self {
60        Secret([0; SECRET_SIZE])
61    }
62}
63
64impl Deref for Secret {
65    type Target = [u8; SECRET_SIZE];
66    fn deref(&self) -> &Self::Target {
67        &self.0
68    }
69}
70
71impl From<[u8; SECRET_SIZE]> for Secret {
72    fn from(bytes: [u8; SECRET_SIZE]) -> Self {
73        Secret(bytes)
74    }
75}
76
77impl Secret {
78    /// Generate a new random secret using a cryptographically secure RNG
79    pub fn generate() -> Self {
80        let mut buff = [0; SECRET_SIZE];
81        getrandom::getrandom(&mut buff).expect("failed to generate random bytes");
82        Self(buff)
83    }
84
85    /// Create a secret from a byte slice
86    ///
87    /// # Errors
88    ///
89    /// Returns an error if the slice length is not exactly `SECRET_SIZE` bytes.
90    pub fn from_slice(data: &[u8]) -> Result<Self, SecretError> {
91        if data.len() != SECRET_SIZE {
92            return Err(anyhow::anyhow!(
93                "invalid secret size, expected {}, got {}",
94                SECRET_SIZE,
95                data.len()
96            )
97            .into());
98        }
99        let mut buff = [0; SECRET_SIZE];
100        buff.copy_from_slice(data);
101        Ok(buff.into())
102    }
103
104    /// Get a reference to the secret key bytes
105    pub fn bytes(&self) -> &[u8] {
106        self.0.as_ref()
107    }
108
109    /// Encrypt data using ChaCha20-Poly1305 AEAD
110    ///
111    /// The output format is: `nonce (12 bytes) || ciphertext || auth_tag (16 bytes)`.
112    /// A random nonce is generated for each encryption operation.
113    ///
114    /// # Errors
115    ///
116    /// Returns an error if encryption fails (should be rare, only on system RNG failure).
117    pub fn encrypt(&self, data: &[u8]) -> Result<Vec<u8>, SecretError> {
118        let key = Key::from_slice(self.bytes());
119        let cipher = ChaCha20Poly1305::new(key);
120
121        // Generate random nonce
122        let mut nonce_bytes = [0u8; NONCE_SIZE];
123        getrandom::getrandom(&mut nonce_bytes)
124            .map_err(|e| anyhow::anyhow!("failed to generate nonce: {}", e))?;
125        let nonce = Nonce::from_slice(&nonce_bytes);
126
127        let ciphertext = cipher
128            .encrypt(nonce, data.as_ref())
129            .map_err(|_| anyhow::anyhow!("encrypt error"))?;
130
131        let mut out = Vec::with_capacity(NONCE_SIZE + ciphertext.len());
132        out.extend_from_slice(nonce.as_ref());
133        out.extend_from_slice(ciphertext.as_ref());
134
135        Ok(out)
136    }
137
138    /// Decrypt data using ChaCha20-Poly1305 AEAD
139    ///
140    /// Expects input in the format: `nonce (12 bytes) || ciphertext || auth_tag (16 bytes)`.
141    ///
142    /// # Errors
143    ///
144    /// Returns an error if:
145    /// - Data is too short to contain a nonce
146    /// - Authentication tag verification fails (data was tampered with or wrong key)
147    pub fn decrypt(&self, data: &[u8]) -> Result<Vec<u8>, SecretError> {
148        if data.len() < NONCE_SIZE {
149            return Err(anyhow::anyhow!("data too short for nonce").into());
150        }
151
152        let key = Key::from_slice(self.bytes());
153        let nonce = Nonce::from_slice(&data[..NONCE_SIZE]);
154        let cipher = ChaCha20Poly1305::new(key);
155        let decrypted = cipher
156            .decrypt(nonce, &data[NONCE_SIZE..])
157            .map_err(|_| anyhow::anyhow!("decrypt error"))?;
158
159        Ok(decrypted.to_vec())
160    }
161
162    /// Create an encrypted reader from a plaintext reader
163    ///
164    /// This buffers all data in memory, encrypts it, and returns a reader over the encrypted data.
165    /// Future optimization: implement true streaming encryption.
166    pub fn encrypt_reader<R>(&self, reader: R) -> Result<impl Read, SecretError>
167    where
168        R: Read,
169    {
170        let mut data = Vec::new();
171        let mut reader = reader;
172        reader.read_to_end(&mut data).map_err(SecretError::Io)?;
173
174        let encrypted = self.encrypt(&data)?;
175        Ok(std::io::Cursor::new(encrypted))
176    }
177
178    /// Create a decrypted reader from an encrypted reader
179    ///
180    /// This buffers all encrypted data in memory, decrypts it, and returns a reader over the plaintext.
181    /// Future optimization: implement true streaming decryption.
182    pub fn decrypt_reader<R>(&self, reader: R) -> Result<impl Read, SecretError>
183    where
184        R: Read,
185    {
186        let mut encrypted_data = Vec::new();
187        let mut reader = reader;
188        reader
189            .read_to_end(&mut encrypted_data)
190            .map_err(SecretError::Io)?;
191
192        let decrypted = self.decrypt(&encrypted_data)?;
193        Ok(std::io::Cursor::new(decrypted))
194    }
195}
196
197#[cfg(test)]
198mod test {
199    use super::*;
200    use std::io::Cursor;
201
202    #[test]
203    fn test_secret_encrypt_decrypt() {
204        let secret = Secret::generate();
205        let data = b"hello world, this is a test message for encryption";
206
207        let encrypted = secret.encrypt(data).unwrap();
208        let decrypted = secret.decrypt(&encrypted).unwrap();
209
210        assert_eq!(data.as_slice(), decrypted.as_slice());
211    }
212
213    #[test]
214    fn test_encrypt_decrypt_reader() {
215        let secret = Secret::generate();
216        let data = b"hello world, this is a test message for reader encryption and decryption";
217
218        // Create encrypted reader
219        let reader = Cursor::new(data.to_vec());
220        let mut encrypted_reader = secret.encrypt_reader(reader).unwrap();
221
222        // Read encrypted data
223        let mut encrypted_data = Vec::new();
224        encrypted_reader.read_to_end(&mut encrypted_data).unwrap();
225
226        // Decrypt using reader
227        let encrypted_cursor = Cursor::new(encrypted_data);
228        let mut decrypted_reader = secret.decrypt_reader(encrypted_cursor).unwrap();
229
230        let mut decrypted_data = Vec::new();
231        decrypted_reader.read_to_end(&mut decrypted_data).unwrap();
232
233        assert_eq!(data.to_vec(), decrypted_data);
234    }
235
236    #[test]
237    fn test_secret_size_validation() {
238        let too_short = [1u8; 16];
239        let too_long = [1u8; 64];
240
241        assert!(Secret::from_slice(&too_short).is_err());
242        assert!(Secret::from_slice(&too_long).is_err());
243
244        let just_right = [1u8; SECRET_SIZE];
245        assert!(Secret::from_slice(&just_right).is_ok());
246    }
247}