1#[cfg(feature = "alloc")]
80use alloc::vec::Vec;
81
82#[cfg(feature = "serde")]
83use serde::{Deserialize, Serialize};
84use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
85
86use crate::classic::crypto_aead_chacha20poly1305_ietf::{
87 crypto_aead_chacha20poly1305_ietf_decrypt_detached,
88 crypto_aead_chacha20poly1305_ietf_encrypt_detached,
89};
90use crate::classic::crypto_kdf::{
91 crypto_kdf_hkdf_sha256_expand, crypto_kdf_hkdf_sha256_extract_final,
92 crypto_kdf_hkdf_sha256_extract_init, crypto_kdf_hkdf_sha256_extract_update,
93};
94use crate::classic::crypto_kem_xwing;
95use crate::constants::{
96 CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES, CRYPTO_AEAD_CHACHA20POLY1305_IETF_KEYBYTES,
97 CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES, CRYPTO_KDF_HKDF_SHA256_KEYBYTES,
98 CRYPTO_KEM_XWING_CIPHERTEXTBYTES, CRYPTO_KEM_XWING_ENCSEEDBYTES,
99 CRYPTO_KEM_XWING_PUBLICKEYBYTES, CRYPTO_KEM_XWING_SECRETKEYBYTES,
100 CRYPTO_KEM_XWING_SHAREDSECRETBYTES,
101};
102use crate::error::{Error, ErrorContext};
103pub use crate::kem::xwing::{KeyPair, PublicKey, SecretKey, StackKeyPair};
104use crate::mlkem::Arith;
105use crate::rng::copy_randombytes;
106use crate::types::*;
107
108pub type EncapsulatedKey = StackByteArray<CRYPTO_KEM_XWING_CIPHERTEXTBYTES>;
111pub type Mac = StackByteArray<CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES>;
113
114pub const SEALBYTES: usize =
116 CRYPTO_KEM_XWING_CIPHERTEXTBYTES + CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES;
117
118#[cfg(any(
119 all(feature = "protected", any(unix, windows)),
120 all(doc, not(doctest), feature = "std")
121))]
122#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "protected")))]
123pub mod protected {
124 use super::DryocSealedBox;
139 use crate::constants::{
140 CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES, CRYPTO_KEM_XWING_CIPHERTEXTBYTES,
141 };
142 pub use crate::kem::xwing::protected::*;
143
144 pub type EncapsulatedKey = HeapByteArray<CRYPTO_KEM_XWING_CIPHERTEXTBYTES>;
146 pub type Mac = HeapByteArray<CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES>;
148 pub type LockedBox = DryocSealedBox<Locked<EncapsulatedKey>, Locked<Mac>, LockedBytes>;
150}
151
152#[derive(Zeroize, Clone, Debug)]
153#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
154pub struct DryocSealedBox<
158 EncapsulatedKey: ByteArray<CRYPTO_KEM_XWING_CIPHERTEXTBYTES> + Zeroize,
159 Mac: ByteArray<CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES> + Zeroize,
160 Data: Bytes + Zeroize,
161> {
162 enc: EncapsulatedKey,
163 tag: Mac,
164 data: Data,
165}
166
167#[cfg(feature = "alloc")]
169pub type VecBox = DryocSealedBox<EncapsulatedKey, Mac, Vec<u8>>;
170
171const SUITE_ID: &[u8; 10] = b"HPKE\x64\x7a\x00\x01\x00\x03";
173
174type AeadKey = [u8; CRYPTO_AEAD_CHACHA20POLY1305_IETF_KEYBYTES];
175type AeadNonce = [u8; CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES];
176
177fn labeled_extract(prk: &mut [u8; 32], salt: &[u8], label: &[u8], ikm: &[u8]) {
179 let mut state = crypto_kdf_hkdf_sha256_extract_init(Some(salt));
180 for part in [b"HPKE-v1".as_slice(), SUITE_ID, label, ikm] {
181 crypto_kdf_hkdf_sha256_extract_update(&mut state, part);
182 }
183 crypto_kdf_hkdf_sha256_extract_final(state, prk);
184}
185
186const KEY_SCHEDULE_CONTEXT_BYTES: usize = 1 + 2 * CRYPTO_KDF_HKDF_SHA256_KEYBYTES;
188
189const LABELED_INFO_MAX_BYTES: usize = 2
193 + <[u8]>::len(b"HPKE-v1")
194 + <[u8]>::len(SUITE_ID)
195 + <[u8]>::len(b"base_nonce")
196 + KEY_SCHEDULE_CONTEXT_BYTES;
197
198fn labeled_expand(output: &mut [u8], prk: &[u8; 32], label: &[u8], info: &[u8]) {
200 let length = u16::try_from(output.len()).expect("short HPKE output");
201 let mut labeled_info = [0u8; LABELED_INFO_MAX_BYTES];
202 let mut labeled_info_len = 0;
203 for part in [&length.to_be_bytes()[..], b"HPKE-v1", SUITE_ID, label, info] {
204 labeled_info[labeled_info_len..labeled_info_len + part.len()].copy_from_slice(part);
205 labeled_info_len += part.len();
206 }
207 crypto_kdf_hkdf_sha256_expand(output, &labeled_info[..labeled_info_len], prk)
208 .expect("HPKE output lengths are within HKDF's limit");
209}
210
211#[derive(Zeroize, ZeroizeOnDrop)]
214struct Context {
215 key: AeadKey,
216 nonce: AeadNonce,
217}
218
219impl Context {
220 fn new() -> Self {
221 Self {
222 key: [0u8; CRYPTO_AEAD_CHACHA20POLY1305_IETF_KEYBYTES],
223 nonce: [0u8; CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES],
224 }
225 }
226
227 fn schedule(&mut self, shared_secret: &[u8], info: &[u8]) {
229 let mut psk_id_hash = [0u8; CRYPTO_KDF_HKDF_SHA256_KEYBYTES];
230 labeled_extract(&mut psk_id_hash, b"", b"psk_id_hash", b"");
231 let mut info_hash = [0u8; CRYPTO_KDF_HKDF_SHA256_KEYBYTES];
232 labeled_extract(&mut info_hash, b"", b"info_hash", info);
233 let mut context = [0u8; KEY_SCHEDULE_CONTEXT_BYTES];
235 let (psk_id_part, info_part) = context[1..].split_at_mut(CRYPTO_KDF_HKDF_SHA256_KEYBYTES);
236 psk_id_part.copy_from_slice(&psk_id_hash);
237 info_part.copy_from_slice(&info_hash);
238 let mut secret = Zeroizing::new([0u8; CRYPTO_KDF_HKDF_SHA256_KEYBYTES]);
239 labeled_extract(&mut secret, shared_secret, b"secret", b"");
240 labeled_expand(&mut self.key, &secret, b"key", &context);
241 labeled_expand(&mut self.nonce, &secret, b"base_nonce", &context);
242 }
243
244 fn setup_sender(
247 &mut self,
248 arith: Arith,
249 enc: &mut [u8; CRYPTO_KEM_XWING_CIPHERTEXTBYTES],
250 public_key: &[u8; CRYPTO_KEM_XWING_PUBLICKEYBYTES],
251 info: &[u8],
252 seed: &[u8; CRYPTO_KEM_XWING_ENCSEEDBYTES],
253 ) -> Result<(), Error> {
254 let mut shared_secret = Zeroizing::new([0u8; CRYPTO_KEM_XWING_SHAREDSECRETBYTES]);
255 crypto_kem_xwing::enc_deterministic(arith, enc, &mut shared_secret, public_key, seed)?;
256 self.schedule(&*shared_secret, info);
257 Ok(())
258 }
259
260 fn setup_receiver(
263 &mut self,
264 arith: Arith,
265 enc: &[u8; CRYPTO_KEM_XWING_CIPHERTEXTBYTES],
266 secret_key: &[u8; CRYPTO_KEM_XWING_SECRETKEYBYTES],
267 info: &[u8],
268 ) -> Result<(), Error> {
269 let mut shared_secret = Zeroizing::new([0u8; CRYPTO_KEM_XWING_SHAREDSECRETBYTES]);
270 crypto_kem_xwing::dec(arith, &mut shared_secret, enc, secret_key)?;
271 self.schedule(&*shared_secret, info);
272 Ok(())
273 }
274}
275
276impl<
277 EncapsulatedKey: NewByteArray<CRYPTO_KEM_XWING_CIPHERTEXTBYTES> + Zeroize,
278 Mac: NewByteArray<CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES> + Zeroize,
279 Data: NewBytes + ResizableBytes + Zeroize,
280> DryocSealedBox<EncapsulatedKey, Mac, Data>
281{
282 pub fn seal<
294 Message: Bytes + ?Sized,
295 RecipientPublicKey: ByteArray<CRYPTO_KEM_XWING_PUBLICKEYBYTES>,
296 >(
297 message: &Message,
298 recipient_public_key: &RecipientPublicKey,
299 ) -> Result<Self, Error> {
300 let mut seed = Zeroizing::new([0u8; CRYPTO_KEM_XWING_ENCSEEDBYTES]);
301 copy_randombytes(seed.as_mut_slice());
302 let mut sealed = Self {
303 enc: EncapsulatedKey::new_byte_array(),
304 tag: Mac::new_byte_array(),
305 data: Data::new_bytes(),
306 };
307 sealed.data.resize(message.as_slice().len(), 0);
308 let mut context = Context::new();
309 context.setup_sender(
310 Arith::detect(),
311 sealed.enc.as_mut_array(),
312 recipient_public_key.as_array(),
313 b"",
314 &seed,
315 )?;
316 crypto_aead_chacha20poly1305_ietf_encrypt_detached(
317 sealed.data.as_mut_slice(),
318 sealed.tag.as_mut_array(),
319 message.as_slice(),
320 None,
321 &context.nonce,
322 &context.key,
323 )?;
324 Ok(sealed)
325 }
326}
327
328impl<
329 'a,
330 EncapsulatedKey: ByteArray<CRYPTO_KEM_XWING_CIPHERTEXTBYTES> + TryFrom<&'a [u8]> + Zeroize,
331 Mac: ByteArray<CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES> + TryFrom<&'a [u8]> + Zeroize,
332 Data: Bytes + From<&'a [u8]> + Zeroize,
333> DryocSealedBox<EncapsulatedKey, Mac, Data>
334{
335 pub fn from_bytes(bytes: &'a [u8]) -> Result<Self, Error> {
343 validate_length!(min SEALBYTES, bytes.len(), ErrorContext::SealedBox);
344 let (enc, rest) = bytes.split_at(CRYPTO_KEM_XWING_CIPHERTEXTBYTES);
345 let (data, tag) = rest.split_at(rest.len() - CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES);
346 Ok(Self {
347 enc: EncapsulatedKey::try_from(enc)
348 .map_err(|_| Error::invalid_encoding(ErrorContext::Ciphertext))?,
349 tag: Mac::try_from(tag)
350 .map_err(|_| Error::invalid_encoding(ErrorContext::AuthenticationTag))?,
351 data: Data::from(data),
352 })
353 }
354}
355
356impl<
357 EncapsulatedKey: ByteArray<CRYPTO_KEM_XWING_CIPHERTEXTBYTES> + Zeroize,
358 Mac: ByteArray<CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES> + Zeroize,
359 Data: Bytes + Zeroize,
360> DryocSealedBox<EncapsulatedKey, Mac, Data>
361{
362 #[must_use]
364 pub fn from_parts(enc: EncapsulatedKey, tag: Mac, data: Data) -> Self {
365 Self { enc, tag, data }
366 }
367
368 pub fn enc(&self) -> &EncapsulatedKey {
370 &self.enc
371 }
372
373 pub fn tag(&self) -> &Mac {
375 &self.tag
376 }
377
378 pub fn data(&self) -> &Data {
380 &self.data
381 }
382
383 #[must_use]
386 pub fn into_parts(self) -> (EncapsulatedKey, Mac, Data) {
387 (self.enc, self.tag, self.data)
388 }
389
390 #[cfg(feature = "alloc")]
392 #[must_use]
393 pub fn to_vec(&self) -> Vec<u8> {
394 self.to_bytes()
395 }
396
397 #[must_use]
399 pub fn to_bytes<Bytes: NewBytes + ResizableBytes>(&self) -> Bytes {
400 let mut bytes = Bytes::new_bytes();
401 bytes.resize(SEALBYTES + self.data.len(), 0);
402 let (enc, rest) = bytes
403 .as_mut_slice()
404 .split_at_mut(CRYPTO_KEM_XWING_CIPHERTEXTBYTES);
405 let (data, tag) = rest.split_at_mut(self.data.len());
406 enc.copy_from_slice(self.enc.as_array());
407 data.copy_from_slice(self.data.as_slice());
408 tag.copy_from_slice(self.tag.as_array());
409 bytes
410 }
411
412 pub fn open<
420 Output: ResizableBytes + NewBytes + Zeroize,
421 RecipientPublicKey: ByteArray<CRYPTO_KEM_XWING_PUBLICKEYBYTES> + Zeroize,
422 RecipientSecretKey: ByteArray<CRYPTO_KEM_XWING_SECRETKEYBYTES> + Zeroize,
423 >(
424 &self,
425 recipient_keypair: &KeyPair<RecipientPublicKey, RecipientSecretKey>,
426 ) -> Result<Output, Error> {
427 let mut message = Output::new_bytes();
428 message.resize(self.data.len(), 0);
429 let mut context = Context::new();
430 context.setup_receiver(
431 Arith::detect(),
432 self.enc.as_array(),
433 recipient_keypair.secret_key.as_array(),
434 b"",
435 )?;
436 crypto_aead_chacha20poly1305_ietf_decrypt_detached(
437 message.as_mut_slice(),
438 self.data.as_slice(),
439 self.tag.as_array(),
440 None,
441 &context.nonce,
442 &context.key,
443 )?;
444 Ok(message)
445 }
446}
447
448#[cfg(feature = "alloc")]
449impl DryocSealedBox<EncapsulatedKey, Mac, Vec<u8>> {
450 pub fn seal_to_vecbox<
457 Message: Bytes + ?Sized,
458 RecipientPublicKey: ByteArray<CRYPTO_KEM_XWING_PUBLICKEYBYTES>,
459 >(
460 message: &Message,
461 recipient_public_key: &RecipientPublicKey,
462 ) -> Result<Self, Error> {
463 Self::seal(message, recipient_public_key)
464 }
465
466 pub fn open_to_vec<
473 RecipientPublicKey: ByteArray<CRYPTO_KEM_XWING_PUBLICKEYBYTES> + Zeroize,
474 RecipientSecretKey: ByteArray<CRYPTO_KEM_XWING_SECRETKEYBYTES> + Zeroize,
475 >(
476 &self,
477 recipient_keypair: &KeyPair<RecipientPublicKey, RecipientSecretKey>,
478 ) -> Result<Vec<u8>, Error> {
479 self.open(recipient_keypair)
480 }
481}
482
483#[cfg(all(test, feature = "alloc"))]
484mod tests {
485 use super::*;
486 use crate::mlkem::tests::{field, records};
487
488 #[test]
492 fn test_hpke_known_answer() {
493 use crate::classic::crypto_aead_chacha20poly1305_ietf::{
494 crypto_aead_chacha20poly1305_ietf_decrypt, crypto_aead_chacha20poly1305_ietf_encrypt,
495 };
496
497 let record = &records(include_str!(
498 "mlkem/test-vectors/hpke_xwing_hkdfsha256_chacha20poly1305.txt"
499 ))[0];
500 let bytes = |key| hex::decode(record[key]).expect("hex");
501 let (info, aad, message) = (bytes("info"), bytes("aad"), bytes("pt"));
502 let (public_key, secret_key, seed) = (
503 field(record, "pkRm"),
504 field(record, "skRm"),
505 field(record, "ikmE"),
506 );
507 let expected_enc: [u8; CRYPTO_KEM_XWING_CIPHERTEXTBYTES] = field(record, "enc");
508 let expected_secret: [u8; CRYPTO_KEM_XWING_SHAREDSECRETBYTES] =
509 field(record, "shared_secret");
510
511 for arith in Arith::all() {
512 let mut enc = [0u8; CRYPTO_KEM_XWING_CIPHERTEXTBYTES];
513 let mut shared_secret = [0u8; CRYPTO_KEM_XWING_SHAREDSECRETBYTES];
514 crypto_kem_xwing::enc_deterministic(
515 arith,
516 &mut enc,
517 &mut shared_secret,
518 &public_key,
519 &seed,
520 )
521 .expect("enc");
522 assert_eq!(enc, expected_enc, "{arith:?}");
523 assert_eq!(shared_secret, expected_secret, "{arith:?}");
524 let mut shared_secret = [0u8; CRYPTO_KEM_XWING_SHAREDSECRETBYTES];
525 crypto_kem_xwing::dec(arith, &mut shared_secret, &expected_enc, &secret_key)
526 .expect("dec");
527 assert_eq!(shared_secret, expected_secret, "{arith:?}");
528
529 let mut enc = [0u8; CRYPTO_KEM_XWING_CIPHERTEXTBYTES];
530 let mut sender = Context::new();
531 sender
532 .setup_sender(arith, &mut enc, &public_key, &info, &seed)
533 .expect("sender");
534 assert_eq!(enc, expected_enc, "{arith:?}");
535 assert_eq!(sender.key, field::<32>(record, "key"), "{arith:?}");
536 assert_eq!(sender.nonce, field::<12>(record, "base_nonce"), "{arith:?}");
537 let mut ciphertext =
538 vec![0u8; message.len() + CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES];
539 crypto_aead_chacha20poly1305_ietf_encrypt(
540 &mut ciphertext,
541 &message,
542 Some(&aad),
543 &sender.nonce,
544 &sender.key,
545 )
546 .expect("encrypt");
547 assert_eq!(ciphertext, bytes("ct"), "{arith:?}");
548
549 let mut receiver = Context::new();
550 receiver
551 .setup_receiver(arith, &expected_enc, &secret_key, &info)
552 .expect("receiver");
553 assert_eq!(receiver.key, sender.key, "{arith:?}");
554 assert_eq!(receiver.nonce, sender.nonce, "{arith:?}");
555 let mut opened = vec![0u8; message.len()];
556 crypto_aead_chacha20poly1305_ietf_decrypt(
557 &mut opened,
558 &ciphertext,
559 Some(&aad),
560 &receiver.nonce,
561 &receiver.key,
562 )
563 .expect("decrypt");
564 assert_eq!(opened, message, "{arith:?}");
565 }
566 }
567
568 #[test]
571 fn test_wire_format_and_tampering() {
572 let keypair = StackKeyPair::generate();
573 let message = b"All the world's a stage";
574 let sealed = VecBox::seal_to_vecbox(message, &keypair.public_key).expect("seal");
575 let bytes = sealed.to_vec();
576 assert_eq!(bytes.len(), SEALBYTES + message.len());
577 let (enc, tag, data) = sealed.clone().into_parts();
578 assert_eq!(bytes, [enc.as_slice(), &data, tag.as_slice()].concat());
579
580 for index in [
581 0,
582 CRYPTO_KEM_XWING_CIPHERTEXTBYTES - 1,
583 bytes.len() - 20,
584 bytes.len() - 1,
585 ] {
586 let mut tampered = bytes.clone();
587 tampered[index] ^= 0x01;
588 let tampered = VecBox::from_bytes(&tampered).expect("parse");
589 assert!(tampered.open_to_vec(&keypair).is_err(), "byte {index}");
590 }
591 let other = StackKeyPair::generate();
592 assert!(matches!(
593 sealed.open_to_vec(&other),
594 Err(Error::AuthenticationFailed)
595 ));
596 for short in [0, CRYPTO_KEM_XWING_CIPHERTEXTBYTES, SEALBYTES - 1] {
597 assert!(matches!(
598 VecBox::from_bytes(&bytes[..short]),
599 Err(Error::InvalidLength {
600 context: ErrorContext::SealedBox,
601 actual,
602 constraint: crate::error::LengthConstraint::AtLeast(SEALBYTES),
603 }) if actual == short
604 ));
605 }
606 let empty = VecBox::seal_to_vecbox(b"", &keypair.public_key).expect("seal");
607 let empty = VecBox::from_bytes(&empty.to_vec()).expect("parse");
608 assert!(empty.open_to_vec(&keypair).expect("open").is_empty());
609 }
610}