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