Skip to main content

secret_manager/
local_encryptor.rs

1use 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/// [`KeyEncryptor`] that encrypts key material locally using AES-256-GCM-SIV.
8///
9/// A random 12-byte nonce is generated for every [`encrypt`](KeyEncryptor::encrypt) call and
10/// stored in [`Encrypted::nonce`].  This is required for [`decrypt`](KeyEncryptor::decrypt) —
11/// ciphertexts produced by other encryptors (e.g. KMS) that carry `nonce: None` will fail.
12///
13/// `LocalEncryptor` is a good fit when you want at-rest encryption without a network round-trip
14/// and your key-encryption key is already available locally (e.g. loaded from a secrets manager
15/// at startup or injected via an environment variable).
16///
17/// # Key versioning
18///
19/// `version` is stored in [`Encrypted::key_version`].  When you rotate the key-encryption key,
20/// bump `version` and keep the old `LocalEncryptor` around for decryption of existing records.
21#[derive(Clone, Copy)]
22pub struct LocalEncryptor {
23    key: [u8; 32],
24    version: u8,
25}
26
27impl LocalEncryptor {
28    /// Create a new `LocalEncryptor`.
29    ///
30    /// - `key` — 32-byte key-encryption key (AES-256)
31    /// - `version` — stored in [`Encrypted::key_version`]; increment when you rotate the KEK
32    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}