Skip to main content

quantum_shield/
seal.rs

1//! Authenticated hybrid encryption: [`seal`] and [`open`].
2
3use crate::constants::*;
4use crate::error::{Error, Result};
5use crate::hybrid_kem::{self, KemCiphertext};
6use crate::keys::{KeyPair, PublicKeyBundle};
7use crate::types::Envelope;
8use aes_gcm::aead::{Aead, Payload};
9use aes_gcm::{Aes256Gcm, KeyInit};
10use alloc::vec::Vec;
11
12/// Encrypt `plaintext` for `recipient`.
13///
14/// A fresh hybrid KEM (X25519 + ML-KEM-1024) run derives a one-time
15/// AES-256-GCM key; the entire envelope header (format version, suite,
16/// both KEM components, and nonce) is bound into the authentication tag
17/// as associated data.
18///
19/// # Errors
20///
21/// Returns [`Error::MessageTooLarge`] for plaintexts over
22/// [`MAX_PLAINTEXT_LEN`] and [`Error::RandomnessUnavailable`] if the OS RNG
23/// fails.
24pub fn seal(plaintext: &[u8], recipient: &PublicKeyBundle) -> Result<Envelope> {
25    if plaintext.len() > MAX_PLAINTEXT_LEN {
26        return Err(Error::MessageTooLarge {
27            len: plaintext.len(),
28            max: MAX_PLAINTEXT_LEN,
29        });
30    }
31
32    let (kem_ct, ss) = hybrid_kem::encapsulate(recipient)?;
33
34    let mut nonce = [0u8; NONCE_LEN];
35    getrandom::fill(&mut nonce).map_err(|_| Error::RandomnessUnavailable)?;
36
37    // Envelope with empty ciphertext: gives us the exact AAD prefix.
38    let mut envelope = Envelope {
39        epk_x25519: kem_ct.epk_x25519,
40        ct_mlkem: kem_ct.ct_mlkem,
41        nonce,
42        ciphertext: Vec::new(),
43    };
44    let mut aad = Vec::with_capacity(ENVELOPE_AAD_LEN);
45    envelope.write_aad(&mut aad);
46
47    let cipher = Aes256Gcm::new((&*ss).into());
48    envelope.ciphertext = cipher
49        .encrypt(
50            (&nonce).into(),
51            Payload {
52                msg: plaintext,
53                aad: &aad,
54            },
55        )
56        .map_err(|_| Error::MessageTooLarge {
57            len: plaintext.len(),
58            max: MAX_PLAINTEXT_LEN,
59        })?;
60
61    Ok(envelope)
62}
63
64/// Decrypt an [`Envelope`] with `keypair`.
65///
66/// # Errors
67///
68/// Returns [`Error::DecryptionFailed`] for *any* cryptographic failure —
69/// wrong recipient, tampered header, tampered ciphertext — with no further
70/// detail, by design.
71pub fn open(keypair: &KeyPair, envelope: &Envelope) -> Result<Vec<u8>> {
72    let kem_ct = KemCiphertext {
73        epk_x25519: envelope.epk_x25519,
74        ct_mlkem: envelope.ct_mlkem.clone(),
75    };
76    let ss = hybrid_kem::decapsulate(keypair, &kem_ct);
77
78    let mut aad = Vec::with_capacity(ENVELOPE_AAD_LEN);
79    envelope.write_aad(&mut aad);
80
81    let cipher = Aes256Gcm::new((&*ss).into());
82    cipher
83        .decrypt(
84            (&envelope.nonce).into(),
85            Payload {
86                msg: &envelope.ciphertext,
87                aad: &aad,
88            },
89        )
90        .map_err(|_| Error::DecryptionFailed)
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn roundtrip() {
99        let kp = KeyPair::generate().unwrap();
100        let msg = b"attack at dawn";
101        let env = seal(msg, kp.public_keys()).unwrap();
102        assert_eq!(open(&kp, &env).unwrap(), msg);
103    }
104
105    #[test]
106    fn empty_plaintext_roundtrip() {
107        let kp = KeyPair::generate().unwrap();
108        let env = seal(b"", kp.public_keys()).unwrap();
109        assert_eq!(open(&kp, &env).unwrap(), b"");
110    }
111
112    #[test]
113    fn wrong_recipient_fails() {
114        let alice = KeyPair::generate().unwrap();
115        let mallory = KeyPair::generate().unwrap();
116        let env = seal(b"secret", alice.public_keys()).unwrap();
117        assert_eq!(open(&mallory, &env).unwrap_err(), Error::DecryptionFailed);
118    }
119
120    #[test]
121    fn oversized_plaintext_rejected() {
122        let kp = KeyPair::generate().unwrap();
123        let big = vec![0u8; MAX_PLAINTEXT_LEN + 1];
124        assert!(matches!(
125            seal(&big, kp.public_keys()).unwrap_err(),
126            Error::MessageTooLarge { .. }
127        ));
128    }
129}