Skip to main content

securedrop_protocol_minimal/schemes/
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 crate::primitives::provider::NoRng;
24use crate::primitives::provider::hpke_rs::{
25    ChaCha20Poly1305, DhKem25519, HkdfSha256, Hpke, HpkeLibcrux, HpkePrivateKey, HpkePublicKey,
26    Mode,
27};
28use crate::primitives::provider::kem::MlKem768;
29use crate::primitives::provider::traits::OwnedKem as Kem;
30use crate::primitives::ristretto255::DHPublicKey;
31use alloc::string::String;
32use alloc::vec::Vec;
33use anyhow::Error;
34use hpke_rs::libcrux::HpkeLibcruxPrng;
35use rand_core::{CryptoRng, RngCore};
36use serde::de::Error as _;
37
38use crate::primitives::dh_akem::{
39    DH_AKEM_ENCAPS_SECRET_LEN, deterministic_keygen as dhakem_derand,
40};
41use crate::primitives::dh_akem::{DhAkemPrivateKey, DhAkemPublicKey, generate_dh_akem_keypair};
42use crate::primitives::mlkem::{
43    MLKEM768PrivateKey, MLKEM768PublicKey, deterministic_keygen as mlkem_derand,
44    generate_mlkem768_keypair,
45};
46
47use crate::primitives::mlkem::LEN_MLKEM_SHAREDSECRET_ENCAPS;
48
49// PSK ID per spec §pskAPKE
50// spec: PSK_ID = "SD-pskAPKE"
51const PSK_ID: &[u8] = b"SD-pskAPKE";
52
53// ML-KEM-768 encaps randomness size (32 bytes, not the 64-byte keygen seed)
54const LEN_MLKEM_ENCAPS_RAND: usize = 32;
55
56/// The SD-APKE public key tuple `pk^APKE = (pk1, pk2)`.
57///
58/// - `pk1`: DHKEM(X25519) component (`pk^AKEM`)
59/// - `pk2`: ML-KEM-768 component (`pk^PQ`)
60#[derive(Debug, Clone)]
61pub struct MessagePublicKey {
62    pub(crate) dhakem: DhAkemPublicKey,  // pk1 in spec
63    pub(crate) mlkem: MLKEM768PublicKey, // pk2 in spec
64}
65
66/// The SD-APKE private key tuple `sk^APKE = (sk1, sk2)`.
67///
68/// - `sk1`: DHKEM(X25519) component (`sk^AKEM`)
69/// - `sk2`: ML-KEM-768 component (`sk^PQ`)
70pub struct MessagePrivateKey {
71    pub(crate) dhakem: DhAkemPrivateKey,  // sk1 in spec
72    pub(crate) mlkem: MLKEM768PrivateKey, // sk2 in spec
73}
74
75/// A `(MessagePrivateKey, MessagePublicKey)` SD-APKE keypair.
76pub struct MessageKeyPair {
77    sk: MessagePrivateKey,
78    pk: MessagePublicKey,
79}
80
81impl MessageKeyPair {
82    pub(crate) fn new(sk: MessagePrivateKey, pk: MessagePublicKey) -> Self {
83        Self { sk, pk }
84    }
85
86    /// Returns the public key.
87    pub fn public_key(&self) -> &MessagePublicKey {
88        &self.pk
89    }
90
91    /// Returns the private key.
92    pub fn private_key(&self) -> &MessagePrivateKey {
93        &self.sk
94    }
95}
96
97impl MessagePublicKey {
98    pub const LEN: usize = DhAkemPublicKey::LEN + MLKEM768PublicKey::LEN;
99
100    /// Serialize the key tuple in canonical byte order: `pk1 || pk2`.
101    pub fn as_bytes(&self) -> Vec<u8> {
102        let mut out = Vec::new();
103        out.extend_from_slice(self.dhakem.as_bytes());
104        out.extend_from_slice(self.mlkem.as_bytes());
105        out
106    }
107
108    /// Deserialize from `pk1 || pk2` bytes.
109    ///
110    /// # Errors
111    ///
112    /// Returns an error if the byte slice has incorrect length.
113    pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
114        if bytes.len() != Self::LEN {
115            return Err(anyhow::anyhow!(
116                "Invalid MessagePublicKey length: expected {}, got {}",
117                Self::LEN,
118                bytes.len()
119            ));
120        }
121
122        let dhakem_bytes: [u8; DhAkemPublicKey::LEN] = bytes[..DhAkemPublicKey::LEN]
123            .try_into()
124            .expect("checked length");
125        let mlkem_bytes: [u8; MLKEM768PublicKey::LEN] = bytes[DhAkemPublicKey::LEN..]
126            .try_into()
127            .expect("checked length");
128
129        Ok(Self {
130            dhakem: DhAkemPublicKey::from_bytes(dhakem_bytes),
131            mlkem: MLKEM768PublicKey::from_bytes(mlkem_bytes),
132        })
133    }
134}
135
136#[cfg_attr(hax, hax_lib::exclude)]
137impl serde::Serialize for MessagePublicKey {
138    fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
139        ser.serialize_str(&hex::encode(self.as_bytes()))
140    }
141}
142
143#[cfg_attr(hax, hax_lib::exclude)]
144impl<'de> serde::Deserialize<'de> for MessagePublicKey {
145    fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
146        let s = String::deserialize(de)?;
147        let bytes = hex::decode(s.trim()).map_err(D::Error::custom)?;
148        Self::from_bytes(&bytes).map_err(D::Error::custom)
149    }
150}
151
152/// SD-APKE ciphertext `((c1, cp), c2)`.
153#[derive(Debug, Clone)]
154pub struct MessageCiphertext {
155    /// ML-KEM-768 encapsulation used as PSK (`c2` in the spec)
156    pub(crate) c2: [u8; LEN_MLKEM_SHAREDSECRET_ENCAPS],
157    /// HPKE encapsulation output (`c1` in the spec)
158    pub(crate) c1: [u8; DH_AKEM_ENCAPS_SECRET_LEN],
159    /// HPKE AEAD ciphertext (`cp` / `c'` in the spec)
160    pub(crate) cp: Vec<u8>,
161}
162
163impl MessageCiphertext {
164    /// Total byte length: `c2 + c1 + cp`.
165    pub fn len(&self) -> usize {
166        self.c2.len() + self.c1.len() + self.cp.len()
167    }
168
169    /// Wire encoding `c2 || c1 || cp`
170    pub fn as_bytes(&self) -> Vec<u8> {
171        let mut out = Vec::with_capacity(self.len());
172        out.extend_from_slice(&self.c2);
173        out.extend_from_slice(&self.c1);
174        out.extend_from_slice(&self.cp);
175        out
176    }
177
178    /// Deserialize from the `c2 || c1 || cp` wire encoding
179    ///
180    /// # Errors
181    ///
182    /// Returns an error if the byte slice is shorter than the fixed-length prefix.
183    pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
184        const FIXED: usize = LEN_MLKEM_SHAREDSECRET_ENCAPS + DH_AKEM_ENCAPS_SECRET_LEN;
185        if bytes.len() < FIXED {
186            return Err(anyhow::anyhow!(
187                "MessageCiphertext too short: expected at least {}, got {}",
188                FIXED,
189                bytes.len()
190            ));
191        }
192
193        let (c2, rest) = bytes.split_at(LEN_MLKEM_SHAREDSECRET_ENCAPS);
194        let (c1, cp) = rest.split_at(DH_AKEM_ENCAPS_SECRET_LEN);
195        Ok(Self {
196            c2: c2.try_into().expect("checked length"),
197            c1: c1.try_into().expect("checked length"),
198            cp: cp.to_vec(),
199        })
200    }
201}
202
203#[cfg_attr(hax, hax_lib::exclude)]
204impl serde::Serialize for MessageCiphertext {
205    fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
206        ser.serialize_str(&hex::encode(self.as_bytes()))
207    }
208}
209
210#[cfg_attr(hax, hax_lib::exclude)]
211impl<'de> serde::Deserialize<'de> for MessageCiphertext {
212    fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
213        let s = String::deserialize(de)?;
214        let bytes = hex::decode(s.trim()).map_err(D::Error::custom)?;
215        Self::from_bytes(&bytes).map_err(D::Error::custom)
216    }
217}
218
219/// SD-APKE.KGen: generate a `MessageKeyPair`.
220///
221/// # Errors
222///
223/// Returns an error if key generation fails.
224pub fn keygen<R: RngCore + CryptoRng>(rng: &mut R) -> Result<MessageKeyPair, Error> {
225    let (sk1, pk1) = generate_dh_akem_keypair(rng)?; // AKEM.KGen()
226    let (sk2, pk2) = generate_mlkem768_keypair(rng)?; // KEM_PQ.KGen()
227    Ok(MessageKeyPair {
228        sk: MessagePrivateKey {
229            dhakem: sk1,
230            mlkem: sk2,
231        },
232        pk: MessagePublicKey {
233            dhakem: pk1,
234            mlkem: pk2,
235        },
236    })
237}
238
239/// SD-APKE.KGen (deterministic): derive a `MessageKeyPair` from seed material.
240///
241/// For use in passphrase-derived key generation only.
242pub(crate) fn deterministic_keygen(
243    dh_seed: [u8; DhAkemPrivateKey::SEED_LEN],
244    mlkem_seed: [u8; MLKEM768PrivateKey::SEED_LEN],
245) -> Result<MessageKeyPair, Error> {
246    let (sk1, pk1) = dhakem_derand(dh_seed)?;
247    let (sk2, pk2) = mlkem_derand(mlkem_seed)?;
248    Ok(MessageKeyPair {
249        sk: MessagePrivateKey {
250            dhakem: sk1,
251            mlkem: sk2,
252        },
253        pk: MessagePublicKey {
254            dhakem: pk1,
255            mlkem: pk2,
256        },
257    })
258}
259
260/// SD-APKE.AuthEnc: encrypt message `m` from sender to recipient.
261///
262/// - `kp = ((skS1, skS2), (pkS1, pkS2))`: sender's SD-APKE keypair
263/// - `pk = (pkR1, pkR2)`: recipient's SD-APKE public key
264/// - `ad`: associated data
265/// - `info_incl`: additional opaque bytes to include in info param, currently receiver's fetch key.
266///
267/// # Errors
268///
269/// Returns an error if ML-KEM encapsulation or HPKE sealing fails.
270pub fn auth_enc<R: RngCore + CryptoRng>(
271    rng: &mut R,
272    kp: &MessageKeyPair,   // ((skS1, skS2), (pkS1, pkS2))
273    pk: &MessagePublicKey, // (pkR1, pkR2)
274    m: &[u8],
275    ad: &[u8], // Todo: should always be empty
276    info_incl: &DHPublicKey,
277) -> Result<MessageCiphertext, Error> {
278    let mut seed = [0u8; 32];
279    rng.fill_bytes(&mut seed);
280
281    let prng = HpkeLibcruxPrng::try_from_seed(seed)
282        .map_err(|e| anyhow::anyhow!("hpke prng instantiation failed: {:?}", e))?;
283    let mut hpke = Hpke::<HpkeLibcrux>::new_with_rng(
284        Mode::AuthPsk,
285        DhKem25519,
286        HkdfSha256,
287        ChaCha20Poly1305,
288        prng,
289    );
290
291    let mut randomness = [0u8; LEN_MLKEM_ENCAPS_RAND];
292    rng.fill_bytes(&mut randomness);
293
294    // (c2, K2) = KEM_PQ.Encap(pkR=pkR2)
295    let (k2, c2) = MlKem768::encaps(pk.mlkem.as_bytes(), &randomness)
296        .map_err(|e| anyhow::anyhow!("ML-KEM encapsulation failed: {:?}", e))?;
297
298    // (c1, cp) = pskAEnc(skS=skS1, pkR=pkR1, psk=K2, m=m, ad=ad, info=c2+info)
299    let pkr1: HpkePublicKey = pk.dhakem.clone().into();
300    let sks1: HpkePrivateKey = kp.private_key().dhakem.clone().into();
301
302    // pskAPKE info param = c2 || pkR_fetch || pkS
303    let mut full_info = Vec::new();
304    full_info.extend_from_slice(&c2);
305    full_info.extend_from_slice(&info_incl.into_bytes());
306    full_info.extend_from_slice(&kp.pk.as_bytes());
307
308    let (c1_vec, cp) = hpke
309        .seal(
310            &pkr1,
311            &full_info,
312            ad,
313            m,
314            Some(&k2),
315            Some(PSK_ID),
316            Some(&sks1),
317        )
318        .map_err(|e| anyhow::anyhow!("SD-APKE AuthEnc failed: {:?}", e))?;
319
320    // c1 is always LEN_DHKEM_SHAREDSECRET_ENCAPS bytes for DHKEM(X25519)
321    let c1: [u8; DH_AKEM_ENCAPS_SECRET_LEN] = c1_vec
322        .as_slice()
323        .try_into()
324        .expect("DHKEM(X25519) encapsulation output has unexpected length");
325
326    Ok(MessageCiphertext { c1, cp, c2 })
327}
328
329/// SD-APKE.AuthDec: decrypt ciphertext from sender.
330///
331/// - `sk = (skR1, skR2)`: recipient's SD-APKE private key
332/// - `pk = (pkS1, pkS2)`: sender's SD-APKE public key
333/// - `ad`: associated data
334/// - `info_incl`: caller-supplied info (spec prepends `c2` internally: `info = c2 + info`)
335///
336/// # Errors
337///
338/// Returns an error if ML-KEM decapsulation or HPKE opening fails.
339pub fn auth_dec(
340    sk: &MessagePrivateKey, // (skR1, skR2)
341    pk: &MessagePublicKey,  // (pkS1, pkS2)
342    ct: &MessageCiphertext,
343    ad: &[u8],
344    info_incl: &DHPublicKey,
345) -> Result<Vec<u8>, Error> {
346    let mut hpke = Hpke::<HpkeLibcrux<NoRng>>::new_with_rng(
347        Mode::AuthPsk,
348        DhKem25519,
349        HkdfSha256,
350        ChaCha20Poly1305,
351        NoRng, // decryption must not sample randomness
352    );
353
354    // K2 = KEM_PQ.Decap(skR=skR2, enc=c2)
355    let k2 = MlKem768::decaps(&ct.c2, sk.mlkem.as_bytes())
356        .map_err(|e| anyhow::anyhow!("ML-KEM decapsulation failed: {:?}", e))?;
357
358    // m = pskADec(pkS=pkS1, skR=skR1, psk=K2, c1=c1, cp=cp, ad=ad, info_incl=c2+info+pkS)
359    let skr1: HpkePrivateKey = sk.dhakem.clone().into();
360    let pks1: HpkePublicKey = pk.dhakem.clone().into();
361
362    // c2 + info + pkS
363    let mut full_info = Vec::new();
364    full_info.extend_from_slice(&ct.c2);
365    full_info.extend_from_slice(&info_incl.into_bytes());
366    full_info.extend_from_slice(&pk.as_bytes());
367
368    hpke.open(
369        &ct.c1,
370        &skr1,
371        &full_info,
372        ad,
373        &ct.cp,
374        Some(&k2),
375        Some(PSK_ID),
376        Some(&pks1),
377    )
378    .map_err(|e| anyhow::anyhow!("SD-APKE AuthDec failed: {:?}", e))
379}
380
381#[cfg(test)]
382mod tests {
383    use crate::primitives::ristretto255::generate_dh_keypair;
384
385    use super::*;
386    use crate::metadata::LEN_METADATA_CIPHERTEXT;
387    use crate::primitives::xwing::LEN_XWING_SHAREDSECRET_ENCAPS;
388
389    use proptest::prelude::*;
390    use rand_chacha::ChaCha20Rng;
391    use rand_core::SeedableRng;
392
393    fn message_ct(c1: Vec<u8>, cp: Vec<u8>, c2: Vec<u8>) -> MessageCiphertext {
394        MessageCiphertext {
395            c1: c1.try_into().expect("c1 length"),
396            cp,
397            c2: c2.try_into().expect("c2 length"),
398        }
399    }
400
401    proptest! {
402        #[test]
403        fn test_message_ciphertext_byte_roundtrip(
404            c1 in prop::collection::vec(any::<u8>(), DH_AKEM_ENCAPS_SECRET_LEN),
405            cp in prop::collection::vec(any::<u8>(), 0..128),
406            c2 in prop::collection::vec(any::<u8>(), LEN_MLKEM_SHAREDSECRET_ENCAPS),
407        ) {
408            let ct = message_ct(c1, cp, c2);
409            let restored = MessageCiphertext::from_bytes(&ct.as_bytes()).expect("valid bytes");
410            prop_assert_eq!(ct.as_bytes(), restored.as_bytes());
411        }
412    }
413
414    fn get_rng() -> ChaCha20Rng {
415        let mut seed = [0u8; 32];
416        getrandom::fill(&mut seed).expect("OS random source failed");
417        ChaCha20Rng::from_seed(seed)
418    }
419
420    proptest! {
421        #[test]
422        fn test_auth_enc_dec_roundtrip(
423            m in proptest::collection::vec(any::<u8>(), 0..200),
424            ad in proptest::collection::vec(any::<u8>(), 0..64),
425        ) {
426            let mut rng = get_rng();
427            let sender_kp = keygen(&mut rng).expect("KGen failed");
428            let recipient_kp = keygen(&mut rng).expect("KGen failed");
429            let dh_fetch = generate_dh_keypair(&mut rng).1;
430
431            let ct = auth_enc(
432                &mut rng,
433                &sender_kp,
434                recipient_kp.public_key(),
435                &m, &ad, &dh_fetch,
436            ).expect("AuthEnc failed");
437
438            let decrypted = auth_dec(
439                recipient_kp.private_key(),
440                sender_kp.public_key(),
441                &ct, &ad, &dh_fetch,
442            ).expect("AuthDec failed");
443
444            prop_assert_eq!(m, decrypted);
445        }
446    }
447
448    #[test]
449    fn test_auth_dec_wrong_recipient_fails() {
450        let mut rng = get_rng();
451        let sender_kp = keygen(&mut rng).expect("KGen failed");
452        let recipient_kp = keygen(&mut rng).expect("KGen failed");
453        let recipient_fetch = generate_dh_keypair(&mut rng).1;
454        let wrong_kp = keygen(&mut rng).expect("KGen failed");
455
456        let ct = auth_enc(
457            &mut rng,
458            &sender_kp,
459            recipient_kp.public_key(),
460            b"secret",
461            b"ad",
462            &recipient_fetch,
463        )
464        .expect("AuthEnc failed");
465
466        assert!(
467            auth_dec(
468                wrong_kp.private_key(),
469                sender_kp.public_key(),
470                &ct,
471                b"ad",
472                &recipient_fetch,
473            )
474            .is_err()
475        );
476    }
477}