rskit_encryption/
aes_gcm.rs1use aes_gcm::{
4 Aes256Gcm, Nonce,
5 aead::{Aead, KeyInit, Payload},
6};
7use rskit_errors::{AppError, AppResult, ErrorCode};
8use sha2::Sha256;
9use zeroize::Zeroize;
10
11use crate::envelope::{Envelope, KEY_LEN, NONCE_SIZE, PBKDF2_ITERATIONS, SALT_SIZE};
12use crate::traits::{Algorithm, Encryptor};
13
14pub struct AesGcmEncryptor {
19 passphrase: Vec<u8>,
20}
21
22impl AesGcmEncryptor {
23 pub fn new(key: &[u8]) -> Self {
27 Self {
28 passphrase: key.to_vec(),
29 }
30 }
31
32 fn derive_key(&self, salt: &[u8]) -> [u8; KEY_LEN] {
33 let mut key = [0u8; KEY_LEN];
34 pbkdf2::pbkdf2_hmac::<Sha256>(&self.passphrase, salt, PBKDF2_ITERATIONS, &mut key);
35 key
36 }
37}
38
39impl Drop for AesGcmEncryptor {
40 fn drop(&mut self) {
41 self.passphrase.zeroize();
42 }
43}
44
45impl Encryptor for AesGcmEncryptor {
46 fn encrypt(&self, plaintext: &[u8]) -> AppResult<String> {
47 let salt: [u8; SALT_SIZE] = rand::random();
48
49 let mut key_bytes = self.derive_key(&salt);
50 let cipher = Aes256Gcm::new((&key_bytes[..]).into());
51 key_bytes.zeroize();
52
53 let nonce_bytes: [u8; NONCE_SIZE] = rand::random();
54 let nonce = Nonce::from_slice(&nonce_bytes);
55 let aad = crate::envelope::associated_data(Algorithm::AesGcm, &salt, &nonce_bytes);
56
57 let ciphertext = cipher
58 .encrypt(
59 nonce,
60 Payload {
61 msg: plaintext,
62 aad: &aad,
63 },
64 )
65 .map_err(|_| AppError::new(ErrorCode::Internal, "encryption failed"))?;
66
67 Ok(Envelope::encode(
68 Algorithm::AesGcm,
69 &salt,
70 &nonce_bytes,
71 &ciphertext,
72 ))
73 }
74
75 fn decrypt(&self, ciphertext: &str) -> AppResult<Vec<u8>> {
76 let envelope = Envelope::decode(ciphertext)?;
77 if envelope.algorithm()? != Algorithm::AesGcm {
78 return Err(AppError::new(
79 ErrorCode::InvalidFormat,
80 "ciphertext algorithm does not match encryptor",
81 ));
82 }
83
84 let mut key_bytes = self.derive_key(envelope.salt());
85 let cipher = Aes256Gcm::new((&key_bytes[..]).into());
86 key_bytes.zeroize();
87
88 let nonce = Nonce::from_slice(envelope.nonce());
89
90 cipher
91 .decrypt(
92 nonce,
93 Payload {
94 msg: envelope.ciphertext(),
95 aad: envelope.associated_data(),
96 },
97 )
98 .map_err(|_| {
99 AppError::new(ErrorCode::InvalidFormat, "ciphertext authentication failed")
100 })
101 }
102
103 fn algorithm(&self) -> Algorithm {
104 Algorithm::AesGcm
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn test_encrypt_decrypt_roundtrip() {
114 let encryptor = AesGcmEncryptor::new(b"my-secret-key");
115 let plaintext = b"Hello, World!";
116
117 let ciphertext = encryptor.encrypt(plaintext).unwrap();
118 let decrypted = encryptor.decrypt(&ciphertext).unwrap();
119
120 assert_eq!(decrypted, plaintext);
121 }
122
123 #[test]
124 fn test_encrypt_produces_different_ciphertext() {
125 let encryptor = AesGcmEncryptor::new(b"my-secret-key");
126 let plaintext = b"Same plaintext";
127
128 let ct1 = encryptor.encrypt(plaintext).unwrap();
129 let ct2 = encryptor.encrypt(plaintext).unwrap();
130
131 assert_ne!(ct1, ct2);
132 }
133
134 #[test]
135 fn test_decrypt_with_wrong_key_fails() {
136 let encryptor1 = AesGcmEncryptor::new(b"key-1");
137 let encryptor2 = AesGcmEncryptor::new(b"key-2");
138
139 let plaintext = b"Secret data";
140 let ciphertext = encryptor1.encrypt(plaintext).unwrap();
141
142 let result = encryptor2.decrypt(&ciphertext);
143 assert!(result.is_err());
144 }
145
146 #[test]
147 fn test_encrypt_empty_plaintext() {
148 let encryptor = AesGcmEncryptor::new(b"my-secret-key");
149 let plaintext = b"";
150
151 let ciphertext = encryptor.encrypt(plaintext).unwrap();
152 let decrypted = encryptor.decrypt(&ciphertext).unwrap();
153
154 assert_eq!(decrypted, plaintext);
155 }
156
157 #[test]
158 fn test_algorithm() {
159 let encryptor = AesGcmEncryptor::new(b"my-secret-key");
160 assert_eq!(encryptor.algorithm(), Algorithm::AesGcm);
161 }
162
163 #[test]
164 fn test_invalid_ciphertext() {
165 let encryptor = AesGcmEncryptor::new(b"my-secret-key");
166 let result = encryptor.decrypt("invalid-base64!@#$");
167 assert!(result.is_err());
168 }
169
170 #[test]
171 fn test_rejects_ciphertext_for_other_algorithm() {
172 let encryptor = AesGcmEncryptor::new(b"my-secret-key");
173 let other = crate::ChaCha20Encryptor::new(b"my-secret-key");
174 let ciphertext = other.encrypt(b"secret").unwrap();
175
176 let err = encryptor.decrypt(&ciphertext).unwrap_err();
177 assert_eq!(err.code(), ErrorCode::InvalidFormat);
178 }
179
180 #[test]
181 fn test_corrupted_ciphertext() {
182 let encryptor = AesGcmEncryptor::new(b"my-secret-key");
183 let plaintext = b"Original";
184 let ciphertext = encryptor.encrypt(plaintext).unwrap();
185
186 let mut corrupted = ciphertext.clone();
187 let chars: Vec<char> = corrupted.chars().collect();
188 if chars.len() > 20 {
189 let mut new_chars = chars.clone();
190 new_chars[20] = if chars[20] == 'A' { 'B' } else { 'A' };
191 corrupted = new_chars.into_iter().collect();
192 }
193
194 let result = encryptor.decrypt(&corrupted);
195 assert!(result.is_err());
196 }
197}