lib

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

lib.rs (21511B)


      1 #![forbid(unsafe_code)]
      2 #![no_std]
      3 
      4 extern crate alloc;
      5 #[cfg(any(feature = "std", test))]
      6 extern crate std;
      7 
      8 pub mod error;
      9 #[cfg(feature = "std")]
     10 pub mod file;
     11 
     12 use alloc::string::String;
     13 use alloc::vec::Vec;
     14 use chacha20poly1305::aead::{Aead, KeyInit, Payload};
     15 use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce};
     16 use error::RadrootsProtectedStoreError;
     17 use getrandom::getrandom;
     18 use radroots_secret_vault::RadrootsSecretKeyWrapping;
     19 use serde::{Deserialize, Serialize};
     20 use zeroize::Zeroize;
     21 
     22 pub const RADROOTS_PROTECTED_STORE_ENVELOPE_VERSION: u8 = 1;
     23 pub const RADROOTS_PROTECTED_STORE_KEY_LENGTH: usize = 32;
     24 pub const RADROOTS_PROTECTED_STORE_NONCE_LENGTH: usize = 24;
     25 
     26 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
     27 #[serde(rename_all = "snake_case")]
     28 pub enum RadrootsProtectedStoreCipher {
     29     XChaCha20Poly1305,
     30 }
     31 
     32 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
     33 #[serde(rename_all = "snake_case")]
     34 pub enum RadrootsProtectedStoreKeySource {
     35     SecretVaultWrapped,
     36 }
     37 
     38 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
     39 pub struct RadrootsProtectedStoreHeader {
     40     pub version: u8,
     41     pub cipher: RadrootsProtectedStoreCipher,
     42     pub key_source: RadrootsProtectedStoreKeySource,
     43     pub key_slot: String,
     44     pub nonce: [u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH],
     45 }
     46 
     47 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
     48 pub struct RadrootsProtectedStoreEnvelope {
     49     pub header: RadrootsProtectedStoreHeader,
     50     pub wrapped_key: Vec<u8>,
     51     pub ciphertext: Vec<u8>,
     52 }
     53 
     54 #[cfg(feature = "std")]
     55 pub use file::{RadrootsProtectedFileKeySource, RadrootsProtectedFileSecretVault, sidecar_path};
     56 
     57 #[derive(Debug, Serialize)]
     58 struct RadrootsProtectedStoreAad<'a> {
     59     version: u8,
     60     cipher: RadrootsProtectedStoreCipher,
     61     key_source: RadrootsProtectedStoreKeySource,
     62     key_slot: &'a str,
     63     nonce: &'a [u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH],
     64     wrapped_key: &'a [u8],
     65 }
     66 
     67 impl RadrootsProtectedStoreEnvelope {
     68     #[inline(always)]
     69     pub fn seal_with_wrapped_key<V: RadrootsSecretKeyWrapping>(
     70         vault: &V,
     71         key_slot: &str,
     72         plaintext: &[u8],
     73     ) -> Result<Self, RadrootsProtectedStoreError> {
     74         Self::seal_with_wrapped_key_internal(vault, key_slot, plaintext)
     75     }
     76 
     77     #[inline(always)]
     78     fn seal_with_wrapped_key_internal<V: RadrootsSecretKeyWrapping>(
     79         vault: &V,
     80         key_slot: &str,
     81         plaintext: &[u8],
     82     ) -> Result<Self, RadrootsProtectedStoreError> {
     83         let mut store_key = [0_u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH];
     84         let mut nonce = [0_u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH];
     85         fill_random_bytes(&mut store_key)?;
     86         fill_random_bytes(&mut nonce)?;
     87         Self::seal_with_generated_material(vault, key_slot, plaintext, store_key, nonce)
     88     }
     89 
     90     fn seal_with_generated_material<V: RadrootsSecretKeyWrapping>(
     91         vault: &V,
     92         key_slot: &str,
     93         plaintext: &[u8],
     94         mut store_key: [u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH],
     95         nonce: [u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH],
     96     ) -> Result<Self, RadrootsProtectedStoreError> {
     97         let result =
     98             Self::seal_with_wrapped_key_and_material(vault, key_slot, plaintext, store_key, nonce);
     99         store_key.zeroize();
    100         result
    101     }
    102 
    103     #[cfg(test)]
    104     #[inline(always)]
    105     fn seal_with_entropy_results<V: RadrootsSecretKeyWrapping>(
    106         vault: &V,
    107         key_slot: &str,
    108         plaintext: &[u8],
    109         store_key: Result<[u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH], RadrootsProtectedStoreError>,
    110         nonce: Result<[u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH], RadrootsProtectedStoreError>,
    111     ) -> Result<Self, RadrootsProtectedStoreError> {
    112         Self::seal_with_generated_material(vault, key_slot, plaintext, store_key?, nonce?)
    113     }
    114 
    115     #[inline(always)]
    116     pub fn seal_with_wrapped_key_and_material<V: RadrootsSecretKeyWrapping>(
    117         vault: &V,
    118         key_slot: &str,
    119         plaintext: &[u8],
    120         mut store_key: [u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH],
    121         nonce: [u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH],
    122     ) -> Result<Self, RadrootsProtectedStoreError> {
    123         let wrapped_key = vault
    124             .wrap_data_key(key_slot, &store_key)
    125             .map_err(|_| RadrootsProtectedStoreError::KeyWrapFailed)?;
    126 
    127         let header = RadrootsProtectedStoreHeader {
    128             version: RADROOTS_PROTECTED_STORE_ENVELOPE_VERSION,
    129             cipher: RadrootsProtectedStoreCipher::XChaCha20Poly1305,
    130             key_source: RadrootsProtectedStoreKeySource::SecretVaultWrapped,
    131             key_slot: String::from(key_slot),
    132             nonce,
    133         };
    134 
    135         let aad = envelope_aad(&header, &wrapped_key)?;
    136         let cipher = XChaCha20Poly1305::new(Key::from_slice(&store_key));
    137         let ciphertext = cipher
    138             .encrypt(
    139                 XNonce::from_slice(&header.nonce),
    140                 Payload {
    141                     msg: plaintext,
    142                     aad: &aad,
    143                 },
    144             )
    145             .map_err(|_| RadrootsProtectedStoreError::EncryptFailed)?;
    146         store_key.zeroize();
    147 
    148         Ok(Self {
    149             header,
    150             wrapped_key,
    151             ciphertext,
    152         })
    153     }
    154 
    155     #[inline(always)]
    156     pub fn open_with_wrapped_key<V: RadrootsSecretKeyWrapping>(
    157         &self,
    158         vault: &V,
    159     ) -> Result<Vec<u8>, RadrootsProtectedStoreError> {
    160         self.validate_header()?;
    161         let mut store_key = vault
    162             .unwrap_data_key(&self.header.key_slot, &self.wrapped_key)
    163             .map_err(|_| RadrootsProtectedStoreError::KeyUnwrapFailed)?;
    164 
    165         if store_key.len() != RADROOTS_PROTECTED_STORE_KEY_LENGTH {
    166             let length = store_key.len();
    167             store_key.zeroize();
    168             return Err(RadrootsProtectedStoreError::InvalidStoreKeyLength(length));
    169         }
    170 
    171         let mut store_key_bytes = [0_u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH];
    172         store_key_bytes.copy_from_slice(&store_key);
    173         store_key.zeroize();
    174 
    175         let aad = envelope_aad(&self.header, &self.wrapped_key)?;
    176         let cipher = XChaCha20Poly1305::new(Key::from_slice(&store_key_bytes));
    177         let decrypted = cipher
    178             .decrypt(
    179                 XNonce::from_slice(&self.header.nonce),
    180                 Payload {
    181                     msg: &self.ciphertext,
    182                     aad: &aad,
    183                 },
    184             )
    185             .map_err(|_| RadrootsProtectedStoreError::DecryptFailed)?;
    186         store_key_bytes.zeroize();
    187         Ok(decrypted)
    188     }
    189 
    190     pub fn encode_json(&self) -> Result<Vec<u8>, RadrootsProtectedStoreError> {
    191         serde_json::to_vec(self).map_err(|_| RadrootsProtectedStoreError::EnvelopeEncodeFailed)
    192     }
    193 
    194     pub fn decode_json(json: &[u8]) -> Result<Self, RadrootsProtectedStoreError> {
    195         let envelope: Self = serde_json::from_slice(json)
    196             .map_err(|_| RadrootsProtectedStoreError::EnvelopeDecodeFailed)?;
    197         envelope.validate_header()?;
    198         Ok(envelope)
    199     }
    200 
    201     fn validate_header(&self) -> Result<(), RadrootsProtectedStoreError> {
    202         if self.header.version != RADROOTS_PROTECTED_STORE_ENVELOPE_VERSION {
    203             return Err(RadrootsProtectedStoreError::UnsupportedEnvelopeVersion(
    204                 self.header.version,
    205             ));
    206         }
    207 
    208         Ok(())
    209     }
    210 }
    211 
    212 fn envelope_aad(
    213     header: &RadrootsProtectedStoreHeader,
    214     wrapped_key: &[u8],
    215 ) -> Result<Vec<u8>, RadrootsProtectedStoreError> {
    216     serde_json::to_vec(&RadrootsProtectedStoreAad {
    217         version: header.version,
    218         cipher: header.cipher,
    219         key_source: header.key_source,
    220         key_slot: &header.key_slot,
    221         nonce: &header.nonce,
    222         wrapped_key,
    223     })
    224     .map_err(|_| RadrootsProtectedStoreError::EnvelopeEncodeFailed)
    225 }
    226 
    227 fn fill_random_bytes(bytes: &mut [u8]) -> Result<(), RadrootsProtectedStoreError> {
    228     getrandom(bytes).map_err(|_| RadrootsProtectedStoreError::EntropyUnavailable)
    229 }
    230 
    231 #[cfg(test)]
    232 mod tests {
    233     use super::*;
    234     use alloc::string::String;
    235     use alloc::vec;
    236     use core::cell::{Cell, RefCell};
    237 
    238     struct FakeVault {
    239         wrap_calls: Cell<usize>,
    240         unwrap_calls: Cell<usize>,
    241         fail_wrap: bool,
    242         fail_unwrap: bool,
    243         unwrap_length: Option<usize>,
    244         last_slot: RefCell<Option<String>>,
    245     }
    246 
    247     impl FakeVault {
    248         fn new() -> Self {
    249             Self {
    250                 wrap_calls: Cell::new(0),
    251                 unwrap_calls: Cell::new(0),
    252                 fail_wrap: false,
    253                 fail_unwrap: false,
    254                 unwrap_length: None,
    255                 last_slot: RefCell::new(None),
    256             }
    257         }
    258 
    259         fn with_wrap_failure() -> Self {
    260             Self {
    261                 fail_wrap: true,
    262                 ..Self::new()
    263             }
    264         }
    265 
    266         fn with_unwrap_failure() -> Self {
    267             Self {
    268                 fail_unwrap: true,
    269                 ..Self::new()
    270             }
    271         }
    272 
    273         fn with_unwrap_length(length: usize) -> Self {
    274             Self {
    275                 unwrap_length: Some(length),
    276                 ..Self::new()
    277             }
    278         }
    279     }
    280 
    281     impl RadrootsSecretKeyWrapping for FakeVault {
    282         type Error = ();
    283 
    284         fn wrap_data_key(
    285             &self,
    286             key_slot: &str,
    287             plaintext_key: &[u8],
    288         ) -> Result<Vec<u8>, Self::Error> {
    289             if self.fail_wrap {
    290                 return Err(());
    291             }
    292             self.wrap_calls.set(self.wrap_calls.get() + 1);
    293             self.last_slot.replace(Some(String::from(key_slot)));
    294             let mut wrapped = key_slot.as_bytes().to_vec();
    295             wrapped.push(0);
    296             wrapped.extend(plaintext_key.iter().map(|byte| byte ^ 0x5a));
    297             Ok(wrapped)
    298         }
    299 
    300         fn unwrap_data_key(
    301             &self,
    302             key_slot: &str,
    303             wrapped_key: &[u8],
    304         ) -> Result<Vec<u8>, Self::Error> {
    305             if self.fail_unwrap {
    306                 return Err(());
    307             }
    308             self.unwrap_calls.set(self.unwrap_calls.get() + 1);
    309             self.last_slot.replace(Some(String::from(key_slot)));
    310 
    311             let separator = wrapped_key.iter().position(|byte| *byte == 0).ok_or(())?;
    312             if &wrapped_key[..separator] != key_slot.as_bytes() {
    313                 return Err(());
    314             }
    315 
    316             let mut unwrapped = wrapped_key[separator + 1..]
    317                 .iter()
    318                 .map(|byte| byte ^ 0x5a)
    319                 .collect::<Vec<_>>();
    320             if let Some(length) = self.unwrap_length {
    321                 unwrapped.truncate(length);
    322             }
    323 
    324             Ok(unwrapped)
    325         }
    326     }
    327 
    328     #[test]
    329     fn wrapped_key_roundtrip_uses_secret_vault_and_stable_envelope() {
    330         let vault = FakeVault::new();
    331         let envelope = RadrootsProtectedStoreEnvelope::seal_with_wrapped_key_and_material(
    332             &vault,
    333             "drafts/default",
    334             b"secret draft body",
    335             [7_u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH],
    336             [9_u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH],
    337         )
    338         .expect("seal succeeds");
    339 
    340         assert_eq!(vault.wrap_calls.get(), 1);
    341         assert_eq!(
    342             envelope.header.version,
    343             RADROOTS_PROTECTED_STORE_ENVELOPE_VERSION
    344         );
    345         assert_eq!(
    346             envelope.header.cipher,
    347             RadrootsProtectedStoreCipher::XChaCha20Poly1305
    348         );
    349         assert_eq!(
    350             envelope.header.key_source,
    351             RadrootsProtectedStoreKeySource::SecretVaultWrapped
    352         );
    353         assert_eq!(envelope.header.key_slot, "drafts/default");
    354 
    355         let encoded = envelope.encode_json().expect("encode succeeds");
    356         let decoded =
    357             RadrootsProtectedStoreEnvelope::decode_json(&encoded).expect("decode succeeds");
    358         let plaintext = decoded
    359             .open_with_wrapped_key(&vault)
    360             .expect("open succeeds");
    361 
    362         assert_eq!(vault.unwrap_calls.get(), 1);
    363         assert_eq!(plaintext, b"secret draft body");
    364     }
    365 
    366     #[test]
    367     fn seal_with_wrapped_key_uses_runtime_entropy_and_roundtrips() {
    368         let vault = FakeVault::new();
    369         let envelope = RadrootsProtectedStoreEnvelope::seal_with_wrapped_key(
    370             &vault,
    371             "drafts/default",
    372             b"runtime entropy body",
    373         )
    374         .expect("seal succeeds");
    375 
    376         assert_eq!(vault.wrap_calls.get(), 1);
    377         assert_eq!(envelope.header.key_slot, "drafts/default");
    378 
    379         let plaintext = envelope
    380             .open_with_wrapped_key(&vault)
    381             .expect("open succeeds");
    382 
    383         assert_eq!(vault.unwrap_calls.get(), 1);
    384         assert_eq!(plaintext, b"runtime entropy body");
    385     }
    386 
    387     #[test]
    388     fn seal_with_wrapped_key_reports_entropy_failure() {
    389         let vault = FakeVault::new();
    390         let err = RadrootsProtectedStoreEnvelope::seal_with_entropy_results(
    391             &vault,
    392             "drafts/default",
    393             b"secret draft body",
    394             Err(RadrootsProtectedStoreError::EntropyUnavailable),
    395             Ok([9_u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH]),
    396         )
    397         .expect_err("entropy failure must surface");
    398 
    399         assert_eq!(err, RadrootsProtectedStoreError::EntropyUnavailable);
    400         assert_eq!(vault.wrap_calls.get(), 0);
    401     }
    402 
    403     #[test]
    404     fn seal_with_wrapped_key_reports_entropy_failure_for_nonce_generation() {
    405         let vault = FakeVault::new();
    406         let err = RadrootsProtectedStoreEnvelope::seal_with_entropy_results(
    407             &vault,
    408             "drafts/default",
    409             b"secret draft body",
    410             Ok([7_u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH]),
    411             Err(RadrootsProtectedStoreError::EntropyUnavailable),
    412         )
    413         .expect_err("second entropy failure must surface");
    414 
    415         assert_eq!(err, RadrootsProtectedStoreError::EntropyUnavailable);
    416         assert_eq!(vault.wrap_calls.get(), 0);
    417     }
    418 
    419     #[test]
    420     fn seal_with_entropy_results_succeeds_when_material_is_provided() {
    421         let vault = FakeVault::new();
    422         let envelope = RadrootsProtectedStoreEnvelope::seal_with_entropy_results(
    423             &vault,
    424             "drafts/default",
    425             b"entropy helper body",
    426             Ok([7_u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH]),
    427             Ok([9_u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH]),
    428         )
    429         .expect("explicit material should succeed");
    430 
    431         let plaintext = envelope
    432             .open_with_wrapped_key(&vault)
    433             .expect("helper envelope opens");
    434         assert_eq!(plaintext, b"entropy helper body");
    435     }
    436 
    437     #[test]
    438     fn seal_with_wrapped_key_surfaces_wrap_failure_after_entropy() {
    439         let vault = FakeVault::with_wrap_failure();
    440         let err = RadrootsProtectedStoreEnvelope::seal_with_wrapped_key(
    441             &vault,
    442             "drafts/default",
    443             b"secret draft body",
    444         )
    445         .expect_err("wrap failure must surface through public seal");
    446 
    447         assert_eq!(err, RadrootsProtectedStoreError::KeyWrapFailed);
    448     }
    449 
    450     #[test]
    451     fn tampered_wrapped_key_fails_authentication() {
    452         let vault = FakeVault::new();
    453         let mut envelope = RadrootsProtectedStoreEnvelope::seal_with_wrapped_key_and_material(
    454             &vault,
    455             "drafts/default",
    456             b"secret draft body",
    457             [3_u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH],
    458             [4_u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH],
    459         )
    460         .expect("seal succeeds");
    461 
    462         let last = envelope.wrapped_key.len() - 1;
    463         envelope.wrapped_key[last] ^= 0x01;
    464 
    465         let err = envelope
    466             .open_with_wrapped_key(&vault)
    467             .expect_err("tampered wrapped key must fail");
    468         assert_eq!(err, RadrootsProtectedStoreError::DecryptFailed);
    469     }
    470 
    471     #[test]
    472     fn unsupported_version_is_rejected() {
    473         let envelope = RadrootsProtectedStoreEnvelope {
    474             header: RadrootsProtectedStoreHeader {
    475                 version: 2,
    476                 cipher: RadrootsProtectedStoreCipher::XChaCha20Poly1305,
    477                 key_source: RadrootsProtectedStoreKeySource::SecretVaultWrapped,
    478                 key_slot: String::from("drafts/default"),
    479                 nonce: [0_u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH],
    480             },
    481             wrapped_key: vec![1, 2, 3],
    482             ciphertext: vec![4, 5, 6],
    483         };
    484 
    485         let encoded = envelope.encode_json().expect("encode succeeds");
    486         let err = RadrootsProtectedStoreEnvelope::decode_json(&encoded)
    487             .expect_err("unsupported version must fail");
    488         assert_eq!(
    489             err,
    490             RadrootsProtectedStoreError::UnsupportedEnvelopeVersion(2)
    491         );
    492     }
    493 
    494     #[test]
    495     fn open_rejects_unsupported_version_before_unwrap() {
    496         let vault = FakeVault::new();
    497         let envelope = RadrootsProtectedStoreEnvelope {
    498             header: RadrootsProtectedStoreHeader {
    499                 version: 2,
    500                 cipher: RadrootsProtectedStoreCipher::XChaCha20Poly1305,
    501                 key_source: RadrootsProtectedStoreKeySource::SecretVaultWrapped,
    502                 key_slot: String::from("drafts/default"),
    503                 nonce: [0_u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH],
    504             },
    505             wrapped_key: vec![1, 2, 3],
    506             ciphertext: vec![4, 5, 6],
    507         };
    508 
    509         let err = envelope
    510             .open_with_wrapped_key(&vault)
    511             .expect_err("unsupported version must fail before unwrap");
    512         assert_eq!(
    513             err,
    514             RadrootsProtectedStoreError::UnsupportedEnvelopeVersion(2)
    515         );
    516         assert_eq!(vault.unwrap_calls.get(), 0);
    517     }
    518 
    519     #[test]
    520     fn decode_json_rejects_invalid_payloads() {
    521         let err = RadrootsProtectedStoreEnvelope::decode_json(br#"{"header":"bad"}"#)
    522             .expect_err("invalid payload must fail decode");
    523         assert_eq!(err, RadrootsProtectedStoreError::EnvelopeDecodeFailed);
    524     }
    525 
    526     #[test]
    527     fn wrap_failures_are_delegated_to_secret_vault() {
    528         let vault = FakeVault::with_wrap_failure();
    529         let err = RadrootsProtectedStoreEnvelope::seal_with_wrapped_key_and_material(
    530             &vault,
    531             "drafts/default",
    532             b"secret draft body",
    533             [7_u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH],
    534             [9_u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH],
    535         )
    536         .expect_err("wrap failure must surface");
    537 
    538         assert_eq!(err, RadrootsProtectedStoreError::KeyWrapFailed);
    539     }
    540 
    541     #[test]
    542     fn unwrap_failures_are_delegated_to_secret_vault() {
    543         let seal_vault = FakeVault::new();
    544         let open_vault = FakeVault::with_unwrap_failure();
    545         let envelope = RadrootsProtectedStoreEnvelope::seal_with_wrapped_key_and_material(
    546             &seal_vault,
    547             "drafts/default",
    548             b"secret draft body",
    549             [7_u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH],
    550             [9_u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH],
    551         )
    552         .expect("seal succeeds");
    553 
    554         let err = envelope
    555             .open_with_wrapped_key(&open_vault)
    556             .expect_err("unwrap failure must surface");
    557         assert_eq!(err, RadrootsProtectedStoreError::KeyUnwrapFailed);
    558     }
    559 
    560     #[test]
    561     fn invalid_store_key_length_is_rejected_after_unwrap() {
    562         let seal_vault = FakeVault::new();
    563         let open_vault = FakeVault::with_unwrap_length(31);
    564         let envelope = RadrootsProtectedStoreEnvelope::seal_with_wrapped_key_and_material(
    565             &seal_vault,
    566             "drafts/default",
    567             b"secret draft body",
    568             [7_u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH],
    569             [9_u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH],
    570         )
    571         .expect("seal succeeds");
    572 
    573         let err = envelope
    574             .open_with_wrapped_key(&open_vault)
    575             .expect_err("short store key must fail");
    576 
    577         assert_eq!(err, RadrootsProtectedStoreError::InvalidStoreKeyLength(31));
    578     }
    579 
    580     #[test]
    581     fn wrapped_key_slot_mismatch_is_rejected_during_unwrap() {
    582         let vault = FakeVault::new();
    583         let mut envelope = RadrootsProtectedStoreEnvelope::seal_with_wrapped_key_and_material(
    584             &vault,
    585             "drafts/default",
    586             b"secret draft body",
    587             [7_u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH],
    588             [9_u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH],
    589         )
    590         .expect("seal succeeds");
    591         envelope.header.key_slot = String::from("drafts/other");
    592 
    593         let err = envelope
    594             .open_with_wrapped_key(&vault)
    595             .expect_err("mismatched key slot must fail");
    596 
    597         assert_eq!(err, RadrootsProtectedStoreError::KeyUnwrapFailed);
    598     }
    599 
    600     #[test]
    601     fn wrapped_key_without_separator_is_rejected_during_unwrap() {
    602         let vault = FakeVault::new();
    603         let envelope = RadrootsProtectedStoreEnvelope {
    604             header: RadrootsProtectedStoreHeader {
    605                 version: RADROOTS_PROTECTED_STORE_ENVELOPE_VERSION,
    606                 cipher: RadrootsProtectedStoreCipher::XChaCha20Poly1305,
    607                 key_source: RadrootsProtectedStoreKeySource::SecretVaultWrapped,
    608                 key_slot: String::from("drafts/default"),
    609                 nonce: [0_u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH],
    610             },
    611             wrapped_key: vec![1, 2, 3, 4],
    612             ciphertext: vec![5, 6, 7],
    613         };
    614 
    615         let err = envelope
    616             .open_with_wrapped_key(&vault)
    617             .expect_err("missing separator must fail");
    618         assert_eq!(err, RadrootsProtectedStoreError::KeyUnwrapFailed);
    619     }
    620 }