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 let timestamp = SystemTime::now()
262 .duration_since(UNIX_EPOCH)
263 .unwrap_or_default();
264 let event = AuditEvent {
265 timestamp,
266 key_name: key_name.to_string(),
267 kind,
268 thread_id: std::thread::current().id(),
269 note,
270 };
271 self.inner.audit.on_event(&event);
272 }
273
274 fn record_failure(&self, key_name: &str) -> (u32, Option<Instant>) {
278 let now = Instant::now();
279 let window = self.inner.config.failure_window;
280 let Ok(mut tracker) = self.inner.failure_tracker.lock() else {
281 return (1, Some(now));
286 };
287 let entries = tracker.entry(key_name.to_string()).or_default();
288 while let Some(front) = entries.front() {
290 if now.saturating_duration_since(*front) > window {
291 let _ = entries.pop_front();
292 } else {
293 break;
294 }
295 }
296 entries.push_back(now);
297 let count = u32::try_from(entries.len()).unwrap_or(u32::MAX);
298 let oldest = entries.front().copied();
299 (count, oldest)
300 }
301
302 #[must_use]
304 pub fn config(&self) -> &VaultConfig {
305 &self.inner.config
306 }
307
308 pub fn fragment(&self, key: &RawKey) -> Result<Fragments> {
327 if self.is_locked_out() {
328 return Err(Error::LockedOut);
329 }
330 let working = if self.inner.config.key_normalization {
331 blake3_normalize(key)
332 } else {
333 RawKey::new(key.as_bytes().to_vec())
334 };
335 let encoded = if let Some(codex) = &self.inner.codex {
336 codex_apply(codex.as_ref(), &working)
337 } else {
338 working
339 };
340 let result = self.inner.fragmenter.fragment(&encoded);
341 if result.is_ok() {
342 self.emit_audit("", AccessKind::OneShotFragment, Cow::Borrowed(""));
343 }
344 result
345 }
346
347 pub fn defragment(&self, fragments: &Fragments) -> Result<RawKey> {
359 if self.is_locked_out() {
360 return Err(Error::LockedOut);
361 }
362 let encoded = self.inner.fragmenter.defragment(fragments)?;
363 let decoded = if let Some(codex) = &self.inner.codex {
364 codex_apply(codex.as_ref(), &encoded)
365 } else {
366 encoded
367 };
368 self.emit_audit("", AccessKind::OneShotDefragment, Cow::Borrowed(""));
369 Ok(decoded)
370 }
371
372 #[allow(clippy::needless_pass_by_value)]
394 pub fn register(&self, name: impl Into<String>, key: RawKey) -> Result<KeyHandle> {
395 if self.is_locked_out() {
396 return Err(Error::LockedOut);
397 }
398 let name: String = name.into();
399
400 let snapshot = self.inner.keys.load();
403 if snapshot.values().any(|e| e.name == name) {
404 return Err(Error::InvalidConfig(format!(
405 "key name {name:?} is already registered"
406 )));
407 }
408 drop(snapshot);
409
410 let key_len = key.len();
411 let fragments = self.fragment(&key)?;
412 let handle = KeyHandle::allocate();
413 let now = SystemTime::now()
414 .duration_since(UNIX_EPOCH)
415 .unwrap_or_default();
416 let metadata = KeyMetadata::new(now, key_len, None);
417
418 let entry = KeyEntry {
419 name,
420 fragments: Arc::new(fragments),
421 metadata,
422 };
423
424 let _previous = self.inner.keys.rcu(|current| {
426 let mut new_map = (**current).clone();
427 let _ = new_map.insert(
428 handle.id(),
429 KeyEntry {
430 name: entry.name.clone(),
431 fragments: Arc::clone(&entry.fragments),
432 metadata: entry.metadata.clone(),
433 },
434 );
435 new_map
436 });
437 self.emit_audit(&entry.name, AccessKind::Register, Cow::Borrowed(""));
438 Ok(handle)
439 }
440
441 pub fn unregister(&self, handle: KeyHandle) -> Result<()> {
450 let name = self
452 .inner
453 .keys
454 .load()
455 .get(&handle.id())
456 .map(|e| e.name.clone());
457 let mut removed = false;
458 let _previous = self.inner.keys.rcu(|current| {
459 let mut new_map = (**current).clone();
460 removed = new_map.remove(&handle.id()).is_some();
461 new_map
462 });
463 if removed {
464 if let Some(name) = name {
465 self.emit_audit(&name, AccessKind::Unregister, Cow::Borrowed(""));
466 }
467 Ok(())
468 } else {
469 Err(Error::KeyNotFound)
470 }
471 }
472
473 pub fn with_key<F, T>(&self, handle: KeyHandle, f: F) -> Result<T>
494 where
495 F: FnOnce(&[u8]) -> T,
496 {
497 if self.is_locked_out() {
498 return Err(Error::LockedOut);
499 }
500 let snapshot = self.inner.keys.load();
501 let entry = snapshot.get(&handle.id()).ok_or(Error::KeyNotFound)?;
502 let fragments = Arc::clone(&entry.fragments);
503 let name = entry.name.clone();
504 drop(snapshot);
507
508 let encoded = self.inner.fragmenter.defragment(&fragments)?;
509 let raw = if let Some(codex) = &self.inner.codex {
510 codex_apply(codex.as_ref(), &encoded)
511 } else {
512 encoded
513 };
514 let result = f(raw.as_bytes());
516 self.emit_audit(&name, AccessKind::Read, Cow::Borrowed(""));
517 Ok(result)
518 }
519
520 #[allow(clippy::needless_pass_by_value)]
539 pub fn rotate(&self, handle: KeyHandle, new_key: RawKey) -> Result<()> {
540 if self.is_locked_out() {
541 return Err(Error::LockedOut);
542 }
543
544 let name = {
548 let snapshot = self.inner.keys.load();
549 snapshot
550 .get(&handle.id())
551 .map(|e| e.name.clone())
552 .ok_or(Error::KeyNotFound)?
553 };
554
555 let new_len = new_key.len();
556 let new_fragments = Arc::new(self.fragment(&new_key)?);
557 let now = SystemTime::now()
558 .duration_since(UNIX_EPOCH)
559 .unwrap_or_default();
560 let new_metadata = KeyMetadata::new(now, new_len, None);
561
562 let mut found = false;
563 let _previous = self.inner.keys.rcu(|current| {
564 let mut new_map = (**current).clone();
565 if let Some(entry) = new_map.get_mut(&handle.id()) {
566 entry.fragments = Arc::clone(&new_fragments);
567 entry.metadata = new_metadata.clone();
568 found = true;
569 }
570 new_map
571 });
572 if found {
573 self.emit_audit(&name, AccessKind::Rotate, Cow::Borrowed(""));
574 Ok(())
575 } else {
576 Err(Error::KeyNotFound)
579 }
580 }
581
582 #[must_use]
584 pub fn contains(&self, handle: KeyHandle) -> bool {
585 self.inner.keys.load().contains_key(&handle.id())
586 }
587
588 #[must_use]
594 pub fn metadata(&self, handle: KeyHandle) -> Option<KeyMetadata> {
595 self.inner
596 .keys
597 .load()
598 .get(&handle.id())
599 .map(|e| e.metadata.clone())
600 }
601
602 #[must_use]
604 pub fn handle_for_name(&self, name: &str) -> Option<KeyHandle> {
605 self.inner
606 .keys
607 .load()
608 .iter()
609 .find_map(|(id, entry)| (entry.name == name).then(|| KeyHandle::from_id(*id)))
610 }
611
612 #[must_use]
614 pub fn key_count(&self) -> usize {
615 self.inner.keys.load().len()
616 }
617
618 pub fn unlock_with_master(&self, attempt: &[u8]) -> Result<()> {
640 let stored = self.inner.master_hash.ok_or_else(|| {
641 Error::InvalidConfig(
642 "vault has no master key registered; pass with_master_key at build time"
643 .to_string(),
644 )
645 })?;
646 let attempt_hash = blake3::hash(attempt);
647 let matched = bool::from(stored.as_slice().ct_eq(attempt_hash.as_bytes()));
648 self.emit_audit(
649 "<master>",
650 AccessKind::MasterUnlockAttempt { matched },
651 Cow::Borrowed(""),
652 );
653 if matched {
654 self.clear_lockout();
655 Ok(())
656 } else {
657 self.report_failure("<master>", Some("invalid master credential"));
660 Err(Error::Acquisition {
661 source: Cow::Borrowed("master"),
662 reason: "master credential did not match".to_string(),
663 })
664 }
665 }
666
667 #[must_use]
669 pub fn has_master_key(&self) -> bool {
670 self.inner.master_hash.is_some()
671 }
672}
673
674fn codex_apply(codex: &dyn Codex, key: &RawKey) -> RawKey {
680 let bytes: Vec<u8> = key.as_bytes().iter().map(|&b| codex.encode(b)).collect();
681 RawKey::new(bytes)
682}
683
684impl core::fmt::Debug for KeyVault {
685 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
686 f.debug_struct("KeyVault")
687 .field("locked_out", &self.is_locked_out())
688 .field("config", &self.inner.config)
689 .finish()
690 }
691}
692
693#[derive(Clone)]
699pub struct KeyVaultBuilder {
700 config: VaultConfig,
701 fragmenter: StandardFragmenter,
702 codex: Option<Arc<dyn Codex>>,
703 monitor: Option<Arc<dyn SecurityMonitor>>,
704 audit: Option<Arc<dyn AuditSink>>,
707 master_hash: Option<[u8; 32]>,
711}
712
713impl Default for KeyVaultBuilder {
714 fn default() -> Self {
715 Self::new()
716 }
717}
718
719impl core::fmt::Debug for KeyVaultBuilder {
720 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
721 f.debug_struct("KeyVaultBuilder")
722 .field("config", &self.config)
723 .field("fragmenter", &self.fragmenter)
724 .field("codex", &self.codex.as_ref().map(|_| "<set>"))
725 .field("monitor", &self.monitor.as_ref().map(|_| "<set>"))
726 .field("audit", &self.audit.as_ref().map(|_| "<set>"))
727 .field("master_key", &self.master_hash.as_ref().map(|_| "<set>"))
728 .finish()
729 }
730}
731
732impl KeyVaultBuilder {
733 #[must_use]
736 pub fn new() -> Self {
737 Self {
738 config: VaultConfig::new(),
739 fragmenter: StandardFragmenter::new(),
740 codex: None,
741 monitor: None,
742 audit: None,
743 master_hash: None,
744 }
745 }
746
747 #[must_use]
754 pub fn normalize_with_blake3(mut self, enabled: bool) -> Self {
755 self.config.key_normalization = enabled;
756 self
757 }
758
759 #[must_use]
766 pub fn with_chunk_range(mut self, min: usize, max: usize) -> Self {
767 self.fragmenter = StandardFragmenter::with_chunk_range(min, max);
768 self
769 }
770
771 #[must_use]
799 pub fn with_codex<C>(mut self, codex: C) -> Self
800 where
801 C: Codex + 'static,
802 {
803 self.codex = Some(Arc::new(codex));
804 self
805 }
806
807 #[must_use]
821 pub fn with_decoy<D>(mut self, decoy: D) -> Self
822 where
823 D: DecoyStrategy + 'static,
824 {
825 self.fragmenter = self.fragmenter.with_decoy(decoy);
826 self
827 }
828
829 #[must_use]
841 pub fn with_monitor<M>(mut self, monitor: M) -> Self
842 where
843 M: SecurityMonitor + 'static,
844 {
845 self.monitor = Some(Arc::new(monitor));
846 self
847 }
848
849 #[must_use]
862 pub fn with_failure_threshold(mut self, max: u32, window: Duration) -> Self {
863 self.config.max_failures_before_lockout = max;
864 self.config.failure_window = window;
865 self
866 }
867
868 #[must_use]
879 pub fn with_audit_sink<A>(mut self, sink: A) -> Self
880 where
881 A: AuditSink + 'static,
882 {
883 self.audit = Some(Arc::new(sink));
884 self
885 }
886
887 #[must_use]
899 pub fn with_master_key(mut self, master: RawKey) -> Self {
900 let hash = blake3::hash(master.as_bytes());
901 let mut bytes = [0u8; 32];
902 bytes.copy_from_slice(hash.as_bytes());
903 self.master_hash = Some(bytes);
904 drop(master);
906 self
907 }
908
909 #[must_use]
914 pub fn build(self) -> KeyVault {
915 let monitor: Arc<dyn SecurityMonitor> = self
916 .monitor
917 .unwrap_or_else(|| Arc::new(crate::monitor::NoMonitor));
918 let audit: Arc<dyn AuditSink> = self
919 .audit
920 .unwrap_or_else(|| Arc::new(crate::audit::NoAudit));
921 KeyVault {
922 inner: Arc::new(VaultInner {
923 config: self.config,
924 fragmenter: self.fragmenter,
925 codex: self.codex,
926 monitor,
927 keys: ArcSwap::from_pointee(HashMap::new()),
928 failure_tracker: Mutex::new(HashMap::new()),
929 locked_out: AtomicBool::new(false),
930 master_hash: self.master_hash,
931 audit,
932 }),
933 }
934 }
935}
936
937#[cfg(test)]
938#[allow(clippy::unwrap_used, clippy::expect_used)]
939mod tests {
940 use super::*;
941 use alloc::format;
942
943 #[test]
944 fn builder_defaults_to_normalization_on() {
945 let v = KeyVaultBuilder::new().build();
946 assert!(v.config().key_normalization);
947 }
948
949 #[test]
950 fn builder_can_disable_normalization() {
951 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
952 assert!(!v.config().key_normalization);
953 }
954
955 #[test]
956 fn fresh_vault_is_not_locked_out() {
957 let v = KeyVaultBuilder::new().build();
958 assert!(!v.is_locked_out());
959 }
960
961 #[test]
962 fn debug_does_not_panic() {
963 let v = KeyVaultBuilder::new().build();
964 let _ = format!("{v:?}");
965 }
966
967 #[test]
968 fn fragment_defragment_roundtrip_with_normalization() {
969 let v = KeyVaultBuilder::new().build(); let raw = RawKey::new(b"hello world".to_vec());
971 let frags = v.fragment(&raw).unwrap();
972 let recovered = v.defragment(&frags).unwrap();
973 assert_eq!(recovered.len(), 32);
976 let frags2 = v.fragment(&raw).unwrap();
979 let recovered2 = v.defragment(&frags2).unwrap();
980 assert_eq!(recovered.as_bytes(), recovered2.as_bytes());
981 }
982
983 #[test]
984 fn fragment_defragment_roundtrip_without_normalization() {
985 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
986 let raw = RawKey::new((0u8..40).collect());
987 let frags = v.fragment(&raw).unwrap();
988 let recovered = v.defragment(&frags).unwrap();
989 assert_eq!(recovered.as_bytes(), raw.as_bytes());
990 }
991
992 #[test]
993 fn fragment_rejects_empty_key() {
994 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
995 let err = v
996 .fragment(&RawKey::new(alloc::vec::Vec::new()))
997 .unwrap_err();
998 assert!(matches!(err, crate::Error::Fragment(_)));
999 }
1000
1001 #[test]
1002 fn chunk_range_propagates_through_builder() {
1003 let v = KeyVaultBuilder::new()
1004 .normalize_with_blake3(false)
1005 .with_chunk_range(4, 6)
1006 .build();
1007 let raw = RawKey::new((0u8..30).collect());
1008 let frags = v.fragment(&raw).unwrap();
1009
1010 let chunks = frags.chunks();
1018 let mut below_min = 0;
1019 let mut total = 0usize;
1020 for c in chunks {
1021 assert!(
1022 c.len() >= 1 && c.len() <= 6,
1023 "chunk size {} not in [1,6]",
1024 c.len()
1025 );
1026 if c.len() < 4 {
1027 below_min += 1;
1028 }
1029 total += c.len();
1030 }
1031 assert!(
1032 below_min <= 1,
1033 "more than one chunk below min size: {below_min}"
1034 );
1035 assert_eq!(total, 30);
1036 }
1037
1038 #[test]
1039 fn fragment_with_random_decoy_roundtrips() {
1040 let v = KeyVaultBuilder::new()
1041 .normalize_with_blake3(false)
1042 .with_decoy(crate::RandomDecoy)
1043 .build();
1044 let raw = RawKey::new((0u8..32).collect());
1045 let frags = v.fragment(&raw).unwrap();
1046 let recovered = v.defragment(&frags).unwrap();
1049 assert_eq!(recovered.as_bytes(), raw.as_bytes());
1050 }
1051
1052 #[test]
1053 fn fragment_with_self_reference_decoy_roundtrips() {
1054 let v = KeyVaultBuilder::new()
1055 .normalize_with_blake3(false)
1056 .with_decoy(crate::SelfReferenceDecoy)
1057 .build();
1058 let raw = RawKey::new(b"some user-supplied key material".to_vec());
1059 let frags = v.fragment(&raw).unwrap();
1060 let recovered = v.defragment(&frags).unwrap();
1061 assert_eq!(recovered.as_bytes(), raw.as_bytes());
1062 }
1063
1064 #[test]
1065 fn fragment_with_key_derived_decoy_roundtrips() {
1066 let v = KeyVaultBuilder::new()
1067 .normalize_with_blake3(false)
1068 .with_decoy(crate::KeyDerivedDecoy)
1069 .build();
1070 let raw = RawKey::new((0u8..64).collect());
1071 let frags = v.fragment(&raw).unwrap();
1072 let recovered = v.defragment(&frags).unwrap();
1073 assert_eq!(recovered.as_bytes(), raw.as_bytes());
1074 }
1075
1076 #[test]
1077 fn decoy_increases_chunk_count_relative_to_no_decoy() {
1078 let no_decoy = KeyVaultBuilder::new()
1079 .normalize_with_blake3(false)
1080 .with_chunk_range(2, 4)
1081 .build();
1082 let with_decoy = KeyVaultBuilder::new()
1083 .normalize_with_blake3(false)
1084 .with_chunk_range(2, 4)
1085 .with_decoy(crate::SelfReferenceDecoy)
1086 .build();
1087 let raw = RawKey::new((0u8..32).collect());
1088
1089 let mut no_decoy_total = 0usize;
1093 let mut decoy_total = 0usize;
1094 for _ in 0..8 {
1095 no_decoy_total += no_decoy.fragment(&raw).unwrap().chunk_count();
1096 decoy_total += with_decoy.fragment(&raw).unwrap().chunk_count();
1097 }
1098 assert!(
1103 decoy_total > no_decoy_total,
1104 "decoy vault produced {decoy_total} chunks vs no-decoy {no_decoy_total}"
1105 );
1106 }
1107
1108 #[test]
1109 fn fragment_with_static_codex_roundtrips() {
1110 use crate::StaticCodex;
1111 let codex = StaticCodex::from_swaps(&[(b'A', b'#'), (b'0', b'%')]).unwrap();
1112 let v = KeyVaultBuilder::new()
1113 .normalize_with_blake3(false)
1114 .with_codex(codex)
1115 .build();
1116 let raw = RawKey::new(b"A0A0A0A0".to_vec());
1117 let frags = v.fragment(&raw).unwrap();
1118 let recovered = v.defragment(&frags).unwrap();
1119 assert_eq!(recovered.as_bytes(), raw.as_bytes());
1122 }
1123
1124 #[test]
1125 fn fragment_with_dynamic_codex_roundtrips() {
1126 use crate::DynamicCodex;
1127 let v = KeyVaultBuilder::new()
1128 .normalize_with_blake3(false)
1129 .with_codex(DynamicCodex::new().unwrap())
1130 .build();
1131 let raw = RawKey::new((0u8..=255).collect());
1132 let frags = v.fragment(&raw).unwrap();
1133 let recovered = v.defragment(&frags).unwrap();
1134 assert_eq!(recovered.as_bytes(), raw.as_bytes());
1135 }
1136
1137 #[test]
1138 fn fragment_with_codex_and_decoy_and_normalization_roundtrips() {
1139 use crate::{DynamicCodex, SelfReferenceDecoy};
1140 let v = KeyVaultBuilder::new()
1143 .normalize_with_blake3(true)
1144 .with_codex(DynamicCodex::new().unwrap())
1145 .with_decoy(SelfReferenceDecoy)
1146 .build();
1147 let raw = RawKey::new(b"my application key".to_vec());
1148 let frags = v.fragment(&raw).unwrap();
1149 let recovered = v.defragment(&frags).unwrap();
1150 assert_eq!(recovered.len(), 32);
1153 let recovered2 = v.defragment(&v.fragment(&raw).unwrap()).unwrap();
1154 assert_eq!(recovered.as_bytes(), recovered2.as_bytes());
1155 }
1156
1157 #[test]
1158 fn codex_visibly_transforms_stored_bytes() {
1159 use crate::StaticCodex;
1164 let v = KeyVaultBuilder::new()
1165 .normalize_with_blake3(false)
1166 .with_codex(crate::DynamicCodex::new().unwrap())
1168 .build();
1169 let raw = RawKey::new(alloc::vec![0xaa; 8]);
1170 let frags = v.fragment(&raw).unwrap();
1171
1172 let mut saw_non_aa = false;
1175 for chunk in frags.chunks() {
1176 for &b in chunk.as_bytes() {
1177 if b != 0xaa {
1178 saw_non_aa = true;
1179 break;
1180 }
1181 }
1182 if saw_non_aa {
1183 break;
1184 }
1185 }
1186 assert!(
1187 saw_non_aa,
1188 "codex did not transform 0xaa — stored bytes still all 0xaa",
1189 );
1190
1191 let recovered = v.defragment(&frags).unwrap();
1193 assert_eq!(recovered.as_bytes(), raw.as_bytes());
1194 let _ = StaticCodex::from_swaps(&[]).unwrap();
1196 }
1197
1198 use core::sync::atomic::AtomicU32;
1201
1202 struct CountingMonitor {
1204 failures: AtomicU32,
1205 anomalies: AtomicU32,
1206 breaches: AtomicU32,
1207 }
1208
1209 impl CountingMonitor {
1210 fn new() -> Self {
1211 Self {
1212 failures: AtomicU32::new(0),
1213 anomalies: AtomicU32::new(0),
1214 breaches: AtomicU32::new(0),
1215 }
1216 }
1217 }
1218
1219 impl SecurityMonitor for CountingMonitor {
1220 fn on_decryption_failure(&self, _ctx: &FailureContext) {
1221 let _ = self.failures.fetch_add(1, Ordering::SeqCst);
1222 }
1223 fn on_anomalous_access(&self, _ctx: &AccessContext) {
1224 let _ = self.anomalies.fetch_add(1, Ordering::SeqCst);
1225 }
1226 fn on_threshold_breach(&self, _ctx: &ThresholdContext) {
1227 let _ = self.breaches.fetch_add(1, Ordering::SeqCst);
1228 }
1229 }
1230
1231 #[test]
1232 fn report_failure_fires_monitor() {
1233 let monitor = Arc::new(CountingMonitor::new());
1234 let v = KeyVaultBuilder::new()
1235 .with_monitor(Arc::clone(&monitor) as Arc<dyn SecurityMonitor>)
1236 .build();
1237 v.report_failure("k", None);
1238 v.report_failure("k", Some("test note"));
1239 assert_eq!(monitor.failures.load(Ordering::SeqCst), 2);
1240 assert_eq!(monitor.breaches.load(Ordering::SeqCst), 0);
1241 assert!(!v.is_locked_out());
1242 }
1243
1244 #[test]
1245 fn report_anomalous_access_fires_monitor() {
1246 let monitor = Arc::new(CountingMonitor::new());
1247 let v = KeyVaultBuilder::new()
1248 .with_monitor(Arc::clone(&monitor) as Arc<dyn SecurityMonitor>)
1249 .build();
1250 v.report_anomalous_access("k", None);
1251 assert_eq!(monitor.anomalies.load(Ordering::SeqCst), 1);
1252 assert!(!v.is_locked_out());
1253 }
1254
1255 #[test]
1256 fn threshold_lockout_fires_after_max_failures() {
1257 let monitor = Arc::new(CountingMonitor::new());
1258 let v = KeyVaultBuilder::new()
1259 .with_monitor(Arc::clone(&monitor) as Arc<dyn SecurityMonitor>)
1260 .with_failure_threshold(3, Duration::from_secs(30))
1261 .build();
1262
1263 v.report_failure("k", None);
1264 assert!(!v.is_locked_out());
1265 v.report_failure("k", None);
1266 assert!(!v.is_locked_out());
1267 v.report_failure("k", None);
1268 assert!(v.is_locked_out());
1270 assert_eq!(monitor.failures.load(Ordering::SeqCst), 3);
1271 assert_eq!(monitor.breaches.load(Ordering::SeqCst), 1);
1272
1273 v.report_failure("k", None);
1276 assert!(v.is_locked_out());
1277 assert_eq!(monitor.failures.load(Ordering::SeqCst), 4);
1278 assert_eq!(monitor.breaches.load(Ordering::SeqCst), 2);
1280 }
1281
1282 #[test]
1283 fn fragment_refuses_when_locked_out() {
1284 let v = KeyVaultBuilder::new()
1285 .normalize_with_blake3(false)
1286 .with_failure_threshold(1, Duration::from_secs(30))
1287 .build();
1288 v.report_failure("k", None);
1289 assert!(v.is_locked_out());
1290
1291 let err = v
1292 .fragment(&RawKey::new(alloc::vec![1u8, 2, 3, 4]))
1293 .unwrap_err();
1294 assert!(matches!(err, Error::LockedOut));
1295 }
1296
1297 #[test]
1298 fn defragment_refuses_when_locked_out() {
1299 let v = KeyVaultBuilder::new()
1300 .normalize_with_blake3(false)
1301 .with_failure_threshold(2, Duration::from_secs(30))
1302 .build();
1303 let raw = RawKey::new(alloc::vec![1u8; 16]);
1305 let frags = v.fragment(&raw).unwrap();
1306 v.report_failure("k", None);
1307 v.report_failure("k", None);
1308 assert!(v.is_locked_out());
1309
1310 let err = v.defragment(&frags).unwrap_err();
1311 assert!(matches!(err, Error::LockedOut));
1312 }
1313
1314 #[test]
1315 fn clear_lockout_resets_state() {
1316 let v = KeyVaultBuilder::new()
1317 .with_failure_threshold(1, Duration::from_secs(30))
1318 .build();
1319 v.report_failure("k", None);
1320 assert!(v.is_locked_out());
1321 v.clear_lockout();
1322 assert!(!v.is_locked_out());
1323 v.clear_lockout();
1331 assert!(!v.is_locked_out());
1332 }
1333
1334 #[test]
1335 fn per_key_failure_counts_are_independent() {
1336 let monitor = Arc::new(CountingMonitor::new());
1337 let v = KeyVaultBuilder::new()
1338 .with_monitor(Arc::clone(&monitor) as Arc<dyn SecurityMonitor>)
1339 .with_failure_threshold(2, Duration::from_secs(30))
1340 .build();
1341 v.report_failure("alpha", None);
1342 v.report_failure("beta", None);
1343 assert!(!v.is_locked_out());
1345 assert_eq!(monitor.failures.load(Ordering::SeqCst), 2);
1346 v.report_failure("alpha", None);
1347 assert!(v.is_locked_out());
1349 }
1350
1351 #[test]
1354 fn register_returns_handle_and_increments_count() {
1355 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1356 assert_eq!(v.key_count(), 0);
1357 let h = v
1358 .register("primary", RawKey::new(alloc::vec![1u8; 32]))
1359 .unwrap();
1360 assert_eq!(v.key_count(), 1);
1361 assert!(v.contains(h));
1362 }
1363
1364 #[test]
1365 fn register_rejects_duplicate_name() {
1366 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1367 let _ = v
1368 .register("primary", RawKey::new(alloc::vec![1u8; 16]))
1369 .unwrap();
1370 let err = v
1371 .register("primary", RawKey::new(alloc::vec![2u8; 16]))
1372 .unwrap_err();
1373 assert!(matches!(err, Error::InvalidConfig(_)));
1374 }
1375
1376 #[test]
1377 fn unregister_removes_key() {
1378 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1379 let h = v
1380 .register("primary", RawKey::new(alloc::vec![1u8; 16]))
1381 .unwrap();
1382 assert!(v.contains(h));
1383 v.unregister(h).unwrap();
1384 assert!(!v.contains(h));
1385 assert_eq!(v.key_count(), 0);
1386 }
1387
1388 #[test]
1389 fn unregister_unknown_handle_errors() {
1390 let v = KeyVaultBuilder::new().build();
1391 let h = KeyHandle::__for_test();
1392 let err = v.unregister(h).unwrap_err();
1393 assert!(matches!(err, Error::KeyNotFound));
1394 }
1395
1396 #[test]
1397 fn with_key_round_trips_bytes() {
1398 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1399 let original = alloc::vec![0xa5u8; 32];
1400 let h = v.register("data", RawKey::new(original.clone())).unwrap();
1401 let observed = v.with_key(h, <[u8]>::to_vec).unwrap();
1402 assert_eq!(observed, original);
1403 }
1404
1405 #[test]
1406 fn with_key_normalization_changes_output_length() {
1407 let v = KeyVaultBuilder::new().build(); let h = v
1409 .register("data", RawKey::new(alloc::vec![0xa5; 17]))
1410 .unwrap();
1411 let observed_len = v.with_key(h, <[u8]>::len).unwrap();
1412 assert_eq!(observed_len, 32);
1414 }
1415
1416 #[test]
1417 fn with_key_unknown_handle_errors() {
1418 let v = KeyVaultBuilder::new().build();
1419 let h = KeyHandle::__for_test();
1420 let err = v.with_key(h, |_| ()).unwrap_err();
1421 assert!(matches!(err, Error::KeyNotFound));
1422 }
1423
1424 #[test]
1425 fn rotate_swaps_key_bytes() {
1426 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1427 let h = v
1428 .register("data", RawKey::new(alloc::vec![1u8; 16]))
1429 .unwrap();
1430
1431 v.rotate(h, RawKey::new(alloc::vec![2u8; 16])).unwrap();
1432 let observed = v.with_key(h, <[u8]>::to_vec).unwrap();
1433 assert_eq!(observed, alloc::vec![2u8; 16]);
1434 }
1435
1436 #[test]
1437 fn rotate_unknown_handle_errors() {
1438 let v = KeyVaultBuilder::new().build();
1439 let h = KeyHandle::__for_test();
1440 let err = v.rotate(h, RawKey::new(alloc::vec![0u8; 16])).unwrap_err();
1441 assert!(matches!(err, Error::KeyNotFound));
1442 }
1443
1444 #[test]
1445 fn handle_for_name_finds_registered_key() {
1446 let v = KeyVaultBuilder::new().build();
1447 let h = v
1448 .register("primary", RawKey::new(alloc::vec![0u8; 16]))
1449 .unwrap();
1450 assert_eq!(v.handle_for_name("primary"), Some(h));
1451 assert_eq!(v.handle_for_name("missing"), None);
1452 }
1453
1454 #[test]
1455 fn metadata_records_registration_length() {
1456 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1457 let h = v
1458 .register("data", RawKey::new(alloc::vec![0u8; 42]))
1459 .unwrap();
1460 let meta = v.metadata(h).expect("metadata");
1461 assert_eq!(meta.length(), 42);
1462 }
1463
1464 #[test]
1465 fn registered_key_refuses_access_when_locked_out() {
1466 let v = KeyVaultBuilder::new()
1467 .with_failure_threshold(1, Duration::from_secs(30))
1468 .build();
1469 let h = v
1470 .register("data", RawKey::new(alloc::vec![0xa5; 16]))
1471 .unwrap();
1472 v.report_failure("data", None);
1473 assert!(v.is_locked_out());
1474
1475 let err = v.with_key(h, |_| ()).unwrap_err();
1476 assert!(matches!(err, Error::LockedOut));
1477 let err = v.rotate(h, RawKey::new(alloc::vec![0u8; 16])).unwrap_err();
1478 assert!(matches!(err, Error::LockedOut));
1479 }
1480
1481 #[test]
1482 fn master_key_unlock_clears_lockout_on_match() {
1483 let master_bytes = b"correct horse battery staple".to_vec();
1484 let v = KeyVaultBuilder::new()
1485 .with_master_key(RawKey::new(master_bytes.clone()))
1486 .with_failure_threshold(1, Duration::from_secs(30))
1487 .build();
1488 assert!(v.has_master_key());
1489
1490 v.report_failure("k", None);
1491 assert!(v.is_locked_out());
1492
1493 let err = v.unlock_with_master(b"wrong").unwrap_err();
1495 assert!(matches!(err, Error::Acquisition { .. }));
1496 assert!(v.is_locked_out());
1497
1498 v.unlock_with_master(&master_bytes).unwrap();
1500 assert!(!v.is_locked_out());
1501 }
1502
1503 struct CapturingAudit {
1507 events: Mutex<Vec<(crate::audit::AccessKind, String)>>,
1508 }
1509
1510 impl CapturingAudit {
1511 fn new() -> Self {
1512 Self {
1513 events: Mutex::new(Vec::new()),
1514 }
1515 }
1516 fn count_of(&self, kind: crate::audit::AccessKind) -> usize {
1517 self.events
1518 .lock()
1519 .unwrap()
1520 .iter()
1521 .filter(|(k, _)| *k == kind)
1522 .count()
1523 }
1524 fn last_for(&self, kind: crate::audit::AccessKind) -> Option<String> {
1525 self.events
1526 .lock()
1527 .unwrap()
1528 .iter()
1529 .rev()
1530 .find_map(|(k, name)| (*k == kind).then(|| name.clone()))
1531 }
1532 }
1533
1534 impl crate::audit::AuditSink for CapturingAudit {
1535 fn on_event(&self, event: &crate::audit::AuditEvent) {
1536 self.events
1537 .lock()
1538 .unwrap()
1539 .push((event.kind, event.key_name.clone()));
1540 }
1541 }
1542
1543 #[test]
1544 fn register_emits_register_event() {
1545 let audit = Arc::new(CapturingAudit::new());
1546 let v = KeyVaultBuilder::new()
1547 .normalize_with_blake3(false)
1548 .with_audit_sink(Arc::clone(&audit) as Arc<dyn crate::audit::AuditSink>)
1549 .build();
1550 let _ = v
1551 .register("primary", RawKey::new(alloc::vec![1u8; 16]))
1552 .unwrap();
1553 assert_eq!(audit.count_of(crate::audit::AccessKind::Register), 1);
1554 assert_eq!(
1555 audit.last_for(crate::audit::AccessKind::Register),
1556 Some("primary".to_string())
1557 );
1558 }
1559
1560 #[test]
1561 fn unregister_emits_unregister_event() {
1562 let audit = Arc::new(CapturingAudit::new());
1563 let v = KeyVaultBuilder::new()
1564 .normalize_with_blake3(false)
1565 .with_audit_sink(Arc::clone(&audit) as Arc<dyn crate::audit::AuditSink>)
1566 .build();
1567 let h = v
1568 .register("primary", RawKey::new(alloc::vec![1u8; 16]))
1569 .unwrap();
1570 v.unregister(h).unwrap();
1571 assert_eq!(audit.count_of(crate::audit::AccessKind::Unregister), 1);
1572 assert_eq!(
1573 audit.last_for(crate::audit::AccessKind::Unregister),
1574 Some("primary".to_string())
1575 );
1576 }
1577
1578 #[test]
1579 fn with_key_emits_read_event() {
1580 let audit = Arc::new(CapturingAudit::new());
1581 let v = KeyVaultBuilder::new()
1582 .normalize_with_blake3(false)
1583 .with_audit_sink(Arc::clone(&audit) as Arc<dyn crate::audit::AuditSink>)
1584 .build();
1585 let h = v
1586 .register("data", RawKey::new(alloc::vec![0xa5u8; 16]))
1587 .unwrap();
1588 let _ = v.with_key(h, <[u8]>::to_vec).unwrap();
1589 assert_eq!(audit.count_of(crate::audit::AccessKind::Read), 1);
1590 assert_eq!(
1591 audit.last_for(crate::audit::AccessKind::Read),
1592 Some("data".to_string())
1593 );
1594 }
1595
1596 #[test]
1597 fn rotate_emits_rotate_event() {
1598 let audit = Arc::new(CapturingAudit::new());
1599 let v = KeyVaultBuilder::new()
1600 .normalize_with_blake3(false)
1601 .with_audit_sink(Arc::clone(&audit) as Arc<dyn crate::audit::AuditSink>)
1602 .build();
1603 let h = v
1604 .register("data", RawKey::new(alloc::vec![1u8; 16]))
1605 .unwrap();
1606 v.rotate(h, RawKey::new(alloc::vec![2u8; 16])).unwrap();
1607 assert_eq!(audit.count_of(crate::audit::AccessKind::Rotate), 1);
1608 assert_eq!(
1609 audit.last_for(crate::audit::AccessKind::Rotate),
1610 Some("data".to_string())
1611 );
1612 }
1613
1614 #[test]
1615 fn fragment_and_defragment_emit_oneshot_events() {
1616 let audit = Arc::new(CapturingAudit::new());
1617 let v = KeyVaultBuilder::new()
1618 .normalize_with_blake3(false)
1619 .with_audit_sink(Arc::clone(&audit) as Arc<dyn crate::audit::AuditSink>)
1620 .build();
1621 let raw = RawKey::new(alloc::vec![0u8; 16]);
1622 let frags = v.fragment(&raw).unwrap();
1623 let _ = v.defragment(&frags).unwrap();
1624 assert_eq!(audit.count_of(crate::audit::AccessKind::OneShotFragment), 1);
1625 assert_eq!(
1626 audit.count_of(crate::audit::AccessKind::OneShotDefragment),
1627 1
1628 );
1629 }
1630
1631 #[test]
1632 fn master_unlock_emits_event_with_match_status() {
1633 let audit = Arc::new(CapturingAudit::new());
1634 let master = b"correct".to_vec();
1635 let v = KeyVaultBuilder::new()
1636 .with_master_key(RawKey::new(master.clone()))
1637 .with_failure_threshold(1, Duration::from_secs(30))
1638 .with_audit_sink(Arc::clone(&audit) as Arc<dyn crate::audit::AuditSink>)
1639 .build();
1640
1641 v.report_failure("k", None);
1642 assert!(v.is_locked_out());
1643
1644 let _ = v.unlock_with_master(b"wrong");
1645 assert_eq!(
1646 audit.count_of(crate::audit::AccessKind::MasterUnlockAttempt { matched: false }),
1647 1
1648 );
1649
1650 v.unlock_with_master(&master).unwrap();
1651 assert_eq!(
1652 audit.count_of(crate::audit::AccessKind::MasterUnlockAttempt { matched: true }),
1653 1
1654 );
1655 }
1656
1657 #[test]
1658 fn no_audit_default_does_not_panic() {
1659 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1662 let h = v.register("k", RawKey::new(alloc::vec![0u8; 16])).unwrap();
1663 let _ = v.with_key(h, <[u8]>::to_vec).unwrap();
1664 v.unregister(h).unwrap();
1665 }
1666
1667 #[test]
1668 fn master_key_unlock_without_registered_master_errors() {
1669 let v = KeyVaultBuilder::new().build();
1670 assert!(!v.has_master_key());
1671 let err = v.unlock_with_master(b"anything").unwrap_err();
1672 assert!(matches!(err, Error::InvalidConfig(_)));
1673 }
1674
1675 #[test]
1676 fn composite_monitor_chains_to_all_inner() {
1677 use crate::CompositeMonitor;
1678 let a = Arc::new(CountingMonitor::new());
1679 let b = Arc::new(CountingMonitor::new());
1680 let composite = CompositeMonitor::new(alloc::vec![
1681 Arc::clone(&a) as Arc<dyn SecurityMonitor>,
1682 Arc::clone(&b) as Arc<dyn SecurityMonitor>,
1683 ]);
1684 let v = KeyVaultBuilder::new()
1685 .with_monitor(composite)
1686 .with_failure_threshold(1, Duration::from_secs(30))
1687 .build();
1688 v.report_failure("k", None);
1689 assert_eq!(a.failures.load(Ordering::SeqCst), 1);
1690 assert_eq!(b.failures.load(Ordering::SeqCst), 1);
1691 assert_eq!(a.breaches.load(Ordering::SeqCst), 1);
1692 assert_eq!(b.breaches.load(Ordering::SeqCst), 1);
1693 }
1694}