use alloc::sync::Arc;
use alloc::vec::Vec;
use core::sync::atomic::AtomicBool;
use core::sync::atomic::Ordering;
use crate::Result;
use crate::codex::Codex;
use crate::decoy::DecoyStrategy;
use crate::fetcher::RawKey;
use crate::fragment::{FragmentStrategy, Fragments, StandardFragmenter};
use crate::normalize::blake3_normalize;
#[derive(Debug, Default, Clone)]
#[non_exhaustive]
pub struct VaultConfig {
pub key_normalization: bool,
}
impl VaultConfig {
#[must_use]
pub fn new() -> Self {
Self {
key_normalization: true,
}
}
}
#[derive(Clone)]
pub struct KeyVault {
inner: Arc<VaultInner>,
}
struct VaultInner {
config: VaultConfig,
fragmenter: StandardFragmenter,
codex: Option<Arc<dyn Codex>>,
locked_out: AtomicBool,
}
impl KeyVault {
#[must_use]
pub fn is_locked_out(&self) -> bool {
self.inner.locked_out.load(Ordering::Acquire)
}
#[must_use]
pub fn config(&self) -> &VaultConfig {
&self.inner.config
}
pub fn fragment(&self, key: &RawKey) -> Result<Fragments> {
let working = if self.inner.config.key_normalization {
blake3_normalize(key)
} else {
RawKey::new(key.as_bytes().to_vec())
};
let encoded = if let Some(codex) = &self.inner.codex {
codex_apply(codex.as_ref(), &working)
} else {
working
};
self.inner.fragmenter.fragment(&encoded)
}
pub fn defragment(&self, fragments: &Fragments) -> Result<RawKey> {
let encoded = self.inner.fragmenter.defragment(fragments)?;
if let Some(codex) = &self.inner.codex {
Ok(codex_apply(codex.as_ref(), &encoded))
} else {
Ok(encoded)
}
}
}
fn codex_apply(codex: &dyn Codex, key: &RawKey) -> RawKey {
let bytes: Vec<u8> = key.as_bytes().iter().map(|&b| codex.encode(b)).collect();
RawKey::new(bytes)
}
impl core::fmt::Debug for KeyVault {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("KeyVault")
.field("locked_out", &self.is_locked_out())
.field("config", &self.inner.config)
.finish()
}
}
#[derive(Default, Clone)]
pub struct KeyVaultBuilder {
config: VaultConfig,
fragmenter: StandardFragmenter,
codex: Option<Arc<dyn Codex>>,
}
impl core::fmt::Debug for KeyVaultBuilder {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("KeyVaultBuilder")
.field("config", &self.config)
.field("fragmenter", &self.fragmenter)
.field("codex", &self.codex.as_ref().map(|_| "<set>"))
.finish()
}
}
impl KeyVaultBuilder {
#[must_use]
pub fn new() -> Self {
Self {
config: VaultConfig::new(),
fragmenter: StandardFragmenter::new(),
codex: None,
}
}
#[must_use]
pub fn normalize_with_blake3(mut self, enabled: bool) -> Self {
self.config.key_normalization = enabled;
self
}
#[must_use]
pub fn with_chunk_range(mut self, min: usize, max: usize) -> Self {
self.fragmenter = StandardFragmenter::with_chunk_range(min, max);
self
}
#[must_use]
pub fn with_codex<C>(mut self, codex: C) -> Self
where
C: Codex + 'static,
{
self.codex = Some(Arc::new(codex));
self
}
#[must_use]
pub fn with_decoy<D>(mut self, decoy: D) -> Self
where
D: DecoyStrategy + 'static,
{
self.fragmenter = self.fragmenter.with_decoy(decoy);
self
}
#[must_use]
pub fn build(self) -> KeyVault {
KeyVault {
inner: Arc::new(VaultInner {
config: self.config,
fragmenter: self.fragmenter,
codex: self.codex,
locked_out: AtomicBool::new(false),
}),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use alloc::format;
#[test]
fn builder_defaults_to_normalization_on() {
let v = KeyVaultBuilder::new().build();
assert!(v.config().key_normalization);
}
#[test]
fn builder_can_disable_normalization() {
let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
assert!(!v.config().key_normalization);
}
#[test]
fn fresh_vault_is_not_locked_out() {
let v = KeyVaultBuilder::new().build();
assert!(!v.is_locked_out());
}
#[test]
fn debug_does_not_panic() {
let v = KeyVaultBuilder::new().build();
let _ = format!("{v:?}");
}
#[test]
fn fragment_defragment_roundtrip_with_normalization() {
let v = KeyVaultBuilder::new().build(); let raw = RawKey::new(b"hello world".to_vec());
let frags = v.fragment(&raw).unwrap();
let recovered = v.defragment(&frags).unwrap();
assert_eq!(recovered.len(), 32);
let frags2 = v.fragment(&raw).unwrap();
let recovered2 = v.defragment(&frags2).unwrap();
assert_eq!(recovered.as_bytes(), recovered2.as_bytes());
}
#[test]
fn fragment_defragment_roundtrip_without_normalization() {
let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
let raw = RawKey::new((0u8..40).collect());
let frags = v.fragment(&raw).unwrap();
let recovered = v.defragment(&frags).unwrap();
assert_eq!(recovered.as_bytes(), raw.as_bytes());
}
#[test]
fn fragment_rejects_empty_key() {
let v = KeyVaultBuilder::new().normalize_with_blake3(false).build();
let err = v
.fragment(&RawKey::new(alloc::vec::Vec::new()))
.unwrap_err();
assert!(matches!(err, crate::Error::Fragment(_)));
}
#[test]
fn chunk_range_propagates_through_builder() {
let v = KeyVaultBuilder::new()
.normalize_with_blake3(false)
.with_chunk_range(4, 6)
.build();
let raw = RawKey::new((0u8..30).collect());
let frags = v.fragment(&raw).unwrap();
let chunks = frags.chunks();
let mut below_min = 0;
let mut total = 0usize;
for c in chunks {
assert!(
c.len() >= 1 && c.len() <= 6,
"chunk size {} not in [1,6]",
c.len()
);
if c.len() < 4 {
below_min += 1;
}
total += c.len();
}
assert!(
below_min <= 1,
"more than one chunk below min size: {below_min}"
);
assert_eq!(total, 30);
}
#[test]
fn fragment_with_random_decoy_roundtrips() {
let v = KeyVaultBuilder::new()
.normalize_with_blake3(false)
.with_decoy(crate::RandomDecoy)
.build();
let raw = RawKey::new((0u8..32).collect());
let frags = v.fragment(&raw).unwrap();
let recovered = v.defragment(&frags).unwrap();
assert_eq!(recovered.as_bytes(), raw.as_bytes());
}
#[test]
fn fragment_with_self_reference_decoy_roundtrips() {
let v = KeyVaultBuilder::new()
.normalize_with_blake3(false)
.with_decoy(crate::SelfReferenceDecoy)
.build();
let raw = RawKey::new(b"some user-supplied key material".to_vec());
let frags = v.fragment(&raw).unwrap();
let recovered = v.defragment(&frags).unwrap();
assert_eq!(recovered.as_bytes(), raw.as_bytes());
}
#[test]
fn fragment_with_key_derived_decoy_roundtrips() {
let v = KeyVaultBuilder::new()
.normalize_with_blake3(false)
.with_decoy(crate::KeyDerivedDecoy)
.build();
let raw = RawKey::new((0u8..64).collect());
let frags = v.fragment(&raw).unwrap();
let recovered = v.defragment(&frags).unwrap();
assert_eq!(recovered.as_bytes(), raw.as_bytes());
}
#[test]
fn decoy_increases_chunk_count_relative_to_no_decoy() {
let no_decoy = KeyVaultBuilder::new()
.normalize_with_blake3(false)
.with_chunk_range(2, 4)
.build();
let with_decoy = KeyVaultBuilder::new()
.normalize_with_blake3(false)
.with_chunk_range(2, 4)
.with_decoy(crate::SelfReferenceDecoy)
.build();
let raw = RawKey::new((0u8..32).collect());
let mut no_decoy_total = 0usize;
let mut decoy_total = 0usize;
for _ in 0..8 {
no_decoy_total += no_decoy.fragment(&raw).unwrap().chunk_count();
decoy_total += with_decoy.fragment(&raw).unwrap().chunk_count();
}
assert!(
decoy_total > no_decoy_total,
"decoy vault produced {decoy_total} chunks vs no-decoy {no_decoy_total}"
);
}
#[test]
fn fragment_with_static_codex_roundtrips() {
use crate::StaticCodex;
let codex = StaticCodex::from_swaps(&[(b'A', b'#'), (b'0', b'%')]).unwrap();
let v = KeyVaultBuilder::new()
.normalize_with_blake3(false)
.with_codex(codex)
.build();
let raw = RawKey::new(b"A0A0A0A0".to_vec());
let frags = v.fragment(&raw).unwrap();
let recovered = v.defragment(&frags).unwrap();
assert_eq!(recovered.as_bytes(), raw.as_bytes());
}
#[test]
fn fragment_with_dynamic_codex_roundtrips() {
use crate::DynamicCodex;
let v = KeyVaultBuilder::new()
.normalize_with_blake3(false)
.with_codex(DynamicCodex::new().unwrap())
.build();
let raw = RawKey::new((0u8..=255).collect());
let frags = v.fragment(&raw).unwrap();
let recovered = v.defragment(&frags).unwrap();
assert_eq!(recovered.as_bytes(), raw.as_bytes());
}
#[test]
fn fragment_with_codex_and_decoy_and_normalization_roundtrips() {
use crate::{DynamicCodex, SelfReferenceDecoy};
let v = KeyVaultBuilder::new()
.normalize_with_blake3(true)
.with_codex(DynamicCodex::new().unwrap())
.with_decoy(SelfReferenceDecoy)
.build();
let raw = RawKey::new(b"my application key".to_vec());
let frags = v.fragment(&raw).unwrap();
let recovered = v.defragment(&frags).unwrap();
assert_eq!(recovered.len(), 32);
let recovered2 = v.defragment(&v.fragment(&raw).unwrap()).unwrap();
assert_eq!(recovered.as_bytes(), recovered2.as_bytes());
}
#[test]
fn codex_visibly_transforms_stored_bytes() {
use crate::StaticCodex;
let v = KeyVaultBuilder::new()
.normalize_with_blake3(false)
.with_codex(crate::DynamicCodex::new().unwrap())
.build();
let raw = RawKey::new(alloc::vec![0xaa; 8]);
let frags = v.fragment(&raw).unwrap();
let mut saw_non_aa = false;
for chunk in frags.chunks() {
for &b in chunk.as_bytes() {
if b != 0xaa {
saw_non_aa = true;
break;
}
}
if saw_non_aa {
break;
}
}
assert!(
saw_non_aa,
"codex did not transform 0xaa — stored bytes still all 0xaa",
);
let recovered = v.defragment(&frags).unwrap();
assert_eq!(recovered.as_bytes(), raw.as_bytes());
let _ = StaticCodex::from_swaps(&[]).unwrap();
}
}