xos_storage/
encryption.rs1use aes_gcm::aead::{Aead, KeyInit};
9use aes_gcm::Aes256Gcm;
10use rand::RngCore;
11use zeroize::Zeroize;
12
13use crate::{Result, StorageError};
14
15pub struct Encryption {
17 key: [u8; 32],
18}
19
20impl Encryption {
21 pub fn from_password(password: &str, salt: &[u8]) -> Result<Self> {
25 if salt.len() < 8 {
26 return Err(StorageError::Encryption(
27 "salt must be at least 8 bytes".into(),
28 ));
29 }
30
31 let argon2 = argon2::Argon2::default();
32 let mut key = [0u8; 32];
33
34 argon2
35 .hash_password_into(password.as_bytes(), salt, &mut key)
36 .map_err(|e| StorageError::Encryption(format!("key derivation failed: {e}")))?;
37
38 Ok(Self { key })
39 }
40
41 pub fn from_raw_key(key: [u8; 32]) -> Self {
43 Self { key }
44 }
45
46 pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
48 let cipher = Aes256Gcm::new(&self.key.into());
49
50 let mut nonce_bytes = [0u8; 12];
51 rand::thread_rng().fill_bytes(&mut nonce_bytes);
52 let nonce = aes_gcm::Nonce::from(nonce_bytes);
53
54 let ciphertext = cipher
55 .encrypt(&nonce, plaintext)
56 .map_err(|e| StorageError::Encryption(format!("encrypt failed: {e}")))?;
57
58 let mut result = Vec::with_capacity(12 + ciphertext.len());
60 result.extend_from_slice(&nonce_bytes);
61 result.extend_from_slice(&ciphertext);
62
63 Ok(result)
64 }
65
66 pub fn decrypt(&self, encrypted: &[u8]) -> Result<Vec<u8>> {
68 if encrypted.len() < 12 + 16 {
69 return Err(StorageError::Encryption("data too short".into()));
71 }
72
73 let (nonce_bytes, ciphertext) = encrypted.split_at(12);
74 let nonce_arr: [u8; 12] = nonce_bytes
75 .try_into()
76 .map_err(|_| StorageError::Encryption("invalid nonce length".into()))?;
77 let nonce = aes_gcm::Nonce::from(nonce_arr);
78 let cipher = Aes256Gcm::new(&self.key.into());
79
80 cipher
81 .decrypt(&nonce, ciphertext)
82 .map_err(|e| StorageError::Encryption(format!("decrypt failed: {e}")))
83 }
84}
85
86impl Drop for Encryption {
87 fn drop(&mut self) {
88 self.key.zeroize();
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 const TEST_SALT: &[u8] = b"xorion_test_salt_16bytes";
97 const TEST_PASSWORD: &str = "hunter2";
98
99 #[test]
100 fn roundtrip() {
101 let enc = Encryption::from_password(TEST_PASSWORD, TEST_SALT).unwrap();
102 let plaintext = b"Hello, Xorion!";
103 let encrypted = enc.encrypt(plaintext).unwrap();
104 let decrypted = enc.decrypt(&encrypted).unwrap();
105 assert_eq!(&decrypted, plaintext);
106 }
107
108 #[test]
109 fn empty_plaintext_roundtrip() {
110 let enc = Encryption::from_password(TEST_PASSWORD, TEST_SALT).unwrap();
111 let encrypted = enc.encrypt(b"").unwrap();
112 let decrypted = enc.decrypt(&encrypted).unwrap();
113 assert!(decrypted.is_empty());
114 }
115
116 #[test]
117 fn large_data_roundtrip() {
118 let enc = Encryption::from_password(TEST_PASSWORD, TEST_SALT).unwrap();
119 let data = vec![0xABu8; 1_000_000]; let encrypted = enc.encrypt(&data).unwrap();
121 let decrypted = enc.decrypt(&encrypted).unwrap();
122 assert_eq!(decrypted, data);
123 }
124
125 #[test]
126 fn wrong_password_fails() {
127 let enc1 = Encryption::from_password("correct", TEST_SALT).unwrap();
128 let enc2 = Encryption::from_password("wrong", TEST_SALT).unwrap();
129 let encrypted = enc1.encrypt(b"secret").unwrap();
130 assert!(enc2.decrypt(&encrypted).is_err());
131 }
132
133 #[test]
134 fn nonce_prepended() {
135 let enc = Encryption::from_password(TEST_PASSWORD, TEST_SALT).unwrap();
136 let encrypted = enc.encrypt(b"data").unwrap();
137 assert_eq!(encrypted.len(), 12 + 4 + 16);
139 }
140
141 #[test]
142 fn different_encryptions_differ() {
143 let enc = Encryption::from_password(TEST_PASSWORD, TEST_SALT).unwrap();
144 let e1 = enc.encrypt(b"same").unwrap();
145 let e2 = enc.encrypt(b"same").unwrap();
146 assert_ne!(e1, e2);
148 }
149
150 #[test]
151 fn short_salt_rejected() {
152 let result = Encryption::from_password("pass", b"short");
153 assert!(result.is_err());
154 }
155
156 #[test]
157 fn truncated_ciphertext_rejected() {
158 let result = Encryption::from_raw_key([0u8; 32]).decrypt(&[0u8; 10]);
159 assert!(result.is_err());
160 }
161
162 #[test]
163 fn from_raw_key_works() {
164 let key = [42u8; 32];
165 let enc = Encryption::from_raw_key(key);
166 let encrypted = enc.encrypt(b"test").unwrap();
167 let enc2 = Encryption::from_raw_key(key);
168 let decrypted = enc2.decrypt(&encrypted).unwrap();
169 assert_eq!(&decrypted, b"test");
170 }
171}