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 }