1use chacha20poly1305::{
2 KeyInit, XChaCha20Poly1305, XNonce,
3 aead::{Aead, Payload},
4};
5use hkdf::Hkdf;
6use rand_core::{OsRng, RngCore};
7use sha2::Sha256;
8use thiserror::Error;
9use x25519_dalek::{PublicKey, StaticSecret};
10use zeroize::Zeroizing;
11
12pub const BODY_MAGIC: [u8; 8] = *b"FOCTETHB";
14pub const BODY_VERSION_V0: u8 = 0x01;
16pub const BODY_PROFILE_V0: u8 = 0x01;
18pub const X25519_PUBLIC_KEY_LEN: usize = 32;
20pub const XCHACHA_NONCE_LEN: usize = 24;
22const CONTENT_KEY_LEN: usize = 32;
23const TAG_LEN: usize = 16;
24const WRAP_INFO_LABEL: &[u8] = b"foctet body wrap v0";
25
26#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct BodyEnvelopeLimits {
29 pub max_header_bytes: usize,
31 pub max_recipients: usize,
33 pub max_key_id_len: usize,
35 pub max_wrapped_key_len: usize,
37 pub max_payload_len: usize,
39}
40
41impl Default for BodyEnvelopeLimits {
42 fn default() -> Self {
43 Self {
44 max_header_bytes: 64 * 1024,
45 max_recipients: 16,
46 max_key_id_len: 512,
47 max_wrapped_key_len: 512,
48 max_payload_len: 64 * 1024 * 1024,
49 }
50 }
51}
52
53#[derive(Debug, Error, Clone, Eq, PartialEq)]
55pub enum BodyEnvelopeError {
56 #[error("invalid body-envelope magic")]
58 InvalidMagic,
59 #[error("unsupported body-envelope version: {0}")]
61 UnsupportedVersion(u8),
62 #[error("unsupported body-envelope profile: {0}")]
64 UnsupportedProfile(u8),
65 #[error("truncated body-envelope input")]
67 Truncated,
68 #[error("invalid body-envelope header: {0}")]
70 InvalidHeader(&'static str),
71 #[error("body-envelope limit exceeded: {0}")]
73 LimitExceeded(&'static str),
74 #[error("recipient not found")]
76 RecipientNotFound,
77 #[error("content-key unwrap failed")]
79 KeyUnwrapFailed,
80 #[error("payload decryption failed")]
82 DecryptFailed,
83 #[error("payload encryption failed")]
85 EncryptFailed,
86 #[error("hkdf expand failed")]
88 Hkdf,
89}
90
91#[derive(Clone, Debug)]
92struct RecipientEntry {
93 key_id: Vec<u8>,
94 wrapped_key: Vec<u8>,
95}
96
97#[derive(Clone, Debug)]
98struct ParsedEnvelope<'a> {
99 header_bytes: &'a [u8],
100 payload_ciphertext: &'a [u8],
101 ephemeral_public: [u8; X25519_PUBLIC_KEY_LEN],
102 payload_nonce: [u8; XCHACHA_NONCE_LEN],
103 recipients: Vec<RecipientEntry>,
104}
105
106pub fn seal_body(
108 plaintext: &[u8],
109 recipient_public_key: [u8; 32],
110 recipient_key_id: &[u8],
111) -> Result<Vec<u8>, BodyEnvelopeError> {
112 seal_body_with_limits(
113 plaintext,
114 recipient_public_key,
115 recipient_key_id,
116 &BodyEnvelopeLimits::default(),
117 )
118}
119
120pub fn seal_body_with_limits(
122 plaintext: &[u8],
123 recipient_public_key: [u8; 32],
124 recipient_key_id: &[u8],
125 limits: &BodyEnvelopeLimits,
126) -> Result<Vec<u8>, BodyEnvelopeError> {
127 if recipient_key_id.is_empty() {
128 return Err(BodyEnvelopeError::InvalidHeader("empty recipient key id"));
129 }
130 if recipient_key_id.len() > limits.max_key_id_len {
131 return Err(BodyEnvelopeError::LimitExceeded("key_id_len"));
132 }
133
134 let payload_len = plaintext
135 .len()
136 .checked_add(TAG_LEN)
137 .ok_or(BodyEnvelopeError::LimitExceeded("payload_len overflow"))?;
138 if payload_len > limits.max_payload_len {
139 return Err(BodyEnvelopeError::LimitExceeded("payload_len"));
140 }
141
142 let mut content_key = Zeroizing::new([0u8; CONTENT_KEY_LEN]);
143 OsRng.fill_bytes(&mut content_key[..]);
144
145 let mut payload_nonce = [0u8; XCHACHA_NONCE_LEN];
146 OsRng.fill_bytes(&mut payload_nonce);
147
148 let eph_priv = StaticSecret::random_from_rng(OsRng);
149 let eph_pub = PublicKey::from(&eph_priv).to_bytes();
150
151 let wrapped_key = wrap_content_key(
152 &content_key,
153 recipient_public_key,
154 eph_priv,
155 eph_pub,
156 recipient_key_id,
157 )?;
158
159 if wrapped_key.len() > limits.max_wrapped_key_len {
160 return Err(BodyEnvelopeError::LimitExceeded("wrapped_key_len"));
161 }
162
163 let header = encode_header(
164 &eph_pub,
165 &payload_nonce,
166 recipient_key_id,
167 &wrapped_key,
168 payload_len,
169 limits,
170 )?;
171
172 if header.len() > limits.max_header_bytes {
173 return Err(BodyEnvelopeError::LimitExceeded("header_len"));
174 }
175
176 let cipher = XChaCha20Poly1305::new_from_slice(&content_key[..])
177 .map_err(|_| BodyEnvelopeError::EncryptFailed)?;
178 let payload_ciphertext = cipher
179 .encrypt(
180 XNonce::from_slice(&payload_nonce),
181 Payload {
182 msg: plaintext,
183 aad: &header,
184 },
185 )
186 .map_err(|_| BodyEnvelopeError::EncryptFailed)?;
187
188 let mut out = Vec::with_capacity(
189 header
190 .len()
191 .checked_add(payload_ciphertext.len())
192 .ok_or(BodyEnvelopeError::LimitExceeded("envelope_len overflow"))?,
193 );
194 out.extend_from_slice(&header);
195 out.extend_from_slice(&payload_ciphertext);
196 Ok(out)
197}
198
199pub fn open_body(
201 envelope: &[u8],
202 recipient_secret_key: [u8; 32],
203) -> Result<Vec<u8>, BodyEnvelopeError> {
204 open_body_with_limits(
205 envelope,
206 recipient_secret_key,
207 &BodyEnvelopeLimits::default(),
208 )
209}
210
211pub fn open_body_with_limits(
213 envelope: &[u8],
214 recipient_secret_key: [u8; 32],
215 limits: &BodyEnvelopeLimits,
216) -> Result<Vec<u8>, BodyEnvelopeError> {
217 let parsed = parse_envelope(envelope, limits)?;
218
219 for recipient in &parsed.recipients {
220 let content_key = match unwrap_content_key(
221 &recipient.wrapped_key,
222 &recipient.key_id,
223 recipient_secret_key,
224 parsed.ephemeral_public,
225 ) {
226 Ok(key) => key,
227 Err(BodyEnvelopeError::KeyUnwrapFailed) => continue,
228 Err(err) => return Err(err),
229 };
230
231 let cipher = XChaCha20Poly1305::new_from_slice(&content_key[..])
232 .map_err(|_| BodyEnvelopeError::DecryptFailed)?;
233
234 let plain = cipher
235 .decrypt(
236 XNonce::from_slice(&parsed.payload_nonce),
237 Payload {
238 msg: parsed.payload_ciphertext,
239 aad: parsed.header_bytes,
240 },
241 )
242 .map_err(|_| BodyEnvelopeError::DecryptFailed)?;
243
244 return Ok(plain);
245 }
246
247 Err(BodyEnvelopeError::RecipientNotFound)
248}
249
250pub fn open_body_for_key_id(
252 envelope: &[u8],
253 recipient_secret_key: [u8; 32],
254 recipient_key_id: &[u8],
255) -> Result<Vec<u8>, BodyEnvelopeError> {
256 open_body_for_key_id_with_limits(
257 envelope,
258 recipient_secret_key,
259 recipient_key_id,
260 &BodyEnvelopeLimits::default(),
261 )
262}
263
264pub fn open_body_for_key_id_with_limits(
266 envelope: &[u8],
267 recipient_secret_key: [u8; 32],
268 recipient_key_id: &[u8],
269 limits: &BodyEnvelopeLimits,
270) -> Result<Vec<u8>, BodyEnvelopeError> {
271 let parsed = parse_envelope(envelope, limits)?;
272
273 let entry = parsed
274 .recipients
275 .iter()
276 .find(|entry| entry.key_id.as_slice() == recipient_key_id)
277 .ok_or(BodyEnvelopeError::RecipientNotFound)?;
278
279 let content_key = unwrap_content_key(
280 &entry.wrapped_key,
281 &entry.key_id,
282 recipient_secret_key,
283 parsed.ephemeral_public,
284 )?;
285
286 let cipher = XChaCha20Poly1305::new_from_slice(&content_key[..])
287 .map_err(|_| BodyEnvelopeError::DecryptFailed)?;
288
289 cipher
290 .decrypt(
291 XNonce::from_slice(&parsed.payload_nonce),
292 Payload {
293 msg: parsed.payload_ciphertext,
294 aad: parsed.header_bytes,
295 },
296 )
297 .map_err(|_| BodyEnvelopeError::DecryptFailed)
298}
299
300fn parse_envelope<'a>(
301 envelope: &'a [u8],
302 limits: &BodyEnvelopeLimits,
303) -> Result<ParsedEnvelope<'a>, BodyEnvelopeError> {
304 let mut cur = 0usize;
305
306 let magic = take(envelope, &mut cur, BODY_MAGIC.len())?;
307 if magic != BODY_MAGIC {
308 return Err(BodyEnvelopeError::InvalidMagic);
309 }
310
311 let version = *take(envelope, &mut cur, 1)?
312 .first()
313 .ok_or(BodyEnvelopeError::Truncated)?;
314 if version != BODY_VERSION_V0 {
315 return Err(BodyEnvelopeError::UnsupportedVersion(version));
316 }
317
318 let profile = *take(envelope, &mut cur, 1)?
319 .first()
320 .ok_or(BodyEnvelopeError::Truncated)?;
321 if profile != BODY_PROFILE_V0 {
322 return Err(BodyEnvelopeError::UnsupportedProfile(profile));
323 }
324
325 let flags = *take(envelope, &mut cur, 1)?
326 .first()
327 .ok_or(BodyEnvelopeError::Truncated)?;
328 if flags != 0 {
329 return Err(BodyEnvelopeError::InvalidHeader("unknown flags"));
330 }
331
332 let eph_len = *take(envelope, &mut cur, 1)?
333 .first()
334 .ok_or(BodyEnvelopeError::Truncated)? as usize;
335 if eph_len != X25519_PUBLIC_KEY_LEN {
336 return Err(BodyEnvelopeError::InvalidHeader(
337 "invalid ephemeral_public_key_len",
338 ));
339 }
340
341 let header_len = decode_varint(envelope, &mut cur)?;
342 let recipient_count = decode_varint(envelope, &mut cur)?;
343 let payload_len = decode_varint(envelope, &mut cur)?;
344
345 let header_len = to_usize(header_len, "header_len")?;
346 let recipient_count = to_usize(recipient_count, "recipient_count")?;
347 let payload_len = to_usize(payload_len, "payload_len")?;
348
349 if header_len > limits.max_header_bytes {
350 return Err(BodyEnvelopeError::LimitExceeded("header_len"));
351 }
352 if recipient_count > limits.max_recipients {
353 return Err(BodyEnvelopeError::LimitExceeded("recipient_count"));
354 }
355 if payload_len > limits.max_payload_len {
356 return Err(BodyEnvelopeError::LimitExceeded("payload_len"));
357 }
358 if header_len > envelope.len() {
359 return Err(BodyEnvelopeError::Truncated);
360 }
361
362 let min_tail = eph_len
363 .checked_add(XCHACHA_NONCE_LEN)
364 .ok_or(BodyEnvelopeError::InvalidHeader("header length overflow"))?;
365 if cur
366 .checked_add(min_tail)
367 .ok_or(BodyEnvelopeError::InvalidHeader("header length overflow"))?
368 > header_len
369 {
370 return Err(BodyEnvelopeError::InvalidHeader("header_len too small"));
371 }
372
373 let mut ephemeral_public = [0u8; X25519_PUBLIC_KEY_LEN];
374 ephemeral_public.copy_from_slice(take(envelope, &mut cur, X25519_PUBLIC_KEY_LEN)?);
375
376 let mut payload_nonce = [0u8; XCHACHA_NONCE_LEN];
377 payload_nonce.copy_from_slice(take(envelope, &mut cur, XCHACHA_NONCE_LEN)?);
378
379 let mut recipients = Vec::new();
380 for _ in 0..recipient_count {
381 if cur >= header_len {
382 return Err(BodyEnvelopeError::Truncated);
383 }
384
385 let key_id_len = to_usize(decode_varint(envelope, &mut cur)?, "key_id_len")?;
386 let wrapped_key_len = to_usize(decode_varint(envelope, &mut cur)?, "wrapped_key_len")?;
387
388 if key_id_len == 0 {
389 return Err(BodyEnvelopeError::InvalidHeader("empty key_id"));
390 }
391 if key_id_len > limits.max_key_id_len {
392 return Err(BodyEnvelopeError::LimitExceeded("key_id_len"));
393 }
394 if wrapped_key_len > limits.max_wrapped_key_len {
395 return Err(BodyEnvelopeError::LimitExceeded("wrapped_key_len"));
396 }
397 if wrapped_key_len != CONTENT_KEY_LEN + TAG_LEN {
398 return Err(BodyEnvelopeError::InvalidHeader(
399 "invalid wrapped_key_len for v0",
400 ));
401 }
402
403 let end = cur
404 .checked_add(key_id_len)
405 .and_then(|v| v.checked_add(wrapped_key_len))
406 .ok_or(BodyEnvelopeError::InvalidHeader("recipient entry overflow"))?;
407 if end > header_len || end > envelope.len() {
408 return Err(BodyEnvelopeError::Truncated);
409 }
410
411 let key_id = take(envelope, &mut cur, key_id_len)?.to_vec();
412 let wrapped_key = take(envelope, &mut cur, wrapped_key_len)?.to_vec();
413 recipients.push(RecipientEntry {
414 key_id,
415 wrapped_key,
416 });
417 }
418
419 if recipients.is_empty() {
420 return Err(BodyEnvelopeError::InvalidHeader(
421 "recipient_count must be >= 1",
422 ));
423 }
424
425 if cur != header_len {
426 return Err(BodyEnvelopeError::InvalidHeader(
427 "unexpected trailing header bytes",
428 ));
429 }
430
431 let expected_total = header_len
432 .checked_add(payload_len)
433 .ok_or(BodyEnvelopeError::InvalidHeader("envelope length overflow"))?;
434 if envelope.len() < expected_total {
435 return Err(BodyEnvelopeError::Truncated);
436 }
437 if envelope.len() != expected_total {
438 return Err(BodyEnvelopeError::InvalidHeader(
439 "unexpected trailing body bytes",
440 ));
441 }
442
443 let payload_ciphertext = &envelope[header_len..expected_total];
444 if payload_ciphertext.len() < TAG_LEN {
445 return Err(BodyEnvelopeError::InvalidHeader(
446 "payload ciphertext too short",
447 ));
448 }
449
450 Ok(ParsedEnvelope {
451 header_bytes: &envelope[..header_len],
452 payload_ciphertext,
453 ephemeral_public,
454 payload_nonce,
455 recipients,
456 })
457}
458
459fn wrap_content_key(
460 content_key: &[u8; CONTENT_KEY_LEN],
461 recipient_public_key: [u8; 32],
462 eph_priv: StaticSecret,
463 eph_pub: [u8; 32],
464 key_id: &[u8],
465) -> Result<Vec<u8>, BodyEnvelopeError> {
466 let recipient = PublicKey::from(recipient_public_key);
467 let shared = Zeroizing::new(eph_priv.diffie_hellman(&recipient).to_bytes());
468
469 let (wrap_key, wrap_nonce) = derive_wrap_material(&shared, eph_pub, recipient_public_key)?;
470
471 let cipher = XChaCha20Poly1305::new_from_slice(&wrap_key[..])
472 .map_err(|_| BodyEnvelopeError::KeyUnwrapFailed)?;
473 cipher
474 .encrypt(
475 XNonce::from_slice(&wrap_nonce),
476 Payload {
477 msg: content_key,
478 aad: key_id,
479 },
480 )
481 .map_err(|_| BodyEnvelopeError::KeyUnwrapFailed)
482}
483
484fn unwrap_content_key(
485 wrapped_key: &[u8],
486 key_id: &[u8],
487 recipient_secret_key: [u8; 32],
488 ephemeral_public_key: [u8; 32],
489) -> Result<[u8; CONTENT_KEY_LEN], BodyEnvelopeError> {
490 let recipient_priv = StaticSecret::from(recipient_secret_key);
491 let recipient_public = PublicKey::from(&recipient_priv).to_bytes();
492 let eph_pub = PublicKey::from(ephemeral_public_key);
493
494 let shared = Zeroizing::new(recipient_priv.diffie_hellman(&eph_pub).to_bytes());
495 let (wrap_key, wrap_nonce) =
496 derive_wrap_material(&shared, ephemeral_public_key, recipient_public)?;
497
498 let cipher = XChaCha20Poly1305::new_from_slice(&wrap_key[..])
499 .map_err(|_| BodyEnvelopeError::KeyUnwrapFailed)?;
500 let unwrapped = cipher
501 .decrypt(
502 XNonce::from_slice(&wrap_nonce),
503 Payload {
504 msg: wrapped_key,
505 aad: key_id,
506 },
507 )
508 .map_err(|_| BodyEnvelopeError::KeyUnwrapFailed)?;
509
510 if unwrapped.len() != CONTENT_KEY_LEN {
511 return Err(BodyEnvelopeError::KeyUnwrapFailed);
512 }
513
514 let mut out = [0u8; CONTENT_KEY_LEN];
515 out.copy_from_slice(&unwrapped);
516 Ok(out)
517}
518
519fn derive_wrap_material(
520 shared_secret: &[u8; 32],
521 ephemeral_public_key: [u8; 32],
522 recipient_public_key: [u8; 32],
523) -> Result<(Zeroizing<[u8; 32]>, [u8; 24]), BodyEnvelopeError> {
524 let mut info = [0u8; WRAP_INFO_LABEL.len() + 64];
525 let label_len = WRAP_INFO_LABEL.len();
526 info[..label_len].copy_from_slice(WRAP_INFO_LABEL);
527 info[label_len..label_len + 32].copy_from_slice(&ephemeral_public_key);
528 info[label_len + 32..].copy_from_slice(&recipient_public_key);
529
530 let hk = Hkdf::<Sha256>::new(None, shared_secret);
531 let mut okm = Zeroizing::new([0u8; CONTENT_KEY_LEN + XCHACHA_NONCE_LEN]);
532 hk.expand(&info, &mut okm[..])
533 .map_err(|_| BodyEnvelopeError::Hkdf)?;
534
535 let mut wrap_key = Zeroizing::new([0u8; CONTENT_KEY_LEN]);
536 wrap_key.copy_from_slice(&okm[..CONTENT_KEY_LEN]);
537
538 let mut wrap_nonce = [0u8; XCHACHA_NONCE_LEN];
539 wrap_nonce.copy_from_slice(&okm[CONTENT_KEY_LEN..]);
540
541 Ok((wrap_key, wrap_nonce))
542}
543
544fn encode_header(
545 ephemeral_public_key: &[u8; 32],
546 payload_nonce: &[u8; XCHACHA_NONCE_LEN],
547 recipient_key_id: &[u8],
548 wrapped_key: &[u8],
549 payload_len: usize,
550 limits: &BodyEnvelopeLimits,
551) -> Result<Vec<u8>, BodyEnvelopeError> {
552 let payload_len_u64 = u64::try_from(payload_len)
553 .map_err(|_| BodyEnvelopeError::LimitExceeded("payload_len overflow"))?;
554
555 let mut header_len_guess = 0u64;
557 let mut converged = false;
558 let mut encoded = Vec::new();
559
560 for _ in 0..4 {
561 encoded.clear();
562 encoded.extend_from_slice(&BODY_MAGIC);
563 encoded.push(BODY_VERSION_V0);
564 encoded.push(BODY_PROFILE_V0);
565 encoded.push(0); encoded.push(X25519_PUBLIC_KEY_LEN as u8);
567 encode_varint(header_len_guess, &mut encoded);
568 encode_varint(1, &mut encoded);
569 encode_varint(payload_len_u64, &mut encoded);
570
571 encoded.extend_from_slice(ephemeral_public_key);
572 encoded.extend_from_slice(payload_nonce);
573 encode_varint(recipient_key_id.len() as u64, &mut encoded);
574 encode_varint(wrapped_key.len() as u64, &mut encoded);
575 encoded.extend_from_slice(recipient_key_id);
576 encoded.extend_from_slice(wrapped_key);
577
578 let actual = u64::try_from(encoded.len())
579 .map_err(|_| BodyEnvelopeError::LimitExceeded("header_len overflow"))?;
580 if actual == header_len_guess {
581 converged = true;
582 break;
583 }
584 header_len_guess = actual;
585 }
586
587 if !converged {
588 return Err(BodyEnvelopeError::InvalidHeader(
589 "header_len encoding did not converge",
590 ));
591 }
592
593 let final_len = encoded.len();
594 if final_len > limits.max_header_bytes {
595 return Err(BodyEnvelopeError::LimitExceeded("header_len"));
596 }
597
598 Ok(encoded)
599}
600
601fn to_usize(v: u64, field: &'static str) -> Result<usize, BodyEnvelopeError> {
602 usize::try_from(v).map_err(|_| BodyEnvelopeError::LimitExceeded(field))
603}
604
605fn take<'a>(input: &'a [u8], cur: &mut usize, len: usize) -> Result<&'a [u8], BodyEnvelopeError> {
606 let end = cur.checked_add(len).ok_or(BodyEnvelopeError::Truncated)?;
607 if end > input.len() {
608 return Err(BodyEnvelopeError::Truncated);
609 }
610 let out = &input[*cur..end];
611 *cur = end;
612 Ok(out)
613}
614
615fn encode_varint(mut value: u64, out: &mut Vec<u8>) {
616 while value >= 0x80 {
617 out.push((value as u8 & 0x7F) | 0x80);
618 value >>= 7;
619 }
620 out.push(value as u8);
621}
622
623fn decode_varint(input: &[u8], cur: &mut usize) -> Result<u64, BodyEnvelopeError> {
624 let mut shift = 0u32;
625 let mut value = 0u64;
626
627 for _ in 0..10 {
628 let byte = *take(input, cur, 1)?
629 .first()
630 .ok_or(BodyEnvelopeError::Truncated)?;
631 let chunk = (byte & 0x7F) as u64;
632
633 if shift >= 64 && chunk != 0 {
634 return Err(BodyEnvelopeError::InvalidHeader("varint overflow"));
635 }
636
637 value |= chunk
638 .checked_shl(shift)
639 .ok_or(BodyEnvelopeError::InvalidHeader("varint overflow"))?;
640
641 if byte & 0x80 == 0 {
642 return Ok(value);
643 }
644
645 shift += 7;
646 }
647
648 Err(BodyEnvelopeError::InvalidHeader("varint too long"))
649}
650
651#[cfg(test)]
652mod tests {
653 use super::*;
654
655 #[test]
656 fn body_roundtrip_single_recipient() {
657 let recipient_priv = StaticSecret::random_from_rng(OsRng);
658 let recipient_pub = PublicKey::from(&recipient_priv).to_bytes();
659
660 let plain = b"hello application/foctet body";
661 let envelope = seal_body(plain, recipient_pub, b"kid-1").expect("seal");
662 let out = open_body(&envelope, recipient_priv.to_bytes()).expect("open");
663
664 assert_eq!(out, plain);
665 }
666
667 #[test]
668 fn open_rejects_invalid_magic() {
669 let recipient_priv = StaticSecret::random_from_rng(OsRng);
670 let recipient_pub = PublicKey::from(&recipient_priv).to_bytes();
671
672 let plain = b"hello";
673 let mut envelope = seal_body(plain, recipient_pub, b"kid").expect("seal");
674 envelope[0] ^= 0xFF;
675
676 let err = open_body(&envelope, recipient_priv.to_bytes()).expect_err("must fail");
677 assert_eq!(err, BodyEnvelopeError::InvalidMagic);
678 }
679
680 #[test]
681 fn open_rejects_unsupported_version() {
682 let recipient_priv = StaticSecret::random_from_rng(OsRng);
683 let recipient_pub = PublicKey::from(&recipient_priv).to_bytes();
684
685 let plain = b"hello";
686 let mut envelope = seal_body(plain, recipient_pub, b"kid").expect("seal");
687 envelope[8] = 0xFF;
688
689 let err = open_body(&envelope, recipient_priv.to_bytes()).expect_err("must fail");
690 assert_eq!(err, BodyEnvelopeError::UnsupportedVersion(0xFF));
691 }
692
693 #[test]
694 fn open_rejects_truncated_input() {
695 let recipient_priv = StaticSecret::random_from_rng(OsRng);
696 let recipient_pub = PublicKey::from(&recipient_priv).to_bytes();
697
698 let plain = b"hello";
699 let envelope = seal_body(plain, recipient_pub, b"kid").expect("seal");
700 let truncated = &envelope[..envelope.len() - 1];
701
702 let err = open_body(truncated, recipient_priv.to_bytes()).expect_err("must fail");
703 assert_eq!(err, BodyEnvelopeError::Truncated);
704 }
705
706 #[test]
707 fn open_rejects_oversized_lengths() {
708 let recipient_priv = StaticSecret::random_from_rng(OsRng);
709 let recipient_pub = PublicKey::from(&recipient_priv).to_bytes();
710
711 let plain = b"hello";
712 let envelope = seal_body(plain, recipient_pub, b"kid").expect("seal");
713
714 let limits = BodyEnvelopeLimits {
715 max_header_bytes: 16,
716 ..BodyEnvelopeLimits::default()
717 };
718
719 let err = open_body_with_limits(&envelope, recipient_priv.to_bytes(), &limits)
720 .expect_err("must fail");
721 assert_eq!(err, BodyEnvelopeError::LimitExceeded("header_len"));
722 }
723
724 #[test]
725 fn open_with_wrong_recipient_fails() {
726 let recipient_priv = StaticSecret::random_from_rng(OsRng);
727 let recipient_pub = PublicKey::from(&recipient_priv).to_bytes();
728 let wrong_priv = StaticSecret::random_from_rng(OsRng);
729
730 let plain = b"hello";
731 let envelope = seal_body(plain, recipient_pub, b"kid").expect("seal");
732
733 let err = open_body(&envelope, wrong_priv.to_bytes()).expect_err("must fail");
734 assert_eq!(err, BodyEnvelopeError::RecipientNotFound);
735 }
736
737 #[test]
738 fn malformed_wrapped_key_is_rejected() {
739 let recipient_priv = StaticSecret::random_from_rng(OsRng);
740 let recipient_pub = PublicKey::from(&recipient_priv).to_bytes();
741
742 let plain = b"hello";
743 let envelope = seal_body(plain, recipient_pub, b"kid").expect("seal");
744
745 let mut cur = 0usize;
747 cur += 8 + 1 + 1 + 1 + 1;
748 let _ = decode_varint(&envelope, &mut cur).expect("header_len");
749 let _ = decode_varint(&envelope, &mut cur).expect("recipient_count");
750 let _ = decode_varint(&envelope, &mut cur).expect("payload_len");
751 cur += X25519_PUBLIC_KEY_LEN + XCHACHA_NONCE_LEN;
752 let _ = decode_varint(&envelope, &mut cur).expect("key_id_len");
753
754 let mut malformed = envelope.clone();
755 malformed[cur] = 47; let err = open_body(&malformed, recipient_priv.to_bytes()).expect_err("must fail");
758 assert_eq!(
759 err,
760 BodyEnvelopeError::InvalidHeader("invalid wrapped_key_len for v0")
761 );
762 }
763}