Skip to main content

haste_encryption/encryption/
aes.rs

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