1use crate::error::Result;
14#[cfg(feature = "encryption")]
16use crate::error::MongrelError;
17#[cfg(feature = "encryption")]
18use zeroize::Zeroizing;
19
20pub const DEK_LEN: usize = 32;
22
23pub fn fill_random(buf: &mut [u8]) {
25 getrandom::getrandom(buf).expect("getrandom: OS CSPRNG unavailable");
26}
27
28pub trait Cipher: Send + Sync {
30 fn encrypt_page(&self, nonce: &[u8; 12], plaintext: &[u8]) -> Result<Vec<u8>>;
32
33 fn decrypt_page(&self, nonce: &[u8; 12], ciphertext: &[u8]) -> Result<Vec<u8>>;
35}
36
37#[derive(Debug, Default, Clone, Copy)]
39pub struct PlaintextCipher;
40
41impl Cipher for PlaintextCipher {
42 fn encrypt_page(&self, _nonce: &[u8; 12], plaintext: &[u8]) -> Result<Vec<u8>> {
43 Ok(plaintext.to_vec())
44 }
45
46 fn decrypt_page(&self, _nonce: &[u8; 12], ciphertext: &[u8]) -> Result<Vec<u8>> {
47 Ok(ciphertext.to_vec())
48 }
49}
50
51#[cfg(feature = "encryption")]
52mod aes {
53 use super::{Cipher, MongrelError, Result};
54 use aes_gcm::aead::Aead;
55 use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
56
57 pub struct AesCipher {
60 cipher: Aes256Gcm,
61 }
62
63 impl AesCipher {
64 pub fn new(key: &[u8]) -> Result<Self> {
66 if key.len() != 32 {
67 return Err(MongrelError::InvalidArgument(format!(
68 "aes-256 key must be 32 bytes, got {}",
69 key.len()
70 )));
71 }
72 Ok(Self {
73 cipher: Aes256Gcm::new_from_slice(key)
74 .map_err(|e| MongrelError::Encryption(format!("aes key init: {e}")))?,
75 })
76 }
77 }
78
79 impl Cipher for AesCipher {
80 fn encrypt_page(&self, nonce: &[u8; 12], plaintext: &[u8]) -> Result<Vec<u8>> {
81 let nonce = Nonce::from_slice(nonce);
82 self.cipher
83 .encrypt(nonce, plaintext)
84 .map_err(|e| MongrelError::Encryption(format!("aes encrypt: {e}")))
85 }
86
87 fn decrypt_page(&self, nonce: &[u8; 12], ciphertext: &[u8]) -> Result<Vec<u8>> {
88 let nonce = Nonce::from_slice(nonce);
89 self.cipher
90 .decrypt(nonce, ciphertext)
91 .map_err(|e| MongrelError::Decryption(format!("aes decrypt: {e}")))
92 }
93 }
94}
95
96#[cfg(feature = "encryption")]
97pub use aes::AesCipher;
98
99#[cfg(feature = "encryption")]
100mod key {
101 use super::{fill_random, Cipher, MongrelError, Result, DEK_LEN};
102 use aes_gcm::aead::{Aead, KeyInit};
103 use aes_gcm::{Aes256Gcm, Nonce};
104 use serde::{Deserialize, Serialize};
105 use zeroize::Zeroizing;
106
107 pub const SALT_LEN: usize = 16;
109 pub const ALGO_AES_GCM: u8 = 1;
111 const KEK_INFO: &[u8] = b"mongreldb/kek/v1";
113 const KEK_RAW_INFO: &[u8] = b"mongreldb/kek-raw/v1";
115 const ARGON2_M_COST: u32 = 19_456;
117 const ARGON2_T_COST: u32 = 2;
119 const ARGON2_P_COST: u32 = 1;
121
122 pub struct Kek(Zeroizing<[u8; DEK_LEN]>);
127
128 impl Kek {
129 pub fn derive(passphrase: &str, salt: &[u8; SALT_LEN]) -> Result<Self> {
132 let params =
133 argon2::Params::new(ARGON2_M_COST, ARGON2_T_COST, ARGON2_P_COST, Some(DEK_LEN))
134 .map_err(|e| MongrelError::Encryption(format!("argon2 params: {e}")))?;
135 let argon =
136 argon2::Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
137 let mut prk = Zeroizing::new([0u8; DEK_LEN]);
139 argon
140 .hash_password_into(passphrase.as_bytes(), salt, prk.as_mut())
141 .map_err(|e| MongrelError::Encryption(format!("argon2 derive: {e}")))?;
142 let hk = hkdf::Hkdf::<sha2::Sha256>::from_prk(prk.as_ref())
144 .map_err(|e| MongrelError::Encryption(format!("hkdf from_prk: {e}")))?;
145 let mut kek = Zeroizing::new([0u8; DEK_LEN]);
146 hk.expand(KEK_INFO, kek.as_mut())
147 .map_err(|e| MongrelError::Encryption(format!("hkdf expand: {e}")))?;
148 Ok(Kek(kek))
149 }
150
151 pub fn from_raw_key(raw: &[u8], salt: &[u8; SALT_LEN]) -> Result<Self> {
156 if raw.len() < DEK_LEN {
157 return Err(MongrelError::InvalidArgument(format!(
158 "raw key must be >= {DEK_LEN} bytes, got {}",
159 raw.len()
160 )));
161 }
162 let hk = hkdf::Hkdf::<sha2::Sha256>::new(Some(salt), raw);
164 let mut kek = Zeroizing::new([0u8; DEK_LEN]);
165 hk.expand(KEK_RAW_INFO, kek.as_mut())
166 .map_err(|e| MongrelError::Encryption(format!("hkdf expand: {e}")))?;
167 Ok(Kek(kek))
168 }
169
170 pub fn derive_wal_key(&self) -> Zeroizing<[u8; DEK_LEN]> {
172 self.derive_subkey(b"mongreldb/wal/v1")
173 }
174
175 pub fn derive_shared_wal_key(&self) -> Zeroizing<[u8; DEK_LEN]> {
179 self.derive_subkey(b"mongreldb/swal/v1")
180 }
181
182 pub fn derive_table_wal_key(&self, table_id: u64) -> Zeroizing<[u8; DEK_LEN]> {
185 let mut info = b"mongreldb/twal/".to_vec();
186 info.extend_from_slice(&table_id.to_be_bytes());
187 info.extend_from_slice(b"/v1");
188 self.derive_subkey(&info)
189 }
190
191 pub fn derive_cache_key(&self) -> Zeroizing<[u8; DEK_LEN]> {
193 self.derive_subkey(b"mongreldb/rcache/v1")
194 }
195
196 pub fn derive_idx_key(&self) -> Zeroizing<[u8; DEK_LEN]> {
198 self.derive_subkey(b"mongreldb/idx/v1")
199 }
200
201 pub fn derive_run_mac_key(&self) -> Zeroizing<[u8; DEK_LEN]> {
203 self.derive_subkey(b"mongreldb/run-mac/v1")
204 }
205
206 pub fn derive_meta_key(&self) -> Zeroizing<[u8; DEK_LEN]> {
209 self.derive_subkey(b"mongreldb/meta/v1")
210 }
211
212 pub fn wrap_dek(&self, dek: &[u8; DEK_LEN], wrap_nonce: &[u8; 12]) -> Result<Vec<u8>> {
216 let cipher = Aes256Gcm::new_from_slice(&self.0[..])
217 .map_err(|e| MongrelError::Encryption(format!("kek aes init: {e}")))?;
218 cipher
219 .encrypt(Nonce::from_slice(wrap_nonce), dek.as_slice())
220 .map_err(|e| MongrelError::Encryption(format!("dek wrap: {e}")))
221 }
222
223 pub fn unwrap_dek(
225 &self,
226 wrapped: &[u8],
227 wrap_nonce: &[u8; 12],
228 ) -> Result<Zeroizing<[u8; DEK_LEN]>> {
229 let cipher = Aes256Gcm::new_from_slice(&self.0[..])
230 .map_err(|e| MongrelError::Encryption(format!("kek aes init: {e}")))?;
231 let pt = Zeroizing::new(
232 cipher
233 .decrypt(Nonce::from_slice(wrap_nonce), wrapped)
234 .map_err(|e| MongrelError::Decryption(format!("dek unwrap: {e}")))?,
235 );
236 if pt.len() != DEK_LEN {
237 return Err(MongrelError::Decryption(format!(
238 "unwrapped dek is {} bytes, expected {DEK_LEN}",
239 pt.len()
240 )));
241 }
242 let mut dek = Zeroizing::new([0u8; DEK_LEN]);
243 dek.copy_from_slice(&pt[..]);
244 Ok(dek)
245 }
246 }
247
248 impl std::fmt::Debug for Kek {
249 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250 f.write_str("Kek(**redacted**)")
251 }
252 }
253
254 pub const SCHEME_HMAC_EQ: u8 = 1;
256 pub const SCHEME_OPE_RANGE: u8 = 2;
257
258 impl Kek {
259 pub fn derive_subkey(&self, info: &[u8]) -> Zeroizing<[u8; DEK_LEN]> {
265 let hk = hkdf::Hkdf::<sha2::Sha256>::from_prk(&self.0[..])
266 .expect("KEK is 32 bytes >= HashLen");
267 let mut k = Zeroizing::new([0u8; DEK_LEN]);
268 hk.expand(info, k.as_mut())
269 .expect("32-byte output <= 255*HashLen");
270 k
271 }
272
273 pub fn derive_column_key(&self, column_id: u16) -> Zeroizing<[u8; DEK_LEN]> {
275 let mut info = b"mongreldb/colkey/".to_vec();
276 info.extend_from_slice(&column_id.to_be_bytes());
277 self.derive_subkey(&info)
278 }
279
280 pub fn wrap_column_key(
282 &self,
283 col_key: &[u8; DEK_LEN],
284 wrap_nonce: &[u8; 12],
285 ) -> Result<Vec<u8>> {
286 self.wrap_dek(col_key, wrap_nonce)
287 }
288 }
289
290 pub fn hmac_token(col_key: &[u8; DEK_LEN], msg: &[u8]) -> [u8; 32] {
294 use hmac::Mac;
295 let mut mac = <hmac::Hmac<sha2::Sha256> as Mac>::new_from_slice(col_key)
296 .expect("HMAC accepts any key size");
297 mac.update(msg);
298 mac.finalize().into_bytes().into()
299 }
300
301 pub fn ope_token_i64(col_key: &[u8; DEK_LEN], x: i64) -> [u8; 16] {
317 let m = (x as u64) ^ (1u64 << 63); monotone_ope(col_key, m)
319 }
320
321 pub fn ope_token_f64(col_key: &[u8; DEK_LEN], x: f64) -> [u8; 16] {
324 let bits = x.to_bits();
325 let m = if bits & (1u64 << 63) != 0 {
326 !bits
327 } else {
328 bits ^ (1u64 << 63)
329 };
330 monotone_ope(col_key, m)
331 }
332
333 const OPE_INFO: &[u8] = b"mongreldb/ope-blk/v2";
335
336 fn monotone_ope(col_key: &[u8; DEK_LEN], m: u64) -> [u8; 16] {
355 let mb = m.to_be_bytes();
356 let mut out = [0u8; 16];
357 for i in 0..8 {
358 let v = mb[i] as usize;
359 let mut o: u32 = v as u32;
360 if v > 0 {
361 let mut info = Vec::with_capacity(OPE_INFO.len() + 1 + i);
364 info.extend_from_slice(OPE_INFO);
365 info.push(i as u8);
366 info.extend_from_slice(&mb[..i]);
367 let hk = hkdf::Hkdf::<sha2::Sha256>::from_prk(&col_key[..])
368 .expect("col_key is 32 bytes >= HashLen");
369 let mut gaps = [0u8; 255];
370 hk.expand(&info, &mut gaps[..v])
371 .expect("v <= 255 <= 255*HashLen");
372 for &g in &gaps[..v] {
373 o += g as u32;
374 }
375 }
376 let chunk = o as u16; out[2 * i..2 * i + 2].copy_from_slice(&chunk.to_be_bytes());
378 }
379 out
380 }
381
382 pub fn encrypt_blob(dek: &[u8; DEK_LEN], plaintext: &[u8]) -> Result<Vec<u8>> {
387 let cipher = crate::encryption::AesCipher::new(&dek[..])?;
388 let mut nonce = [0u8; 12];
389 fill_random(&mut nonce);
390 let ct = cipher.encrypt_page(&nonce, plaintext)?;
391 let mut out = Vec::with_capacity(12 + ct.len());
392 out.extend_from_slice(&nonce);
393 out.extend_from_slice(&ct);
394 Ok(out)
395 }
396
397 pub fn decrypt_blob(dek: &[u8; DEK_LEN], bytes: &[u8]) -> Result<Vec<u8>> {
400 if bytes.len() < 12 + 16 {
401 return Err(MongrelError::Decryption("blob too short".into()));
402 }
403 let cipher = crate::encryption::AesCipher::new(&dek[..])?;
404 let nonce: [u8; 12] = bytes[..12].try_into().unwrap();
405 cipher.decrypt_page(&nonce, &bytes[12..])
406 }
407
408 pub fn run_metadata_mac(
416 mac_key: &[u8; DEK_LEN],
417 header: &[u8],
418 dir: &[u8],
419 descriptor: &[u8],
420 ) -> [u8; 32] {
421 use hmac::Mac;
422 let mut mac = <hmac::Hmac<sha2::Sha256> as Mac>::new_from_slice(mac_key)
423 .expect("HMAC accepts any key size");
424 mac.update(b"mongreldb/run-meta-mac/v1");
425 for part in [header, dir, descriptor] {
426 mac.update(&(part.len() as u64).to_le_bytes());
427 mac.update(part);
428 }
429 mac.finalize().into_bytes().into()
430 }
431
432 pub(super) fn wrap_nonce(nonce_prefix: [u8; 12], kind: u8, column_id: u16) -> [u8; 12] {
439 let mut n = nonce_prefix;
440 n[8] = kind;
441 n[9..11].copy_from_slice(&column_id.to_le_bytes());
442 n[11] = 0;
443 n
444 }
445
446 pub(super) const WRAP_KIND_DEK: u8 = 0;
448 pub(super) const WRAP_KIND_COLUMN: u8 = 1;
450
451 #[derive(Clone, Serialize, Deserialize)]
455 pub struct ColumnKeyDescriptor {
456 pub column_id: u16,
457 pub scheme: u8,
459 pub wrapped_column_key: Vec<u8>,
460 }
461
462 #[derive(Clone, Serialize, Deserialize)]
466 pub struct EncryptionDescriptor {
467 pub algo: u8,
469 pub nonce_prefix: [u8; 12],
473 pub wrapped_dek: Vec<u8>,
475 pub column_descriptors: Vec<ColumnKeyDescriptor>,
477 }
478
479 pub fn generate_dek() -> Zeroizing<[u8; DEK_LEN]> {
481 let mut k = Zeroizing::new([0u8; DEK_LEN]);
482 fill_random(k.as_mut());
483 k
484 }
485
486 pub fn random_salt() -> [u8; SALT_LEN] {
488 let mut s = [0u8; SALT_LEN];
489 fill_random(&mut s);
490 s
491 }
492
493 pub fn random_nonce_prefix() -> [u8; 12] {
496 let mut n = [0u8; 12];
497 fill_random(&mut n[..8]);
498 n
499 }
500
501 pub fn build_page_nonce(nonce_prefix: [u8; 12], column_id: u16, page_seq: u32) -> [u8; 12] {
506 let mut n = nonce_prefix;
507 n[8..10].copy_from_slice(&column_id.to_le_bytes());
508 n[10..12].copy_from_slice(&(page_seq as u16).to_le_bytes());
511 n
512 }
513
514 pub fn setup_run_encryption(
520 kek: &Kek,
521 indexable_columns: &[(u16, u8)],
522 ) -> Result<crate::encryption::RunEncryption> {
523 let dek = generate_dek();
524 let nonce_prefix = random_nonce_prefix();
525 let cipher: Box<dyn Cipher> = Box::new(crate::encryption::AesCipher::new(&dek[..])?);
526 let dek_nonce = wrap_nonce(nonce_prefix, WRAP_KIND_DEK, 0);
527 let wrapped = kek.wrap_dek(&dek, &dek_nonce)?;
528 let mut column_descriptors = Vec::with_capacity(indexable_columns.len());
529 for &(column_id, scheme) in indexable_columns {
530 let col_key = kek.derive_column_key(column_id);
531 let col_nonce = wrap_nonce(nonce_prefix, WRAP_KIND_COLUMN, column_id);
532 let wrapped_col = kek.wrap_column_key(&col_key, &col_nonce)?;
533 column_descriptors.push(ColumnKeyDescriptor {
534 column_id,
535 scheme,
536 wrapped_column_key: wrapped_col,
537 });
538 }
539 let desc = EncryptionDescriptor {
540 algo: ALGO_AES_GCM,
541 nonce_prefix,
542 wrapped_dek: wrapped,
543 column_descriptors,
544 };
545 let descriptor_bytes = bincode::serialize(&desc)?;
546 Ok(crate::encryption::RunEncryption {
547 cipher,
548 nonce_prefix,
549 descriptor_bytes,
550 mac_key: Some(*kek.derive_run_mac_key()),
551 })
552 }
553
554 pub fn build_run_cipher(
557 kek: &Kek,
558 descriptor_bytes: &[u8],
559 ) -> Result<crate::encryption::RunEncryption> {
560 let desc: EncryptionDescriptor = bincode::deserialize(descriptor_bytes)
561 .map_err(|e| MongrelError::Decryption(format!("bad encryption descriptor: {e}")))?;
562 if desc.algo != ALGO_AES_GCM {
563 return Err(MongrelError::Decryption(format!(
564 "unsupported encryption algo {}",
565 desc.algo
566 )));
567 }
568 let dek_nonce = wrap_nonce(desc.nonce_prefix, WRAP_KIND_DEK, 0);
569 let dek = kek.unwrap_dek(&desc.wrapped_dek, &dek_nonce)?;
570 let cipher: Box<dyn Cipher> = Box::new(crate::encryption::AesCipher::new(&dek[..])?);
571 Ok(crate::encryption::RunEncryption {
572 cipher,
573 nonce_prefix: desc.nonce_prefix,
574 descriptor_bytes: Vec::new(),
575 mac_key: None,
576 })
577 }
578}
579
580#[cfg(feature = "encryption")]
581pub use key::{
582 build_page_nonce, build_run_cipher, decrypt_blob, encrypt_blob, generate_dek, hmac_token,
583 ope_token_f64, ope_token_i64, random_nonce_prefix, random_salt, run_metadata_mac,
584 setup_run_encryption, ColumnKeyDescriptor, EncryptionDescriptor, Kek, ALGO_AES_GCM, SALT_LEN,
585 SCHEME_HMAC_EQ, SCHEME_OPE_RANGE,
586};
587
588pub struct RunEncryption {
594 pub cipher: Box<dyn Cipher>,
595 pub nonce_prefix: [u8; 12],
596 pub descriptor_bytes: Vec<u8>,
597 pub mac_key: Option<[u8; 32]>,
600}
601
602#[cfg(not(feature = "encryption"))]
607pub struct Kek {
608 _private: (),
609}
610
611#[cfg(not(feature = "encryption"))]
614#[doc(hidden)]
615pub fn setup_run_encryption(
616 _kek: &Kek,
617 _indexable_columns: &[(u16, u8)],
618) -> crate::Result<RunEncryption> {
619 unreachable!("Kek is unconstructable without the encryption feature")
620}
621
622#[cfg(not(feature = "encryption"))]
624#[doc(hidden)]
625pub fn build_run_cipher(_kek: &Kek, _descriptor_bytes: &[u8]) -> crate::Result<RunEncryption> {
626 unreachable!("Kek is unconstructable without the encryption feature")
627}
628
629#[cfg(feature = "encryption")]
632pub fn meta_dek_for(kek: Option<&Kek>) -> Option<[u8; DEK_LEN]> {
633 kek.map(|k| *k.derive_meta_key())
634}
635
636#[cfg(not(feature = "encryption"))]
637pub fn meta_dek_for(_kek: Option<&Kek>) -> Option<[u8; DEK_LEN]> {
638 None
639}
640
641#[cfg(feature = "encryption")]
644pub fn wal_dek_for(kek: Option<&Kek>) -> Option<Zeroizing<[u8; DEK_LEN]>> {
645 kek.map(|k| k.derive_shared_wal_key())
646}
647
648#[cfg(not(feature = "encryption"))]
649pub fn wal_dek_for(_kek: Option<&Kek>) -> Option<zeroize::Zeroizing<[u8; DEK_LEN]>> {
650 None
651}
652
653#[cfg(test)]
654mod tests {
655 use super::*;
656
657 #[test]
658 fn plaintext_is_identity() {
659 let c = PlaintextCipher;
660 let ct = c.encrypt_page(&[0; 12], b"hello").unwrap();
661 assert_eq!(ct, b"hello");
662 let pt = c.decrypt_page(&[0; 12], &ct).unwrap();
663 assert_eq!(pt, b"hello");
664 }
665
666 #[cfg(feature = "encryption")]
667 #[test]
668 fn aes_round_trip() {
669 let c = AesCipher::new(&[7u8; 32]).unwrap();
670 let nonce = [1u8; 12];
671 let ct = c.encrypt_page(&nonce, b"secret page").unwrap();
672 assert_ne!(ct, b"secret page");
673 let pt = c.decrypt_page(&nonce, &ct).unwrap();
674 assert_eq!(pt, b"secret page");
675 }
676
677 #[cfg(feature = "encryption")]
678 #[test]
679 fn kek_derive_is_deterministic_for_same_passphrase_and_salt() {
680 let salt = random_salt();
681 let k1 = Kek::derive("correct horse battery staple", &salt).unwrap();
682 let k2 = Kek::derive("correct horse battery staple", &salt).unwrap();
683 let dek = generate_dek();
684 let np = random_nonce_prefix();
685 let w1 = k1.wrap_dek(&dek, &np).unwrap();
686 let w2 = k2.wrap_dek(&dek, &np).unwrap();
687 assert_eq!(w1, w2, "same passphrase+salt must yield same KEK");
688 }
689
690 #[cfg(feature = "encryption")]
691 #[test]
692 fn kek_differs_for_different_salt() {
693 let s1 = random_salt();
694 let s2 = random_salt();
695 let k1 = Kek::derive("passphrase", &s1).unwrap();
696 let k2 = Kek::derive("passphrase", &s2).unwrap();
697 let dek = generate_dek();
698 let np = random_nonce_prefix();
699 let w1 = k1.wrap_dek(&dek, &np).unwrap();
700 let w2 = k2.wrap_dek(&dek, &np).unwrap();
701 assert_ne!(w1, w2, "different salts must yield different KEKs");
702 }
703
704 #[cfg(feature = "encryption")]
705 #[test]
706 fn dek_wrap_unwrap_round_trip() {
707 let salt = random_salt();
708 let kek = Kek::derive("hunter2", &salt).unwrap();
709 let dek = generate_dek();
710 let np = random_nonce_prefix();
711 let wrapped = kek.wrap_dek(&dek, &np).unwrap();
712 assert_eq!(wrapped.len(), DEK_LEN + 16);
713 let unwrapped = kek.unwrap_dek(&wrapped, &np).unwrap();
714 assert_eq!(unwrapped.as_ref(), dek.as_ref());
715 }
716
717 #[cfg(feature = "encryption")]
718 #[test]
719 fn unwrap_rejects_wrong_passphrase() {
720 let salt = random_salt();
721 let enc_kek = Kek::derive("right-pass", &salt).unwrap();
722 let dec_kek = Kek::derive("wrong-pass", &salt).unwrap();
723 let dek = generate_dek();
724 let np = random_nonce_prefix();
725 let wrapped = enc_kek.wrap_dek(&dek, &np).unwrap();
726 assert!(dec_kek.unwrap_dek(&wrapped, &np).is_err());
727 }
728
729 #[cfg(feature = "encryption")]
730 #[test]
731 fn page_nonce_overlays_column_and_page() {
732 let np = random_nonce_prefix();
733 let n = build_page_nonce(np, 0x0304, 0x0506);
734 assert_eq!(&n[..8], &np[..8]);
736 assert_eq!(n[8..10], [0x04, 0x03]);
737 assert_eq!(n[10..12], [0x06, 0x05]);
738 }
739
740 #[cfg(feature = "encryption")]
741 #[test]
742 fn page_nonce_unique_per_column_and_page() {
743 let np = random_nonce_prefix();
744 let a = build_page_nonce(np, 1, 0);
745 let b = build_page_nonce(np, 1, 1);
746 let c = build_page_nonce(np, 2, 0);
747 assert_ne!(a, b);
748 assert_ne!(a, c);
749 assert_ne!(b, c);
750 }
751
752 #[cfg(feature = "encryption")]
753 #[test]
754 fn column_key_is_deterministic_from_kek() {
755 let salt = random_salt();
756 let k1 = Kek::derive("pass", &salt).unwrap();
757 let k2 = Kek::derive("pass", &salt).unwrap();
758 let c1 = k1.derive_column_key(7);
759 let c2 = k2.derive_column_key(7);
760 assert_eq!(c1.as_ref(), c2.as_ref(), "same KEK + column => same key");
761 let c3 = k1.derive_column_key(8);
763 assert_ne!(c1.as_ref(), c3.as_ref());
764 }
765
766 #[cfg(feature = "encryption")]
767 #[test]
768 fn hmac_token_collides_only_for_equal_values() {
769 let salt = random_salt();
770 let k = Kek::derive("pass", &salt).unwrap();
771 let ck = k.derive_column_key(1);
772 let a = hmac_token(&ck, b"hello");
773 let b = hmac_token(&ck, b"hello");
774 let c = hmac_token(&ck, b"world");
775 assert_eq!(a, b, "equal plaintexts => equal tokens");
776 assert_ne!(a, c, "unequal plaintexts => distinct tokens");
777 let ck2 = k.derive_column_key(2);
779 assert_ne!(a, hmac_token(&ck2, b"hello"));
780 }
781
782 #[cfg(feature = "encryption")]
783 #[test]
784 fn ope_token_i64_preserves_order() {
785 let salt = random_salt();
786 let k = Kek::derive("pass", &salt).unwrap();
787 let ck = k.derive_column_key(3);
788 let vals = [i64::MIN, -1_000_000, -1, 0, 1, 42, 1_000_000, i64::MAX];
789 let tokens: Vec<_> = vals.iter().map(|&x| ope_token_i64(&ck, x)).collect();
790 for w in tokens.windows(2) {
792 assert!(w[0] < w[1], "OPE must preserve order");
793 }
794 assert_eq!(ope_token_i64(&ck, 0), ope_token_i64(&ck, 0));
796 }
797
798 #[cfg(feature = "encryption")]
803 #[test]
804 fn ope_token_is_non_linear() {
805 let salt = random_salt();
806 let k = Kek::derive("pass", &salt).unwrap();
807 let ck = k.derive_column_key(9);
808 let t = |x: i64| u128::from_be_bytes(ope_token_i64(&ck, x));
809 let g1 = t(101).wrapping_sub(t(100));
811 let g2 = t(102).wrapping_sub(t(101));
812 let g3 = t(103).wrapping_sub(t(102));
813 assert!(
814 !(g1 == g2 && g2 == g3),
815 "constant token gaps => OPE is still affine/linear"
816 );
817 assert!(t(100) < t(101) && t(101) < t(102) && t(102) < t(103));
819 assert_eq!(ope_token_i64(&ck, 100), ope_token_i64(&ck, 100));
820 }
821
822 #[cfg(feature = "encryption")]
823 #[test]
824 fn ope_token_f64_preserves_order() {
825 let salt = random_salt();
826 let k = Kek::derive("pass", &salt).unwrap();
827 let ck = k.derive_column_key(4);
828 let vals = [
829 f64::NEG_INFINITY,
830 -1.5,
831 0.0,
832 std::f64::consts::PI,
833 1e9,
834 f64::INFINITY,
835 ];
836 let tokens: Vec<_> = vals.iter().map(|&x| ope_token_f64(&ck, x)).collect();
837 for w in tokens.windows(2) {
838 assert!(w[0] < w[1], "OPE over f64 must preserve total order");
839 }
840 assert!(ope_token_f64(&ck, -1.0) < ope_token_f64(&ck, 1.0));
842 }
843
844 #[cfg(feature = "encryption")]
848 #[test]
849 fn wrap_nonces_are_distinct_within_a_run() {
850 use super::key::{wrap_nonce, WRAP_KIND_COLUMN, WRAP_KIND_DEK};
851 let salt = random_salt();
852 let kek = Kek::derive("pass", &salt).unwrap();
853 let np = random_nonce_prefix();
854
855 let dek_n = wrap_nonce(np, WRAP_KIND_DEK, 0);
857 let col1 = wrap_nonce(np, WRAP_KIND_COLUMN, 1);
858 let col2 = wrap_nonce(np, WRAP_KIND_COLUMN, 2);
859 assert_ne!(dek_n, col1);
860 assert_ne!(dek_n, col2);
861 assert_ne!(col1, col2);
862
863 let k = generate_dek();
866 let w_dek = kek.wrap_dek(&k, &dek_n).unwrap();
867 let w_col = kek.wrap_column_key(&k, &col1).unwrap();
868 assert_ne!(
869 w_dek, w_col,
870 "DEK and column-key wraps must not share a nonce"
871 );
872
873 let enc =
875 setup_run_encryption(&kek, &[(1, SCHEME_HMAC_EQ), (2, SCHEME_OPE_RANGE)]).unwrap();
876 let built = build_run_cipher(&kek, &enc.descriptor_bytes).unwrap();
877 assert_eq!(built.nonce_prefix, enc.nonce_prefix);
878 }
879
880 #[cfg(feature = "encryption")]
885 #[test]
886 fn wal_deks_are_domain_separated() {
887 let salt = random_salt();
888 let k = Kek::derive("pass", &salt).unwrap();
889 let shared = k.derive_shared_wal_key();
890 let tbl1 = k.derive_table_wal_key(1);
891 let tbl2 = k.derive_table_wal_key(2);
892 let legacy = k.derive_wal_key();
893 assert_ne!(shared.as_ref(), tbl1.as_ref(), "shared != table1");
894 assert_ne!(shared.as_ref(), tbl2.as_ref(), "shared != table2");
895 assert_ne!(shared.as_ref(), legacy.as_ref(), "shared != legacy");
896 assert_ne!(tbl1.as_ref(), tbl2.as_ref(), "table1 != table2");
897 assert_ne!(tbl1.as_ref(), legacy.as_ref(), "table1 != legacy");
898 }
899}