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 encoded = self.inner.fragmenter.defragment(&fragments)?;
521 let raw = if let Some(codex) = &self.inner.codex {
522 codex_apply(codex.as_ref(), &encoded)
523 } else {
524 encoded
525 };
526 let result = f(raw.as_bytes());
528 if let Some(name) = name {
529 self.emit_audit(&name, AccessKind::Read, Cow::Borrowed(""));
530 }
531 Ok(result)
532 }
533
534 #[allow(clippy::needless_pass_by_value)]
553 pub fn rotate(&self, handle: KeyHandle, new_key: RawKey) -> Result<()> {
554 if self.is_locked_out() {
555 return Err(Error::LockedOut);
556 }
557
558 let name = {
562 let snapshot = self.inner.keys.load();
563 snapshot
564 .get(&handle.id())
565 .map(|e| e.name.clone())
566 .ok_or(Error::KeyNotFound)?
567 };
568
569 let new_len = new_key.len();
570 let new_fragments = Arc::new(self.fragment(&new_key)?);
571 let now = SystemTime::now()
572 .duration_since(UNIX_EPOCH)
573 .unwrap_or_default();
574 let new_metadata = KeyMetadata::new(now, new_len, None);
575
576 let mut found = false;
577 let _previous = self.inner.keys.rcu(|current| {
578 let mut new_map = (**current).clone();
579 if let Some(entry) = new_map.get_mut(&handle.id()) {
580 entry.fragments = Arc::clone(&new_fragments);
581 entry.metadata = new_metadata.clone();
582 found = true;
583 }
584 new_map
585 });
586 if found {
587 self.emit_audit(&name, AccessKind::Rotate, Cow::Borrowed(""));
588 Ok(())
589 } else {
590 Err(Error::KeyNotFound)
593 }
594 }
595
596 #[must_use]
598 pub fn contains(&self, handle: KeyHandle) -> bool {
599 self.inner.keys.load().contains_key(&handle.id())
600 }
601
602 #[must_use]
608 pub fn metadata(&self, handle: KeyHandle) -> Option<KeyMetadata> {
609 self.inner
610 .keys
611 .load()
612 .get(&handle.id())
613 .map(|e| e.metadata.clone())
614 }
615
616 #[must_use]
618 pub fn handle_for_name(&self, name: &str) -> Option<KeyHandle> {
619 self.inner
620 .keys
621 .load()
622 .iter()
623 .find_map(|(id, entry)| (entry.name == name).then(|| KeyHandle::from_id(*id)))
624 }
625
626 #[must_use]
628 pub fn key_count(&self) -> usize {
629 self.inner.keys.load().len()
630 }
631
632 pub fn unlock_with_master(&self, attempt: &[u8]) -> Result<()> {
654 let stored = self.inner.master_hash.ok_or_else(|| {
655 Error::InvalidConfig(
656 "vault has no master key registered; pass with_master_key at build time"
657 .to_string(),
658 )
659 })?;
660 let attempt_hash = blake3::hash(attempt);
661 let matched = bool::from(stored.as_slice().ct_eq(attempt_hash.as_bytes()));
662 self.emit_audit(
663 "<master>",
664 AccessKind::MasterUnlockAttempt { matched },
665 Cow::Borrowed(""),
666 );
667 if matched {
668 self.clear_lockout();
669 Ok(())
670 } else {
671 self.report_failure("<master>", Some("invalid master credential"));
674 Err(Error::Acquisition {
675 source: Cow::Borrowed("master"),
676 reason: "master credential did not match".to_string(),
677 })
678 }
679 }
680
681 #[must_use]
683 pub fn has_master_key(&self) -> bool {
684 self.inner.master_hash.is_some()
685 }
686}
687
688fn codex_apply(codex: &dyn Codex, key: &RawKey) -> RawKey {
694 let bytes: Vec<u8> = key.as_bytes().iter().map(|&b| codex.encode(b)).collect();
695 RawKey::new(bytes)
696}
697
698impl core::fmt::Debug for KeyVault {
699 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
700 f.debug_struct("KeyVault")
701 .field("locked_out", &self.is_locked_out())
702 .field("config", &self.inner.config)
703 .finish()
704 }
705}
706
707#[derive(Clone)]
713pub struct KeyVaultBuilder {
714 config: VaultConfig,
715 fragmenter: StandardFragmenter,
716 codex: Option<Arc<dyn Codex>>,
717 monitor: Option<Arc<dyn SecurityMonitor>>,
718 audit: Option<Arc<dyn AuditSink>>,
721 master_hash: Option<[u8; 32]>,
725}
726
727impl Default for KeyVaultBuilder {
728 fn default() -> Self {
729 Self::new()
730 }
731}
732
733impl core::fmt::Debug for KeyVaultBuilder {
734 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
735 f.debug_struct("KeyVaultBuilder")
736 .field("config", &self.config)
737 .field("fragmenter", &self.fragmenter)
738 .field("codex", &self.codex.as_ref().map(|_| "<set>"))
739 .field("monitor", &self.monitor.as_ref().map(|_| "<set>"))
740 .field("audit", &self.audit.as_ref().map(|_| "<set>"))
741 .field("master_key", &self.master_hash.as_ref().map(|_| "<set>"))
742 .finish()
743 }
744}
745
746impl KeyVaultBuilder {
747 #[must_use]
750 pub fn new() -> Self {
751 Self {
752 config: VaultConfig::new(),
753 fragmenter: StandardFragmenter::new(),
754 codex: None,
755 monitor: None,
756 audit: None,
757 master_hash: None,
758 }
759 }
760
761 #[must_use]
768 pub fn normalize_with_blake3(mut self, enabled: bool) -> Self {
769 self.config.key_normalization = enabled;
770 self
771 }
772
773 #[must_use]
780 pub fn with_chunk_range(mut self, min: usize, max: usize) -> Self {
781 self.fragmenter = StandardFragmenter::with_chunk_range(min, max);
782 self
783 }
784
785 #[must_use]
813 pub fn with_codex<C>(mut self, codex: C) -> Self
814 where
815 C: Codex + 'static,
816 {
817 self.codex = Some(Arc::new(codex));
818 self
819 }
820
821 #[must_use]
835 pub fn with_decoy<D>(mut self, decoy: D) -> Self
836 where
837 D: DecoyStrategy + 'static,
838 {
839 self.fragmenter = self.fragmenter.with_decoy(decoy);
840 self
841 }
842
843 #[must_use]
855 pub fn with_monitor<M>(mut self, monitor: M) -> Self
856 where
857 M: SecurityMonitor + 'static,
858 {
859 self.monitor = Some(Arc::new(monitor));
860 self
861 }
862
863 #[must_use]
876 pub fn with_failure_threshold(mut self, max: u32, window: Duration) -> Self {
877 self.config.max_failures_before_lockout = max;
878 self.config.failure_window = window;
879 self
880 }
881
882 #[must_use]
893 pub fn with_audit_sink<A>(mut self, sink: A) -> Self
894 where
895 A: AuditSink + 'static,
896 {
897 self.audit = Some(Arc::new(sink));
898 self
899 }
900
901 #[must_use]
913 pub fn with_master_key(mut self, master: RawKey) -> Self {
914 let hash = blake3::hash(master.as_bytes());
915 let mut bytes = [0u8; 32];
916 bytes.copy_from_slice(hash.as_bytes());
917 self.master_hash = Some(bytes);
918 drop(master);
920 self
921 }
922
923 #[must_use]
928 pub fn build(self) -> KeyVault {
929 let monitor: Arc<dyn SecurityMonitor> = self
930 .monitor
931 .unwrap_or_else(|| Arc::new(crate::monitor::NoMonitor));
932 let audit: Arc<dyn AuditSink> = self
933 .audit
934 .unwrap_or_else(|| Arc::new(crate::audit::NoAudit));
935 KeyVault {
936 inner: Arc::new(VaultInner {
937 config: self.config,
938 fragmenter: self.fragmenter,
939 codex: self.codex,
940 monitor,
941 keys: ArcSwap::from_pointee(HashMap::new()),
942 failure_tracker: Mutex::new(HashMap::new()),
943 locked_out: AtomicBool::new(false),
944 master_hash: self.master_hash,
945 audit,
946 }),
947 }
948 }
949}
950
951#[cfg(test)]
952#[allow(clippy::unwrap_used, clippy::expect_used)]
953mod tests {
954 use super::*;
955 use alloc::format;
956
957 #[test]
958 fn builder_defaults_to_normalization_on() {
959 let v = KeyVaultBuilder::new().build();
960 assert!(v.config().key_normalization);
961 }
962
963 #[test]
964 fn builder_can_disable_normalization() {
965 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
966 assert!(!v.config().key_normalization);
967 }
968
969 #[test]
970 fn fresh_vault_is_not_locked_out() {
971 let v = KeyVaultBuilder::new().build();
972 assert!(!v.is_locked_out());
973 }
974
975 #[test]
976 fn debug_does_not_panic() {
977 let v = KeyVaultBuilder::new().build();
978 let _ = format!("{v:?}");
979 }
980
981 #[test]
982 fn fragment_defragment_roundtrip_with_normalization() {
983 let v = KeyVaultBuilder::new().build(); let raw = RawKey::new(b"hello world".to_vec());
985 let frags = v.fragment(&raw).unwrap();
986 let recovered = v.defragment(&frags).unwrap();
987 assert_eq!(recovered.len(), 32);
990 let frags2 = v.fragment(&raw).unwrap();
993 let recovered2 = v.defragment(&frags2).unwrap();
994 assert_eq!(recovered.as_bytes(), recovered2.as_bytes());
995 }
996
997 #[test]
998 fn fragment_defragment_roundtrip_without_normalization() {
999 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1000 let raw = RawKey::new((0u8..40).collect());
1001 let frags = v.fragment(&raw).unwrap();
1002 let recovered = v.defragment(&frags).unwrap();
1003 assert_eq!(recovered.as_bytes(), raw.as_bytes());
1004 }
1005
1006 #[test]
1007 fn fragment_rejects_empty_key() {
1008 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1009 let err = v
1010 .fragment(&RawKey::new(alloc::vec::Vec::new()))
1011 .unwrap_err();
1012 assert!(matches!(err, crate::Error::Fragment(_)));
1013 }
1014
1015 #[test]
1016 fn chunk_range_propagates_through_builder() {
1017 let v = KeyVaultBuilder::new()
1018 .normalize_with_blake3(false)
1019 .with_chunk_range(4, 6)
1020 .build();
1021 let raw = RawKey::new((0u8..30).collect());
1022 let frags = v.fragment(&raw).unwrap();
1023
1024 let chunks = frags.chunks();
1032 let mut below_min = 0;
1033 let mut total = 0usize;
1034 for c in chunks {
1035 assert!(
1036 c.len() >= 1 && c.len() <= 6,
1037 "chunk size {} not in [1,6]",
1038 c.len()
1039 );
1040 if c.len() < 4 {
1041 below_min += 1;
1042 }
1043 total += c.len();
1044 }
1045 assert!(
1046 below_min <= 1,
1047 "more than one chunk below min size: {below_min}"
1048 );
1049 assert_eq!(total, 30);
1050 }
1051
1052 #[test]
1053 fn fragment_with_random_decoy_roundtrips() {
1054 let v = KeyVaultBuilder::new()
1055 .normalize_with_blake3(false)
1056 .with_decoy(crate::RandomDecoy)
1057 .build();
1058 let raw = RawKey::new((0u8..32).collect());
1059 let frags = v.fragment(&raw).unwrap();
1060 let recovered = v.defragment(&frags).unwrap();
1063 assert_eq!(recovered.as_bytes(), raw.as_bytes());
1064 }
1065
1066 #[test]
1067 fn fragment_with_self_reference_decoy_roundtrips() {
1068 let v = KeyVaultBuilder::new()
1069 .normalize_with_blake3(false)
1070 .with_decoy(crate::SelfReferenceDecoy)
1071 .build();
1072 let raw = RawKey::new(b"some user-supplied key material".to_vec());
1073 let frags = v.fragment(&raw).unwrap();
1074 let recovered = v.defragment(&frags).unwrap();
1075 assert_eq!(recovered.as_bytes(), raw.as_bytes());
1076 }
1077
1078 #[test]
1079 fn fragment_with_key_derived_decoy_roundtrips() {
1080 let v = KeyVaultBuilder::new()
1081 .normalize_with_blake3(false)
1082 .with_decoy(crate::KeyDerivedDecoy)
1083 .build();
1084 let raw = RawKey::new((0u8..64).collect());
1085 let frags = v.fragment(&raw).unwrap();
1086 let recovered = v.defragment(&frags).unwrap();
1087 assert_eq!(recovered.as_bytes(), raw.as_bytes());
1088 }
1089
1090 #[test]
1091 fn decoy_increases_chunk_count_relative_to_no_decoy() {
1092 let no_decoy = KeyVaultBuilder::new()
1093 .normalize_with_blake3(false)
1094 .with_chunk_range(2, 4)
1095 .build();
1096 let with_decoy = KeyVaultBuilder::new()
1097 .normalize_with_blake3(false)
1098 .with_chunk_range(2, 4)
1099 .with_decoy(crate::SelfReferenceDecoy)
1100 .build();
1101 let raw = RawKey::new((0u8..32).collect());
1102
1103 let mut no_decoy_total = 0usize;
1107 let mut decoy_total = 0usize;
1108 for _ in 0..8 {
1109 no_decoy_total += no_decoy.fragment(&raw).unwrap().chunk_count();
1110 decoy_total += with_decoy.fragment(&raw).unwrap().chunk_count();
1111 }
1112 assert!(
1117 decoy_total > no_decoy_total,
1118 "decoy vault produced {decoy_total} chunks vs no-decoy {no_decoy_total}"
1119 );
1120 }
1121
1122 #[test]
1123 fn fragment_with_static_codex_roundtrips() {
1124 use crate::StaticCodex;
1125 let codex = StaticCodex::from_swaps(&[(b'A', b'#'), (b'0', b'%')]).unwrap();
1126 let v = KeyVaultBuilder::new()
1127 .normalize_with_blake3(false)
1128 .with_codex(codex)
1129 .build();
1130 let raw = RawKey::new(b"A0A0A0A0".to_vec());
1131 let frags = v.fragment(&raw).unwrap();
1132 let recovered = v.defragment(&frags).unwrap();
1133 assert_eq!(recovered.as_bytes(), raw.as_bytes());
1136 }
1137
1138 #[test]
1139 fn fragment_with_dynamic_codex_roundtrips() {
1140 use crate::DynamicCodex;
1141 let v = KeyVaultBuilder::new()
1142 .normalize_with_blake3(false)
1143 .with_codex(DynamicCodex::new().unwrap())
1144 .build();
1145 let raw = RawKey::new((0u8..=255).collect());
1146 let frags = v.fragment(&raw).unwrap();
1147 let recovered = v.defragment(&frags).unwrap();
1148 assert_eq!(recovered.as_bytes(), raw.as_bytes());
1149 }
1150
1151 #[test]
1152 fn fragment_with_codex_and_decoy_and_normalization_roundtrips() {
1153 use crate::{DynamicCodex, SelfReferenceDecoy};
1154 let v = KeyVaultBuilder::new()
1157 .normalize_with_blake3(true)
1158 .with_codex(DynamicCodex::new().unwrap())
1159 .with_decoy(SelfReferenceDecoy)
1160 .build();
1161 let raw = RawKey::new(b"my application key".to_vec());
1162 let frags = v.fragment(&raw).unwrap();
1163 let recovered = v.defragment(&frags).unwrap();
1164 assert_eq!(recovered.len(), 32);
1167 let recovered2 = v.defragment(&v.fragment(&raw).unwrap()).unwrap();
1168 assert_eq!(recovered.as_bytes(), recovered2.as_bytes());
1169 }
1170
1171 #[test]
1172 fn codex_visibly_transforms_stored_bytes() {
1173 use crate::StaticCodex;
1178 let v = KeyVaultBuilder::new()
1179 .normalize_with_blake3(false)
1180 .with_codex(crate::DynamicCodex::new().unwrap())
1182 .build();
1183 let raw = RawKey::new(alloc::vec![0xaa; 8]);
1184 let frags = v.fragment(&raw).unwrap();
1185
1186 let mut saw_non_aa = false;
1189 for chunk in frags.chunks() {
1190 for &b in chunk.as_bytes() {
1191 if b != 0xaa {
1192 saw_non_aa = true;
1193 break;
1194 }
1195 }
1196 if saw_non_aa {
1197 break;
1198 }
1199 }
1200 assert!(
1201 saw_non_aa,
1202 "codex did not transform 0xaa — stored bytes still all 0xaa",
1203 );
1204
1205 let recovered = v.defragment(&frags).unwrap();
1207 assert_eq!(recovered.as_bytes(), raw.as_bytes());
1208 let _ = StaticCodex::from_swaps(&[]).unwrap();
1210 }
1211
1212 use core::sync::atomic::AtomicU32;
1215
1216 struct CountingMonitor {
1218 failures: AtomicU32,
1219 anomalies: AtomicU32,
1220 breaches: AtomicU32,
1221 }
1222
1223 impl CountingMonitor {
1224 fn new() -> Self {
1225 Self {
1226 failures: AtomicU32::new(0),
1227 anomalies: AtomicU32::new(0),
1228 breaches: AtomicU32::new(0),
1229 }
1230 }
1231 }
1232
1233 impl SecurityMonitor for CountingMonitor {
1234 fn on_decryption_failure(&self, _ctx: &FailureContext) {
1235 let _ = self.failures.fetch_add(1, Ordering::SeqCst);
1236 }
1237 fn on_anomalous_access(&self, _ctx: &AccessContext) {
1238 let _ = self.anomalies.fetch_add(1, Ordering::SeqCst);
1239 }
1240 fn on_threshold_breach(&self, _ctx: &ThresholdContext) {
1241 let _ = self.breaches.fetch_add(1, Ordering::SeqCst);
1242 }
1243 }
1244
1245 #[test]
1246 fn report_failure_fires_monitor() {
1247 let monitor = Arc::new(CountingMonitor::new());
1248 let v = KeyVaultBuilder::new()
1249 .with_monitor(Arc::clone(&monitor) as Arc<dyn SecurityMonitor>)
1250 .build();
1251 v.report_failure("k", None);
1252 v.report_failure("k", Some("test note"));
1253 assert_eq!(monitor.failures.load(Ordering::SeqCst), 2);
1254 assert_eq!(monitor.breaches.load(Ordering::SeqCst), 0);
1255 assert!(!v.is_locked_out());
1256 }
1257
1258 #[test]
1259 fn report_anomalous_access_fires_monitor() {
1260 let monitor = Arc::new(CountingMonitor::new());
1261 let v = KeyVaultBuilder::new()
1262 .with_monitor(Arc::clone(&monitor) as Arc<dyn SecurityMonitor>)
1263 .build();
1264 v.report_anomalous_access("k", None);
1265 assert_eq!(monitor.anomalies.load(Ordering::SeqCst), 1);
1266 assert!(!v.is_locked_out());
1267 }
1268
1269 #[test]
1270 fn threshold_lockout_fires_after_max_failures() {
1271 let monitor = Arc::new(CountingMonitor::new());
1272 let v = KeyVaultBuilder::new()
1273 .with_monitor(Arc::clone(&monitor) as Arc<dyn SecurityMonitor>)
1274 .with_failure_threshold(3, Duration::from_secs(30))
1275 .build();
1276
1277 v.report_failure("k", None);
1278 assert!(!v.is_locked_out());
1279 v.report_failure("k", None);
1280 assert!(!v.is_locked_out());
1281 v.report_failure("k", None);
1282 assert!(v.is_locked_out());
1284 assert_eq!(monitor.failures.load(Ordering::SeqCst), 3);
1285 assert_eq!(monitor.breaches.load(Ordering::SeqCst), 1);
1286
1287 v.report_failure("k", None);
1290 assert!(v.is_locked_out());
1291 assert_eq!(monitor.failures.load(Ordering::SeqCst), 4);
1292 assert_eq!(monitor.breaches.load(Ordering::SeqCst), 2);
1294 }
1295
1296 #[test]
1297 fn fragment_refuses_when_locked_out() {
1298 let v = KeyVaultBuilder::new()
1299 .normalize_with_blake3(false)
1300 .with_failure_threshold(1, Duration::from_secs(30))
1301 .build();
1302 v.report_failure("k", None);
1303 assert!(v.is_locked_out());
1304
1305 let err = v
1306 .fragment(&RawKey::new(alloc::vec![1u8, 2, 3, 4]))
1307 .unwrap_err();
1308 assert!(matches!(err, Error::LockedOut));
1309 }
1310
1311 #[test]
1312 fn defragment_refuses_when_locked_out() {
1313 let v = KeyVaultBuilder::new()
1314 .normalize_with_blake3(false)
1315 .with_failure_threshold(2, Duration::from_secs(30))
1316 .build();
1317 let raw = RawKey::new(alloc::vec![1u8; 16]);
1319 let frags = v.fragment(&raw).unwrap();
1320 v.report_failure("k", None);
1321 v.report_failure("k", None);
1322 assert!(v.is_locked_out());
1323
1324 let err = v.defragment(&frags).unwrap_err();
1325 assert!(matches!(err, Error::LockedOut));
1326 }
1327
1328 #[test]
1329 fn clear_lockout_resets_state() {
1330 let v = KeyVaultBuilder::new()
1331 .with_failure_threshold(1, Duration::from_secs(30))
1332 .build();
1333 v.report_failure("k", None);
1334 assert!(v.is_locked_out());
1335 v.clear_lockout();
1336 assert!(!v.is_locked_out());
1337 v.clear_lockout();
1345 assert!(!v.is_locked_out());
1346 }
1347
1348 #[test]
1349 fn per_key_failure_counts_are_independent() {
1350 let monitor = Arc::new(CountingMonitor::new());
1351 let v = KeyVaultBuilder::new()
1352 .with_monitor(Arc::clone(&monitor) as Arc<dyn SecurityMonitor>)
1353 .with_failure_threshold(2, Duration::from_secs(30))
1354 .build();
1355 v.report_failure("alpha", None);
1356 v.report_failure("beta", None);
1357 assert!(!v.is_locked_out());
1359 assert_eq!(monitor.failures.load(Ordering::SeqCst), 2);
1360 v.report_failure("alpha", None);
1361 assert!(v.is_locked_out());
1363 }
1364
1365 #[test]
1368 fn register_returns_handle_and_increments_count() {
1369 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1370 assert_eq!(v.key_count(), 0);
1371 let h = v
1372 .register("primary", RawKey::new(alloc::vec![1u8; 32]))
1373 .unwrap();
1374 assert_eq!(v.key_count(), 1);
1375 assert!(v.contains(h));
1376 }
1377
1378 #[test]
1379 fn register_rejects_duplicate_name() {
1380 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1381 let _ = v
1382 .register("primary", RawKey::new(alloc::vec![1u8; 16]))
1383 .unwrap();
1384 let err = v
1385 .register("primary", RawKey::new(alloc::vec![2u8; 16]))
1386 .unwrap_err();
1387 assert!(matches!(err, Error::InvalidConfig(_)));
1388 }
1389
1390 #[test]
1391 fn unregister_removes_key() {
1392 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1393 let h = v
1394 .register("primary", RawKey::new(alloc::vec![1u8; 16]))
1395 .unwrap();
1396 assert!(v.contains(h));
1397 v.unregister(h).unwrap();
1398 assert!(!v.contains(h));
1399 assert_eq!(v.key_count(), 0);
1400 }
1401
1402 #[test]
1403 fn unregister_unknown_handle_errors() {
1404 let v = KeyVaultBuilder::new().build();
1405 let h = KeyHandle::__for_test();
1406 let err = v.unregister(h).unwrap_err();
1407 assert!(matches!(err, Error::KeyNotFound));
1408 }
1409
1410 #[test]
1411 fn with_key_round_trips_bytes() {
1412 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1413 let original = alloc::vec![0xa5u8; 32];
1414 let h = v.register("data", RawKey::new(original.clone())).unwrap();
1415 let observed = v.with_key(h, <[u8]>::to_vec).unwrap();
1416 assert_eq!(observed, original);
1417 }
1418
1419 #[test]
1420 fn with_key_normalization_changes_output_length() {
1421 let v = KeyVaultBuilder::new().build(); let h = v
1423 .register("data", RawKey::new(alloc::vec![0xa5; 17]))
1424 .unwrap();
1425 let observed_len = v.with_key(h, <[u8]>::len).unwrap();
1426 assert_eq!(observed_len, 32);
1428 }
1429
1430 #[test]
1431 fn with_key_unknown_handle_errors() {
1432 let v = KeyVaultBuilder::new().build();
1433 let h = KeyHandle::__for_test();
1434 let err = v.with_key(h, |_| ()).unwrap_err();
1435 assert!(matches!(err, Error::KeyNotFound));
1436 }
1437
1438 #[test]
1439 fn rotate_swaps_key_bytes() {
1440 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1441 let h = v
1442 .register("data", RawKey::new(alloc::vec![1u8; 16]))
1443 .unwrap();
1444
1445 v.rotate(h, RawKey::new(alloc::vec![2u8; 16])).unwrap();
1446 let observed = v.with_key(h, <[u8]>::to_vec).unwrap();
1447 assert_eq!(observed, alloc::vec![2u8; 16]);
1448 }
1449
1450 #[test]
1451 fn rotate_unknown_handle_errors() {
1452 let v = KeyVaultBuilder::new().build();
1453 let h = KeyHandle::__for_test();
1454 let err = v.rotate(h, RawKey::new(alloc::vec![0u8; 16])).unwrap_err();
1455 assert!(matches!(err, Error::KeyNotFound));
1456 }
1457
1458 #[test]
1459 fn handle_for_name_finds_registered_key() {
1460 let v = KeyVaultBuilder::new().build();
1461 let h = v
1462 .register("primary", RawKey::new(alloc::vec![0u8; 16]))
1463 .unwrap();
1464 assert_eq!(v.handle_for_name("primary"), Some(h));
1465 assert_eq!(v.handle_for_name("missing"), None);
1466 }
1467
1468 #[test]
1469 fn metadata_records_registration_length() {
1470 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1471 let h = v
1472 .register("data", RawKey::new(alloc::vec![0u8; 42]))
1473 .unwrap();
1474 let meta = v.metadata(h).expect("metadata");
1475 assert_eq!(meta.length(), 42);
1476 }
1477
1478 #[test]
1479 fn registered_key_refuses_access_when_locked_out() {
1480 let v = KeyVaultBuilder::new()
1481 .with_failure_threshold(1, Duration::from_secs(30))
1482 .build();
1483 let h = v
1484 .register("data", RawKey::new(alloc::vec![0xa5; 16]))
1485 .unwrap();
1486 v.report_failure("data", None);
1487 assert!(v.is_locked_out());
1488
1489 let err = v.with_key(h, |_| ()).unwrap_err();
1490 assert!(matches!(err, Error::LockedOut));
1491 let err = v.rotate(h, RawKey::new(alloc::vec![0u8; 16])).unwrap_err();
1492 assert!(matches!(err, Error::LockedOut));
1493 }
1494
1495 #[test]
1496 fn master_key_unlock_clears_lockout_on_match() {
1497 let master_bytes = b"correct horse battery staple".to_vec();
1498 let v = KeyVaultBuilder::new()
1499 .with_master_key(RawKey::new(master_bytes.clone()))
1500 .with_failure_threshold(1, Duration::from_secs(30))
1501 .build();
1502 assert!(v.has_master_key());
1503
1504 v.report_failure("k", None);
1505 assert!(v.is_locked_out());
1506
1507 let err = v.unlock_with_master(b"wrong").unwrap_err();
1509 assert!(matches!(err, Error::Acquisition { .. }));
1510 assert!(v.is_locked_out());
1511
1512 v.unlock_with_master(&master_bytes).unwrap();
1514 assert!(!v.is_locked_out());
1515 }
1516
1517 struct CapturingAudit {
1521 events: Mutex<Vec<(crate::audit::AccessKind, String)>>,
1522 }
1523
1524 impl CapturingAudit {
1525 fn new() -> Self {
1526 Self {
1527 events: Mutex::new(Vec::new()),
1528 }
1529 }
1530 fn count_of(&self, kind: crate::audit::AccessKind) -> usize {
1531 self.events
1532 .lock()
1533 .unwrap()
1534 .iter()
1535 .filter(|(k, _)| *k == kind)
1536 .count()
1537 }
1538 fn last_for(&self, kind: crate::audit::AccessKind) -> Option<String> {
1539 self.events
1540 .lock()
1541 .unwrap()
1542 .iter()
1543 .rev()
1544 .find_map(|(k, name)| (*k == kind).then(|| name.clone()))
1545 }
1546 }
1547
1548 impl crate::audit::AuditSink for CapturingAudit {
1549 fn on_event(&self, event: &crate::audit::AuditEvent) {
1550 self.events
1551 .lock()
1552 .unwrap()
1553 .push((event.kind, event.key_name.clone()));
1554 }
1555 }
1556
1557 #[test]
1558 fn register_emits_register_event() {
1559 let audit = Arc::new(CapturingAudit::new());
1560 let v = KeyVaultBuilder::new()
1561 .normalize_with_blake3(false)
1562 .with_audit_sink(Arc::clone(&audit) as Arc<dyn crate::audit::AuditSink>)
1563 .build();
1564 let _ = v
1565 .register("primary", RawKey::new(alloc::vec![1u8; 16]))
1566 .unwrap();
1567 assert_eq!(audit.count_of(crate::audit::AccessKind::Register), 1);
1568 assert_eq!(
1569 audit.last_for(crate::audit::AccessKind::Register),
1570 Some("primary".to_string())
1571 );
1572 }
1573
1574 #[test]
1575 fn unregister_emits_unregister_event() {
1576 let audit = Arc::new(CapturingAudit::new());
1577 let v = KeyVaultBuilder::new()
1578 .normalize_with_blake3(false)
1579 .with_audit_sink(Arc::clone(&audit) as Arc<dyn crate::audit::AuditSink>)
1580 .build();
1581 let h = v
1582 .register("primary", RawKey::new(alloc::vec![1u8; 16]))
1583 .unwrap();
1584 v.unregister(h).unwrap();
1585 assert_eq!(audit.count_of(crate::audit::AccessKind::Unregister), 1);
1586 assert_eq!(
1587 audit.last_for(crate::audit::AccessKind::Unregister),
1588 Some("primary".to_string())
1589 );
1590 }
1591
1592 #[test]
1593 fn with_key_emits_read_event() {
1594 let audit = Arc::new(CapturingAudit::new());
1595 let v = KeyVaultBuilder::new()
1596 .normalize_with_blake3(false)
1597 .with_audit_sink(Arc::clone(&audit) as Arc<dyn crate::audit::AuditSink>)
1598 .build();
1599 let h = v
1600 .register("data", RawKey::new(alloc::vec![0xa5u8; 16]))
1601 .unwrap();
1602 let _ = v.with_key(h, <[u8]>::to_vec).unwrap();
1603 assert_eq!(audit.count_of(crate::audit::AccessKind::Read), 1);
1604 assert_eq!(
1605 audit.last_for(crate::audit::AccessKind::Read),
1606 Some("data".to_string())
1607 );
1608 }
1609
1610 #[test]
1611 fn rotate_emits_rotate_event() {
1612 let audit = Arc::new(CapturingAudit::new());
1613 let v = KeyVaultBuilder::new()
1614 .normalize_with_blake3(false)
1615 .with_audit_sink(Arc::clone(&audit) as Arc<dyn crate::audit::AuditSink>)
1616 .build();
1617 let h = v
1618 .register("data", RawKey::new(alloc::vec![1u8; 16]))
1619 .unwrap();
1620 v.rotate(h, RawKey::new(alloc::vec![2u8; 16])).unwrap();
1621 assert_eq!(audit.count_of(crate::audit::AccessKind::Rotate), 1);
1622 assert_eq!(
1623 audit.last_for(crate::audit::AccessKind::Rotate),
1624 Some("data".to_string())
1625 );
1626 }
1627
1628 #[test]
1629 fn fragment_and_defragment_emit_oneshot_events() {
1630 let audit = Arc::new(CapturingAudit::new());
1631 let v = KeyVaultBuilder::new()
1632 .normalize_with_blake3(false)
1633 .with_audit_sink(Arc::clone(&audit) as Arc<dyn crate::audit::AuditSink>)
1634 .build();
1635 let raw = RawKey::new(alloc::vec![0u8; 16]);
1636 let frags = v.fragment(&raw).unwrap();
1637 let _ = v.defragment(&frags).unwrap();
1638 assert_eq!(audit.count_of(crate::audit::AccessKind::OneShotFragment), 1);
1639 assert_eq!(
1640 audit.count_of(crate::audit::AccessKind::OneShotDefragment),
1641 1
1642 );
1643 }
1644
1645 #[test]
1646 fn master_unlock_emits_event_with_match_status() {
1647 let audit = Arc::new(CapturingAudit::new());
1648 let master = b"correct".to_vec();
1649 let v = KeyVaultBuilder::new()
1650 .with_master_key(RawKey::new(master.clone()))
1651 .with_failure_threshold(1, Duration::from_secs(30))
1652 .with_audit_sink(Arc::clone(&audit) as Arc<dyn crate::audit::AuditSink>)
1653 .build();
1654
1655 v.report_failure("k", None);
1656 assert!(v.is_locked_out());
1657
1658 let _ = v.unlock_with_master(b"wrong");
1659 assert_eq!(
1660 audit.count_of(crate::audit::AccessKind::MasterUnlockAttempt { matched: false }),
1661 1
1662 );
1663
1664 v.unlock_with_master(&master).unwrap();
1665 assert_eq!(
1666 audit.count_of(crate::audit::AccessKind::MasterUnlockAttempt { matched: true }),
1667 1
1668 );
1669 }
1670
1671 #[test]
1672 fn no_audit_default_does_not_panic() {
1673 let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
1676 let h = v.register("k", RawKey::new(alloc::vec![0u8; 16])).unwrap();
1677 let _ = v.with_key(h, <[u8]>::to_vec).unwrap();
1678 v.unregister(h).unwrap();
1679 }
1680
1681 #[test]
1682 fn master_key_unlock_without_registered_master_errors() {
1683 let v = KeyVaultBuilder::new().build();
1684 assert!(!v.has_master_key());
1685 let err = v.unlock_with_master(b"anything").unwrap_err();
1686 assert!(matches!(err, Error::InvalidConfig(_)));
1687 }
1688
1689 #[test]
1690 fn composite_monitor_chains_to_all_inner() {
1691 use crate::CompositeMonitor;
1692 let a = Arc::new(CountingMonitor::new());
1693 let b = Arc::new(CountingMonitor::new());
1694 let composite = CompositeMonitor::new(alloc::vec![
1695 Arc::clone(&a) as Arc<dyn SecurityMonitor>,
1696 Arc::clone(&b) as Arc<dyn SecurityMonitor>,
1697 ]);
1698 let v = KeyVaultBuilder::new()
1699 .with_monitor(composite)
1700 .with_failure_threshold(1, Duration::from_secs(30))
1701 .build();
1702 v.report_failure("k", None);
1703 assert_eq!(a.failures.load(Ordering::SeqCst), 1);
1704 assert_eq!(b.failures.load(Ordering::SeqCst), 1);
1705 assert_eq!(a.breaches.load(Ordering::SeqCst), 1);
1706 assert_eq!(b.breaches.load(Ordering::SeqCst), 1);
1707 }
1708}