secret_manager/
local_encryptor.rs1use crate::encryptor::{Encrypted, EncryptorError, KeyEncryptor};
2use aes_gcm_siv::aead::{Aead, KeyInit};
3use aes_gcm_siv::{Aes256GcmSiv, Nonce};
4use async_trait::async_trait;
5use rand::{Rng, rng};
6
7#[derive(Clone, Copy)]
22pub struct LocalEncryptor {
23 key: [u8; 32],
24 version: u8,
25}
26
27impl LocalEncryptor {
28 pub fn new(key: &[u8; 32], version: u8) -> Self {
33 Self { key: *key, version }
34 }
35
36 fn cipher(&self) -> Aes256GcmSiv {
37 Aes256GcmSiv::new_from_slice(&self.key).expect("key is exactly 32 bytes")
38 }
39}
40
41#[async_trait]
42impl KeyEncryptor for LocalEncryptor {
43 async fn encrypt(&self, plaintext: &[u8]) -> Result<Encrypted, EncryptorError> {
44 let mut nonce_bytes = [0u8; 12];
45 rng().fill_bytes(&mut nonce_bytes);
46 let nonce = Nonce::from(nonce_bytes);
47
48 let ciphertext = self
49 .cipher()
50 .encrypt(&nonce, plaintext)
51 .map_err(|e| EncryptorError::EncryptionFailed(format!("{e:?}")))?;
52
53 Ok(Encrypted {
54 ciphertext,
55 nonce: Some(nonce_bytes),
56 key_version: self.version,
57 })
58 }
59
60 async fn decrypt(&self, encrypted: &Encrypted) -> Result<Vec<u8>, EncryptorError> {
61 let nonce_bytes = encrypted.nonce.ok_or(EncryptorError::MissingNonce)?;
62 let nonce = Nonce::from(nonce_bytes);
63
64 self.cipher()
65 .decrypt(&nonce, encrypted.ciphertext.as_ref())
66 .map_err(|e| EncryptorError::DecryptionFailed(format!("{e:?}")))
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 const TEST_KEY: [u8; 32] = [0x42; 32];
75 const TEST_VERSION: u8 = 1;
76
77 #[tokio::test]
78 async fn test_local_encrypt_decrypt() {
79 let encryptor = LocalEncryptor::new(&TEST_KEY, TEST_VERSION);
80 let plaintext = b"local-secret-key-material";
81
82 let encrypted = encryptor.encrypt(plaintext).await.unwrap();
83 assert_ne!(encrypted.ciphertext, plaintext);
84 assert_eq!(encrypted.key_version, TEST_VERSION);
85 assert!(encrypted.nonce.is_some());
86 assert_eq!(encrypted.nonce.as_ref().unwrap().len(), 12);
87
88 let decrypted = encryptor.decrypt(&encrypted).await.unwrap();
89 assert_eq!(decrypted, plaintext);
90 }
91
92 #[tokio::test]
93 async fn test_local_unique_nonces() {
94 let encryptor = LocalEncryptor::new(&TEST_KEY, TEST_VERSION);
95 let plaintext = b"same-plaintext";
96
97 let encrypted1 = encryptor.encrypt(plaintext).await.unwrap();
98 let encrypted2 = encryptor.encrypt(plaintext).await.unwrap();
99
100 assert_ne!(encrypted1.nonce, encrypted2.nonce);
101 assert_ne!(encrypted1.ciphertext, encrypted2.ciphertext);
102 }
103
104 #[tokio::test]
105 async fn test_local_decrypt_wrong_key() {
106 let encryptor1 = LocalEncryptor::new(&TEST_KEY, TEST_VERSION);
107 let mut wrong_key = TEST_KEY;
108 wrong_key[0] ^= 1;
109 let encryptor2 = LocalEncryptor::new(&wrong_key, TEST_VERSION);
110
111 let plaintext = b"secret-stuff";
112 let encrypted = encryptor1.encrypt(plaintext).await.unwrap();
113
114 let result = encryptor2.decrypt(&encrypted).await;
115 assert!(result.is_err());
116 }
117
118 #[tokio::test]
119 async fn test_local_decrypt_missing_nonce() {
120 let encryptor = LocalEncryptor::new(&TEST_KEY, TEST_VERSION);
121 let mut encrypted = encryptor.encrypt(b"data").await.unwrap();
122 encrypted.nonce = None;
123
124 let result = encryptor.decrypt(&encrypted).await;
125 assert!(result.is_err());
126 assert!(result.unwrap_err().to_string().contains("missing nonce"));
127 }
128
129 #[tokio::test]
130 async fn test_local_decrypt_tampered_ciphertext() {
131 let encryptor = LocalEncryptor::new(&TEST_KEY, TEST_VERSION);
132 let mut encrypted = encryptor.encrypt(b"sensitive-data").await.unwrap();
133 encrypted.ciphertext[0] ^= 1;
134
135 let result = encryptor.decrypt(&encrypted).await;
136 assert!(result.is_err());
137 }
138}