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