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