lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit a0935e6b64d76ebe2ea83260df3b7c39ffefc41e
parent b791b5859584d6172b9fe9ccd1cdee6d80395001
Author: triesap <tyson@radroots.org>
Date:   Fri, 10 Apr 2026 21:28:45 +0000

protected_store: add coverage probes

Diffstat:
Mcrates/protected_store/src/lib.rs | 159++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Acrates/protected_store/tests/coverage.rs | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 186 insertions(+), 34 deletions(-)

diff --git a/crates/protected_store/src/lib.rs b/crates/protected_store/src/lib.rs @@ -60,47 +60,61 @@ struct RadrootsProtectedStoreAad<'a> { } impl RadrootsProtectedStoreEnvelope { - pub fn seal_with_wrapped_key<V>( + #[inline(always)] + pub fn seal_with_wrapped_key<V: RadrootsSecretKeyWrapping>( vault: &V, key_slot: &str, plaintext: &[u8], - ) -> Result<Self, RadrootsProtectedStoreError> - where - V: RadrootsSecretKeyWrapping, - { - Self::seal_with_wrapped_key_with_entropy(vault, key_slot, plaintext, fill_random_bytes) + ) -> Result<Self, RadrootsProtectedStoreError> { + Self::seal_with_wrapped_key_internal(vault, key_slot, plaintext) } - fn seal_with_wrapped_key_with_entropy<V, F>( + #[inline(always)] + fn seal_with_wrapped_key_internal<V: RadrootsSecretKeyWrapping>( vault: &V, key_slot: &str, plaintext: &[u8], - mut fill_entropy: F, - ) -> Result<Self, RadrootsProtectedStoreError> - where - V: RadrootsSecretKeyWrapping, - F: FnMut(&mut [u8]) -> Result<(), RadrootsProtectedStoreError>, - { + ) -> Result<Self, RadrootsProtectedStoreError> { let mut store_key = [0_u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH]; let mut nonce = [0_u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH]; - fill_entropy(&mut store_key)?; - fill_entropy(&mut nonce)?; + fill_random_bytes(&mut store_key)?; + fill_random_bytes(&mut nonce)?; + Self::seal_with_generated_material(vault, key_slot, plaintext, store_key, nonce) + } + + fn seal_with_generated_material<V: RadrootsSecretKeyWrapping>( + vault: &V, + key_slot: &str, + plaintext: &[u8], + mut store_key: [u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH], + nonce: [u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH], + ) -> Result<Self, RadrootsProtectedStoreError> { let result = Self::seal_with_wrapped_key_and_material(vault, key_slot, plaintext, store_key, nonce); store_key.zeroize(); result } - pub fn seal_with_wrapped_key_and_material<V>( + #[cfg(test)] + #[inline(always)] + fn seal_with_entropy_results<V: RadrootsSecretKeyWrapping>( + vault: &V, + key_slot: &str, + plaintext: &[u8], + store_key: Result<[u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH], RadrootsProtectedStoreError>, + nonce: Result<[u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH], RadrootsProtectedStoreError>, + ) -> Result<Self, RadrootsProtectedStoreError> { + Self::seal_with_generated_material(vault, key_slot, plaintext, store_key?, nonce?) + } + + #[inline(always)] + pub fn seal_with_wrapped_key_and_material<V: RadrootsSecretKeyWrapping>( vault: &V, key_slot: &str, plaintext: &[u8], mut store_key: [u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH], nonce: [u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH], - ) -> Result<Self, RadrootsProtectedStoreError> - where - V: RadrootsSecretKeyWrapping, - { + ) -> Result<Self, RadrootsProtectedStoreError> { let wrapped_key = vault .wrap_data_key(key_slot, &store_key) .map_err(|_| RadrootsProtectedStoreError::KeyWrapFailed)?; @@ -133,13 +147,11 @@ impl RadrootsProtectedStoreEnvelope { }) } - pub fn open_with_wrapped_key<V>( + #[inline(always)] + pub fn open_with_wrapped_key<V: RadrootsSecretKeyWrapping>( &self, vault: &V, - ) -> Result<Vec<u8>, RadrootsProtectedStoreError> - where - V: RadrootsSecretKeyWrapping, - { + ) -> Result<Vec<u8>, RadrootsProtectedStoreError> { self.validate_header()?; let mut store_key = vault .unwrap_data_key(&self.header.key_slot, &self.wrapped_key) @@ -223,6 +235,7 @@ mod tests { unwrap_calls: Cell<usize>, fail_wrap: bool, fail_unwrap: bool, + unwrap_length: Option<usize>, last_slot: RefCell<Option<String>>, } @@ -233,6 +246,7 @@ mod tests { unwrap_calls: Cell::new(0), fail_wrap: false, fail_unwrap: false, + unwrap_length: None, last_slot: RefCell::new(None), } } @@ -250,6 +264,13 @@ mod tests { ..Self::new() } } + + fn with_unwrap_length(length: usize) -> Self { + Self { + unwrap_length: Some(length), + ..Self::new() + } + } } impl RadrootsSecretKeyWrapping for FakeVault { @@ -287,10 +308,15 @@ mod tests { return Err(()); } - Ok(wrapped_key[separator + 1..] + let mut unwrapped = wrapped_key[separator + 1..] .iter() .map(|byte| byte ^ 0x5a) - .collect()) + .collect::<Vec<_>>(); + if let Some(length) = self.unwrap_length { + unwrapped.truncate(length); + } + + Ok(unwrapped) } } @@ -356,24 +382,49 @@ mod tests { #[test] fn seal_with_wrapped_key_reports_entropy_failure() { let vault = FakeVault::new(); - let mut attempts = 0usize; - let err = RadrootsProtectedStoreEnvelope::seal_with_wrapped_key_with_entropy( + let err = RadrootsProtectedStoreEnvelope::seal_with_entropy_results( &vault, "drafts/default", b"secret draft body", - |_bytes| { - attempts += 1; - Err(RadrootsProtectedStoreError::EntropyUnavailable) - }, + Err(RadrootsProtectedStoreError::EntropyUnavailable), + Ok([9_u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH]), ) .expect_err("entropy failure must surface"); - assert_eq!(attempts, 1); assert_eq!(err, RadrootsProtectedStoreError::EntropyUnavailable); assert_eq!(vault.wrap_calls.get(), 0); } #[test] + fn seal_with_wrapped_key_reports_entropy_failure_for_nonce_generation() { + let vault = FakeVault::new(); + let err = RadrootsProtectedStoreEnvelope::seal_with_entropy_results( + &vault, + "drafts/default", + b"secret draft body", + Ok([7_u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH]), + Err(RadrootsProtectedStoreError::EntropyUnavailable), + ) + .expect_err("second entropy failure must surface"); + + assert_eq!(err, RadrootsProtectedStoreError::EntropyUnavailable); + assert_eq!(vault.wrap_calls.get(), 0); + } + + #[test] + fn seal_with_wrapped_key_surfaces_wrap_failure_after_entropy() { + let vault = FakeVault::with_wrap_failure(); + let err = RadrootsProtectedStoreEnvelope::seal_with_wrapped_key( + &vault, + "drafts/default", + b"secret draft body", + ) + .expect_err("wrap failure must surface through public seal"); + + assert_eq!(err, RadrootsProtectedStoreError::KeyWrapFailed); + } + + #[test] fn tampered_wrapped_key_fails_authentication() { let vault = FakeVault::new(); let mut envelope = RadrootsProtectedStoreEnvelope::seal_with_wrapped_key_and_material( @@ -450,4 +501,44 @@ mod tests { .expect_err("unwrap failure must surface"); assert_eq!(err, RadrootsProtectedStoreError::KeyUnwrapFailed); } + + #[test] + fn invalid_store_key_length_is_rejected_after_unwrap() { + let seal_vault = FakeVault::new(); + let open_vault = FakeVault::with_unwrap_length(31); + let envelope = RadrootsProtectedStoreEnvelope::seal_with_wrapped_key_and_material( + &seal_vault, + "drafts/default", + b"secret draft body", + [7_u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH], + [9_u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH], + ) + .expect("seal succeeds"); + + let err = envelope + .open_with_wrapped_key(&open_vault) + .expect_err("short store key must fail"); + + assert_eq!(err, RadrootsProtectedStoreError::InvalidStoreKeyLength(31)); + } + + #[test] + fn wrapped_key_slot_mismatch_is_rejected_during_unwrap() { + let vault = FakeVault::new(); + let mut envelope = RadrootsProtectedStoreEnvelope::seal_with_wrapped_key_and_material( + &vault, + "drafts/default", + b"secret draft body", + [7_u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH], + [9_u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH], + ) + .expect("seal succeeds"); + envelope.header.key_slot = String::from("drafts/other"); + + let err = envelope + .open_with_wrapped_key(&vault) + .expect_err("mismatched key slot must fail"); + + assert_eq!(err, RadrootsProtectedStoreError::KeyUnwrapFailed); + } } diff --git a/crates/protected_store/tests/coverage.rs b/crates/protected_store/tests/coverage.rs @@ -0,0 +1,61 @@ +use radroots_protected_store::{ + RADROOTS_PROTECTED_STORE_KEY_LENGTH, RADROOTS_PROTECTED_STORE_NONCE_LENGTH, + RadrootsProtectedStoreEnvelope, +}; +use radroots_secret_vault::RadrootsSecretKeyWrapping; + +#[derive(Default)] +struct TestVault; + +impl RadrootsSecretKeyWrapping for TestVault { + type Error = (); + + fn wrap_data_key(&self, key_slot: &str, plaintext_key: &[u8]) -> Result<Vec<u8>, Self::Error> { + let mut wrapped = key_slot.as_bytes().to_vec(); + wrapped.push(0); + wrapped.extend(plaintext_key.iter().map(|byte| byte ^ 0x5a)); + Ok(wrapped) + } + + fn unwrap_data_key(&self, key_slot: &str, wrapped_key: &[u8]) -> Result<Vec<u8>, Self::Error> { + let separator = wrapped_key.iter().position(|byte| *byte == 0).ok_or(())?; + if &wrapped_key[..separator] != key_slot.as_bytes() { + return Err(()); + } + + Ok(wrapped_key[separator + 1..] + .iter() + .map(|byte| byte ^ 0x5a) + .collect()) + } +} + +#[test] +fn public_roundtrip_apis_cover_external_lib_regions() { + let vault = TestVault; + let generated = RadrootsProtectedStoreEnvelope::seal_with_wrapped_key( + &vault, + "drafts/default", + b"generated roundtrip", + ) + .expect("seal with runtime entropy succeeds"); + let generated_plaintext = generated + .open_with_wrapped_key(&vault) + .expect("generated envelope opens"); + assert_eq!(generated_plaintext, b"generated roundtrip"); + + let deterministic = RadrootsProtectedStoreEnvelope::seal_with_wrapped_key_and_material( + &vault, + "drafts/default", + b"deterministic roundtrip", + [7_u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH], + [9_u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH], + ) + .expect("deterministic seal succeeds"); + let encoded = deterministic.encode_json().expect("encode succeeds"); + let decoded = RadrootsProtectedStoreEnvelope::decode_json(&encoded).expect("decode succeeds"); + let deterministic_plaintext = decoded + .open_with_wrapped_key(&vault) + .expect("deterministic envelope opens"); + assert_eq!(deterministic_plaintext, b"deterministic roundtrip"); +}