lib

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

envelope.rs (27877B)


      1 //! Versioned context-bound encrypted-envelope contracts.
      2 
      3 use crate::context::{
      4     ENVELOPE_CONTEXT_DOMAIN, ENVELOPE_CONTEXT_VERSION, ENVELOPE_PURPOSE_MAX_BYTES,
      5     ENVELOPE_SUBJECT_TYPE_MAX_BYTES, ENVELOPE_SUBJECT_VALUE_MAX_BYTES, EnvelopeContext,
      6     EnvelopePurpose, EnvelopeSubject, PAYLOAD_SCHEMA_MAX_BYTES, PayloadSchemaId,
      7 };
      8 use crate::error::Error;
      9 use crate::id::{BackendKind, KeyVersion};
     10 use crate::wrapping::{
     11     KeyWrapping, LegacyV1UnwrapRequest, SecretMaterial, UnwrapRequest, WrapRequest, WrappedSecret,
     12 };
     13 use crate::{SecretId, SecretRef};
     14 use alloc::string::String;
     15 use alloc::vec::Vec;
     16 use chacha20poly1305::aead::{Aead, KeyInit, Payload};
     17 use chacha20poly1305::{XChaCha20Poly1305, XNonce};
     18 use core::fmt;
     19 use sha2::{Digest, Sha256};
     20 use subtle::ConstantTimeEq;
     21 
     22 const MAGIC: [u8; 4] = *b"RRS1";
     23 const DATA_KEY_BYTES: usize = 32;
     24 const AEAD_TAG_BYTES: usize = 16;
     25 const NONCE_BYTES: usize = 24;
     26 
     27 /// Legacy structurally authenticated envelope format.
     28 pub const LEGACY_ENVELOPE_VERSION: u16 = 1;
     29 /// Current context-bound authenticated envelope format.
     30 pub const ENVELOPE_VERSION: u16 = 2;
     31 /// Maximum encoded envelope size accepted from storage.
     32 pub const ENVELOPE_MAX_BYTES: usize = 256 * 1024;
     33 
     34 /// Authenticated-encryption algorithm used by an envelope.
     35 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     36 #[non_exhaustive]
     37 pub enum Cipher {
     38     /// XChaCha20-Poly1305 with a 192-bit nonce.
     39     XChaCha20Poly1305,
     40 }
     41 
     42 impl Cipher {
     43     const fn code(self) -> u8 {
     44         match self {
     45             Self::XChaCha20Poly1305 => 1,
     46         }
     47     }
     48 
     49     const fn from_code(code: u8) -> Result<Self, Error> {
     50         match code {
     51             1 => Ok(Self::XChaCha20Poly1305),
     52             cipher => Err(Error::UnsupportedCipher { cipher }),
     53         }
     54     }
     55 }
     56 
     57 /// How the data-encryption key is protected.
     58 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     59 #[non_exhaustive]
     60 pub enum KeySource {
     61     /// The host-selected [`KeyWrapping`] provider protects the data key.
     62     ProviderWrapped,
     63 }
     64 
     65 impl KeySource {
     66     const fn code(self) -> u8 {
     67         match self {
     68             Self::ProviderWrapped => 1,
     69         }
     70     }
     71 
     72     const fn from_code(code: u8) -> Result<Self, Error> {
     73         match code {
     74             1 => Ok(Self::ProviderWrapped),
     75             key_source => Err(Error::UnsupportedKeySource { key_source }),
     76         }
     77     }
     78 }
     79 
     80 /// Explicit 192-bit nonce supplied by the host.
     81 #[derive(Clone, Copy, PartialEq, Eq)]
     82 pub struct Nonce([u8; NONCE_BYTES]);
     83 
     84 impl Nonce {
     85     /// Creates a nonce from exact caller-supplied bytes.
     86     #[must_use]
     87     pub const fn new(bytes: [u8; NONCE_BYTES]) -> Self {
     88         Self(bytes)
     89     }
     90 
     91     /// Returns the nonce bytes.
     92     #[must_use]
     93     pub const fn as_bytes(&self) -> &[u8; NONCE_BYTES] {
     94         &self.0
     95     }
     96 }
     97 
     98 impl fmt::Debug for Nonce {
     99     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    100         formatter.write_str("Nonce(<redacted>)")
    101     }
    102 }
    103 
    104 /// Caller-supplied cryptographic material for one sealing operation.
    105 pub struct SealMaterial {
    106     data_key: SecretMaterial,
    107     nonce: Nonce,
    108 }
    109 
    110 impl SealMaterial {
    111     /// Couples an explicitly generated data key and nonce.
    112     #[must_use]
    113     pub const fn new(data_key: SecretMaterial, nonce: Nonce) -> Self {
    114         Self { data_key, nonce }
    115     }
    116 }
    117 
    118 impl fmt::Debug for SealMaterial {
    119     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    120         formatter.write_str("SealMaterial(<redacted>)")
    121     }
    122 }
    123 
    124 /// Explicit capability required to read and reseal a decoded v1 envelope.
    125 ///
    126 /// Hosts must construct this value only inside their authorized migration
    127 /// boundary. It cannot be derived from envelope bytes and is intentionally not
    128 /// cloneable or serializable.
    129 pub struct LegacyV1ResealAuthority {
    130     _private: (),
    131 }
    132 
    133 #[allow(clippy::new_without_default)]
    134 impl LegacyV1ResealAuthority {
    135     /// Grants one explicitly scoped host migration boundary v1 access.
    136     #[must_use]
    137     pub const fn new() -> Self {
    138         Self { _private: () }
    139     }
    140 }
    141 
    142 impl fmt::Debug for LegacyV1ResealAuthority {
    143     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    144         formatter.write_str("LegacyV1ResealAuthority(<redacted>)")
    145     }
    146 }
    147 
    148 /// Result of an authenticated v1 read and fresh-material v2 reseal.
    149 pub struct LegacyV1ResealResult {
    150     envelope: EncryptedEnvelope,
    151     plaintext_commitment: [u8; 32],
    152 }
    153 
    154 impl LegacyV1ResealResult {
    155     /// Returns the new context-bound v2 envelope.
    156     #[must_use]
    157     pub const fn envelope(&self) -> &EncryptedEnvelope {
    158         &self.envelope
    159     }
    160 
    161     /// Consumes the result and returns the new v2 envelope.
    162     #[must_use]
    163     pub fn into_envelope(self) -> EncryptedEnvelope {
    164         self.envelope
    165     }
    166 
    167     /// Returns the SHA-256 commitment to the authenticated plaintext.
    168     #[must_use]
    169     pub const fn plaintext_commitment(&self) -> &[u8; 32] {
    170         &self.plaintext_commitment
    171     }
    172 }
    173 
    174 impl fmt::Debug for LegacyV1ResealResult {
    175     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    176         formatter.write_str("LegacyV1ResealResult(<redacted>)")
    177     }
    178 }
    179 
    180 /// Complete input for one context-bound envelope sealing operation.
    181 pub struct SealRequest<'a> {
    182     reference: SecretRef,
    183     context: EnvelopeContext,
    184     plaintext: &'a SecretMaterial,
    185     material: SealMaterial,
    186 }
    187 
    188 impl<'a> SealRequest<'a> {
    189     /// Creates a request without generating entropy or selecting a provider.
    190     #[must_use]
    191     pub const fn new(
    192         reference: SecretRef,
    193         context: EnvelopeContext,
    194         plaintext: &'a SecretMaterial,
    195         material: SealMaterial,
    196     ) -> Self {
    197         Self {
    198             reference,
    199             context,
    200             plaintext,
    201             material,
    202         }
    203     }
    204 }
    205 
    206 impl fmt::Debug for SealRequest<'_> {
    207     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    208         formatter.write_str("SealRequest(<redacted>)")
    209     }
    210 }
    211 
    212 /// A versioned authenticated envelope with provider-wrapped key material.
    213 pub struct EncryptedEnvelope {
    214     version: u16,
    215     cipher: Cipher,
    216     key_source: KeySource,
    217     reference: SecretRef,
    218     context: Option<EnvelopeContext>,
    219     nonce: Nonce,
    220     wrapped_key: WrappedSecret,
    221     ciphertext: Vec<u8>,
    222 }
    223 
    224 impl EncryptedEnvelope {
    225     /// Seals plaintext as v2 using explicit context, key, and nonce material.
    226     pub async fn seal(wrapping: &dyn KeyWrapping, request: SealRequest<'_>) -> Result<Self, Error> {
    227         let SealRequest {
    228             reference,
    229             context,
    230             plaintext,
    231             material,
    232         } = request;
    233         validate_data_key(&material.data_key)?;
    234         let wrapped_key = wrapping
    235             .wrap(WrapRequest::new(&reference, &context, &material.data_key))
    236             .await?;
    237         let ciphertext_len =
    238             plaintext
    239                 .len()
    240                 .checked_add(AEAD_TAG_BYTES)
    241                 .ok_or(Error::EnvelopeTooLarge {
    242                     actual_bytes: usize::MAX,
    243                     max_bytes: ENVELOPE_MAX_BYTES,
    244                 })?;
    245         let aad = encode_header(
    246             ENVELOPE_VERSION,
    247             Cipher::XChaCha20Poly1305,
    248             KeySource::ProviderWrapped,
    249             &reference,
    250             Some(&context),
    251             material.nonce,
    252             &wrapped_key,
    253             ciphertext_len,
    254         )?;
    255         let ciphertext = material.data_key.expose_secret(|data_key| {
    256             plaintext.expose_secret(|plaintext| {
    257                 let cipher = XChaCha20Poly1305::new_from_slice(data_key)
    258                     .map_err(|_| Error::EncryptFailed)?;
    259                 let nonce = XNonce::from(*material.nonce.as_bytes());
    260                 cipher
    261                     .encrypt(
    262                         &nonce,
    263                         Payload {
    264                             msg: plaintext,
    265                             aad: aad.as_slice(),
    266                         },
    267                     )
    268                     .map_err(|_| Error::EncryptFailed)
    269             })
    270         })?;
    271         let envelope = Self {
    272             version: ENVELOPE_VERSION,
    273             cipher: Cipher::XChaCha20Poly1305,
    274             key_source: KeySource::ProviderWrapped,
    275             reference,
    276             context: Some(context),
    277             nonce: material.nonce,
    278             wrapped_key,
    279             ciphertext,
    280         };
    281         envelope.validate()?;
    282         Ok(envelope)
    283     }
    284 
    285     /// Authenticates v2 using independently expected context before releasing plaintext.
    286     pub async fn open(
    287         &self,
    288         wrapping: &dyn KeyWrapping,
    289         expected_context: &EnvelopeContext,
    290     ) -> Result<SecretMaterial, Error> {
    291         self.validate()?;
    292         if self.version == LEGACY_ENVELOPE_VERSION {
    293             return Err(Error::LegacyEnvelopeDenied);
    294         }
    295         let stored_context = self.context.as_ref().ok_or(Error::EnvelopeMalformed)?;
    296         if stored_context != expected_context {
    297             return Err(Error::ContextMismatch);
    298         }
    299         let data_key = wrapping
    300             .unwrap(UnwrapRequest::new(
    301                 &self.reference,
    302                 expected_context,
    303                 &self.wrapped_key,
    304             ))
    305             .await?;
    306         validate_data_key(&data_key)?;
    307         let aad = self.encoded_header()?;
    308         let plaintext = data_key.expose_secret(|data_key| {
    309             let cipher =
    310                 XChaCha20Poly1305::new_from_slice(data_key).map_err(|_| Error::DecryptFailed)?;
    311             let nonce = XNonce::from(*self.nonce.as_bytes());
    312             cipher
    313                 .decrypt(
    314                     &nonce,
    315                     Payload {
    316                         msg: self.ciphertext.as_slice(),
    317                         aad: aad.as_slice(),
    318                     },
    319                 )
    320                 .map_err(|_| Error::DecryptFailed)
    321         })?;
    322         SecretMaterial::from_owned(plaintext)
    323     }
    324 
    325     /// Authenticates and opens v1 only under explicit migration authority.
    326     pub async fn open_legacy_v1(
    327         &self,
    328         wrapping: &dyn KeyWrapping,
    329         authority: &LegacyV1ResealAuthority,
    330         expected_reference: &SecretRef,
    331         _destination_context: &EnvelopeContext,
    332     ) -> Result<SecretMaterial, Error> {
    333         let (plaintext, _) = self
    334             .open_legacy_parts(wrapping, authority, expected_reference)
    335             .await?;
    336         Ok(plaintext)
    337     }
    338 
    339     /// Authenticates v1, validates its payload, and reseals as v2 with fresh material.
    340     #[allow(clippy::too_many_arguments)]
    341     pub async fn reseal_legacy_v1<V>(
    342         &self,
    343         wrapping: &dyn KeyWrapping,
    344         authority: &LegacyV1ResealAuthority,
    345         expected_reference: &SecretRef,
    346         new_reference: SecretRef,
    347         new_context: EnvelopeContext,
    348         validator: &V,
    349         material: SealMaterial,
    350     ) -> Result<LegacyV1ResealResult, Error>
    351     where
    352         V: Fn(&[u8]) -> bool + Send + Sync,
    353     {
    354         if material.nonce == self.nonce {
    355             return Err(Error::LegacyEntropyReuse);
    356         }
    357         validate_data_key(&material.data_key)?;
    358         let (plaintext, legacy_data_key) = self
    359             .open_legacy_parts(wrapping, authority, expected_reference)
    360             .await?;
    361         let reused_key = legacy_data_key.expose_secret(|legacy| {
    362             material
    363                 .data_key
    364                 .expose_secret(|fresh| bool::from(legacy.ct_eq(fresh)))
    365         });
    366         if reused_key {
    367             return Err(Error::LegacyEntropyReuse);
    368         }
    369         if !plaintext.expose_secret(validator) {
    370             return Err(Error::LegacyPayloadValidationFailed);
    371         }
    372         let plaintext_commitment =
    373             plaintext.expose_secret(|bytes| <[u8; 32]>::from(Sha256::digest(bytes)));
    374         let envelope = Self::seal(
    375             wrapping,
    376             SealRequest::new(new_reference, new_context, &plaintext, material),
    377         )
    378         .await?;
    379         let resealed_commitment =
    380             plaintext.expose_secret(|bytes| <[u8; 32]>::from(Sha256::digest(bytes)));
    381         if plaintext_commitment.ct_eq(&resealed_commitment).unwrap_u8() != 1 {
    382             return Err(Error::EncryptFailed);
    383         }
    384         Ok(LegacyV1ResealResult {
    385             envelope,
    386             plaintext_commitment,
    387         })
    388     }
    389 
    390     /// Returns the authenticated provider reference.
    391     #[must_use]
    392     pub const fn reference(&self) -> &SecretRef {
    393         &self.reference
    394     }
    395 
    396     /// Returns the format version.
    397     #[must_use]
    398     pub const fn version(&self) -> u16 {
    399         self.version
    400     }
    401 
    402     /// Returns the authenticated v2 context, or `None` for a legacy v1 envelope.
    403     #[must_use]
    404     pub const fn context(&self) -> Option<&EnvelopeContext> {
    405         self.context.as_ref()
    406     }
    407 
    408     /// Returns the authenticated cipher identifier.
    409     #[must_use]
    410     pub const fn cipher(&self) -> Cipher {
    411         self.cipher
    412     }
    413 
    414     /// Returns the authenticated key-source identifier.
    415     #[must_use]
    416     pub const fn key_source(&self) -> KeySource {
    417         self.key_source
    418     }
    419 
    420     /// Encodes the validated envelope into its deterministic binary form.
    421     pub fn encode(&self) -> Result<Vec<u8>, Error> {
    422         self.validate()?;
    423         let mut encoded = self.encoded_header()?;
    424         encoded.extend_from_slice(self.ciphertext.as_slice());
    425         if encoded.len() > ENVELOPE_MAX_BYTES {
    426             return Err(Error::EnvelopeTooLarge {
    427                 actual_bytes: encoded.len(),
    428                 max_bytes: ENVELOPE_MAX_BYTES,
    429             });
    430         }
    431         Ok(encoded)
    432     }
    433 
    434     /// Decodes and validates v1 or v2 without accessing a provider.
    435     pub fn decode(encoded: &[u8]) -> Result<Self, Error> {
    436         if encoded.len() > ENVELOPE_MAX_BYTES {
    437             return Err(Error::EnvelopeTooLarge {
    438                 actual_bytes: encoded.len(),
    439                 max_bytes: ENVELOPE_MAX_BYTES,
    440             });
    441         }
    442         let mut decoder = Decoder::new(encoded);
    443         if decoder.take_array::<4>()? != MAGIC {
    444             return Err(Error::EnvelopeMalformed);
    445         }
    446         let version = decoder.u16()?;
    447         if !matches!(version, LEGACY_ENVELOPE_VERSION | ENVELOPE_VERSION) {
    448             return Err(Error::UnsupportedEnvelopeVersion { version });
    449         }
    450         let cipher = Cipher::from_code(decoder.u8()?)?;
    451         let key_source = KeySource::from_code(decoder.u8()?)?;
    452         let backend = BackendKind::from_code(decoder.u8()?)?;
    453         let key_version = KeyVersion::new(decoder.u32()?)?;
    454         let id = decoder.bounded_string(u16::MAX.into())?;
    455         let reference = SecretRef::new(SecretId::parse(id)?, backend, key_version);
    456         let context = if version == ENVELOPE_VERSION {
    457             Some(decode_context(&mut decoder)?)
    458         } else {
    459             None
    460         };
    461         let nonce = Nonce::new(decoder.take_array::<NONCE_BYTES>()?);
    462         let wrapped_len = decoder.u32_usize()?;
    463         let wrapped_key = WrappedSecret::from_bytes(decoder.take(wrapped_len)?.to_vec())?;
    464         let ciphertext_len = decoder.u32_usize()?;
    465         let ciphertext = decoder.take(ciphertext_len)?.to_vec();
    466         if !decoder.is_empty() {
    467             return Err(Error::EnvelopeMalformed);
    468         }
    469         let envelope = Self {
    470             version,
    471             cipher,
    472             key_source,
    473             reference,
    474             context,
    475             nonce,
    476             wrapped_key,
    477             ciphertext,
    478         };
    479         envelope.validate()?;
    480         Ok(envelope)
    481     }
    482 
    483     fn encoded_header(&self) -> Result<Vec<u8>, Error> {
    484         encode_header(
    485             self.version,
    486             self.cipher,
    487             self.key_source,
    488             &self.reference,
    489             self.context.as_ref(),
    490             self.nonce,
    491             &self.wrapped_key,
    492             self.ciphertext.len(),
    493         )
    494     }
    495 
    496     fn validate(&self) -> Result<(), Error> {
    497         match (self.version, self.context.is_some()) {
    498             (LEGACY_ENVELOPE_VERSION, false) | (ENVELOPE_VERSION, true) => {}
    499             (LEGACY_ENVELOPE_VERSION | ENVELOPE_VERSION, _) => {
    500                 return Err(Error::EnvelopeMalformed);
    501             }
    502             (version, _) => return Err(Error::UnsupportedEnvelopeVersion { version }),
    503         }
    504         if self.ciphertext.len() < AEAD_TAG_BYTES {
    505             return Err(Error::EnvelopeMalformed);
    506         }
    507         let total = self.encoded_header()?.len() + self.ciphertext.len();
    508         if total > ENVELOPE_MAX_BYTES {
    509             return Err(Error::EnvelopeTooLarge {
    510                 actual_bytes: total,
    511                 max_bytes: ENVELOPE_MAX_BYTES,
    512             });
    513         }
    514         Ok(())
    515     }
    516 
    517     async fn open_legacy_parts(
    518         &self,
    519         wrapping: &dyn KeyWrapping,
    520         authority: &LegacyV1ResealAuthority,
    521         expected_reference: &SecretRef,
    522     ) -> Result<(SecretMaterial, SecretMaterial), Error> {
    523         self.validate()?;
    524         if self.version != LEGACY_ENVELOPE_VERSION {
    525             return Err(Error::LegacyEnvelopeDenied);
    526         }
    527         if !references_match(&self.reference, expected_reference) {
    528             return Err(Error::ProviderReferenceMismatch);
    529         }
    530         let data_key = wrapping
    531             .unwrap_legacy_v1(LegacyV1UnwrapRequest::new(
    532                 &self.reference,
    533                 &self.wrapped_key,
    534                 authority,
    535             ))
    536             .await?;
    537         validate_data_key(&data_key)?;
    538         let aad = self.encoded_header()?;
    539         let plaintext = data_key.expose_secret(|data_key| {
    540             let cipher =
    541                 XChaCha20Poly1305::new_from_slice(data_key).map_err(|_| Error::DecryptFailed)?;
    542             let nonce = XNonce::from(*self.nonce.as_bytes());
    543             cipher
    544                 .decrypt(
    545                     &nonce,
    546                     Payload {
    547                         msg: self.ciphertext.as_slice(),
    548                         aad: aad.as_slice(),
    549                     },
    550                 )
    551                 .map_err(|_| Error::DecryptFailed)
    552         })?;
    553         Ok((SecretMaterial::from_owned(plaintext)?, data_key))
    554     }
    555 }
    556 
    557 impl fmt::Debug for EncryptedEnvelope {
    558     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    559         formatter
    560             .debug_struct("EncryptedEnvelope")
    561             .field("version", &self.version)
    562             .field("cipher", &self.cipher)
    563             .field("key_source", &self.key_source)
    564             .field("reference", &self.reference)
    565             .field("context", &self.context)
    566             .field("nonce", &"<redacted>")
    567             .field("wrapped_key", &"<redacted>")
    568             .field("ciphertext", &"<redacted>")
    569             .finish()
    570     }
    571 }
    572 
    573 #[cfg(feature = "serde")]
    574 impl serde::Serialize for EncryptedEnvelope {
    575     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    576     where
    577         S: serde::Serializer,
    578     {
    579         let encoded = self.encode().map_err(serde::ser::Error::custom)?;
    580         serde::Serialize::serialize(&encoded, serializer)
    581     }
    582 }
    583 
    584 #[cfg(feature = "serde")]
    585 impl<'de> serde::Deserialize<'de> for EncryptedEnvelope {
    586     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    587     where
    588         D: serde::Deserializer<'de>,
    589     {
    590         let encoded = <Vec<u8> as serde::Deserialize>::deserialize(deserializer)?;
    591         Self::decode(encoded.as_slice()).map_err(serde::de::Error::custom)
    592     }
    593 }
    594 
    595 fn decode_context(decoder: &mut Decoder<'_>) -> Result<EnvelopeContext, Error> {
    596     let version = decoder.u16()?;
    597     if version != ENVELOPE_CONTEXT_VERSION {
    598         return Err(Error::UnsupportedContextVersion { version });
    599     }
    600     if decoder.take(ENVELOPE_CONTEXT_DOMAIN.len())? != ENVELOPE_CONTEXT_DOMAIN {
    601         return Err(Error::EnvelopeMalformed);
    602     }
    603     let purpose = EnvelopePurpose::parse(decoder.bounded_string(ENVELOPE_PURPOSE_MAX_BYTES)?)?;
    604     let subject_type = decoder.bounded_string(ENVELOPE_SUBJECT_TYPE_MAX_BYTES)?;
    605     let subject_value = decoder.bounded_string(ENVELOPE_SUBJECT_VALUE_MAX_BYTES)?;
    606     let subject = EnvelopeSubject::parse(subject_type, subject_value)?;
    607     let payload_schema = PayloadSchemaId::parse(decoder.bounded_string(PAYLOAD_SCHEMA_MAX_BYTES)?)?;
    608     Ok(EnvelopeContext::new(purpose, subject, payload_schema))
    609 }
    610 
    611 fn validate_data_key(data_key: &SecretMaterial) -> Result<(), Error> {
    612     if data_key.len() != DATA_KEY_BYTES {
    613         return Err(Error::InvalidDataKeyLength {
    614             actual_bytes: data_key.len(),
    615         });
    616     }
    617     Ok(())
    618 }
    619 
    620 fn references_match(left: &SecretRef, right: &SecretRef) -> bool {
    621     left.backend() == right.backend()
    622         && left.key_version() == right.key_version()
    623         && left.id().as_str() == right.id().as_str()
    624 }
    625 
    626 #[allow(clippy::too_many_arguments)]
    627 fn encode_header(
    628     version: u16,
    629     cipher: Cipher,
    630     key_source: KeySource,
    631     reference: &SecretRef,
    632     context: Option<&EnvelopeContext>,
    633     nonce: Nonce,
    634     wrapped_key: &WrappedSecret,
    635     ciphertext_len: usize,
    636 ) -> Result<Vec<u8>, Error> {
    637     let id = reference.id().as_str().as_bytes();
    638     let id_len = u16::try_from(id.len()).map_err(|_| Error::EnvelopeMalformed)?;
    639     let wrapped_len =
    640         u32::try_from(wrapped_key.as_bytes().len()).map_err(|_| Error::EnvelopeMalformed)?;
    641     let ciphertext_len = u32::try_from(ciphertext_len).map_err(|_| Error::EnvelopeTooLarge {
    642         actual_bytes: ciphertext_len,
    643         max_bytes: ENVELOPE_MAX_BYTES,
    644     })?;
    645     let context_len = context.map_or(0, |value| value.to_canonical_bytes().len());
    646     let capacity = 4
    647         + 2
    648         + 1
    649         + 1
    650         + 1
    651         + 4
    652         + 2
    653         + id.len()
    654         + context_len
    655         + NONCE_BYTES
    656         + 4
    657         + wrapped_key.as_bytes().len()
    658         + 4;
    659     let mut encoded = Vec::with_capacity(capacity);
    660     encoded.extend_from_slice(&MAGIC);
    661     encoded.extend_from_slice(&version.to_be_bytes());
    662     encoded.push(cipher.code());
    663     encoded.push(key_source.code());
    664     encoded.push(reference.backend().code());
    665     encoded.extend_from_slice(&reference.key_version().get().to_be_bytes());
    666     encoded.extend_from_slice(&id_len.to_be_bytes());
    667     encoded.extend_from_slice(id);
    668     match (version, context) {
    669         (LEGACY_ENVELOPE_VERSION, None) => {}
    670         (ENVELOPE_VERSION, Some(context)) => {
    671             encoded.extend_from_slice(&context.to_canonical_bytes());
    672         }
    673         (LEGACY_ENVELOPE_VERSION | ENVELOPE_VERSION, _) => {
    674             return Err(Error::EnvelopeMalformed);
    675         }
    676         (version, _) => return Err(Error::UnsupportedEnvelopeVersion { version }),
    677     }
    678     encoded.extend_from_slice(nonce.as_bytes());
    679     encoded.extend_from_slice(&wrapped_len.to_be_bytes());
    680     encoded.extend_from_slice(wrapped_key.as_bytes());
    681     encoded.extend_from_slice(&ciphertext_len.to_be_bytes());
    682     Ok(encoded)
    683 }
    684 
    685 struct Decoder<'a> {
    686     remaining: &'a [u8],
    687 }
    688 
    689 impl<'a> Decoder<'a> {
    690     const fn new(encoded: &'a [u8]) -> Self {
    691         Self { remaining: encoded }
    692     }
    693 
    694     fn take(&mut self, length: usize) -> Result<&'a [u8], Error> {
    695         if length > self.remaining.len() {
    696             return Err(Error::EnvelopeMalformed);
    697         }
    698         let (value, remaining) = self.remaining.split_at(length);
    699         self.remaining = remaining;
    700         Ok(value)
    701     }
    702 
    703     fn take_array<const N: usize>(&mut self) -> Result<[u8; N], Error> {
    704         self.take(N)?
    705             .try_into()
    706             .map_err(|_| Error::EnvelopeMalformed)
    707     }
    708 
    709     fn u8(&mut self) -> Result<u8, Error> {
    710         Ok(self.take_array::<1>()?[0])
    711     }
    712 
    713     fn u16(&mut self) -> Result<u16, Error> {
    714         Ok(u16::from_be_bytes(self.take_array()?))
    715     }
    716 
    717     fn u32(&mut self) -> Result<u32, Error> {
    718         Ok(u32::from_be_bytes(self.take_array()?))
    719     }
    720 
    721     fn u32_usize(&mut self) -> Result<usize, Error> {
    722         usize::try_from(self.u32()?).map_err(|_| Error::EnvelopeMalformed)
    723     }
    724 
    725     fn bounded_string(&mut self, max: usize) -> Result<String, Error> {
    726         let length = usize::from(self.u16()?);
    727         if length > max {
    728             return Err(Error::EnvelopeMalformed);
    729         }
    730         let value =
    731             core::str::from_utf8(self.take(length)?).map_err(|_| Error::EnvelopeMalformed)?;
    732         Ok(String::from(value))
    733     }
    734 
    735     const fn is_empty(&self) -> bool {
    736         self.remaining.is_empty()
    737     }
    738 }
    739 
    740 #[cfg(test)]
    741 mod tests {
    742     use super::*;
    743     use alloc::vec;
    744 
    745     fn context() -> EnvelopeContext {
    746         EnvelopeContext::new(
    747             EnvelopePurpose::parse("radroots.private_artifact").expect("purpose"),
    748             EnvelopeSubject::parse("private_artifact", "01010101010101010101010101010101")
    749                 .expect("subject"),
    750             PayloadSchemaId::parse("trade.private_terms.v1").expect("schema"),
    751         )
    752     }
    753 
    754     fn envelope() -> EncryptedEnvelope {
    755         EncryptedEnvelope {
    756             version: ENVELOPE_VERSION,
    757             cipher: Cipher::XChaCha20Poly1305,
    758             key_source: KeySource::ProviderWrapped,
    759             reference: SecretRef::new(
    760                 SecretId::parse("coverage-key").expect("id"),
    761                 BackendKind::Memory,
    762                 KeyVersion::new(1).expect("version"),
    763             ),
    764             context: Some(context()),
    765             nonce: Nonce::new([7; NONCE_BYTES]),
    766             wrapped_key: WrappedSecret::from_bytes(vec![8; 32]).expect("wrapped"),
    767             ciphertext: vec![9; AEAD_TAG_BYTES],
    768         }
    769     }
    770 
    771     #[test]
    772     fn decode_and_validation_reject_every_bounded_wire_failure() {
    773         let encoded = envelope().encode().expect("encoded");
    774         assert_eq!(
    775             EncryptedEnvelope::decode(&encoded)
    776                 .expect("decode")
    777                 .version(),
    778             ENVELOPE_VERSION
    779         );
    780         assert!(matches!(
    781             EncryptedEnvelope::decode(&vec![0; ENVELOPE_MAX_BYTES + 1]),
    782             Err(Error::EnvelopeTooLarge { .. })
    783         ));
    784         assert_eq!(
    785             EncryptedEnvelope::decode(&[]).err(),
    786             Some(Error::EnvelopeMalformed)
    787         );
    788 
    789         let mut malformed = encoded.clone();
    790         malformed[0] = b'X';
    791         assert_eq!(
    792             EncryptedEnvelope::decode(&malformed).err(),
    793             Some(Error::EnvelopeMalformed)
    794         );
    795         for (offset, expected) in [
    796             (5, Error::UnsupportedEnvelopeVersion { version: 3 }),
    797             (6, Error::UnsupportedCipher { cipher: 9 }),
    798             (7, Error::UnsupportedKeySource { key_source: 9 }),
    799             (8, Error::UnsupportedBackend { backend: 9 }),
    800         ] {
    801             let mut unsupported = encoded.clone();
    802             unsupported[offset] = if offset == 5 { 3 } else { 9 };
    803             assert_eq!(
    804                 EncryptedEnvelope::decode(&unsupported).err(),
    805                 Some(expected)
    806             );
    807         }
    808         let context_version_offset = 4 + 2 + 1 + 1 + 1 + 4 + 2 + "coverage-key".len();
    809         let mut unsupported = encoded.clone();
    810         unsupported[context_version_offset + 1] = 2;
    811         assert_eq!(
    812             EncryptedEnvelope::decode(&unsupported).err(),
    813             Some(Error::UnsupportedContextVersion { version: 2 })
    814         );
    815         let mut trailing = encoded;
    816         trailing.push(0);
    817         assert_eq!(
    818             EncryptedEnvelope::decode(&trailing).err(),
    819             Some(Error::EnvelopeMalformed)
    820         );
    821 
    822         let mut invalid = envelope();
    823         invalid.version = 3;
    824         assert_eq!(
    825             invalid.encode().err(),
    826             Some(Error::UnsupportedEnvelopeVersion { version: 3 })
    827         );
    828         let mut invalid = envelope();
    829         invalid.context = None;
    830         assert_eq!(invalid.encode().err(), Some(Error::EnvelopeMalformed));
    831         let mut invalid = envelope();
    832         invalid.ciphertext.clear();
    833         assert_eq!(invalid.encode().err(), Some(Error::EnvelopeMalformed));
    834         let mut invalid = envelope();
    835         invalid.ciphertext = vec![0; ENVELOPE_MAX_BYTES];
    836         assert!(matches!(
    837             invalid.encode(),
    838             Err(Error::EnvelopeTooLarge { .. })
    839         ));
    840     }
    841 }