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 alloc::string::String;
30use alloc::vec::Vec;
31use anyhow::Error;
32use rand_core::{CryptoRng, RngCore};
33use serde::de::Error as _;
34
35use crate::primitives::dh_akem::{
36    DH_AKEM_ENCAPS_SECRET_LEN, deterministic_keygen as dhakem_derand,
37};
38use crate::primitives::dh_akem::{DhAkemPrivateKey, DhAkemPublicKey, generate_dh_akem_keypair};
39use crate::primitives::mlkem::{
40    MLKEM768PrivateKey, MLKEM768PublicKey, deterministic_keygen as mlkem_derand,
41    generate_mlkem768_keypair,
42};
43
44use crate::primitives::mlkem::LEN_MLKEM_SHAREDSECRET_ENCAPS;
45
46// PSK ID per spec §pskAPKE
47// spec: PSK_ID = "SD-pskAPKE"
48const PSK_ID: &[u8] = b"SD-pskAPKE";
49
50// ML-KEM-768 encaps randomness size (32 bytes, not the 64-byte keygen seed)
51const LEN_MLKEM_ENCAPS_RAND: usize = 32;
52
53/// The SD-APKE public key tuple `pk^APKE = (pk1, pk2)`.
54///
55/// - `pk1`: DHKEM(X25519) component (`pk^AKEM`)
56/// - `pk2`: ML-KEM-768 component (`pk^PQ`)
57#[derive(Debug, Clone)]
58pub struct MessagePublicKey {
59    pub(crate) dhakem: DhAkemPublicKey,  // pk1 in spec
60    pub(crate) mlkem: MLKEM768PublicKey, // pk2 in spec
61}
62
63/// The SD-APKE private key tuple `sk^APKE = (sk1, sk2)`.
64///
65/// - `sk1`: DHKEM(X25519) component (`sk^AKEM`)
66/// - `sk2`: ML-KEM-768 component (`sk^PQ`)
67pub struct MessagePrivateKey {
68    pub(crate) dhakem: DhAkemPrivateKey,  // sk1 in spec
69    pub(crate) mlkem: MLKEM768PrivateKey, // sk2 in spec
70}
71
72/// A `(MessagePrivateKey, MessagePublicKey)` SD-APKE keypair.
73pub struct MessageKeyPair {
74    sk: MessagePrivateKey,
75    pk: MessagePublicKey,
76}
77
78impl MessageKeyPair {
79    pub(crate) fn new(sk: MessagePrivateKey, pk: MessagePublicKey) -> Self {
80        Self { sk, pk }
81    }
82
83    /// Returns the public key.
84    pub fn public_key(&self) -> &MessagePublicKey {
85        &self.pk
86    }
87
88    /// Returns the private key.
89    pub fn private_key(&self) -> &MessagePrivateKey {
90        &self.sk
91    }
92}
93
94impl MessagePublicKey {
95    /// Serialize the key tuple in canonical byte order: `pk1 || pk2`.
96    pub fn as_bytes(&self) -> Vec<u8> {
97        let mut out = Vec::new();
98        out.extend_from_slice(self.dhakem.as_bytes());
99        out.extend_from_slice(self.mlkem.as_bytes());
100        out
101    }
102
103    /// Deserialize from `pk1 || pk2` bytes.
104    ///
105    /// # Errors
106    ///
107    /// Returns an error if the byte slice has incorrect length.
108    pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
109        use crate::primitives::dh_akem::DH_AKEM_PUBLIC_KEY_LEN;
110        use crate::primitives::mlkem::MLKEM768_PUBLIC_KEY_LEN;
111
112        if bytes.len() != DH_AKEM_PUBLIC_KEY_LEN + MLKEM768_PUBLIC_KEY_LEN {
113            return Err(anyhow::anyhow!(
114                "Invalid MessagePublicKey length: expected {}, got {}",
115                DH_AKEM_PUBLIC_KEY_LEN + MLKEM768_PUBLIC_KEY_LEN,
116                bytes.len()
117            ));
118        }
119
120        let dhakem_bytes: [u8; DH_AKEM_PUBLIC_KEY_LEN] = bytes[..DH_AKEM_PUBLIC_KEY_LEN]
121            .try_into()
122            .expect("checked length");
123        let mlkem_bytes: [u8; MLKEM768_PUBLIC_KEY_LEN] = bytes[DH_AKEM_PUBLIC_KEY_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    /// HPKE encapsulation output (`c1` in the spec)
154    pub(crate) c1: [u8; DH_AKEM_ENCAPS_SECRET_LEN],
155    /// HPKE AEAD ciphertext (`cp` / `c'` in the spec)
156    pub(crate) cp: Vec<u8>,
157    /// ML-KEM-768 encapsulation used as PSK (`c2` in the spec)
158    pub(crate) c2: [u8; LEN_MLKEM_SHAREDSECRET_ENCAPS],
159}
160
161impl MessageCiphertext {
162    /// Total byte length: `c1 + cp + c2`.
163    pub fn len(&self) -> usize {
164        self.c1.len() + self.cp.len() + self.c2.len()
165    }
166
167    /// Wire encoding `c1 || c2 || cp`
168    pub fn as_bytes(&self) -> Vec<u8> {
169        let mut out = Vec::with_capacity(self.len());
170        out.extend_from_slice(&self.c1);
171        out.extend_from_slice(&self.c2);
172        out.extend_from_slice(&self.cp);
173        out
174    }
175
176    /// Deserialize from the `c1 || c2 || 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 = DH_AKEM_ENCAPS_SECRET_LEN + LEN_MLKEM_SHAREDSECRET_ENCAPS;
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 (c1, rest) = bytes.split_at(DH_AKEM_ENCAPS_SECRET_LEN);
192        let (c2, cp) = rest.split_at(LEN_MLKEM_SHAREDSECRET_ENCAPS);
193        Ok(Self {
194            c1: c1.try_into().expect("checked length"),
195            cp: cp.to_vec(),
196            c2: c2.try_into().expect("checked length"),
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; 32],
242    mlkem_seed: [u8; 64],
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/// - `sk = (skS1, skS2)`: sender's SD-APKE private key
261/// - `pk = (pkR1, pkR2)`: recipient's SD-APKE public key
262/// - `ad`: associated data
263/// - `info`: caller-supplied info (spec prepends `c2` internally: `info = c2 + info`)
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    sk: &MessagePrivateKey, // (skS1, skS2)
271    pk: &MessagePublicKey,  // (pkR1, pkR2)
272    m: &[u8],
273    ad: &[u8],
274    info: &[u8],
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 = sk.dhakem.clone().into();
289
290    let mut full_info = Vec::new();
291    full_info.extend_from_slice(&c2);
292    full_info.extend_from_slice(info);
293
294    let (c1_vec, cp) = hpke
295        .seal(
296            &pkr1,
297            &full_info,
298            ad,
299            m,
300            Some(&k2),
301            Some(PSK_ID),
302            Some(&sks1),
303        )
304        .map_err(|e| anyhow::anyhow!("SD-APKE AuthEnc failed: {:?}", e))?;
305
306    // c1 is always LEN_DHKEM_SHAREDSECRET_ENCAPS bytes for DHKEM(X25519)
307    let c1: [u8; DH_AKEM_ENCAPS_SECRET_LEN] = c1_vec
308        .as_slice()
309        .try_into()
310        .expect("DHKEM(X25519) encapsulation output has unexpected length");
311
312    Ok(MessageCiphertext { c1, cp, c2 })
313}
314
315/// SD-APKE.AuthDec: decrypt ciphertext from sender.
316///
317/// - `sk = (skR1, skR2)`: recipient's SD-APKE private key
318/// - `pk = (pkS1, pkS2)`: sender's SD-APKE public key
319/// - `ad`: associated data
320/// - `info`: caller-supplied info (spec prepends `c2` internally: `info = c2 + info`)
321///
322/// # Errors
323///
324/// Returns an error if ML-KEM decapsulation or HPKE opening fails.
325pub fn auth_dec(
326    sk: &MessagePrivateKey, // (skR1, skR2)
327    pk: &MessagePublicKey,  // (pkS1, pkS2)
328    ct: &MessageCiphertext,
329    ad: &[u8],
330    info: &[u8],
331) -> Result<Vec<u8>, Error> {
332    let hpke = Hpke::<HpkeLibcrux>::new(Mode::AuthPsk, DhKem25519, HkdfSha256, ChaCha20Poly1305);
333
334    // K2 = KEM_PQ.Decap(skR=skR2, enc=c2)
335    let k2 = MlKem768::decaps(&ct.c2, sk.mlkem.as_bytes())
336        .map_err(|e| anyhow::anyhow!("ML-KEM decapsulation failed: {:?}", e))?;
337
338    // m = pskADec(pkS=pkS1, skR=skR1, psk=K2, c1=c1, cp=cp, ad=ad, info=c2+info)
339    let skr1: HpkePrivateKey = sk.dhakem.clone().into();
340    let pks1: HpkePublicKey = pk.dhakem.clone().into();
341
342    let mut full_info = Vec::new();
343    full_info.extend_from_slice(&ct.c2);
344    full_info.extend_from_slice(info);
345
346    hpke.open(
347        &ct.c1,
348        &skr1,
349        &full_info,
350        ad,
351        &ct.cp,
352        Some(&k2),
353        Some(PSK_ID),
354        Some(&pks1),
355    )
356    .map_err(|e| anyhow::anyhow!("SD-APKE AuthDec failed: {:?}", e))
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362    use proptest::prelude::*;
363    use rand_chacha::ChaCha20Rng;
364    use rand_core::SeedableRng;
365
366    fn get_rng() -> ChaCha20Rng {
367        let mut seed = [0u8; 32];
368        getrandom::fill(&mut seed).expect("OS random source failed");
369        ChaCha20Rng::from_seed(seed)
370    }
371
372    proptest! {
373        #[test]
374        fn test_auth_enc_dec_roundtrip(
375            m in proptest::collection::vec(any::<u8>(), 0..200),
376            ad in proptest::collection::vec(any::<u8>(), 0..64),
377            info in proptest::collection::vec(any::<u8>(), 0..64),
378        ) {
379            let mut rng = get_rng();
380            let sender_kp = keygen(&mut rng).expect("KGen failed");
381            let recipient_kp = keygen(&mut rng).expect("KGen failed");
382
383            let ct = auth_enc(
384                &mut rng,
385                sender_kp.private_key(),
386                recipient_kp.public_key(),
387                &m, &ad, &info,
388            ).expect("AuthEnc failed");
389
390            let decrypted = auth_dec(
391                recipient_kp.private_key(),
392                sender_kp.public_key(),
393                &ct, &ad, &info,
394            ).expect("AuthDec failed");
395
396            prop_assert_eq!(m, decrypted);
397        }
398    }
399
400    #[test]
401    fn test_auth_dec_wrong_recipient_fails() {
402        let mut rng = get_rng();
403        let sender_kp = keygen(&mut rng).expect("KGen failed");
404        let recipient_kp = keygen(&mut rng).expect("KGen failed");
405        let wrong_kp = keygen(&mut rng).expect("KGen failed");
406
407        let ct = auth_enc(
408            &mut rng,
409            sender_kp.private_key(),
410            recipient_kp.public_key(),
411            b"secret",
412            b"ad",
413            b"info",
414        )
415        .expect("AuthEnc failed");
416
417        assert!(
418            auth_dec(
419                wrong_kp.private_key(),
420                sender_kp.public_key(),
421                &ct,
422                b"ad",
423                b"info",
424            )
425            .is_err()
426        );
427    }
428}