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