Skip to main content

rskit_encryption/
aes_gcm.rs

1//! AES-256-GCM encryption implementation.
2
3use 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
14/// AES-256-GCM encryptor.
15///
16/// Uses PBKDF2-SHA256 for key derivation with a random 16-byte salt per encryption. Output format:
17/// `base64(version[1] || algorithm[1] || salt[16] || nonce[12] || ciphertext)`.
18pub struct AesGcmEncryptor {
19    passphrase: Vec<u8>,
20}
21
22impl AesGcmEncryptor {
23    /// Creates a new AES-256-GCM encryptor.
24    ///
25    /// The passphrase is stored and used with PBKDF2-SHA256 to derive keys per operation.
26    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}