Skip to main content

shadow_crypto/
encryption.rs

1use chacha20poly1305::{
2    aead::{Aead, KeyInit, OsRng},
3    ChaCha20Poly1305, Nonce,
4};
5use serde::{Deserialize, Serialize};
6use zeroize::Zeroize;
7use anyhow::{Result, Context};
8
9/// Encryption key for ChaCha20-Poly1305
10#[derive(Clone, Zeroize, Serialize, Deserialize)]
11#[zeroize(drop)]
12pub struct EncryptionKey([u8; 32]);
13
14impl EncryptionKey {
15    /// Generate a new random encryption key
16    pub fn generate() -> Self {
17        let mut key = [0u8; 32];
18        use rand::RngCore;
19        rand::rngs::OsRng.fill_bytes(&mut key);
20        Self(key)
21    }
22
23    /// Create key from bytes
24    pub fn from_bytes(bytes: [u8; 32]) -> Self {
25        Self(bytes)
26    }
27
28    /// Get key bytes
29    pub fn as_bytes(&self) -> &[u8; 32] {
30        &self.0
31    }
32
33    /// Derive key from password using HKDF
34    pub fn from_password(password: &str, salt: &[u8]) -> Result<Self> {
35        use hkdf::Hkdf;
36        use sha2::Sha256;
37
38        let hk = Hkdf::<Sha256>::new(Some(salt), password.as_bytes());
39        let mut okm = [0u8; 32];
40        hk.expand(b"shadow-network-encryption", &mut okm)
41            .map_err(|e| anyhow::anyhow!("HKDF expansion failed: {:?}", e))?;
42        
43        Ok(Self(okm))
44    }
45}
46
47impl std::fmt::Debug for EncryptionKey {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        write!(f, "EncryptionKey([REDACTED])")
50    }
51}
52
53/// Encrypt data using ChaCha20-Poly1305 AEAD
54pub fn encrypt(key: &EncryptionKey, plaintext: &[u8]) -> Result<Vec<u8>> {
55    let cipher = ChaCha20Poly1305::new(key.as_bytes().into());
56    
57    // Generate random nonce (12 bytes for ChaCha20-Poly1305)
58    let mut nonce_bytes = [0u8; 12];
59    use rand::RngCore;
60    rand::rngs::OsRng.fill_bytes(&mut nonce_bytes);
61    let nonce = Nonce::from_slice(&nonce_bytes);
62    
63    // Encrypt
64    let ciphertext = cipher
65        .encrypt(nonce, plaintext)
66        .map_err(|e| anyhow::anyhow!("Encryption failed: {:?}", e))?;
67    
68    // Prepend nonce to ciphertext (standard construction)
69    let mut result = Vec::with_capacity(12 + ciphertext.len());
70    result.extend_from_slice(&nonce_bytes);
71    result.extend_from_slice(&ciphertext);
72    
73    Ok(result)
74}
75
76/// Decrypt data using ChaCha20-Poly1305 AEAD
77pub fn decrypt(key: &EncryptionKey, ciphertext: &[u8]) -> Result<Vec<u8>> {
78    if ciphertext.len() < 12 {
79        anyhow::bail!("Ciphertext too short (missing nonce)");
80    }
81    
82    let cipher = ChaCha20Poly1305::new(key.as_bytes().into());
83    
84    // Extract nonce from first 12 bytes
85    let (nonce_bytes, encrypted_data) = ciphertext.split_at(12);
86    let nonce = Nonce::from_slice(nonce_bytes);
87    
88    // Decrypt
89    let plaintext = cipher
90        .decrypt(nonce, encrypted_data)
91        .map_err(|e| anyhow::anyhow!("Decryption failed: {:?}", e))?;
92    
93    Ok(plaintext)
94}
95
96/// Encrypt with associated data (AEAD)
97pub fn encrypt_with_ad(
98    key: &EncryptionKey,
99    plaintext: &[u8],
100    associated_data: &[u8],
101) -> Result<Vec<u8>> {
102    let cipher = ChaCha20Poly1305::new(key.as_bytes().into());
103    
104    let mut nonce_bytes = [0u8; 12];
105    use rand::RngCore;
106    rand::rngs::OsRng.fill_bytes(&mut nonce_bytes);
107    let nonce = Nonce::from_slice(&nonce_bytes);
108    
109    // Encrypt with AD
110    let ciphertext = cipher
111        .encrypt(nonce, chacha20poly1305::aead::Payload {
112            msg: plaintext,
113            aad: associated_data,
114        })
115        .map_err(|e| anyhow::anyhow!("Encryption with AD failed: {:?}", e))?;
116    
117    let mut result = Vec::with_capacity(12 + ciphertext.len());
118    result.extend_from_slice(&nonce_bytes);
119    result.extend_from_slice(&ciphertext);
120    
121    Ok(result)
122}
123
124/// Decrypt with associated data (AEAD)
125pub fn decrypt_with_ad(
126    key: &EncryptionKey,
127    ciphertext: &[u8],
128    associated_data: &[u8],
129) -> Result<Vec<u8>> {
130    if ciphertext.len() < 12 {
131        anyhow::bail!("Ciphertext too short (missing nonce)");
132    }
133    
134    let cipher = ChaCha20Poly1305::new(key.as_bytes().into());
135    
136    let (nonce_bytes, encrypted_data) = ciphertext.split_at(12);
137    let nonce = Nonce::from_slice(nonce_bytes);
138    
139    let plaintext = cipher
140        .decrypt(nonce, chacha20poly1305::aead::Payload {
141            msg: encrypted_data,
142            aad: associated_data,
143        })
144        .map_err(|e| anyhow::anyhow!("Decryption with AD failed: {:?}", e))?;
145    
146    Ok(plaintext)
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn test_encrypt_decrypt() {
155        let key = EncryptionKey::generate();
156        let plaintext = b"Hello, Shadow Network!";
157        
158        let ciphertext = encrypt(&key, plaintext).unwrap();
159        assert_ne!(ciphertext.as_slice(), plaintext);
160        
161        let decrypted = decrypt(&key, &ciphertext).unwrap();
162        assert_eq!(decrypted.as_slice(), plaintext);
163    }
164
165    #[test]
166    fn test_wrong_key_fails() {
167        let key1 = EncryptionKey::generate();
168        let key2 = EncryptionKey::generate();
169        let plaintext = b"Secret data";
170        
171        let ciphertext = encrypt(&key1, plaintext).unwrap();
172        assert!(decrypt(&key2, &ciphertext).is_err());
173    }
174
175    #[test]
176    fn test_password_derivation() {
177        let salt = b"some-random-salt";
178        let key1 = EncryptionKey::from_password("my-password", salt).unwrap();
179        let key2 = EncryptionKey::from_password("my-password", salt).unwrap();
180        
181        assert_eq!(key1.as_bytes(), key2.as_bytes());
182    }
183}