Skip to main content

haste_encryption/encryption/
aes.rs

1use crate::{
2    error::EncryptionError,
3    traits::{EncryptionResult, Encryptor},
4};
5use aes_gcm::{
6    Aes256Gcm, Key,
7    aead::{Aead, Generate, KeyInit, Nonce},
8};
9use haste_fhir_operation_error::OperationOutcomeError;
10
11const KEY_LEN: usize = 32;
12
13/// AES-256-GCM encryption. Output is `nonce || ciphertext`, with a fresh
14/// random nonce generated for every `encrypt` call.
15pub struct AesGcmEncryptor {
16    cipher: Aes256Gcm,
17}
18
19impl AesGcmEncryptor {
20    /// Creates a new AES-256-GCM cipher from the provided key.
21    ///
22    /// # Errors
23    ///
24    /// Returns [`OperationOutcomeError`] if `key` is not exactly `KEY_LEN` bytes
25    /// long.
26    pub fn new(key: &[u8]) -> Result<Self, OperationOutcomeError> {
27        let key_array = Key::<Aes256Gcm>::try_from(key)
28            .map_err(|_| EncryptionError::InvalidKeyLength(KEY_LEN, key.len()))?;
29
30        Ok(Self {
31            cipher: Aes256Gcm::new(&key_array),
32        })
33    }
34}
35
36impl Encryptor for AesGcmEncryptor {
37    fn encrypt(&self, plaintext: &[u8]) -> Result<EncryptionResult, OperationOutcomeError> {
38        let nonce = Nonce::<Aes256Gcm>::generate();
39
40        let ciphertext = self
41            .cipher
42            .encrypt(&nonce, plaintext)
43            .map_err(|e| EncryptionError::EncryptionFailed(e.to_string()))?;
44
45        Ok(EncryptionResult {
46            nonce: nonce.to_vec(),
47            ciphertext,
48        })
49    }
50
51    fn decrypt(
52        &self,
53        encyrpted_result: &EncryptionResult,
54    ) -> Result<Vec<u8>, OperationOutcomeError> {
55        let nonce = Nonce::<Aes256Gcm>::try_from(encyrpted_result.nonce.as_slice())
56            .map_err(|e| EncryptionError::DecryptionFailed(e.to_string()))?;
57
58        let plaintext = self
59            .cipher
60            .decrypt(&nonce, encyrpted_result.ciphertext.as_slice())
61            .map_err(|e| EncryptionError::DecryptionFailed(e.to_string()))?;
62
63        Ok(plaintext)
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn round_trips_plaintext() {
73        let encryptor = AesGcmEncryptor::new(&[7u8; KEY_LEN]).unwrap();
74        let plaintext = b"totp-secret-material";
75
76        let ciphertext = encryptor.encrypt(plaintext).unwrap();
77        assert_ne!(ciphertext.ciphertext, plaintext);
78
79        let decrypted = encryptor.decrypt(&ciphertext).unwrap();
80        assert_eq!(decrypted, plaintext);
81    }
82
83    #[test]
84    fn rejects_wrong_key_length() {
85        assert!(AesGcmEncryptor::new(&[0u8; 16]).is_err());
86    }
87
88    #[test]
89    fn rejects_tampered_ciphertext() {
90        let encryptor = AesGcmEncryptor::new(&[7u8; KEY_LEN]).unwrap();
91        let mut result = encryptor.encrypt(b"totp-secret-material").unwrap();
92
93        let last = result.ciphertext.len() - 1;
94        result.ciphertext[last] ^= 0xFF;
95
96        assert!(encryptor.decrypt(&result).is_err());
97    }
98}