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::codex::Codex;
34use crate::decoy::DecoyStrategy;
35use crate::error::Error;
36use crate::fetcher::RawKey;
37use crate::fragment::{FragmentStrategy, Fragments, StandardFragmenter};
38use crate::handle::{KeyHandle, KeyId};
39use crate::metadata::KeyMetadata;
40use crate::monitor::{AccessContext, FailureContext, SecurityMonitor, ThresholdContext};
41use crate::normalize::blake3_normalize;
42
43const DEFAULT_MAX_FAILURES: u32 = 0;
48
49const DEFAULT_FAILURE_WINDOW: Duration = Duration::from_secs(60);
51
52#[derive(Debug, Clone)]
57#[non_exhaustive]
58pub struct VaultConfig {
59 pub key_normalization: bool,
62
63 pub max_failures_before_lockout: u32,
68
69 pub failure_window: Duration,
72}
73
74impl Default for VaultConfig {
75 fn default() -> Self {
76 Self::new()
77 }
78}
79
80impl VaultConfig {
81 #[must_use]
83 pub fn new() -> Self {
84 Self {
85 key_normalization: true,
86 max_failures_before_lockout: DEFAULT_MAX_FAILURES,
87 failure_window: DEFAULT_FAILURE_WINDOW,
88 }
89 }
90}
91
92#[derive(Clone)]
105pub struct KeyVault {
106 inner: Arc<VaultInner>,
107}
108
109#[derive(Clone)]
122struct KeyEntry {
123 name: String,
124 fragments: Arc<Fragments>,
129 metadata: KeyMetadata,
130}
131
132struct VaultInner {
133 config: VaultConfig,
134 fragmenter: StandardFragmenter,
135 codex: Option<Arc<dyn Codex>>,
139 monitor: Arc<dyn SecurityMonitor>,
142 keys: ArcSwap<HashMap<KeyId, KeyEntry>>,
146 failure_tracker: Mutex<HashMap<String, VecDeque<Instant>>>,
150 locked_out: AtomicBool,
154 master_hash: Option<[u8; 32]>,
159}
160
161impl KeyVault {
162 #[must_use]
172 pub fn is_locked_out(&self) -> bool {
173 self.inner.locked_out.load(Ordering::Acquire)
174 }
175
176 pub fn clear_lockout(&self) {
182 self.inner.locked_out.store(false, Ordering::Release);
183 if let Ok(mut tracker) = self.inner.failure_tracker.lock() {
184 tracker.clear();
185 }
186 }
187
188 pub fn report_failure(&self, key_name: &str, note: Option<&'static str>) {
204 let note = note.map_or(Cow::Borrowed(""), Cow::Borrowed);
205 let (count, oldest_in_window) = self.record_failure(key_name);
206 let window_elapsed = oldest_in_window.map(|t| t.elapsed()).unwrap_or_default();
207
208 let ctx = FailureContext {
210 key_name: key_name.to_string(),
211 consecutive_failures: count,
212 window_elapsed,
213 note: note.clone(),
214 };
215 self.inner.monitor.on_decryption_failure(&ctx);
216
217 let threshold = self.inner.config.max_failures_before_lockout;
219 if threshold > 0 && count >= threshold {
220 let was_locked = self.inner.locked_out.swap(true, Ordering::AcqRel);
223 let breach = ThresholdContext {
224 key_name: key_name.to_string(),
225 failures_in_window: count,
226 window: self.inner.config.failure_window,
227 lockout_triggered: !was_locked,
228 };
229 self.inner.monitor.on_threshold_breach(&breach);
230 }
231 }
232
233 pub fn report_anomalous_access(&self, key_name: &str, note: Option<&'static str>) {
240 let note = note.map_or(Cow::Borrowed(""), Cow::Borrowed);
241 let ctx = AccessContext {
242 key_name: key_name.to_string(),
243 note,
244 };
245 self.inner.monitor.on_anomalous_access(&ctx);
246 }
247
248 fn record_failure(&self, key_name: &str) -> (u32, Option<Instant>) {
252 let now = Instant::now();
253 let window = self.inner.config.failure_window;
254 let Ok(mut tracker) = self.inner.failure_tracker.lock() else {
255 return (1, Some(now));
260 };
261 let entries = tracker.entry(key_name.to_string()).or_default();
262 while let Some(front) = entries.front() {
264 if now.saturating_duration_since(*front) > window {
265 let _ = entries.pop_front();
266 } else {
267 break;
268 }
269 }
270 entries.push_back(now);
271 let count = u32::try_from(entries.len()).unwrap_or(u32::MAX);
272 let oldest = entries.front().copied();
273 (count, oldest)
274 }
275
276 #[must_use]
278 pub fn config(&self) -> &VaultConfig {
279 &self.inner.config
280 }
281
282 pub fn fragment(&self, key: &RawKey) -> Result<Fragments> {
301 if self.is_locked_out() {
302 return Err(Error::LockedOut);
303 }
304 let working = if self.inner.config.key_normalization {
305 blake3_normalize(key)
306 } else {
307 RawKey::new(key.as_bytes().to_vec())
308 };
309 let encoded = if let Some(codex) = &self.inner.codex {
310 codex_apply(codex.as_ref(), &working)
311 } else {
312 working
313 };
314 self.inner.fragmenter.fragment(&encoded)
315 }
316
317 pub fn defragment(&self, fragments: &Fragments) -> Result<RawKey> {
329 if self.is_locked_out() {
330 return Err(Error::LockedOut);
331 }
332 let encoded = self.inner.fragmenter.defragment(fragments)?;
333 if let Some(codex) = &self.inner.codex {
334 Ok(codex_apply(codex.as_ref(), &encoded))
335 } else {
336 Ok(encoded)
337 }
338 }
339
340 #[allow(clippy::needless_pass_by_value)]
362 pub fn register(&self, name: impl Into<String>, key: RawKey) -> Result<KeyHandle> {
363 if self.is_locked_out() {
364 return Err(Error::LockedOut);
365 }
366 let name: String = name.into();
367
368 let snapshot = self.inner.keys.load();
371 if snapshot.values().any(|e| e.name == name) {
372 return Err(Error::InvalidConfig(format!(
373 "key name {name:?} is already registered"
374 )));
375 }
376 drop(snapshot);
377
378 let key_len = key.len();
379 let fragments = self.fragment(&key)?;
380 let handle = KeyHandle::allocate();
381 let now = SystemTime::now()
382 .duration_since(UNIX_EPOCH)
383 .unwrap_or_default();
384 let metadata = KeyMetadata::new(now, key_len, None);
385
386 let entry = KeyEntry {
387 name,
388 fragments: Arc::new(fragments),
389 metadata,
390 };
391
392 let _previous = self.inner.keys.rcu(|current| {
394 let mut new_map = (**current).clone();
395 let _ = new_map.insert(
396 handle.id(),
397 KeyEntry {
398 name: entry.name.clone(),
399 fragments: Arc::clone(&entry.fragments),
400 metadata: entry.metadata.clone(),
401 },
402 );
403 new_map
404 });
405 Ok(handle)
406 }
407
408 pub fn unregister(&self, handle: KeyHandle) -> Result<()> {
417 let mut removed = false;
418 let _previous = self.inner.keys.rcu(|current| {
419 let mut new_map = (**current).clone();
420 removed = new_map.remove(&handle.id()).is_some();
421 new_map
422 });
423 if removed {
424 Ok(())
425 } else {
426 Err(Error::KeyNotFound)
427 }
428 }
429
430 pub fn with_key<F, T>(&self, handle: KeyHandle, f: F) -> Result<T>
451 where
452 F: FnOnce(&[u8]) -> T,
453 {
454 if self.is_locked_out() {
455 return Err(Error::LockedOut);
456 }
457 let snapshot = self.inner.keys.load();
458 let entry = snapshot.get(&handle.id()).ok_or(Error::KeyNotFound)?;
459 let fragments = Arc::clone(&entry.fragments);
460 drop(snapshot);
463
464 let encoded = self.inner.fragmenter.defragment(&fragments)?;
465 let raw = if let Some(codex) = &self.inner.codex {
466 codex_apply(codex.as_ref(), &encoded)
467 } else {
468 encoded
469 };
470 let result = f(raw.as_bytes());
472 Ok(result)
473 }
474
475 #[allow(clippy::needless_pass_by_value)]
494 pub fn rotate(&self, handle: KeyHandle, new_key: RawKey) -> Result<()> {
495 if self.is_locked_out() {
496 return Err(Error::LockedOut);
497 }
498
499 {
502 let snapshot = self.inner.keys.load();
503 if !snapshot.contains_key(&handle.id()) {
504 return Err(Error::KeyNotFound);
505 }
506 }
507
508 let new_len = new_key.len();
509 let new_fragments = Arc::new(self.fragment(&new_key)?);
510 let now = SystemTime::now()
511 .duration_since(UNIX_EPOCH)
512 .unwrap_or_default();
513 let new_metadata = KeyMetadata::new(now, new_len, None);
514
515 let mut found = false;
516 let _previous = self.inner.keys.rcu(|current| {
517 let mut new_map = (**current).clone();
518 if let Some(entry) = new_map.get_mut(&handle.id()) {
519 entry.fragments = Arc::clone(&new_fragments);
520 entry.metadata = new_metadata.clone();
521 found = true;
522 }
523 new_map
524 });
525 if found {
526 Ok(())
527 } else {
528 Err(Error::KeyNotFound)
531 }
532 }
533
534 #[must_use]
536 pub fn contains(&self, handle: KeyHandle) -> bool {
537 self.inner.keys.load().contains_key(&handle.id())
538 }
539
540 #[must_use]
546 pub fn metadata(&self, handle: KeyHandle) -> Option<KeyMetadata> {
547 self.inner
548 .keys
549 .load()
550 .get(&handle.id())
551 .map(|e| e.metadata.clone())
552 }
553
554 #[must_use]
556 pub fn handle_for_name(&self, name: &str) -> Option<KeyHandle> {
557 self.inner
558 .keys
559 .load()
560 .iter()
561 .find_map(|(id, entry)| (entry.name == name).then(|| KeyHandle::from_id(*id)))
562 }
563
564 #[must_use]
566 pub fn key_count(&self) -> usize {
567 self.inner.keys.load().len()
568 }
569
570 pub fn unlock_with_master(&self, attempt: &[u8]) -> Result<()> {
592 let stored = self.inner.master_hash.ok_or_else(|| {
593 Error::InvalidConfig(
594 "vault has no master key registered; pass with_master_key at build time"
595 .to_string(),
596 )
597 })?;
598 let attempt_hash = blake3::hash(attempt);
599 if bool::from(stored.as_slice().ct_eq(attempt_hash.as_bytes())) {
600 self.clear_lockout();
601 Ok(())
602 } else {
603 self.report_failure("<master>", Some("invalid master credential"));
606 Err(Error::Acquisition {
607 source: Cow::Borrowed("master"),
608 reason: "master credential did not match".to_string(),
609 })
610 }
611 }
612
613 #[must_use]
615 pub fn has_master_key(&self) -> bool {
616 self.inner.master_hash.is_some()
617 }
618}
619
620fn codex_apply(codex: &dyn Codex, key: &RawKey) -> RawKey {
626 let bytes: Vec<u8> = key.as_bytes().iter().map(|&b| codex.encode(b)).collect();
627 RawKey::new(bytes)
628}
629
630impl core::fmt::Debug for KeyVault {
631 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
632 f.debug_struct("KeyVault")
633 .field("locked_out", &self.is_locked_out())
634 .field("config", &self.inner.config)
635 .finish()
636 }
637}
638
639#[derive(Clone)]
645pub struct KeyVaultBuilder {
646 config: VaultConfig,
647 fragmenter: StandardFragmenter,
648 codex: Option<Arc<dyn Codex>>,
649 monitor: Option<Arc<dyn SecurityMonitor>>,
650 master_hash: Option<[u8; 32]>,
654}
655
656impl Default for KeyVaultBuilder {
657 fn default() -> Self {
658 Self::new()
659 }
660}
661
662impl core::fmt::Debug for KeyVaultBuilder {
663 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
664 f.debug_struct("KeyVaultBuilder")
665 .field("config", &self.config)
666 .field("fragmenter", &self.fragmenter)
667 .field("codex", &self.codex.as_ref().map(|_| "<set>"))
668 .field("monitor", &self.monitor.as_ref().map(|_| "<set>"))
669 .field("master_key", &self.master_hash.as_ref().map(|_| "<set>"))
670 .finish()
671 }
672}
673
674impl KeyVaultBuilder {
675 #[must_use]
678 pub fn new() -> Self {
679 Self {
680 config: VaultConfig::new(),
681 fragmenter: StandardFragmenter::new(),
682 codex: None,
683 monitor: None,
684 master_hash: None,
685 }
686 }
687
688 #[must_use]
695 pub fn normalize_with_blake3(mut self, enabled: bool) -> Self {
696 self.config.key_normalization = enabled;
697 self
698 }
699
700 #[must_use]
707 pub fn with_chunk_range(mut self, min: usize, max: usize) -> Self {
708 self.fragmenter = StandardFragmenter::with_chunk_range(min, max);
709 self
710 }
711
712 #[must_use]
740 pub fn with_codex<C>(mut self, codex: C) -> Self
741 where
742 C: Codex + 'static,
743 {
744 self.codex = Some(Arc::new(codex));
745 self
746 }
747
748 #[must_use]
762 pub fn with_decoy<D>(mut self, decoy: D) -> Self
763 where
764 D: DecoyStrategy + 'static,
765 {
766 self.fragmenter = self.fragmenter.with_decoy(decoy);
767 self
768 }
769
770 #[must_use]
782 pub fn with_monitor<M>(mut self, monitor: M) -> Self
783 where
784 M: SecurityMonitor + 'static,
785 {
786 self.monitor = Some(Arc::new(monitor));
787 self
788 }
789
790 #[must_use]
803 pub fn with_failure_threshold(mut self, max: u32, window: Duration) -> Self {
804 self.config.max_failures_before_lockout = max;
805 self.config.failure_window = window;
806 self
807 }
808
809 #[must_use]
821 pub fn with_master_key(mut self, master: RawKey) -> Self {
822 let hash = blake3::hash(master.as_bytes());
823 let mut bytes = [0u8; 32];
824 bytes.copy_from_slice(hash.as_bytes());
825 self.master_hash = Some(bytes);
826 drop(master);
828 self
829 }
830
831 #[must_use]
836 pub fn build(self) -> KeyVault {
837 let monitor: Arc<dyn SecurityMonitor> = self
838 .monitor
839 .unwrap_or_else(|| Arc::new(crate::monitor::NoMonitor));
840 KeyVault {
841 inner: Arc::new(VaultInner {
842 config: self.config,
843 fragmenter: self.fragmenter,
844 codex: self.codex,
845 monitor,
846 keys: ArcSwap::from_pointee(HashMap::new()),
847 failure_tracker: Mutex::new(HashMap::new()),
848 locked_out: AtomicBool::new(false),
849 master_hash: self.master_hash,
850 }),
851 }
852 }
853}
854
855#[cfg(test)]
856#[allow(clippy::unwrap_used, clippy::expect_used)]
857mod tests {
858 use super::*;
859 use alloc::format;
860
861 #[test]
862 fn builder_defaults_to_normalization_on() {
863 let v = KeyVaultBuilder::new().build();
864 assert!(v.config().key_normalization);
865 }
866
867 #[test]
868 fn builder_can_disable_normalization() {
869 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
870 assert!(!v.config().key_normalization);
871 }
872
873 #[test]
874 fn fresh_vault_is_not_locked_out() {
875 let v = KeyVaultBuilder::new().build();
876 assert!(!v.is_locked_out());
877 }
878
879 #[test]
880 fn debug_does_not_panic() {
881 let v = KeyVaultBuilder::new().build();
882 let _ = format!("{v:?}");
883 }
884
885 #[test]
886 fn fragment_defragment_roundtrip_with_normalization() {
887 let v = KeyVaultBuilder::new().build(); let raw = RawKey::new(b"hello world".to_vec());
889 let frags = v.fragment(&raw).unwrap();
890 let recovered = v.defragment(&frags).unwrap();
891 assert_eq!(recovered.len(), 32);
894 let frags2 = v.fragment(&raw).unwrap();
897 let recovered2 = v.defragment(&frags2).unwrap();
898 assert_eq!(recovered.as_bytes(), recovered2.as_bytes());
899 }
900
901 #[test]
902 fn fragment_defragment_roundtrip_without_normalization() {
903 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
904 let raw = RawKey::new((0u8..40).collect());
905 let frags = v.fragment(&raw).unwrap();
906 let recovered = v.defragment(&frags).unwrap();
907 assert_eq!(recovered.as_bytes(), raw.as_bytes());
908 }
909
910 #[test]
911 fn fragment_rejects_empty_key() {
912 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
913 let err = v
914 .fragment(&RawKey::new(alloc::vec::Vec::new()))
915 .unwrap_err();
916 assert!(matches!(err, crate::Error::Fragment(_)));
917 }
918
919 #[test]
920 fn chunk_range_propagates_through_builder() {
921 let v = KeyVaultBuilder::new()
922 .normalize_with_blake3(false)
923 .with_chunk_range(4, 6)
924 .build();
925 let raw = RawKey::new((0u8..30).collect());
926 let frags = v.fragment(&raw).unwrap();
927
928 let chunks = frags.chunks();
936 let mut below_min = 0;
937 let mut total = 0usize;
938 for c in chunks {
939 assert!(
940 c.len() >= 1 && c.len() <= 6,
941 "chunk size {} not in [1,6]",
942 c.len()
943 );
944 if c.len() < 4 {
945 below_min += 1;
946 }
947 total += c.len();
948 }
949 assert!(
950 below_min <= 1,
951 "more than one chunk below min size: {below_min}"
952 );
953 assert_eq!(total, 30);
954 }
955
956 #[test]
957 fn fragment_with_random_decoy_roundtrips() {
958 let v = KeyVaultBuilder::new()
959 .normalize_with_blake3(false)
960 .with_decoy(crate::RandomDecoy)
961 .build();
962 let raw = RawKey::new((0u8..32).collect());
963 let frags = v.fragment(&raw).unwrap();
964 let recovered = v.defragment(&frags).unwrap();
967 assert_eq!(recovered.as_bytes(), raw.as_bytes());
968 }
969
970 #[test]
971 fn fragment_with_self_reference_decoy_roundtrips() {
972 let v = KeyVaultBuilder::new()
973 .normalize_with_blake3(false)
974 .with_decoy(crate::SelfReferenceDecoy)
975 .build();
976 let raw = RawKey::new(b"some user-supplied key material".to_vec());
977 let frags = v.fragment(&raw).unwrap();
978 let recovered = v.defragment(&frags).unwrap();
979 assert_eq!(recovered.as_bytes(), raw.as_bytes());
980 }
981
982 #[test]
983 fn fragment_with_key_derived_decoy_roundtrips() {
984 let v = KeyVaultBuilder::new()
985 .normalize_with_blake3(false)
986 .with_decoy(crate::KeyDerivedDecoy)
987 .build();
988 let raw = RawKey::new((0u8..64).collect());
989 let frags = v.fragment(&raw).unwrap();
990 let recovered = v.defragment(&frags).unwrap();
991 assert_eq!(recovered.as_bytes(), raw.as_bytes());
992 }
993
994 #[test]
995 fn decoy_increases_chunk_count_relative_to_no_decoy() {
996 let no_decoy = KeyVaultBuilder::new()
997 .normalize_with_blake3(false)
998 .with_chunk_range(2, 4)
999 .build();
1000 let with_decoy = KeyVaultBuilder::new()
1001 .normalize_with_blake3(false)
1002 .with_chunk_range(2, 4)
1003 .with_decoy(crate::SelfReferenceDecoy)
1004 .build();
1005 let raw = RawKey::new((0u8..32).collect());
1006
1007 let mut no_decoy_total = 0usize;
1011 let mut decoy_total = 0usize;
1012 for _ in 0..8 {
1013 no_decoy_total += no_decoy.fragment(&raw).unwrap().chunk_count();
1014 decoy_total += with_decoy.fragment(&raw).unwrap().chunk_count();
1015 }
1016 assert!(
1021 decoy_total > no_decoy_total,
1022 "decoy vault produced {decoy_total} chunks vs no-decoy {no_decoy_total}"
1023 );
1024 }
1025
1026 #[test]
1027 fn fragment_with_static_codex_roundtrips() {
1028 use crate::StaticCodex;
1029 let codex = StaticCodex::from_swaps(&[(b'A', b'#'), (b'0', b'%')]).unwrap();
1030 let v = KeyVaultBuilder::new()
1031 .normalize_with_blake3(false)
1032 .with_codex(codex)
1033 .build();
1034 let raw = RawKey::new(b"A0A0A0A0".to_vec());
1035 let frags = v.fragment(&raw).unwrap();
1036 let recovered = v.defragment(&frags).unwrap();
1037 assert_eq!(recovered.as_bytes(), raw.as_bytes());
1040 }
1041
1042 #[test]
1043 fn fragment_with_dynamic_codex_roundtrips() {
1044 use crate::DynamicCodex;
1045 let v = KeyVaultBuilder::new()
1046 .normalize_with_blake3(false)
1047 .with_codex(DynamicCodex::new().unwrap())
1048 .build();
1049 let raw = RawKey::new((0u8..=255).collect());
1050 let frags = v.fragment(&raw).unwrap();
1051 let recovered = v.defragment(&frags).unwrap();
1052 assert_eq!(recovered.as_bytes(), raw.as_bytes());
1053 }
1054
1055 #[test]
1056 fn fragment_with_codex_and_decoy_and_normalization_roundtrips() {
1057 use crate::{DynamicCodex, SelfReferenceDecoy};
1058 let v = KeyVaultBuilder::new()
1061 .normalize_with_blake3(true)
1062 .with_codex(DynamicCodex::new().unwrap())
1063 .with_decoy(SelfReferenceDecoy)
1064 .build();
1065 let raw = RawKey::new(b"my application key".to_vec());
1066 let frags = v.fragment(&raw).unwrap();
1067 let recovered = v.defragment(&frags).unwrap();
1068 assert_eq!(recovered.len(), 32);
1071 let recovered2 = v.defragment(&v.fragment(&raw).unwrap()).unwrap();
1072 assert_eq!(recovered.as_bytes(), recovered2.as_bytes());
1073 }
1074
1075 #[test]
1076 fn codex_visibly_transforms_stored_bytes() {
1077 use crate::StaticCodex;
1082 let v = KeyVaultBuilder::new()
1083 .normalize_with_blake3(false)
1084 .with_codex(crate::DynamicCodex::new().unwrap())
1086 .build();
1087 let raw = RawKey::new(alloc::vec![0xaa; 8]);
1088 let frags = v.fragment(&raw).unwrap();
1089
1090 let mut saw_non_aa = false;
1093 for chunk in frags.chunks() {
1094 for &b in chunk.as_bytes() {
1095 if b != 0xaa {
1096 saw_non_aa = true;
1097 break;
1098 }
1099 }
1100 if saw_non_aa {
1101 break;
1102 }
1103 }
1104 assert!(
1105 saw_non_aa,
1106 "codex did not transform 0xaa — stored bytes still all 0xaa",
1107 );
1108
1109 let recovered = v.defragment(&frags).unwrap();
1111 assert_eq!(recovered.as_bytes(), raw.as_bytes());
1112 let _ = StaticCodex::from_swaps(&[]).unwrap();
1114 }
1115
1116 use core::sync::atomic::AtomicU32;
1119
1120 struct CountingMonitor {
1122 failures: AtomicU32,
1123 anomalies: AtomicU32,
1124 breaches: AtomicU32,
1125 }
1126
1127 impl CountingMonitor {
1128 fn new() -> Self {
1129 Self {
1130 failures: AtomicU32::new(0),
1131 anomalies: AtomicU32::new(0),
1132 breaches: AtomicU32::new(0),
1133 }
1134 }
1135 }
1136
1137 impl SecurityMonitor for CountingMonitor {
1138 fn on_decryption_failure(&self, _ctx: &FailureContext) {
1139 let _ = self.failures.fetch_add(1, Ordering::SeqCst);
1140 }
1141 fn on_anomalous_access(&self, _ctx: &AccessContext) {
1142 let _ = self.anomalies.fetch_add(1, Ordering::SeqCst);
1143 }
1144 fn on_threshold_breach(&self, _ctx: &ThresholdContext) {
1145 let _ = self.breaches.fetch_add(1, Ordering::SeqCst);
1146 }
1147 }
1148
1149 #[test]
1150 fn report_failure_fires_monitor() {
1151 let monitor = Arc::new(CountingMonitor::new());
1152 let v = KeyVaultBuilder::new()
1153 .with_monitor(Arc::clone(&monitor) as Arc<dyn SecurityMonitor>)
1154 .build();
1155 v.report_failure("k", None);
1156 v.report_failure("k", Some("test note"));
1157 assert_eq!(monitor.failures.load(Ordering::SeqCst), 2);
1158 assert_eq!(monitor.breaches.load(Ordering::SeqCst), 0);
1159 assert!(!v.is_locked_out());
1160 }
1161
1162 #[test]
1163 fn report_anomalous_access_fires_monitor() {
1164 let monitor = Arc::new(CountingMonitor::new());
1165 let v = KeyVaultBuilder::new()
1166 .with_monitor(Arc::clone(&monitor) as Arc<dyn SecurityMonitor>)
1167 .build();
1168 v.report_anomalous_access("k", None);
1169 assert_eq!(monitor.anomalies.load(Ordering::SeqCst), 1);
1170 assert!(!v.is_locked_out());
1171 }
1172
1173 #[test]
1174 fn threshold_lockout_fires_after_max_failures() {
1175 let monitor = Arc::new(CountingMonitor::new());
1176 let v = KeyVaultBuilder::new()
1177 .with_monitor(Arc::clone(&monitor) as Arc<dyn SecurityMonitor>)
1178 .with_failure_threshold(3, Duration::from_secs(30))
1179 .build();
1180
1181 v.report_failure("k", None);
1182 assert!(!v.is_locked_out());
1183 v.report_failure("k", None);
1184 assert!(!v.is_locked_out());
1185 v.report_failure("k", None);
1186 assert!(v.is_locked_out());
1188 assert_eq!(monitor.failures.load(Ordering::SeqCst), 3);
1189 assert_eq!(monitor.breaches.load(Ordering::SeqCst), 1);
1190
1191 v.report_failure("k", None);
1194 assert!(v.is_locked_out());
1195 assert_eq!(monitor.failures.load(Ordering::SeqCst), 4);
1196 assert_eq!(monitor.breaches.load(Ordering::SeqCst), 2);
1198 }
1199
1200 #[test]
1201 fn fragment_refuses_when_locked_out() {
1202 let v = KeyVaultBuilder::new()
1203 .normalize_with_blake3(false)
1204 .with_failure_threshold(1, Duration::from_secs(30))
1205 .build();
1206 v.report_failure("k", None);
1207 assert!(v.is_locked_out());
1208
1209 let err = v
1210 .fragment(&RawKey::new(alloc::vec![1u8, 2, 3, 4]))
1211 .unwrap_err();
1212 assert!(matches!(err, Error::LockedOut));
1213 }
1214
1215 #[test]
1216 fn defragment_refuses_when_locked_out() {
1217 let v = KeyVaultBuilder::new()
1218 .normalize_with_blake3(false)
1219 .with_failure_threshold(2, Duration::from_secs(30))
1220 .build();
1221 let raw = RawKey::new(alloc::vec![1u8; 16]);
1223 let frags = v.fragment(&raw).unwrap();
1224 v.report_failure("k", None);
1225 v.report_failure("k", None);
1226 assert!(v.is_locked_out());
1227
1228 let err = v.defragment(&frags).unwrap_err();
1229 assert!(matches!(err, Error::LockedOut));
1230 }
1231
1232 #[test]
1233 fn clear_lockout_resets_state() {
1234 let v = KeyVaultBuilder::new()
1235 .with_failure_threshold(1, Duration::from_secs(30))
1236 .build();
1237 v.report_failure("k", None);
1238 assert!(v.is_locked_out());
1239 v.clear_lockout();
1240 assert!(!v.is_locked_out());
1241 v.clear_lockout();
1249 assert!(!v.is_locked_out());
1250 }
1251
1252 #[test]
1253 fn per_key_failure_counts_are_independent() {
1254 let monitor = Arc::new(CountingMonitor::new());
1255 let v = KeyVaultBuilder::new()
1256 .with_monitor(Arc::clone(&monitor) as Arc<dyn SecurityMonitor>)
1257 .with_failure_threshold(2, Duration::from_secs(30))
1258 .build();
1259 v.report_failure("alpha", None);
1260 v.report_failure("beta", None);
1261 assert!(!v.is_locked_out());
1263 assert_eq!(monitor.failures.load(Ordering::SeqCst), 2);
1264 v.report_failure("alpha", None);
1265 assert!(v.is_locked_out());
1267 }
1268
1269 #[test]
1272 fn register_returns_handle_and_increments_count() {
1273 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1274 assert_eq!(v.key_count(), 0);
1275 let h = v
1276 .register("primary", RawKey::new(alloc::vec![1u8; 32]))
1277 .unwrap();
1278 assert_eq!(v.key_count(), 1);
1279 assert!(v.contains(h));
1280 }
1281
1282 #[test]
1283 fn register_rejects_duplicate_name() {
1284 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1285 let _ = v
1286 .register("primary", RawKey::new(alloc::vec![1u8; 16]))
1287 .unwrap();
1288 let err = v
1289 .register("primary", RawKey::new(alloc::vec![2u8; 16]))
1290 .unwrap_err();
1291 assert!(matches!(err, Error::InvalidConfig(_)));
1292 }
1293
1294 #[test]
1295 fn unregister_removes_key() {
1296 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1297 let h = v
1298 .register("primary", RawKey::new(alloc::vec![1u8; 16]))
1299 .unwrap();
1300 assert!(v.contains(h));
1301 v.unregister(h).unwrap();
1302 assert!(!v.contains(h));
1303 assert_eq!(v.key_count(), 0);
1304 }
1305
1306 #[test]
1307 fn unregister_unknown_handle_errors() {
1308 let v = KeyVaultBuilder::new().build();
1309 let h = KeyHandle::__for_test();
1310 let err = v.unregister(h).unwrap_err();
1311 assert!(matches!(err, Error::KeyNotFound));
1312 }
1313
1314 #[test]
1315 fn with_key_round_trips_bytes() {
1316 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1317 let original = alloc::vec![0xa5u8; 32];
1318 let h = v.register("data", RawKey::new(original.clone())).unwrap();
1319 let observed = v.with_key(h, <[u8]>::to_vec).unwrap();
1320 assert_eq!(observed, original);
1321 }
1322
1323 #[test]
1324 fn with_key_normalization_changes_output_length() {
1325 let v = KeyVaultBuilder::new().build(); let h = v
1327 .register("data", RawKey::new(alloc::vec![0xa5; 17]))
1328 .unwrap();
1329 let observed_len = v.with_key(h, <[u8]>::len).unwrap();
1330 assert_eq!(observed_len, 32);
1332 }
1333
1334 #[test]
1335 fn with_key_unknown_handle_errors() {
1336 let v = KeyVaultBuilder::new().build();
1337 let h = KeyHandle::__for_test();
1338 let err = v.with_key(h, |_| ()).unwrap_err();
1339 assert!(matches!(err, Error::KeyNotFound));
1340 }
1341
1342 #[test]
1343 fn rotate_swaps_key_bytes() {
1344 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1345 let h = v
1346 .register("data", RawKey::new(alloc::vec![1u8; 16]))
1347 .unwrap();
1348
1349 v.rotate(h, RawKey::new(alloc::vec![2u8; 16])).unwrap();
1350 let observed = v.with_key(h, <[u8]>::to_vec).unwrap();
1351 assert_eq!(observed, alloc::vec![2u8; 16]);
1352 }
1353
1354 #[test]
1355 fn rotate_unknown_handle_errors() {
1356 let v = KeyVaultBuilder::new().build();
1357 let h = KeyHandle::__for_test();
1358 let err = v.rotate(h, RawKey::new(alloc::vec![0u8; 16])).unwrap_err();
1359 assert!(matches!(err, Error::KeyNotFound));
1360 }
1361
1362 #[test]
1363 fn handle_for_name_finds_registered_key() {
1364 let v = KeyVaultBuilder::new().build();
1365 let h = v
1366 .register("primary", RawKey::new(alloc::vec![0u8; 16]))
1367 .unwrap();
1368 assert_eq!(v.handle_for_name("primary"), Some(h));
1369 assert_eq!(v.handle_for_name("missing"), None);
1370 }
1371
1372 #[test]
1373 fn metadata_records_registration_length() {
1374 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1375 let h = v
1376 .register("data", RawKey::new(alloc::vec![0u8; 42]))
1377 .unwrap();
1378 let meta = v.metadata(h).expect("metadata");
1379 assert_eq!(meta.length(), 42);
1380 }
1381
1382 #[test]
1383 fn registered_key_refuses_access_when_locked_out() {
1384 let v = KeyVaultBuilder::new()
1385 .with_failure_threshold(1, Duration::from_secs(30))
1386 .build();
1387 let h = v
1388 .register("data", RawKey::new(alloc::vec![0xa5; 16]))
1389 .unwrap();
1390 v.report_failure("data", None);
1391 assert!(v.is_locked_out());
1392
1393 let err = v.with_key(h, |_| ()).unwrap_err();
1394 assert!(matches!(err, Error::LockedOut));
1395 let err = v.rotate(h, RawKey::new(alloc::vec![0u8; 16])).unwrap_err();
1396 assert!(matches!(err, Error::LockedOut));
1397 }
1398
1399 #[test]
1400 fn master_key_unlock_clears_lockout_on_match() {
1401 let master_bytes = b"correct horse battery staple".to_vec();
1402 let v = KeyVaultBuilder::new()
1403 .with_master_key(RawKey::new(master_bytes.clone()))
1404 .with_failure_threshold(1, Duration::from_secs(30))
1405 .build();
1406 assert!(v.has_master_key());
1407
1408 v.report_failure("k", None);
1409 assert!(v.is_locked_out());
1410
1411 let err = v.unlock_with_master(b"wrong").unwrap_err();
1413 assert!(matches!(err, Error::Acquisition { .. }));
1414 assert!(v.is_locked_out());
1415
1416 v.unlock_with_master(&master_bytes).unwrap();
1418 assert!(!v.is_locked_out());
1419 }
1420
1421 #[test]
1422 fn master_key_unlock_without_registered_master_errors() {
1423 let v = KeyVaultBuilder::new().build();
1424 assert!(!v.has_master_key());
1425 let err = v.unlock_with_master(b"anything").unwrap_err();
1426 assert!(matches!(err, Error::InvalidConfig(_)));
1427 }
1428
1429 #[test]
1430 fn composite_monitor_chains_to_all_inner() {
1431 use crate::CompositeMonitor;
1432 let a = Arc::new(CountingMonitor::new());
1433 let b = Arc::new(CountingMonitor::new());
1434 let composite = CompositeMonitor::new(alloc::vec![
1435 Arc::clone(&a) as Arc<dyn SecurityMonitor>,
1436 Arc::clone(&b) as Arc<dyn SecurityMonitor>,
1437 ]);
1438 let v = KeyVaultBuilder::new()
1439 .with_monitor(composite)
1440 .with_failure_threshold(1, Duration::from_secs(30))
1441 .build();
1442 v.report_failure("k", None);
1443 assert_eq!(a.failures.load(Ordering::SeqCst), 1);
1444 assert_eq!(b.failures.load(Ordering::SeqCst), 1);
1445 assert_eq!(a.breaches.load(Ordering::SeqCst), 1);
1446 assert_eq!(b.breaches.load(Ordering::SeqCst), 1);
1447 }
1448}