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 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 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#[derive(Debug, Clone)]
153pub struct MessageCiphertext {
154 pub(crate) c1: [u8; DH_AKEM_ENCAPS_SECRET_LEN],
156 pub(crate) cp: Vec<u8>,
158 pub(crate) c2: [u8; LEN_MLKEM_SHAREDSECRET_ENCAPS],
160}
161
162impl MessageCiphertext {
163 pub fn len(&self) -> usize {
165 self.c1.len() + self.cp.len() + self.c2.len()
166 }
167
168 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 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
218pub fn keygen<R: RngCore + CryptoRng>(rng: &mut R) -> Result<MessageKeyPair, Error> {
224 let (sk1, pk1) = generate_dh_akem_keypair(rng)?; let (sk2, pk2) = generate_mlkem768_keypair(rng)?; 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
238pub(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
259pub fn auth_enc<R: RngCore + CryptoRng>(
270 rng: &mut R,
271 kp: &MessageKeyPair, pk: &MessagePublicKey, m: &[u8],
274 ad: &[u8], 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 let (k2, c2) = MlKem768::encaps(pk.mlkem.as_bytes(), &randomness)
285 .map_err(|e| anyhow::anyhow!("ML-KEM encapsulation failed: {:?}", e))?;
286
287 let pkr1: HpkePublicKey = pk.dhakem.clone().into();
289 let sks1: HpkePrivateKey = kp.private_key().dhakem.clone().into();
290
291 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 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
318pub fn auth_dec(
329 sk: &MessagePrivateKey, pk: &MessagePublicKey, 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 let k2 = MlKem768::decaps(&ct.c2, sk.mlkem.as_bytes())
339 .map_err(|e| anyhow::anyhow!("ML-KEM decapsulation failed: {:?}", e))?;
340
341 let skr1: HpkePrivateKey = sk.dhakem.clone().into();
343 let pks1: HpkePublicKey = pk.dhakem.clone().into();
344
345 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}