1use aes_gcm::aead::{Aead, KeyInit, Payload};
31use aes_gcm::{Aes256Gcm, Key, Nonce};
32use rand::rngs::OsRng;
33use rand::RngCore;
34use serde::{Deserialize, Serialize};
35use thiserror::Error;
36use zeroize::Zeroizing;
37
38use crate::error::StoreError;
39
40pub const KEY_LEN: usize = 32;
42pub const NONCE_LEN: usize = 12;
44pub const TAG_LEN: usize = 16;
46
47#[derive(Debug, Error)]
48pub enum CryptoError {
49 #[error("vault is cold; unlock required")]
52 Cold,
53 #[error("AEAD failure: {0}")]
57 Aead(String),
58 #[error("unwrapped DEK has wrong length: expected {KEY_LEN}, got {0}")]
61 BadDekLength(usize),
62 #[error("sealed row too short: {0} bytes")]
64 ShortRow(usize),
65}
66
67impl From<CryptoError> for StoreError {
68 fn from(e: CryptoError) -> Self {
69 match e {
70 CryptoError::Cold => StoreError::Cold,
71 other => StoreError::Crypto(other.to_string()),
72 }
73 }
74}
75
76pub trait KeyProvider: Send + Sync {
83 fn vault_key(&self) -> Result<Zeroizing<[u8; KEY_LEN]>, CryptoError>;
85}
86
87#[derive(Clone)]
91pub struct StaticKeyProvider {
92 key: Option<[u8; KEY_LEN]>,
93}
94
95impl StaticKeyProvider {
96 pub fn new(key: [u8; KEY_LEN]) -> Self {
98 Self { key: Some(key) }
99 }
100
101 pub fn cold() -> Self {
103 Self { key: None }
104 }
105}
106
107impl KeyProvider for StaticKeyProvider {
108 fn vault_key(&self) -> Result<Zeroizing<[u8; KEY_LEN]>, CryptoError> {
109 self.key.map(Zeroizing::new).ok_or(CryptoError::Cold)
110 }
111}
112
113#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
117pub struct WrappedDek {
118 pub wrap_nonce: [u8; NONCE_LEN],
120 pub wrap_ciphertext: Vec<u8>,
123}
124
125fn cipher_for(key: &[u8; KEY_LEN]) -> Aes256Gcm {
126 Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key.as_slice()))
127}
128
129pub fn wrap_fresh_dek(
133 vault_key: &[u8; KEY_LEN],
134 collection_id: &str,
135) -> Result<(WrappedDek, Zeroizing<[u8; KEY_LEN]>), CryptoError> {
136 let mut dek = Zeroizing::new([0u8; KEY_LEN]);
137 OsRng.fill_bytes(dek.as_mut());
138
139 let mut nonce = [0u8; NONCE_LEN];
140 OsRng.fill_bytes(&mut nonce);
141
142 let ciphertext = cipher_for(vault_key)
143 .encrypt(
144 Nonce::from_slice(&nonce),
145 Payload {
146 msg: dek.as_slice(),
147 aad: collection_id.as_bytes(),
148 },
149 )
150 .map_err(|e| CryptoError::Aead(format!("wrap: {e}")))?;
151
152 Ok((
153 WrappedDek {
154 wrap_nonce: nonce,
155 wrap_ciphertext: ciphertext,
156 },
157 dek,
158 ))
159}
160
161pub fn unwrap_dek(
165 vault_key: &[u8; KEY_LEN],
166 collection_id: &str,
167 wrapped: &WrappedDek,
168) -> Result<Zeroizing<[u8; KEY_LEN]>, CryptoError> {
169 let plaintext = Zeroizing::new(
170 cipher_for(vault_key)
171 .decrypt(
172 Nonce::from_slice(&wrapped.wrap_nonce),
173 Payload {
174 msg: &wrapped.wrap_ciphertext,
175 aad: collection_id.as_bytes(),
176 },
177 )
178 .map_err(|e| CryptoError::Aead(format!("unwrap: {e}")))?,
179 );
180 if plaintext.len() != KEY_LEN {
181 return Err(CryptoError::BadDekLength(plaintext.len()));
182 }
183 let mut dek = Zeroizing::new([0u8; KEY_LEN]);
184 dek.copy_from_slice(&plaintext);
185 Ok(dek)
186}
187
188pub fn rewrap_dek(
193 old_vault_key: &[u8; KEY_LEN],
194 new_vault_key: &[u8; KEY_LEN],
195 collection_id: &str,
196 wrapped: &WrappedDek,
197) -> Result<WrappedDek, CryptoError> {
198 let dek = unwrap_dek(old_vault_key, collection_id, wrapped)?;
199
200 let mut nonce = [0u8; NONCE_LEN];
201 OsRng.fill_bytes(&mut nonce);
202 let ciphertext = cipher_for(new_vault_key)
203 .encrypt(
204 Nonce::from_slice(&nonce),
205 Payload {
206 msg: dek.as_slice(),
207 aad: collection_id.as_bytes(),
208 },
209 )
210 .map_err(|e| CryptoError::Aead(format!("rewrap: {e}")))?;
211
212 Ok(WrappedDek {
213 wrap_nonce: nonce,
214 wrap_ciphertext: ciphertext,
215 })
216}
217
218fn row_aad(table: &str, key: &[u8], schema_version: u32) -> Vec<u8> {
223 let mut aad = Vec::with_capacity(4 + table.len() + 4 + key.len() + 4);
224 aad.extend_from_slice(&(table.len() as u32).to_be_bytes());
225 aad.extend_from_slice(table.as_bytes());
226 aad.extend_from_slice(&(key.len() as u32).to_be_bytes());
227 aad.extend_from_slice(key);
228 aad.extend_from_slice(&schema_version.to_be_bytes());
229 aad
230}
231
232pub fn seal_row(
235 dek: &[u8; KEY_LEN],
236 table: &str,
237 key: &[u8],
238 schema_version: u32,
239 plaintext: &[u8],
240) -> Result<Vec<u8>, CryptoError> {
241 let mut nonce = [0u8; NONCE_LEN];
242 OsRng.fill_bytes(&mut nonce);
243 let aad = row_aad(table, key, schema_version);
244 let ciphertext = cipher_for(dek)
245 .encrypt(
246 Nonce::from_slice(&nonce),
247 Payload {
248 msg: plaintext,
249 aad: &aad,
250 },
251 )
252 .map_err(|e| CryptoError::Aead(format!("seal: {e}")))?;
253 let mut out = Vec::with_capacity(NONCE_LEN + ciphertext.len());
254 out.extend_from_slice(&nonce);
255 out.extend_from_slice(&ciphertext);
256 Ok(out)
257}
258
259pub fn open_row(
263 dek: &[u8; KEY_LEN],
264 table: &str,
265 key: &[u8],
266 schema_version: u32,
267 sealed: &[u8],
268) -> Result<Vec<u8>, CryptoError> {
269 if sealed.len() < NONCE_LEN {
270 return Err(CryptoError::ShortRow(sealed.len()));
271 }
272 let (nonce, ciphertext) = sealed.split_at(NONCE_LEN);
273 let aad = row_aad(table, key, schema_version);
274 cipher_for(dek)
275 .decrypt(
276 Nonce::from_slice(nonce),
277 Payload {
278 msg: ciphertext,
279 aad: &aad,
280 },
281 )
282 .map_err(|e| CryptoError::Aead(format!("open: {e}")))
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 const VK_A: [u8; KEY_LEN] = [0xAA; KEY_LEN];
290 const VK_B: [u8; KEY_LEN] = [0xBB; KEY_LEN];
291
292 #[test]
295 fn wrap_then_unwrap_yields_same_dek() {
296 let (w, dek) = wrap_fresh_dek(&VK_A, "people").unwrap();
297 let dek2 = unwrap_dek(&VK_A, "people", &w).unwrap();
298 assert_eq!(dek.as_slice(), dek2.as_slice());
299 }
300
301 #[test]
302 fn fresh_dek_is_not_all_zero() {
303 let (_w, dek) = wrap_fresh_dek(&VK_A, "c").unwrap();
304 assert!(dek.iter().any(|b| *b != 0));
305 }
306
307 #[test]
308 fn different_collections_get_different_deks() {
309 let (_w1, d1) = wrap_fresh_dek(&VK_A, "c1").unwrap();
310 let (_w2, d2) = wrap_fresh_dek(&VK_A, "c2").unwrap();
311 assert_ne!(d1.as_slice(), d2.as_slice());
312 }
313
314 #[test]
315 fn unwrap_with_wrong_vault_key_fails() {
316 let (w, _dek) = wrap_fresh_dek(&VK_A, "c").unwrap();
317 let err = unwrap_dek(&VK_B, "c", &w).unwrap_err();
318 assert!(matches!(err, CryptoError::Aead(_)));
319 }
320
321 #[test]
322 fn unwrap_with_wrong_collection_id_fails_on_aad() {
323 let (w, _dek) = wrap_fresh_dek(&VK_A, "people").unwrap();
324 let err = unwrap_dek(&VK_A, "passwords", &w).unwrap_err();
325 assert!(matches!(err, CryptoError::Aead(_)));
326 }
327
328 #[test]
329 fn tampered_wrap_ciphertext_fails() {
330 let (mut w, _dek) = wrap_fresh_dek(&VK_A, "c").unwrap();
331 w.wrap_ciphertext[0] ^= 0x01;
332 let err = unwrap_dek(&VK_A, "c", &w).unwrap_err();
333 assert!(matches!(err, CryptoError::Aead(_)));
334 }
335
336 #[test]
337 fn rewrap_rotates_vault_key_without_changing_the_dek() {
338 let (w_a, dek_a) = wrap_fresh_dek(&VK_A, "c").unwrap();
339 let w_b = rewrap_dek(&VK_A, &VK_B, "c", &w_a).unwrap();
340 assert!(unwrap_dek(&VK_A, "c", &w_b).is_err());
342 let dek_b = unwrap_dek(&VK_B, "c", &w_b).unwrap();
344 assert_eq!(dek_a.as_slice(), dek_b.as_slice());
345 }
346
347 #[test]
350 fn seal_then_open_round_trips() {
351 let dek = [0x11; KEY_LEN];
352 let sealed = seal_row(&dek, "people", b"alice", 1, b"payload").unwrap();
353 assert!(!sealed
354 .windows(b"payload".len())
355 .any(|w| w == b"payload"), "engine bytes must be ciphertext");
356 let opened = open_row(&dek, "people", b"alice", 1, &sealed).unwrap();
357 assert_eq!(opened, b"payload");
358 }
359
360 #[test]
361 fn row_with_wrong_dek_fails() {
362 let sealed = seal_row(&[0x11; KEY_LEN], "t", b"k", 1, b"v").unwrap();
363 let err = open_row(&[0x22; KEY_LEN], "t", b"k", 1, &sealed).unwrap_err();
364 assert!(matches!(err, CryptoError::Aead(_)));
365 }
366
367 #[test]
368 fn row_relocated_to_different_key_fails_on_aad() {
369 let dek = [0x11; KEY_LEN];
370 let sealed = seal_row(&dek, "t", b"key1", 1, b"v").unwrap();
371 assert!(open_row(&dek, "t", b"key2", 1, &sealed).is_err());
372 }
373
374 #[test]
375 fn row_relocated_to_different_table_fails_on_aad() {
376 let dek = [0x11; KEY_LEN];
377 let sealed = seal_row(&dek, "table_a", b"k", 1, b"v").unwrap();
378 assert!(open_row(&dek, "table_b", b"k", 1, &sealed).is_err());
379 }
380
381 #[test]
382 fn row_with_wrong_schema_version_fails_on_aad() {
383 let dek = [0x11; KEY_LEN];
384 let sealed = seal_row(&dek, "t", b"k", 1, b"v").unwrap();
385 assert!(open_row(&dek, "t", b"k", 2, &sealed).is_err());
386 }
387
388 #[test]
389 fn tampered_row_fails() {
390 let dek = [0x11; KEY_LEN];
391 let mut sealed = seal_row(&dek, "t", b"k", 1, b"v").unwrap();
392 let last = sealed.len() - 1;
393 sealed[last] ^= 0x01; assert!(open_row(&dek, "t", b"k", 1, &sealed).is_err());
395 }
396
397 #[test]
398 fn open_short_row_is_typed_error() {
399 let err = open_row(&[0; KEY_LEN], "t", b"k", 1, &[0u8; 4]).unwrap_err();
400 assert!(matches!(err, CryptoError::ShortRow(4)));
401 }
402
403 #[test]
406 fn static_provider_warm_and_cold() {
407 assert_eq!(StaticKeyProvider::new(VK_A).vault_key().unwrap().as_slice(), &VK_A);
408 assert!(matches!(
409 StaticKeyProvider::cold().vault_key().unwrap_err(),
410 CryptoError::Cold
411 ));
412 }
413}