1use alloc::borrow::Cow;
19use alloc::collections::VecDeque;
20use alloc::string::{String, ToString};
21use alloc::sync::Arc;
22use alloc::vec::Vec;
23use core::sync::atomic::{AtomicBool, Ordering};
24use core::time::Duration;
25use std::collections::HashMap;
26use std::sync::Mutex;
27use std::time::{Instant, SystemTime, UNIX_EPOCH};
28
29use arc_swap::ArcSwap;
30use subtle::ConstantTimeEq;
31
32use crate::Result;
33use crate::audit::{AccessKind, AuditEvent, AuditSink};
34use crate::codex::Codex;
35use crate::decoy::DecoyStrategy;
36use crate::error::Error;
37use crate::fetcher::RawKey;
38use crate::fragment::{FragmentStrategy, Fragments, StandardFragmenter};
39use crate::handle::{KeyHandle, KeyId};
40use crate::metadata::KeyMetadata;
41use crate::monitor::{AccessContext, FailureContext, SecurityMonitor, ThresholdContext};
42use crate::normalize::blake3_normalize;
43
44const DEFAULT_MAX_FAILURES: u32 = 0;
49
50const DEFAULT_FAILURE_WINDOW: Duration = Duration::from_secs(60);
52
53#[derive(Debug, Clone)]
58#[non_exhaustive]
59pub struct VaultConfig {
60 pub key_normalization: bool,
63
64 pub max_failures_before_lockout: u32,
69
70 pub failure_window: Duration,
73}
74
75impl Default for VaultConfig {
76 fn default() -> Self {
77 Self::new()
78 }
79}
80
81impl VaultConfig {
82 #[must_use]
84 pub fn new() -> Self {
85 Self {
86 key_normalization: true,
87 max_failures_before_lockout: DEFAULT_MAX_FAILURES,
88 failure_window: DEFAULT_FAILURE_WINDOW,
89 }
90 }
91}
92
93#[derive(Clone)]
106pub struct KeyVault {
107 inner: Arc<VaultInner>,
108}
109
110#[derive(Clone)]
123struct KeyEntry {
124 name: String,
125 fragments: Arc<Fragments>,
130 metadata: KeyMetadata,
131}
132
133struct VaultInner {
134 config: VaultConfig,
135 fragmenter: StandardFragmenter,
136 codex: Option<Arc<dyn Codex>>,
140 monitor: Arc<dyn SecurityMonitor>,
143 keys: ArcSwap<HashMap<KeyId, KeyEntry>>,
147 failure_tracker: Mutex<HashMap<String, VecDeque<Instant>>>,
151 locked_out: AtomicBool,
155 master_hash: Option<[u8; 32]>,
160 audit: Arc<dyn AuditSink>,
166}
167
168impl KeyVault {
169 #[must_use]
179 pub fn is_locked_out(&self) -> bool {
180 self.inner.locked_out.load(Ordering::Acquire)
181 }
182
183 pub fn clear_lockout(&self) {
189 self.inner.locked_out.store(false, Ordering::Release);
190 if let Ok(mut tracker) = self.inner.failure_tracker.lock() {
191 tracker.clear();
192 }
193 }
194
195 pub fn report_failure(&self, key_name: &str, note: Option<&'static str>) {
211 let note = note.map_or(Cow::Borrowed(""), Cow::Borrowed);
212 let (count, oldest_in_window) = self.record_failure(key_name);
213 let window_elapsed = oldest_in_window.map(|t| t.elapsed()).unwrap_or_default();
214
215 let ctx = FailureContext {
217 key_name: key_name.to_string(),
218 consecutive_failures: count,
219 window_elapsed,
220 note: note.clone(),
221 };
222 self.inner.monitor.on_decryption_failure(&ctx);
223
224 let threshold = self.inner.config.max_failures_before_lockout;
226 if threshold > 0 && count >= threshold {
227 let was_locked = self.inner.locked_out.swap(true, Ordering::AcqRel);
230 let breach = ThresholdContext {
231 key_name: key_name.to_string(),
232 failures_in_window: count,
233 window: self.inner.config.failure_window,
234 lockout_triggered: !was_locked,
235 };
236 self.inner.monitor.on_threshold_breach(&breach);
237 }
238 }
239
240 pub fn report_anomalous_access(&self, key_name: &str, note: Option<&'static str>) {
247 let note = note.map_or(Cow::Borrowed(""), Cow::Borrowed);
248 let ctx = AccessContext {
249 key_name: key_name.to_string(),
250 note,
251 };
252 self.inner.monitor.on_anomalous_access(&ctx);
253 }
254
255 fn emit_audit(&self, key_name: &str, kind: AccessKind, note: Cow<'static, str>) {
261 if self.inner.audit.is_no_op() {
264 return;
265 }
266 let timestamp = SystemTime::now()
267 .duration_since(UNIX_EPOCH)
268 .unwrap_or_default();
269 let event = AuditEvent {
270 timestamp,
271 key_name: key_name.to_string(),
272 kind,
273 thread_id: std::thread::current().id(),
274 note,
275 };
276 self.inner.audit.on_event(&event);
277 }
278
279 fn record_failure(&self, key_name: &str) -> (u32, Option<Instant>) {
283 let now = Instant::now();
284 let window = self.inner.config.failure_window;
285 let Ok(mut tracker) = self.inner.failure_tracker.lock() else {
286 return (1, Some(now));
291 };
292 let entries = tracker.entry(key_name.to_string()).or_default();
293 while let Some(front) = entries.front() {
295 if now.saturating_duration_since(*front) > window {
296 let _ = entries.pop_front();
297 } else {
298 break;
299 }
300 }
301 entries.push_back(now);
302 let count = u32::try_from(entries.len()).unwrap_or(u32::MAX);
303 let oldest = entries.front().copied();
304 (count, oldest)
305 }
306
307 #[must_use]
309 pub fn config(&self) -> &VaultConfig {
310 &self.inner.config
311 }
312
313 pub fn fragment(&self, key: &RawKey) -> Result<Fragments> {
332 if self.is_locked_out() {
333 return Err(Error::LockedOut);
334 }
335 let working = if self.inner.config.key_normalization {
336 blake3_normalize(key)
337 } else {
338 RawKey::new(key.as_bytes().to_vec())
339 };
340 let encoded = if let Some(codex) = &self.inner.codex {
341 codex_apply(codex.as_ref(), &working)
342 } else {
343 working
344 };
345 let result = self.inner.fragmenter.fragment(&encoded);
346 if result.is_ok() {
347 self.emit_audit("", AccessKind::OneShotFragment, Cow::Borrowed(""));
348 }
349 result
350 }
351
352 pub fn defragment(&self, fragments: &Fragments) -> Result<RawKey> {
364 if self.is_locked_out() {
365 return Err(Error::LockedOut);
366 }
367 let encoded = self.inner.fragmenter.defragment(fragments)?;
368 let decoded = if let Some(codex) = &self.inner.codex {
369 codex_apply(codex.as_ref(), &encoded)
370 } else {
371 encoded
372 };
373 self.emit_audit("", AccessKind::OneShotDefragment, Cow::Borrowed(""));
374 Ok(decoded)
375 }
376
377 #[allow(clippy::needless_pass_by_value)]
399 pub fn register(&self, name: impl Into<String>, key: RawKey) -> Result<KeyHandle> {
400 if self.is_locked_out() {
401 return Err(Error::LockedOut);
402 }
403 let name: String = name.into();
404
405 let snapshot = self.inner.keys.load();
408 if snapshot.values().any(|e| e.name == name) {
409 return Err(Error::InvalidConfig(format!(
410 "key name {name:?} is already registered"
411 )));
412 }
413 drop(snapshot);
414
415 let key_len = key.len();
416 let fragments = self.fragment(&key)?;
417 let handle = KeyHandle::allocate();
418 let now = SystemTime::now()
419 .duration_since(UNIX_EPOCH)
420 .unwrap_or_default();
421 let metadata = KeyMetadata::new(now, key_len, None);
422
423 let entry = KeyEntry {
424 name,
425 fragments: Arc::new(fragments),
426 metadata,
427 };
428
429 let _previous = self.inner.keys.rcu(|current| {
431 let mut new_map = (**current).clone();
432 let _ = new_map.insert(
433 handle.id(),
434 KeyEntry {
435 name: entry.name.clone(),
436 fragments: Arc::clone(&entry.fragments),
437 metadata: entry.metadata.clone(),
438 },
439 );
440 new_map
441 });
442 self.emit_audit(&entry.name, AccessKind::Register, Cow::Borrowed(""));
443 Ok(handle)
444 }
445
446 pub fn unregister(&self, handle: KeyHandle) -> Result<()> {
455 let name = self
457 .inner
458 .keys
459 .load()
460 .get(&handle.id())
461 .map(|e| e.name.clone());
462 let mut removed = false;
463 let _previous = self.inner.keys.rcu(|current| {
464 let mut new_map = (**current).clone();
465 removed = new_map.remove(&handle.id()).is_some();
466 new_map
467 });
468 if removed {
469 if let Some(name) = name {
470 self.emit_audit(&name, AccessKind::Unregister, Cow::Borrowed(""));
471 }
472 Ok(())
473 } else {
474 Err(Error::KeyNotFound)
475 }
476 }
477
478 pub fn with_key<F, T>(&self, handle: KeyHandle, f: F) -> Result<T>
499 where
500 F: FnOnce(&[u8]) -> T,
501 {
502 if self.is_locked_out() {
503 return Err(Error::LockedOut);
504 }
505 let snapshot = self.inner.keys.load();
506 let entry = snapshot.get(&handle.id()).ok_or(Error::KeyNotFound)?;
507 let fragments = Arc::clone(&entry.fragments);
508 let name: Option<String> = if self.inner.audit.is_no_op() {
512 None
513 } else {
514 Some(entry.name.clone())
515 };
516 drop(snapshot);
519
520 let total = fragments.total_len();
521 let codex = self.inner.codex.clone();
522 let result = SCRATCH.with(|cell| -> Result<T> {
523 let mut buf = cell.borrow_mut();
524 if buf.len() < total {
527 buf.resize(total, 0);
528 }
529 self.inner
532 .fragmenter
533 .defragment_into(&fragments, &mut buf[..total])?;
534 if let Some(c) = &codex {
536 codex_decode_in_place(c.as_ref(), &mut buf[..total]);
537 }
538 let guard = ZeroOnExit(&mut buf[..total]);
541 Ok(f(&*guard.0))
542 })?;
543 if let Some(name) = name {
544 self.emit_audit(&name, AccessKind::Read, Cow::Borrowed(""));
545 }
546 Ok(result)
547 }
548
549 #[allow(clippy::needless_pass_by_value)]
568 pub fn rotate(&self, handle: KeyHandle, new_key: RawKey) -> Result<()> {
569 if self.is_locked_out() {
570 return Err(Error::LockedOut);
571 }
572
573 let name = {
577 let snapshot = self.inner.keys.load();
578 snapshot
579 .get(&handle.id())
580 .map(|e| e.name.clone())
581 .ok_or(Error::KeyNotFound)?
582 };
583
584 let new_len = new_key.len();
585 let new_fragments = Arc::new(self.fragment(&new_key)?);
586 let now = SystemTime::now()
587 .duration_since(UNIX_EPOCH)
588 .unwrap_or_default();
589 let new_metadata = KeyMetadata::new(now, new_len, None);
590
591 let mut found = false;
592 let _previous = self.inner.keys.rcu(|current| {
593 let mut new_map = (**current).clone();
594 if let Some(entry) = new_map.get_mut(&handle.id()) {
595 entry.fragments = Arc::clone(&new_fragments);
596 entry.metadata = new_metadata.clone();
597 found = true;
598 }
599 new_map
600 });
601 if found {
602 self.emit_audit(&name, AccessKind::Rotate, Cow::Borrowed(""));
603 Ok(())
604 } else {
605 Err(Error::KeyNotFound)
608 }
609 }
610
611 #[must_use]
613 pub fn contains(&self, handle: KeyHandle) -> bool {
614 self.inner.keys.load().contains_key(&handle.id())
615 }
616
617 #[must_use]
623 pub fn metadata(&self, handle: KeyHandle) -> Option<KeyMetadata> {
624 self.inner
625 .keys
626 .load()
627 .get(&handle.id())
628 .map(|e| e.metadata.clone())
629 }
630
631 #[must_use]
633 pub fn handle_for_name(&self, name: &str) -> Option<KeyHandle> {
634 self.inner
635 .keys
636 .load()
637 .iter()
638 .find_map(|(id, entry)| (entry.name == name).then(|| KeyHandle::from_id(*id)))
639 }
640
641 #[must_use]
643 pub fn key_count(&self) -> usize {
644 self.inner.keys.load().len()
645 }
646
647 pub fn unlock_with_master(&self, attempt: &[u8]) -> Result<()> {
669 let stored = self.inner.master_hash.ok_or_else(|| {
670 Error::InvalidConfig(
671 "vault has no master key registered; pass with_master_key at build time"
672 .to_string(),
673 )
674 })?;
675 let attempt_hash = blake3::hash(attempt);
676 let matched = bool::from(stored.as_slice().ct_eq(attempt_hash.as_bytes()));
677 self.emit_audit(
678 "<master>",
679 AccessKind::MasterUnlockAttempt { matched },
680 Cow::Borrowed(""),
681 );
682 if matched {
683 self.clear_lockout();
684 Ok(())
685 } else {
686 self.report_failure("<master>", Some("invalid master credential"));
689 Err(Error::Acquisition {
690 source: Cow::Borrowed("master"),
691 reason: "master credential did not match".to_string(),
692 })
693 }
694 }
695
696 #[must_use]
698 pub fn has_master_key(&self) -> bool {
699 self.inner.master_hash.is_some()
700 }
701}
702
703fn codex_apply(codex: &dyn Codex, key: &RawKey) -> RawKey {
710 let bytes: Vec<u8> = key.as_bytes().iter().map(|&b| codex.encode(b)).collect();
711 RawKey::new(bytes)
712}
713
714fn codex_decode_in_place(codex: &dyn Codex, bytes: &mut [u8]) {
721 for b in bytes.iter_mut() {
722 *b = codex.decode(*b);
723 }
724}
725
726thread_local! {
734 static SCRATCH: core::cell::RefCell<Vec<u8>> = const { core::cell::RefCell::new(Vec::new()) };
735}
736
737struct ZeroOnExit<'a>(&'a mut [u8]);
741
742impl Drop for ZeroOnExit<'_> {
743 fn drop(&mut self) {
744 volatile_zero_slice(self.0);
745 }
746}
747
748fn volatile_zero_slice(s: &mut [u8]) {
751 let len = s.len();
752 if len == 0 {
753 return;
754 }
755 let ptr = s.as_mut_ptr();
756 for i in 0..len {
757 unsafe {
761 core::ptr::write_volatile(ptr.add(i), 0u8);
762 }
763 }
764 core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
765}
766
767impl core::fmt::Debug for KeyVault {
768 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
769 f.debug_struct("KeyVault")
770 .field("locked_out", &self.is_locked_out())
771 .field("config", &self.inner.config)
772 .finish()
773 }
774}
775
776#[derive(Clone)]
782pub struct KeyVaultBuilder {
783 config: VaultConfig,
784 fragmenter: StandardFragmenter,
785 codex: Option<Arc<dyn Codex>>,
786 monitor: Option<Arc<dyn SecurityMonitor>>,
787 audit: Option<Arc<dyn AuditSink>>,
790 master_hash: Option<[u8; 32]>,
794}
795
796impl Default for KeyVaultBuilder {
797 fn default() -> Self {
798 Self::new()
799 }
800}
801
802impl core::fmt::Debug for KeyVaultBuilder {
803 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
804 f.debug_struct("KeyVaultBuilder")
805 .field("config", &self.config)
806 .field("fragmenter", &self.fragmenter)
807 .field("codex", &self.codex.as_ref().map(|_| "<set>"))
808 .field("monitor", &self.monitor.as_ref().map(|_| "<set>"))
809 .field("audit", &self.audit.as_ref().map(|_| "<set>"))
810 .field("master_key", &self.master_hash.as_ref().map(|_| "<set>"))
811 .finish()
812 }
813}
814
815impl KeyVaultBuilder {
816 #[must_use]
819 pub fn new() -> Self {
820 Self {
821 config: VaultConfig::new(),
822 fragmenter: StandardFragmenter::new(),
823 codex: None,
824 monitor: None,
825 audit: None,
826 master_hash: None,
827 }
828 }
829
830 #[must_use]
837 pub fn normalize_with_blake3(mut self, enabled: bool) -> Self {
838 self.config.key_normalization = enabled;
839 self
840 }
841
842 #[must_use]
849 pub fn with_chunk_range(mut self, min: usize, max: usize) -> Self {
850 self.fragmenter = StandardFragmenter::with_chunk_range(min, max);
851 self
852 }
853
854 #[must_use]
882 pub fn with_codex<C>(mut self, codex: C) -> Self
883 where
884 C: Codex + 'static,
885 {
886 self.codex = Some(Arc::new(codex));
887 self
888 }
889
890 #[must_use]
904 pub fn with_decoy<D>(mut self, decoy: D) -> Self
905 where
906 D: DecoyStrategy + 'static,
907 {
908 self.fragmenter = self.fragmenter.with_decoy(decoy);
909 self
910 }
911
912 #[must_use]
924 pub fn with_monitor<M>(mut self, monitor: M) -> Self
925 where
926 M: SecurityMonitor + 'static,
927 {
928 self.monitor = Some(Arc::new(monitor));
929 self
930 }
931
932 #[must_use]
945 pub fn with_failure_threshold(mut self, max: u32, window: Duration) -> Self {
946 self.config.max_failures_before_lockout = max;
947 self.config.failure_window = window;
948 self
949 }
950
951 #[must_use]
962 pub fn with_audit_sink<A>(mut self, sink: A) -> Self
963 where
964 A: AuditSink + 'static,
965 {
966 self.audit = Some(Arc::new(sink));
967 self
968 }
969
970 #[must_use]
982 pub fn with_master_key(mut self, master: RawKey) -> Self {
983 let hash = blake3::hash(master.as_bytes());
984 let mut bytes = [0u8; 32];
985 bytes.copy_from_slice(hash.as_bytes());
986 self.master_hash = Some(bytes);
987 drop(master);
989 self
990 }
991
992 #[must_use]
997 pub fn build(self) -> KeyVault {
998 let monitor: Arc<dyn SecurityMonitor> = self
999 .monitor
1000 .unwrap_or_else(|| Arc::new(crate::monitor::NoMonitor));
1001 let audit: Arc<dyn AuditSink> = self
1002 .audit
1003 .unwrap_or_else(|| Arc::new(crate::audit::NoAudit));
1004 KeyVault {
1005 inner: Arc::new(VaultInner {
1006 config: self.config,
1007 fragmenter: self.fragmenter,
1008 codex: self.codex,
1009 monitor,
1010 keys: ArcSwap::from_pointee(HashMap::new()),
1011 failure_tracker: Mutex::new(HashMap::new()),
1012 locked_out: AtomicBool::new(false),
1013 master_hash: self.master_hash,
1014 audit,
1015 }),
1016 }
1017 }
1018}
1019
1020#[cfg(test)]
1021#[allow(clippy::unwrap_used, clippy::expect_used)]
1022mod tests {
1023 use super::*;
1024 use alloc::format;
1025
1026 #[test]
1027 fn builder_defaults_to_normalization_on() {
1028 let v = KeyVaultBuilder::new().build();
1029 assert!(v.config().key_normalization);
1030 }
1031
1032 #[test]
1033 fn builder_can_disable_normalization() {
1034 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1035 assert!(!v.config().key_normalization);
1036 }
1037
1038 #[test]
1039 fn fresh_vault_is_not_locked_out() {
1040 let v = KeyVaultBuilder::new().build();
1041 assert!(!v.is_locked_out());
1042 }
1043
1044 #[test]
1045 fn debug_does_not_panic() {
1046 let v = KeyVaultBuilder::new().build();
1047 let _ = format!("{v:?}");
1048 }
1049
1050 #[test]
1051 fn fragment_defragment_roundtrip_with_normalization() {
1052 let v = KeyVaultBuilder::new().build(); let raw = RawKey::new(b"hello world".to_vec());
1054 let frags = v.fragment(&raw).unwrap();
1055 let recovered = v.defragment(&frags).unwrap();
1056 assert_eq!(recovered.len(), 32);
1059 let frags2 = v.fragment(&raw).unwrap();
1062 let recovered2 = v.defragment(&frags2).unwrap();
1063 assert_eq!(recovered.as_bytes(), recovered2.as_bytes());
1064 }
1065
1066 #[test]
1067 fn fragment_defragment_roundtrip_without_normalization() {
1068 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1069 let raw = RawKey::new((0u8..40).collect());
1070 let frags = v.fragment(&raw).unwrap();
1071 let recovered = v.defragment(&frags).unwrap();
1072 assert_eq!(recovered.as_bytes(), raw.as_bytes());
1073 }
1074
1075 #[test]
1076 fn fragment_rejects_empty_key() {
1077 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1078 let err = v
1079 .fragment(&RawKey::new(alloc::vec::Vec::new()))
1080 .unwrap_err();
1081 assert!(matches!(err, crate::Error::Fragment(_)));
1082 }
1083
1084 #[test]
1085 fn chunk_range_propagates_through_builder() {
1086 let v = KeyVaultBuilder::new()
1087 .normalize_with_blake3(false)
1088 .with_chunk_range(4, 6)
1089 .build();
1090 let raw = RawKey::new((0u8..30).collect());
1091 let frags = v.fragment(&raw).unwrap();
1092
1093 let chunks = frags.chunks();
1101 let mut below_min = 0;
1102 let mut total = 0usize;
1103 for c in chunks {
1104 assert!(
1105 c.len() >= 1 && c.len() <= 6,
1106 "chunk size {} not in [1,6]",
1107 c.len()
1108 );
1109 if c.len() < 4 {
1110 below_min += 1;
1111 }
1112 total += c.len();
1113 }
1114 assert!(
1115 below_min <= 1,
1116 "more than one chunk below min size: {below_min}"
1117 );
1118 assert_eq!(total, 30);
1119 }
1120
1121 #[test]
1122 fn fragment_with_random_decoy_roundtrips() {
1123 let v = KeyVaultBuilder::new()
1124 .normalize_with_blake3(false)
1125 .with_decoy(crate::RandomDecoy)
1126 .build();
1127 let raw = RawKey::new((0u8..32).collect());
1128 let frags = v.fragment(&raw).unwrap();
1129 let recovered = v.defragment(&frags).unwrap();
1132 assert_eq!(recovered.as_bytes(), raw.as_bytes());
1133 }
1134
1135 #[test]
1136 fn fragment_with_self_reference_decoy_roundtrips() {
1137 let v = KeyVaultBuilder::new()
1138 .normalize_with_blake3(false)
1139 .with_decoy(crate::SelfReferenceDecoy)
1140 .build();
1141 let raw = RawKey::new(b"some user-supplied key material".to_vec());
1142 let frags = v.fragment(&raw).unwrap();
1143 let recovered = v.defragment(&frags).unwrap();
1144 assert_eq!(recovered.as_bytes(), raw.as_bytes());
1145 }
1146
1147 #[test]
1148 fn fragment_with_key_derived_decoy_roundtrips() {
1149 let v = KeyVaultBuilder::new()
1150 .normalize_with_blake3(false)
1151 .with_decoy(crate::KeyDerivedDecoy)
1152 .build();
1153 let raw = RawKey::new((0u8..64).collect());
1154 let frags = v.fragment(&raw).unwrap();
1155 let recovered = v.defragment(&frags).unwrap();
1156 assert_eq!(recovered.as_bytes(), raw.as_bytes());
1157 }
1158
1159 #[test]
1160 fn decoy_increases_chunk_count_relative_to_no_decoy() {
1161 let no_decoy = KeyVaultBuilder::new()
1162 .normalize_with_blake3(false)
1163 .with_chunk_range(2, 4)
1164 .build();
1165 let with_decoy = KeyVaultBuilder::new()
1166 .normalize_with_blake3(false)
1167 .with_chunk_range(2, 4)
1168 .with_decoy(crate::SelfReferenceDecoy)
1169 .build();
1170 let raw = RawKey::new((0u8..32).collect());
1171
1172 let mut no_decoy_total = 0usize;
1176 let mut decoy_total = 0usize;
1177 for _ in 0..8 {
1178 no_decoy_total += no_decoy.fragment(&raw).unwrap().chunk_count();
1179 decoy_total += with_decoy.fragment(&raw).unwrap().chunk_count();
1180 }
1181 assert!(
1186 decoy_total > no_decoy_total,
1187 "decoy vault produced {decoy_total} chunks vs no-decoy {no_decoy_total}"
1188 );
1189 }
1190
1191 #[test]
1192 fn fragment_with_static_codex_roundtrips() {
1193 use crate::StaticCodex;
1194 let codex = StaticCodex::from_swaps(&[(b'A', b'#'), (b'0', b'%')]).unwrap();
1195 let v = KeyVaultBuilder::new()
1196 .normalize_with_blake3(false)
1197 .with_codex(codex)
1198 .build();
1199 let raw = RawKey::new(b"A0A0A0A0".to_vec());
1200 let frags = v.fragment(&raw).unwrap();
1201 let recovered = v.defragment(&frags).unwrap();
1202 assert_eq!(recovered.as_bytes(), raw.as_bytes());
1205 }
1206
1207 #[test]
1208 fn fragment_with_dynamic_codex_roundtrips() {
1209 use crate::DynamicCodex;
1210 let v = KeyVaultBuilder::new()
1211 .normalize_with_blake3(false)
1212 .with_codex(DynamicCodex::new().unwrap())
1213 .build();
1214 let raw = RawKey::new((0u8..=255).collect());
1215 let frags = v.fragment(&raw).unwrap();
1216 let recovered = v.defragment(&frags).unwrap();
1217 assert_eq!(recovered.as_bytes(), raw.as_bytes());
1218 }
1219
1220 #[test]
1221 fn fragment_with_codex_and_decoy_and_normalization_roundtrips() {
1222 use crate::{DynamicCodex, SelfReferenceDecoy};
1223 let v = KeyVaultBuilder::new()
1226 .normalize_with_blake3(true)
1227 .with_codex(DynamicCodex::new().unwrap())
1228 .with_decoy(SelfReferenceDecoy)
1229 .build();
1230 let raw = RawKey::new(b"my application key".to_vec());
1231 let frags = v.fragment(&raw).unwrap();
1232 let recovered = v.defragment(&frags).unwrap();
1233 assert_eq!(recovered.len(), 32);
1236 let recovered2 = v.defragment(&v.fragment(&raw).unwrap()).unwrap();
1237 assert_eq!(recovered.as_bytes(), recovered2.as_bytes());
1238 }
1239
1240 #[test]
1241 fn codex_visibly_transforms_stored_bytes() {
1242 use crate::StaticCodex;
1247 let v = KeyVaultBuilder::new()
1248 .normalize_with_blake3(false)
1249 .with_codex(crate::DynamicCodex::new().unwrap())
1251 .build();
1252 let raw = RawKey::new(alloc::vec![0xaa; 8]);
1253 let frags = v.fragment(&raw).unwrap();
1254
1255 let mut saw_non_aa = false;
1258 for chunk in frags.chunks() {
1259 for &b in chunk.as_bytes() {
1260 if b != 0xaa {
1261 saw_non_aa = true;
1262 break;
1263 }
1264 }
1265 if saw_non_aa {
1266 break;
1267 }
1268 }
1269 assert!(
1270 saw_non_aa,
1271 "codex did not transform 0xaa — stored bytes still all 0xaa",
1272 );
1273
1274 let recovered = v.defragment(&frags).unwrap();
1276 assert_eq!(recovered.as_bytes(), raw.as_bytes());
1277 let _ = StaticCodex::from_swaps(&[]).unwrap();
1279 }
1280
1281 use core::sync::atomic::AtomicU32;
1284
1285 struct CountingMonitor {
1287 failures: AtomicU32,
1288 anomalies: AtomicU32,
1289 breaches: AtomicU32,
1290 }
1291
1292 impl CountingMonitor {
1293 fn new() -> Self {
1294 Self {
1295 failures: AtomicU32::new(0),
1296 anomalies: AtomicU32::new(0),
1297 breaches: AtomicU32::new(0),
1298 }
1299 }
1300 }
1301
1302 impl SecurityMonitor for CountingMonitor {
1303 fn on_decryption_failure(&self, _ctx: &FailureContext) {
1304 let _ = self.failures.fetch_add(1, Ordering::SeqCst);
1305 }
1306 fn on_anomalous_access(&self, _ctx: &AccessContext) {
1307 let _ = self.anomalies.fetch_add(1, Ordering::SeqCst);
1308 }
1309 fn on_threshold_breach(&self, _ctx: &ThresholdContext) {
1310 let _ = self.breaches.fetch_add(1, Ordering::SeqCst);
1311 }
1312 }
1313
1314 #[test]
1315 fn report_failure_fires_monitor() {
1316 let monitor = Arc::new(CountingMonitor::new());
1317 let v = KeyVaultBuilder::new()
1318 .with_monitor(Arc::clone(&monitor) as Arc<dyn SecurityMonitor>)
1319 .build();
1320 v.report_failure("k", None);
1321 v.report_failure("k", Some("test note"));
1322 assert_eq!(monitor.failures.load(Ordering::SeqCst), 2);
1323 assert_eq!(monitor.breaches.load(Ordering::SeqCst), 0);
1324 assert!(!v.is_locked_out());
1325 }
1326
1327 #[test]
1328 fn report_anomalous_access_fires_monitor() {
1329 let monitor = Arc::new(CountingMonitor::new());
1330 let v = KeyVaultBuilder::new()
1331 .with_monitor(Arc::clone(&monitor) as Arc<dyn SecurityMonitor>)
1332 .build();
1333 v.report_anomalous_access("k", None);
1334 assert_eq!(monitor.anomalies.load(Ordering::SeqCst), 1);
1335 assert!(!v.is_locked_out());
1336 }
1337
1338 #[test]
1339 fn threshold_lockout_fires_after_max_failures() {
1340 let monitor = Arc::new(CountingMonitor::new());
1341 let v = KeyVaultBuilder::new()
1342 .with_monitor(Arc::clone(&monitor) as Arc<dyn SecurityMonitor>)
1343 .with_failure_threshold(3, Duration::from_secs(30))
1344 .build();
1345
1346 v.report_failure("k", None);
1347 assert!(!v.is_locked_out());
1348 v.report_failure("k", None);
1349 assert!(!v.is_locked_out());
1350 v.report_failure("k", None);
1351 assert!(v.is_locked_out());
1353 assert_eq!(monitor.failures.load(Ordering::SeqCst), 3);
1354 assert_eq!(monitor.breaches.load(Ordering::SeqCst), 1);
1355
1356 v.report_failure("k", None);
1359 assert!(v.is_locked_out());
1360 assert_eq!(monitor.failures.load(Ordering::SeqCst), 4);
1361 assert_eq!(monitor.breaches.load(Ordering::SeqCst), 2);
1363 }
1364
1365 #[test]
1366 fn fragment_refuses_when_locked_out() {
1367 let v = KeyVaultBuilder::new()
1368 .normalize_with_blake3(false)
1369 .with_failure_threshold(1, Duration::from_secs(30))
1370 .build();
1371 v.report_failure("k", None);
1372 assert!(v.is_locked_out());
1373
1374 let err = v
1375 .fragment(&RawKey::new(alloc::vec![1u8, 2, 3, 4]))
1376 .unwrap_err();
1377 assert!(matches!(err, Error::LockedOut));
1378 }
1379
1380 #[test]
1381 fn defragment_refuses_when_locked_out() {
1382 let v = KeyVaultBuilder::new()
1383 .normalize_with_blake3(false)
1384 .with_failure_threshold(2, Duration::from_secs(30))
1385 .build();
1386 let raw = RawKey::new(alloc::vec![1u8; 16]);
1388 let frags = v.fragment(&raw).unwrap();
1389 v.report_failure("k", None);
1390 v.report_failure("k", None);
1391 assert!(v.is_locked_out());
1392
1393 let err = v.defragment(&frags).unwrap_err();
1394 assert!(matches!(err, Error::LockedOut));
1395 }
1396
1397 #[test]
1398 fn clear_lockout_resets_state() {
1399 let v = KeyVaultBuilder::new()
1400 .with_failure_threshold(1, Duration::from_secs(30))
1401 .build();
1402 v.report_failure("k", None);
1403 assert!(v.is_locked_out());
1404 v.clear_lockout();
1405 assert!(!v.is_locked_out());
1406 v.clear_lockout();
1414 assert!(!v.is_locked_out());
1415 }
1416
1417 #[test]
1418 fn per_key_failure_counts_are_independent() {
1419 let monitor = Arc::new(CountingMonitor::new());
1420 let v = KeyVaultBuilder::new()
1421 .with_monitor(Arc::clone(&monitor) as Arc<dyn SecurityMonitor>)
1422 .with_failure_threshold(2, Duration::from_secs(30))
1423 .build();
1424 v.report_failure("alpha", None);
1425 v.report_failure("beta", None);
1426 assert!(!v.is_locked_out());
1428 assert_eq!(monitor.failures.load(Ordering::SeqCst), 2);
1429 v.report_failure("alpha", None);
1430 assert!(v.is_locked_out());
1432 }
1433
1434 #[test]
1437 fn register_returns_handle_and_increments_count() {
1438 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1439 assert_eq!(v.key_count(), 0);
1440 let h = v
1441 .register("primary", RawKey::new(alloc::vec![1u8; 32]))
1442 .unwrap();
1443 assert_eq!(v.key_count(), 1);
1444 assert!(v.contains(h));
1445 }
1446
1447 #[test]
1448 fn register_rejects_duplicate_name() {
1449 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1450 let _ = v
1451 .register("primary", RawKey::new(alloc::vec![1u8; 16]))
1452 .unwrap();
1453 let err = v
1454 .register("primary", RawKey::new(alloc::vec![2u8; 16]))
1455 .unwrap_err();
1456 assert!(matches!(err, Error::InvalidConfig(_)));
1457 }
1458
1459 #[test]
1460 fn unregister_removes_key() {
1461 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1462 let h = v
1463 .register("primary", RawKey::new(alloc::vec![1u8; 16]))
1464 .unwrap();
1465 assert!(v.contains(h));
1466 v.unregister(h).unwrap();
1467 assert!(!v.contains(h));
1468 assert_eq!(v.key_count(), 0);
1469 }
1470
1471 #[test]
1472 fn unregister_unknown_handle_errors() {
1473 let v = KeyVaultBuilder::new().build();
1474 let h = KeyHandle::__for_test();
1475 let err = v.unregister(h).unwrap_err();
1476 assert!(matches!(err, Error::KeyNotFound));
1477 }
1478
1479 #[test]
1480 fn with_key_round_trips_bytes() {
1481 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1482 let original = alloc::vec![0xa5u8; 32];
1483 let h = v.register("data", RawKey::new(original.clone())).unwrap();
1484 let observed = v.with_key(h, <[u8]>::to_vec).unwrap();
1485 assert_eq!(observed, original);
1486 }
1487
1488 #[test]
1489 fn with_key_normalization_changes_output_length() {
1490 let v = KeyVaultBuilder::new().build(); let h = v
1492 .register("data", RawKey::new(alloc::vec![0xa5; 17]))
1493 .unwrap();
1494 let observed_len = v.with_key(h, <[u8]>::len).unwrap();
1495 assert_eq!(observed_len, 32);
1497 }
1498
1499 #[test]
1500 fn with_key_unknown_handle_errors() {
1501 let v = KeyVaultBuilder::new().build();
1502 let h = KeyHandle::__for_test();
1503 let err = v.with_key(h, |_| ()).unwrap_err();
1504 assert!(matches!(err, Error::KeyNotFound));
1505 }
1506
1507 #[test]
1508 fn rotate_swaps_key_bytes() {
1509 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1510 let h = v
1511 .register("data", RawKey::new(alloc::vec![1u8; 16]))
1512 .unwrap();
1513
1514 v.rotate(h, RawKey::new(alloc::vec![2u8; 16])).unwrap();
1515 let observed = v.with_key(h, <[u8]>::to_vec).unwrap();
1516 assert_eq!(observed, alloc::vec![2u8; 16]);
1517 }
1518
1519 #[test]
1520 fn rotate_unknown_handle_errors() {
1521 let v = KeyVaultBuilder::new().build();
1522 let h = KeyHandle::__for_test();
1523 let err = v.rotate(h, RawKey::new(alloc::vec![0u8; 16])).unwrap_err();
1524 assert!(matches!(err, Error::KeyNotFound));
1525 }
1526
1527 #[test]
1528 fn handle_for_name_finds_registered_key() {
1529 let v = KeyVaultBuilder::new().build();
1530 let h = v
1531 .register("primary", RawKey::new(alloc::vec![0u8; 16]))
1532 .unwrap();
1533 assert_eq!(v.handle_for_name("primary"), Some(h));
1534 assert_eq!(v.handle_for_name("missing"), None);
1535 }
1536
1537 #[test]
1538 fn metadata_records_registration_length() {
1539 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1540 let h = v
1541 .register("data", RawKey::new(alloc::vec![0u8; 42]))
1542 .unwrap();
1543 let meta = v.metadata(h).expect("metadata");
1544 assert_eq!(meta.length(), 42);
1545 }
1546
1547 #[test]
1548 fn registered_key_refuses_access_when_locked_out() {
1549 let v = KeyVaultBuilder::new()
1550 .with_failure_threshold(1, Duration::from_secs(30))
1551 .build();
1552 let h = v
1553 .register("data", RawKey::new(alloc::vec![0xa5; 16]))
1554 .unwrap();
1555 v.report_failure("data", None);
1556 assert!(v.is_locked_out());
1557
1558 let err = v.with_key(h, |_| ()).unwrap_err();
1559 assert!(matches!(err, Error::LockedOut));
1560 let err = v.rotate(h, RawKey::new(alloc::vec![0u8; 16])).unwrap_err();
1561 assert!(matches!(err, Error::LockedOut));
1562 }
1563
1564 #[test]
1565 fn master_key_unlock_clears_lockout_on_match() {
1566 let master_bytes = b"correct horse battery staple".to_vec();
1567 let v = KeyVaultBuilder::new()
1568 .with_master_key(RawKey::new(master_bytes.clone()))
1569 .with_failure_threshold(1, Duration::from_secs(30))
1570 .build();
1571 assert!(v.has_master_key());
1572
1573 v.report_failure("k", None);
1574 assert!(v.is_locked_out());
1575
1576 let err = v.unlock_with_master(b"wrong").unwrap_err();
1578 assert!(matches!(err, Error::Acquisition { .. }));
1579 assert!(v.is_locked_out());
1580
1581 v.unlock_with_master(&master_bytes).unwrap();
1583 assert!(!v.is_locked_out());
1584 }
1585
1586 struct CapturingAudit {
1590 events: Mutex<Vec<(crate::audit::AccessKind, String)>>,
1591 }
1592
1593 impl CapturingAudit {
1594 fn new() -> Self {
1595 Self {
1596 events: Mutex::new(Vec::new()),
1597 }
1598 }
1599 fn count_of(&self, kind: crate::audit::AccessKind) -> usize {
1600 self.events
1601 .lock()
1602 .unwrap()
1603 .iter()
1604 .filter(|(k, _)| *k == kind)
1605 .count()
1606 }
1607 fn last_for(&self, kind: crate::audit::AccessKind) -> Option<String> {
1608 self.events
1609 .lock()
1610 .unwrap()
1611 .iter()
1612 .rev()
1613 .find_map(|(k, name)| (*k == kind).then(|| name.clone()))
1614 }
1615 }
1616
1617 impl crate::audit::AuditSink for CapturingAudit {
1618 fn on_event(&self, event: &crate::audit::AuditEvent) {
1619 self.events
1620 .lock()
1621 .unwrap()
1622 .push((event.kind, event.key_name.clone()));
1623 }
1624 }
1625
1626 #[test]
1627 fn register_emits_register_event() {
1628 let audit = Arc::new(CapturingAudit::new());
1629 let v = KeyVaultBuilder::new()
1630 .normalize_with_blake3(false)
1631 .with_audit_sink(Arc::clone(&audit) as Arc<dyn crate::audit::AuditSink>)
1632 .build();
1633 let _ = v
1634 .register("primary", RawKey::new(alloc::vec![1u8; 16]))
1635 .unwrap();
1636 assert_eq!(audit.count_of(crate::audit::AccessKind::Register), 1);
1637 assert_eq!(
1638 audit.last_for(crate::audit::AccessKind::Register),
1639 Some("primary".to_string())
1640 );
1641 }
1642
1643 #[test]
1644 fn unregister_emits_unregister_event() {
1645 let audit = Arc::new(CapturingAudit::new());
1646 let v = KeyVaultBuilder::new()
1647 .normalize_with_blake3(false)
1648 .with_audit_sink(Arc::clone(&audit) as Arc<dyn crate::audit::AuditSink>)
1649 .build();
1650 let h = v
1651 .register("primary", RawKey::new(alloc::vec![1u8; 16]))
1652 .unwrap();
1653 v.unregister(h).unwrap();
1654 assert_eq!(audit.count_of(crate::audit::AccessKind::Unregister), 1);
1655 assert_eq!(
1656 audit.last_for(crate::audit::AccessKind::Unregister),
1657 Some("primary".to_string())
1658 );
1659 }
1660
1661 #[test]
1662 fn with_key_emits_read_event() {
1663 let audit = Arc::new(CapturingAudit::new());
1664 let v = KeyVaultBuilder::new()
1665 .normalize_with_blake3(false)
1666 .with_audit_sink(Arc::clone(&audit) as Arc<dyn crate::audit::AuditSink>)
1667 .build();
1668 let h = v
1669 .register("data", RawKey::new(alloc::vec![0xa5u8; 16]))
1670 .unwrap();
1671 let _ = v.with_key(h, <[u8]>::to_vec).unwrap();
1672 assert_eq!(audit.count_of(crate::audit::AccessKind::Read), 1);
1673 assert_eq!(
1674 audit.last_for(crate::audit::AccessKind::Read),
1675 Some("data".to_string())
1676 );
1677 }
1678
1679 #[test]
1680 fn rotate_emits_rotate_event() {
1681 let audit = Arc::new(CapturingAudit::new());
1682 let v = KeyVaultBuilder::new()
1683 .normalize_with_blake3(false)
1684 .with_audit_sink(Arc::clone(&audit) as Arc<dyn crate::audit::AuditSink>)
1685 .build();
1686 let h = v
1687 .register("data", RawKey::new(alloc::vec![1u8; 16]))
1688 .unwrap();
1689 v.rotate(h, RawKey::new(alloc::vec![2u8; 16])).unwrap();
1690 assert_eq!(audit.count_of(crate::audit::AccessKind::Rotate), 1);
1691 assert_eq!(
1692 audit.last_for(crate::audit::AccessKind::Rotate),
1693 Some("data".to_string())
1694 );
1695 }
1696
1697 #[test]
1698 fn fragment_and_defragment_emit_oneshot_events() {
1699 let audit = Arc::new(CapturingAudit::new());
1700 let v = KeyVaultBuilder::new()
1701 .normalize_with_blake3(false)
1702 .with_audit_sink(Arc::clone(&audit) as Arc<dyn crate::audit::AuditSink>)
1703 .build();
1704 let raw = RawKey::new(alloc::vec![0u8; 16]);
1705 let frags = v.fragment(&raw).unwrap();
1706 let _ = v.defragment(&frags).unwrap();
1707 assert_eq!(audit.count_of(crate::audit::AccessKind::OneShotFragment), 1);
1708 assert_eq!(
1709 audit.count_of(crate::audit::AccessKind::OneShotDefragment),
1710 1
1711 );
1712 }
1713
1714 #[test]
1715 fn master_unlock_emits_event_with_match_status() {
1716 let audit = Arc::new(CapturingAudit::new());
1717 let master = b"correct".to_vec();
1718 let v = KeyVaultBuilder::new()
1719 .with_master_key(RawKey::new(master.clone()))
1720 .with_failure_threshold(1, Duration::from_secs(30))
1721 .with_audit_sink(Arc::clone(&audit) as Arc<dyn crate::audit::AuditSink>)
1722 .build();
1723
1724 v.report_failure("k", None);
1725 assert!(v.is_locked_out());
1726
1727 let _ = v.unlock_with_master(b"wrong");
1728 assert_eq!(
1729 audit.count_of(crate::audit::AccessKind::MasterUnlockAttempt { matched: false }),
1730 1
1731 );
1732
1733 v.unlock_with_master(&master).unwrap();
1734 assert_eq!(
1735 audit.count_of(crate::audit::AccessKind::MasterUnlockAttempt { matched: true }),
1736 1
1737 );
1738 }
1739
1740 #[test]
1741 fn no_audit_default_does_not_panic() {
1742 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1745 let h = v.register("k", RawKey::new(alloc::vec![0u8; 16])).unwrap();
1746 let _ = v.with_key(h, <[u8]>::to_vec).unwrap();
1747 v.unregister(h).unwrap();
1748 }
1749
1750 #[test]
1751 fn master_key_unlock_without_registered_master_errors() {
1752 let v = KeyVaultBuilder::new().build();
1753 assert!(!v.has_master_key());
1754 let err = v.unlock_with_master(b"anything").unwrap_err();
1755 assert!(matches!(err, Error::InvalidConfig(_)));
1756 }
1757
1758 #[test]
1759 fn composite_monitor_chains_to_all_inner() {
1760 use crate::CompositeMonitor;
1761 let a = Arc::new(CountingMonitor::new());
1762 let b = Arc::new(CountingMonitor::new());
1763 let composite = CompositeMonitor::new(alloc::vec![
1764 Arc::clone(&a) as Arc<dyn SecurityMonitor>,
1765 Arc::clone(&b) as Arc<dyn SecurityMonitor>,
1766 ]);
1767 let v = KeyVaultBuilder::new()
1768 .with_monitor(composite)
1769 .with_failure_threshold(1, Duration::from_secs(30))
1770 .build();
1771 v.report_failure("k", None);
1772 assert_eq!(a.failures.load(Ordering::SeqCst), 1);
1773 assert_eq!(b.failures.load(Ordering::SeqCst), 1);
1774 assert_eq!(a.breaches.load(Ordering::SeqCst), 1);
1775 assert_eq!(b.breaches.load(Ordering::SeqCst), 1);
1776 }
1777}