1use aes_gcm::Aes256Gcm;
2use aes_gcm::aead::array::Array;
3use aes_gcm::aead::{Aead as _, Generate, KeyInit, Nonce};
4use sha2::{Digest, Sha256};
5use thiserror::Error;
6
7#[derive(Debug, Error)]
8pub enum CryptoError {
9 #[error("encryption failed")]
10 Seal,
11 #[error("decryption failed (tampered or wrong key)")]
12 Open,
13}
14
15pub trait Aead: Send + Sync {
16 fn seal(&self, plaintext: &[u8]) -> Result<Vec<u8>, CryptoError>;
17 fn open(&self, ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError>;
18
19 fn is_current(&self, _blob: &[u8]) -> bool {
23 true
24 }
25
26 fn active_key_id(&self) -> String {
29 "unversioned".to_string()
30 }
31}
32
33const VERSIONED_MARKER: u8 = 0x01;
38const KEY_ID_LEN: usize = 4;
39const NONCE_LEN: usize = 12;
40
41pub fn key_id(key: &[u8; 32]) -> u32 {
45 let digest = Sha256::digest(key);
46 u32::from_be_bytes(digest[..KEY_ID_LEN].try_into().expect("sha256 is 32 bytes"))
47}
48
49struct Key {
50 id: u32,
51 cipher: Aes256Gcm,
52}
53
54pub struct KeyRing {
58 active: Key,
59 retired: Vec<Key>,
60}
61
62impl KeyRing {
63 pub fn new(active: &[u8; 32], retired: &[[u8; 32]]) -> Self {
64 Self {
65 active: Key {
66 id: key_id(active),
67 cipher: Aes256Gcm::new(active.into()),
68 },
69 retired: retired
70 .iter()
71 .map(|key| Key {
72 id: key_id(key),
73 cipher: Aes256Gcm::new(key.into()),
74 })
75 .collect(),
76 }
77 }
78
79 pub fn active_key_id(&self) -> u32 {
80 self.active.id
81 }
82
83 pub fn retired_key_count(&self) -> usize {
86 self.retired.len()
87 }
88
89 fn embedded_key_id(blob: &[u8]) -> Option<u32> {
91 if blob.len() < 1 + KEY_ID_LEN + NONCE_LEN || blob[0] != VERSIONED_MARKER {
92 return None;
93 }
94 Some(u32::from_be_bytes(
95 blob[1..1 + KEY_ID_LEN].try_into().expect("checked length"),
96 ))
97 }
98
99 fn keys(&self) -> impl Iterator<Item = &Key> {
100 std::iter::once(&self.active).chain(self.retired.iter())
101 }
102}
103
104impl Aead for KeyRing {
105 fn seal(&self, plaintext: &[u8]) -> Result<Vec<u8>, CryptoError> {
106 let nonce = Nonce::<Aes256Gcm>::generate();
107 let ciphertext = self
108 .active
109 .cipher
110 .encrypt(&nonce, plaintext)
111 .map_err(|_| CryptoError::Seal)?;
112
113 let mut out = Vec::with_capacity(1 + KEY_ID_LEN + nonce.len() + ciphertext.len());
114 out.push(VERSIONED_MARKER);
115 out.extend_from_slice(&self.active.id.to_be_bytes());
116 out.extend_from_slice(&nonce);
117 out.extend_from_slice(&ciphertext);
118 Ok(out)
119 }
120
121 fn open(&self, blob: &[u8]) -> Result<Vec<u8>, CryptoError> {
122 if let Some(id) = Self::embedded_key_id(blob)
125 && let Some(key) = self.keys().find(|key| key.id == id)
126 {
127 let (nonce, ciphertext) = blob[1 + KEY_ID_LEN..].split_at(NONCE_LEN);
128 let nonce = Array::try_from(nonce).map_err(|_| CryptoError::Open)?;
129 if let Ok(plaintext) = key.cipher.decrypt(&nonce, ciphertext) {
130 return Ok(plaintext);
131 }
132 }
133
134 if blob.len() >= NONCE_LEN {
138 let (nonce, ciphertext) = blob.split_at(NONCE_LEN);
139 if let Ok(nonce) = Array::try_from(nonce) {
140 for key in self.keys() {
141 if let Ok(plaintext) = key.cipher.decrypt(&nonce, ciphertext) {
142 return Ok(plaintext);
143 }
144 }
145 }
146 }
147 Err(CryptoError::Open)
148 }
149
150 fn is_current(&self, blob: &[u8]) -> bool {
151 Self::embedded_key_id(blob) == Some(self.active.id)
152 }
153
154 fn active_key_id(&self) -> String {
155 format!("{:08x}", self.active.id)
156 }
157}
158
159pub struct Aes256GcmAead {
164 cipher: Aes256Gcm,
165}
166
167impl Aes256GcmAead {
168 pub fn new(key: &[u8; 32]) -> Self {
169 Self {
170 cipher: Aes256Gcm::new(key.into()),
171 }
172 }
173}
174
175impl Aead for Aes256GcmAead {
176 fn seal(&self, plaintext: &[u8]) -> Result<Vec<u8>, CryptoError> {
177 let nonce = Nonce::<Aes256Gcm>::generate();
178 let ciphertext = self
179 .cipher
180 .encrypt(&nonce, plaintext)
181 .map_err(|_| CryptoError::Seal)?;
182 let mut out = Vec::with_capacity(nonce.len() + ciphertext.len());
183 out.extend_from_slice(&nonce);
184 out.extend_from_slice(&ciphertext);
185 Ok(out)
186 }
187
188 fn open(&self, blob: &[u8]) -> Result<Vec<u8>, CryptoError> {
189 if blob.len() < 12 {
190 return Err(CryptoError::Open);
191 }
192 let (nonce, ciphertext) = blob.split_at(12);
193 let nonce = Array::try_from(nonce).map_err(|_| CryptoError::Open)?;
194 self.cipher
195 .decrypt(&nonce, ciphertext)
196 .map_err(|_| CryptoError::Open)
197 }
198}
199
200pub trait MasterKeyProvider: Send + Sync {
201 fn current_key(&self) -> [u8; 32];
202
203 fn retired_keys(&self) -> Vec<[u8; 32]> {
206 Vec::new()
207 }
208}
209
210pub struct StaticMasterKeyProvider {
214 key: [u8; 32],
215 retired: Vec<[u8; 32]>,
216}
217
218impl StaticMasterKeyProvider {
219 pub fn from_hex(hex_key: &str) -> Result<Self, CryptoError> {
220 Ok(Self {
221 key: decode_key(hex_key)?,
222 retired: Vec::new(),
223 })
224 }
225
226 pub fn from_env(var: &str) -> Result<Self, CryptoError> {
227 Self::from_hex(&read_env_or_file(var)?)
228 }
229
230 pub fn with_retired_from_env(mut self, var: &str) -> Result<Self, CryptoError> {
233 let Ok(raw) = read_env_or_file(var) else {
234 return Ok(self);
235 };
236 for candidate in raw.split(',').map(str::trim).filter(|c| !c.is_empty()) {
237 self.retired.push(decode_key(candidate)?);
238 }
239 Ok(self)
240 }
241}
242
243fn decode_key(hex_key: &str) -> Result<[u8; 32], CryptoError> {
244 let bytes = hex::decode(hex_key.trim()).map_err(|_| CryptoError::Seal)?;
245 bytes.try_into().map_err(|_| CryptoError::Seal)
246}
247
248fn read_env_or_file(var: &str) -> Result<String, CryptoError> {
249 let value = std::env::var(var).map_err(|_| CryptoError::Seal)?;
250 Ok(std::fs::read_to_string(&value).unwrap_or(value))
252}
253
254impl MasterKeyProvider for StaticMasterKeyProvider {
255 fn current_key(&self) -> [u8; 32] {
256 self.key
257 }
258
259 fn retired_keys(&self) -> Vec<[u8; 32]> {
260 self.retired.clone()
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 fn aead() -> Aes256GcmAead {
269 Aes256GcmAead::new(&[7u8; 32])
270 }
271
272 #[test]
273 fn round_trip() {
274 let aead = aead();
275 let plaintext = b"super secret value";
276 let sealed = aead.seal(plaintext).unwrap();
277 assert_eq!(aead.open(&sealed).unwrap(), plaintext);
278 }
279
280 #[test]
281 fn tamper_detection() {
282 let aead = aead();
283 let mut sealed = aead.seal(b"super secret value").unwrap();
284 let last = sealed.len() - 1;
285 sealed[last] ^= 0xFF;
286 assert!(aead.open(&sealed).is_err());
287 }
288
289 #[test]
290 fn wrong_key_fails() {
291 let sealed = aead().seal(b"super secret value").unwrap();
292 let other = Aes256GcmAead::new(&[9u8; 32]);
293 assert!(other.open(&sealed).is_err());
294 }
295
296 const OLD: [u8; 32] = [7u8; 32];
297 const NEW: [u8; 32] = [11u8; 32];
298
299 #[test]
300 fn key_ids_are_derived_and_distinct() {
301 assert_eq!(key_id(&OLD), key_id(&OLD));
302 assert_ne!(key_id(&OLD), key_id(&NEW));
303 }
304
305 #[test]
306 fn keyring_round_trip_and_marks_its_own_output_current() {
307 let ring = KeyRing::new(&NEW, &[]);
308 let sealed = ring.seal(b"hunter2").unwrap();
309 assert_eq!(ring.open(&sealed).unwrap(), b"hunter2");
310 assert!(ring.is_current(&sealed));
311 assert_eq!(ring.active_key_id(), key_id(&NEW));
312 }
313
314 #[test]
317 fn keyring_reads_pre_rotation_values() {
318 let legacy = Aes256GcmAead::new(&OLD).seal(b"written in 0.1").unwrap();
319 let ring = KeyRing::new(&OLD, &[]);
320 assert_eq!(ring.open(&legacy).unwrap(), b"written in 0.1");
321 assert!(!ring.is_current(&legacy));
323 }
324
325 #[test]
327 fn keyring_opens_values_sealed_by_a_retired_key() {
328 let before = KeyRing::new(&OLD, &[]);
329 let sealed = before.seal(b"sealed under the old key").unwrap();
330
331 let rotating = KeyRing::new(&NEW, &[OLD]);
332 assert_eq!(rotating.open(&sealed).unwrap(), b"sealed under the old key");
333 assert!(
334 !rotating.is_current(&sealed),
335 "a value on the retired key must be reported as needing rewrap"
336 );
337 assert_eq!(rotating.retired_key_count(), 1);
338 }
339
340 #[test]
343 fn keyring_refuses_a_value_whose_key_is_gone() {
344 let sealed = KeyRing::new(&OLD, &[]).seal(b"orphaned").unwrap();
345 assert!(KeyRing::new(&NEW, &[]).open(&sealed).is_err());
346 }
347
348 #[test]
349 fn keyring_detects_tampering() {
350 let ring = KeyRing::new(&NEW, &[]);
351 let mut sealed = ring.seal(b"hunter2").unwrap();
352 let last = sealed.len() - 1;
353 sealed[last] ^= 0xFF;
354 assert!(ring.open(&sealed).is_err());
355 }
356
357 #[test]
359 fn keyring_rejects_short_blobs_without_panicking() {
360 let ring = KeyRing::new(&NEW, &[]);
361 for len in 0..20 {
362 assert!(ring.open(&vec![VERSIONED_MARKER; len]).is_err());
363 }
364 }
365}