Skip to main content

securedrop_protocol_minimal/
message.rs

1//! SD-APKE: SecureDrop authenticated public-key encryption.
2//!
3//! Spec pseudocode:
4//! ```text
5//! def KGen():
6//!     (sk1, pk1) = AKEM.KGen()
7//!     (sk2, pk2) = KEM_PQ.KGen()
8//!     sk = (sk1, sk2)
9//!     pk = (pk1, pk2)
10//!     return (sk, pk)
11//!
12//! def AuthEnc(sk=(skS1, skS2), pk=(pkR1, pkR2), m, ad, info):
13//!     (c2, K2) = KEM_PQ.Encap(pkR=pkR2)
14//!     (c1, cp) = pskAEnc(skS=skS1, pkR=pkR1, psk=K2, m=m, ad=ad, info=c2+info)
15//!     return ((c1, cp), c2)
16//!
17//! def AuthDec(sk=(skR1, skR2), pk=(pkS1, pkS2), c1, cp, c2, ad, info):
18//!     K2 = KEM_PQ.Decap(skR=skR2, enc=c2)
19//!     m = pskADec(pkS=pkS1, skR=skR1, psk=K2, c1=c1, cp=cp, ad=ad, info=c2+info)
20//!     return m
21//! ```
22
23use alloc::vec::Vec;
24use anyhow::Error;
25use hpke_rs::{
26    Hpke, HpkePrivateKey, HpkePublicKey, Mode, hpke_types::AeadAlgorithm::Aes256Gcm,
27    hpke_types::KdfAlgorithm::HkdfSha256, hpke_types::KemAlgorithm::DhKem25519,
28    libcrux::HpkeLibcrux,
29};
30use libcrux_kem::MlKem768;
31use libcrux_traits::kem::owned::Kem;
32use rand_core::{CryptoRng, RngCore};
33
34use crate::constants::{LEN_DHKEM_SHAREDSECRET_ENCAPS, LEN_MLKEM_SHAREDSECRET_ENCAPS};
35use crate::primitives::dh_akem::deterministic_keygen as dhakem_derand;
36use crate::primitives::dh_akem::{DhAkemPrivateKey, DhAkemPublicKey, generate_dh_akem_keypair};
37use crate::primitives::mlkem::{
38    MLKEM768PrivateKey, MLKEM768PublicKey, deterministic_keygen as mlkem_derand,
39    generate_mlkem768_keypair,
40};
41
42// PSK ID per spec §pskAPKE
43// spec: PSK_ID = "SD-pskAPKE"
44const PSK_ID: &[u8] = b"SD-pskAPKE";
45
46// ML-KEM-768 encaps randomness size (32 bytes, not the 64-byte keygen seed)
47const LEN_MLKEM_ENCAPS_RAND: usize = 32;
48
49/// The SD-APKE public key tuple `pk^APKE = (pk1, pk2)`.
50///
51/// - `pk1`: DHKEM(X25519) component (`pk^AKEM`)
52/// - `pk2`: ML-KEM-768 component (`pk^PQ`)
53#[derive(Debug, Clone)]
54pub struct MessagePublicKey {
55    pub(crate) dhakem: DhAkemPublicKey,  // pk1 in spec
56    pub(crate) mlkem: MLKEM768PublicKey, // pk2 in spec
57}
58
59/// The SD-APKE private key tuple `sk^APKE = (sk1, sk2)`.
60///
61/// - `sk1`: DHKEM(X25519) component (`sk^AKEM`)
62/// - `sk2`: ML-KEM-768 component (`sk^PQ`)
63pub struct MessagePrivateKey {
64    pub(crate) dhakem: DhAkemPrivateKey,  // sk1 in spec
65    pub(crate) mlkem: MLKEM768PrivateKey, // sk2 in spec
66}
67
68/// A `(MessagePrivateKey, MessagePublicKey)` SD-APKE keypair.
69pub struct MessageKeyPair {
70    sk: MessagePrivateKey,
71    pk: MessagePublicKey,
72}
73
74impl MessageKeyPair {
75    /// Returns the public key.
76    pub fn public_key(&self) -> &MessagePublicKey {
77        &self.pk
78    }
79
80    /// Returns the private key.
81    pub fn private_key(&self) -> &MessagePrivateKey {
82        &self.sk
83    }
84}
85
86impl MessagePublicKey {
87    /// Serialize the key tuple in canonical byte order: `pk1 || pk2`.
88    pub fn as_bytes(&self) -> Vec<u8> {
89        let mut out = Vec::new();
90        out.extend_from_slice(self.dhakem.as_bytes());
91        out.extend_from_slice(self.mlkem.as_bytes());
92        out
93    }
94
95    /// Deserialize from `pk1 || pk2` bytes.
96    ///
97    /// # Errors
98    ///
99    /// Returns an error if the byte slice has incorrect length.
100    pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
101        use crate::primitives::dh_akem::DH_AKEM_PUBLIC_KEY_LEN;
102        use crate::primitives::mlkem::MLKEM768_PUBLIC_KEY_LEN;
103
104        if bytes.len() != DH_AKEM_PUBLIC_KEY_LEN + MLKEM768_PUBLIC_KEY_LEN {
105            return Err(anyhow::anyhow!(
106                "Invalid MessagePublicKey length: expected {}, got {}",
107                DH_AKEM_PUBLIC_KEY_LEN + MLKEM768_PUBLIC_KEY_LEN,
108                bytes.len()
109            ));
110        }
111
112        let dhakem_bytes: [u8; DH_AKEM_PUBLIC_KEY_LEN] = bytes[..DH_AKEM_PUBLIC_KEY_LEN]
113            .try_into()
114            .expect("checked length");
115        let mlkem_bytes: [u8; MLKEM768_PUBLIC_KEY_LEN] = bytes[DH_AKEM_PUBLIC_KEY_LEN..]
116            .try_into()
117            .expect("checked length");
118
119        Ok(Self {
120            dhakem: DhAkemPublicKey::from_bytes(dhakem_bytes),
121            mlkem: MLKEM768PublicKey::from_bytes(mlkem_bytes),
122        })
123    }
124}
125
126/// SD-APKE ciphertext `((c1, cp), c2)`.
127#[derive(Debug, Clone)]
128pub struct MessageCiphertext {
129    /// HPKE encapsulation output (`c1` in the spec)
130    pub(crate) c1: [u8; LEN_DHKEM_SHAREDSECRET_ENCAPS],
131    /// HPKE AEAD ciphertext (`cp` / `c'` in the spec)
132    pub(crate) cp: Vec<u8>,
133    /// ML-KEM-768 encapsulation used as PSK (`c2` in the spec)
134    pub(crate) c2: [u8; LEN_MLKEM_SHAREDSECRET_ENCAPS],
135}
136
137impl MessageCiphertext {
138    /// Total byte length: `c1 + cp + c2`.
139    pub fn len(&self) -> usize {
140        self.c1.len() + self.cp.len() + self.c2.len()
141    }
142}
143
144/// SD-APKE.KGen: generate a `MessageKeyPair`.
145///
146/// # Errors
147///
148/// Returns an error if key generation fails.
149pub fn keygen<R: RngCore + CryptoRng>(rng: &mut R) -> Result<MessageKeyPair, Error> {
150    let (sk1, pk1) = generate_dh_akem_keypair(rng)?; // AKEM.KGen()
151    let (sk2, pk2) = generate_mlkem768_keypair(rng)?; // KEM_PQ.KGen()
152    Ok(MessageKeyPair {
153        sk: MessagePrivateKey {
154            dhakem: sk1,
155            mlkem: sk2,
156        },
157        pk: MessagePublicKey {
158            dhakem: pk1,
159            mlkem: pk2,
160        },
161    })
162}
163
164/// SD-APKE.KGen (deterministic): derive a `MessageKeyPair` from seed material.
165///
166/// For use in passphrase-derived key generation only.
167pub(crate) fn deterministic_keygen(
168    dh_seed: [u8; 32],
169    mlkem_seed: [u8; 64],
170) -> Result<MessageKeyPair, Error> {
171    let (sk1, pk1) = dhakem_derand(dh_seed)?;
172    let (sk2, pk2) = mlkem_derand(mlkem_seed)?;
173    Ok(MessageKeyPair {
174        sk: MessagePrivateKey {
175            dhakem: sk1,
176            mlkem: sk2,
177        },
178        pk: MessagePublicKey {
179            dhakem: pk1,
180            mlkem: pk2,
181        },
182    })
183}
184
185/// SD-APKE.AuthEnc: encrypt message `m` from sender to recipient.
186///
187/// - `sk = (skS1, skS2)`: sender's SD-APKE private key
188/// - `pk = (pkR1, pkR2)`: recipient's SD-APKE public key
189/// - `ad`: associated data
190/// - `info`: caller-supplied info (spec prepends `c2` internally: `info = c2 + info`)
191///
192/// # Errors
193///
194/// Returns an error if ML-KEM encapsulation or HPKE sealing fails.
195pub fn auth_enc<R: RngCore + CryptoRng>(
196    rng: &mut R,
197    sk: &MessagePrivateKey, // (skS1, skS2)
198    pk: &MessagePublicKey,  // (pkR1, pkR2)
199    m: &[u8],
200    ad: &[u8],
201    info: &[u8],
202) -> Result<MessageCiphertext, Error> {
203    let mut hpke = Hpke::<HpkeLibcrux>::new(Mode::AuthPsk, DhKem25519, HkdfSha256, Aes256Gcm);
204
205    let mut randomness = [0u8; LEN_MLKEM_ENCAPS_RAND];
206    rng.fill_bytes(&mut randomness);
207
208    // (c2, K2) = KEM_PQ.Encap(pkR=pkR2)
209    let (k2, c2) = MlKem768::encaps(pk.mlkem.as_bytes(), &randomness)
210        .map_err(|e| anyhow::anyhow!("ML-KEM encapsulation failed: {:?}", e))?;
211
212    // (c1, cp) = pskAEnc(skS=skS1, pkR=pkR1, psk=K2, m=m, ad=ad, info=c2+info)
213    let pkr1: HpkePublicKey = pk.dhakem.clone().into();
214    let sks1: HpkePrivateKey = sk.dhakem.clone().into();
215
216    let mut full_info = Vec::new();
217    full_info.extend_from_slice(&c2);
218    full_info.extend_from_slice(info);
219
220    let (c1_vec, cp) = hpke
221        .seal(
222            &pkr1,
223            &full_info,
224            ad,
225            m,
226            Some(&k2),
227            Some(PSK_ID),
228            Some(&sks1),
229        )
230        .map_err(|e| anyhow::anyhow!("SD-APKE AuthEnc failed: {:?}", e))?;
231
232    // c1 is always LEN_DHKEM_SHAREDSECRET_ENCAPS bytes for DHKEM(X25519)
233    let c1: [u8; LEN_DHKEM_SHAREDSECRET_ENCAPS] = c1_vec
234        .try_into()
235        .expect("DHKEM(X25519) encapsulation output has unexpected length");
236
237    Ok(MessageCiphertext { c1, cp, c2 })
238}
239
240/// SD-APKE.AuthDec: decrypt ciphertext from sender.
241///
242/// - `sk = (skR1, skR2)`: recipient's SD-APKE private key
243/// - `pk = (pkS1, pkS2)`: sender's SD-APKE public key
244/// - `ad`: associated data
245/// - `info`: caller-supplied info (spec prepends `c2` internally: `info = c2 + info`)
246///
247/// # Errors
248///
249/// Returns an error if ML-KEM decapsulation or HPKE opening fails.
250pub fn auth_dec(
251    sk: &MessagePrivateKey, // (skR1, skR2)
252    pk: &MessagePublicKey,  // (pkS1, pkS2)
253    ct: &MessageCiphertext,
254    ad: &[u8],
255    info: &[u8],
256) -> Result<Vec<u8>, Error> {
257    let hpke = Hpke::<HpkeLibcrux>::new(Mode::AuthPsk, DhKem25519, HkdfSha256, Aes256Gcm);
258
259    // K2 = KEM_PQ.Decap(skR=skR2, enc=c2)
260    let k2 = MlKem768::decaps(&ct.c2, sk.mlkem.as_bytes())
261        .map_err(|e| anyhow::anyhow!("ML-KEM decapsulation failed: {:?}", e))?;
262
263    // m = pskADec(pkS=pkS1, skR=skR1, psk=K2, c1=c1, cp=cp, ad=ad, info=c2+info)
264    let skr1: HpkePrivateKey = sk.dhakem.clone().into();
265    let pks1: HpkePublicKey = pk.dhakem.clone().into();
266
267    let mut full_info = Vec::new();
268    full_info.extend_from_slice(&ct.c2);
269    full_info.extend_from_slice(info);
270
271    hpke.open(
272        &ct.c1,
273        &skr1,
274        &full_info,
275        ad,
276        &ct.cp,
277        Some(&k2),
278        Some(PSK_ID),
279        Some(&pks1),
280    )
281    .map_err(|e| anyhow::anyhow!("SD-APKE AuthDec failed: {:?}", e))
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287    use proptest::prelude::*;
288    use rand_chacha::ChaCha20Rng;
289    use rand_core::SeedableRng;
290
291    fn get_rng() -> ChaCha20Rng {
292        let mut seed = [0u8; 32];
293        getrandom::fill(&mut seed).expect("OS random source failed");
294        ChaCha20Rng::from_seed(seed)
295    }
296
297    proptest! {
298        #[test]
299        fn test_auth_enc_dec_roundtrip(
300            m in proptest::collection::vec(any::<u8>(), 0..200),
301            ad in proptest::collection::vec(any::<u8>(), 0..64),
302            info in proptest::collection::vec(any::<u8>(), 0..64),
303        ) {
304            let mut rng = get_rng();
305            let sender_kp = keygen(&mut rng).expect("KGen failed");
306            let recipient_kp = keygen(&mut rng).expect("KGen failed");
307
308            let ct = auth_enc(
309                &mut rng,
310                sender_kp.private_key(),
311                recipient_kp.public_key(),
312                &m, &ad, &info,
313            ).expect("AuthEnc failed");
314
315            let decrypted = auth_dec(
316                recipient_kp.private_key(),
317                sender_kp.public_key(),
318                &ct, &ad, &info,
319            ).expect("AuthDec failed");
320
321            prop_assert_eq!(m, decrypted);
322        }
323    }
324
325    #[test]
326    fn test_auth_dec_wrong_recipient_fails() {
327        let mut rng = get_rng();
328        let sender_kp = keygen(&mut rng).expect("KGen failed");
329        let recipient_kp = keygen(&mut rng).expect("KGen failed");
330        let wrong_kp = keygen(&mut rng).expect("KGen failed");
331
332        let ct = auth_enc(
333            &mut rng,
334            sender_kp.private_key(),
335            recipient_kp.public_key(),
336            b"secret",
337            b"ad",
338            b"info",
339        )
340        .expect("AuthEnc failed");
341
342        assert!(
343            auth_dec(
344                wrong_kp.private_key(),
345                sender_kp.public_key(),
346                &ct,
347                b"ad",
348                b"info",
349            )
350            .is_err()
351        );
352    }
353}