haste_encryption/encryption/
aes.rs1use 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
14pub 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}