haste_encryption/encryption/
aes.rs1use 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
13pub struct AesGcmEncryptor {
16 cipher: Aes256Gcm,
17}
18
19impl AesGcmEncryptor {
20 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}