securedrop_protocol_minimal/
message.rs1use 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
47const PSK_ID: &[u8] = b"SD-pskAPKE";
50
51const LEN_MLKEM_ENCAPS_RAND: usize = 32;
53
54#[derive(Debug, Clone)]
59pub struct MessagePublicKey {
60 pub(crate) dhakem: DhAkemPublicKey, pub(crate) mlkem: MLKEM768PublicKey, }
63
64pub struct MessagePrivateKey {
69 pub(crate) dhakem: DhAkemPrivateKey, pub(crate) mlkem: MLKEM768PrivateKey, }
72
73pub 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 pub fn public_key(&self) -> &MessagePublicKey {
86 &self.pk
87 }
88
89 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 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 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#[derive(Debug, Clone)]
152pub struct MessageCiphertext {
153 pub(crate) c2: [u8; LEN_MLKEM_SHAREDSECRET_ENCAPS],
155 pub(crate) c1: [u8; DH_AKEM_ENCAPS_SECRET_LEN],
157 pub(crate) cp: Vec<u8>,
159}
160
161impl MessageCiphertext {
162 pub fn len(&self) -> usize {
164 self.c2.len() + self.c1.len() + self.cp.len()
165 }
166
167 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 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
217pub fn keygen<R: RngCore + CryptoRng>(rng: &mut R) -> Result<MessageKeyPair, Error> {
223 let (sk1, pk1) = generate_dh_akem_keypair(rng)?; let (sk2, pk2) = generate_mlkem768_keypair(rng)?; 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
237pub(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
258pub fn auth_enc<R: RngCore + CryptoRng>(
269 rng: &mut R,
270 kp: &MessageKeyPair, pk: &MessagePublicKey, m: &[u8],
273 ad: &[u8], 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 let (k2, c2) = MlKem768::encaps(pk.mlkem.as_bytes(), &randomness)
284 .map_err(|e| anyhow::anyhow!("ML-KEM encapsulation failed: {:?}", e))?;
285
286 let pkr1: HpkePublicKey = pk.dhakem.clone().into();
288 let sks1: HpkePrivateKey = kp.private_key().dhakem.clone().into();
289
290 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 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
317pub fn auth_dec(
328 sk: &MessagePrivateKey, pk: &MessagePublicKey, 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 let k2 = MlKem768::decaps(&ct.c2, sk.mlkem.as_bytes())
338 .map_err(|e| anyhow::anyhow!("ML-KEM decapsulation failed: {:?}", e))?;
339
340 let skr1: HpkePrivateKey = sk.dhakem.clone().into();
342 let pks1: HpkePublicKey = pk.dhakem.clone().into();
343
344 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}