securedrop_protocol_minimal/schemes/
metadata.rs1use crate::primitives::provider::NoRng;
19use crate::{
20 message::MessagePublicKey,
21 primitives::provider::hpke_rs::{
22 ChaCha20Poly1305, HkdfSha256, Hpke, HpkeLibcrux, Mode, XWingDraft06,
23 },
24};
25use alloc::string::String;
26use alloc::vec::Vec;
27use hpke_rs::libcrux::HpkeLibcruxPrng;
28use rand_core::{CryptoRng, RngCore};
29use serde::de::Error as _;
30
31use crate::primitives::xwing::{
32 LEN_XWING_SHAREDSECRET_ENCAPS, XWingPrivateKey, XWingPublicKey, generate_xwing_keypair,
33};
34
35pub(crate) const LEN_METADATA_CIPHERTEXT: usize = 1232;
38
39#[derive(Debug, Clone)]
41pub struct MetadataPublicKey(pub(crate) XWingPublicKey);
42
43pub struct MetadataPrivateKey(pub(crate) XWingPrivateKey);
45
46pub struct MetadataKeyPair {
48 sk: MetadataPrivateKey,
49 pk: MetadataPublicKey,
50}
51
52impl MetadataKeyPair {
53 pub fn public_key(&self) -> &MetadataPublicKey {
55 &self.pk
56 }
57
58 pub fn private_key(&self) -> &MetadataPrivateKey {
60 &self.sk
61 }
62
63 pub(crate) fn from_key_bytes(
65 sk: [u8; XWingPrivateKey::LEN],
66 pk: [u8; XWingPublicKey::LEN],
67 ) -> Self {
68 Self {
69 sk: MetadataPrivateKey(XWingPrivateKey::from_bytes(sk)),
70 pk: MetadataPublicKey(XWingPublicKey::from_bytes(pk)),
71 }
72 }
73
74 pub(crate) fn secret_bytes(&self) -> &[u8; XWingPrivateKey::LEN] {
76 self.sk.0.as_bytes()
77 }
78
79 pub(crate) fn public_bytes(&self) -> &[u8; XWingPublicKey::LEN] {
81 self.pk.0.as_bytes()
82 }
83}
84
85#[derive(Debug, Clone)]
88pub struct MetadataCiphertext {
89 pub(crate) c: [u8; LEN_XWING_SHAREDSECRET_ENCAPS],
91 pub(crate) cp: [u8; LEN_METADATA_CIPHERTEXT],
93}
94
95impl MetadataCiphertext {
96 pub fn len(&self) -> usize {
98 self.c.len() + self.cp.len()
99 }
100
101 pub fn as_bytes(&self) -> Vec<u8> {
103 let mut out = Vec::with_capacity(self.len());
104 out.extend_from_slice(&self.c);
105 out.extend_from_slice(&self.cp);
106 out
107 }
108
109 pub fn from_bytes(bytes: &[u8]) -> Result<Self, anyhow::Error> {
115 const TOTAL_LEN: usize = LEN_XWING_SHAREDSECRET_ENCAPS + LEN_METADATA_CIPHERTEXT;
116
117 if bytes.len() != TOTAL_LEN {
118 return Err(anyhow::anyhow!(
119 "Invalid MetadataCiphertext length: expected {}, got {}",
120 TOTAL_LEN,
121 bytes.len()
122 ));
123 }
124
125 let (c, cp) = bytes.split_at(LEN_XWING_SHAREDSECRET_ENCAPS);
126
127 Ok(Self {
128 c: c.try_into().expect("checked length"),
129 cp: cp.try_into().expect("checked length"),
130 })
131 }
132}
133
134#[cfg_attr(hax, hax_lib::exclude)]
135impl serde::Serialize for MetadataCiphertext {
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 MetadataCiphertext {
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
150pub fn keygen<R: RngCore + CryptoRng>(rng: &mut R) -> Result<MetadataKeyPair, anyhow::Error> {
156 let (sk_s, pk_s) = generate_xwing_keypair(rng)?;
157 Ok(MetadataKeyPair {
158 sk: MetadataPrivateKey(sk_s),
159 pk: MetadataPublicKey(pk_s),
160 })
161}
162
163pub(crate) fn deterministic_keygen(
172 randomness: [u8; XWingPrivateKey::SEED_LEN],
173) -> Result<MetadataKeyPair, anyhow::Error> {
174 use crate::primitives::xwing::deterministic_keygen as xwing_derand;
175 let (sk_s, pk_s) = xwing_derand(randomness)?;
176 Ok(MetadataKeyPair {
177 sk: MetadataPrivateKey(sk_s),
178 pk: MetadataPublicKey(pk_s),
179 })
180}
181
182impl MetadataPublicKey {
183 pub const LEN: usize = XWingPublicKey::LEN;
184
185 pub fn as_bytes(&self) -> &[u8] {
187 self.0.as_bytes()
188 }
189
190 pub fn from_bytes(bytes: &[u8]) -> Result<Self, anyhow::Error> {
196 let arr: [u8; XWingPublicKey::LEN] = bytes.try_into().map_err(|_| {
197 anyhow::anyhow!(
198 "Invalid MetadataPublicKey length: expected {}, got {}",
199 XWingPublicKey::LEN,
200 bytes.len()
201 )
202 })?;
203 Ok(Self(XWingPublicKey::from_bytes(arr)))
204 }
205}
206
207#[cfg_attr(hax, hax_lib::exclude)]
208impl serde::Serialize for MetadataPublicKey {
209 fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
210 ser.serialize_str(&hex::encode(self.as_bytes()))
211 }
212}
213
214#[cfg_attr(hax, hax_lib::exclude)]
215impl<'de> serde::Deserialize<'de> for MetadataPublicKey {
216 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
217 let s = String::deserialize(de)?;
218 let bytes = hex::decode(s.trim()).map_err(D::Error::custom)?;
219 Self::from_bytes(&bytes).map_err(D::Error::custom)
220 }
221}
222
223impl MetadataPrivateKey {
224 #[cfg(test)]
226 pub(crate) fn as_bytes(&self) -> &[u8] {
227 self.0.as_bytes()
228 }
229}
230
231pub(crate) fn encrypt<R: RngCore + CryptoRng>(
235 rng: &mut R,
236 pk_r: &MetadataPublicKey,
237 m: &MessagePublicKey,
238) -> Result<MetadataCiphertext, anyhow::Error> {
239 let mut seed = [0u8; 32];
240 rng.fill_bytes(&mut seed);
241 let prng = HpkeLibcruxPrng::try_from_seed(seed)
242 .map_err(|e| anyhow::anyhow!("hpke prng instantiation failed: {:?}", e))?;
243 let mut hpke = Hpke::<HpkeLibcrux>::new_with_rng(
244 Mode::Base,
245 XWingDraft06,
246 HkdfSha256,
247 ChaCha20Poly1305,
248 prng,
249 );
250
251 let pk_r_hpke = pk_r.0.clone().into();
252
253 let (c_vec, cp_vec) = match hpke.seal(&pk_r_hpke, b"", b"", &m.as_bytes(), None, None, None) {
255 Ok((c_vec, cp_vec)) => (c_vec, cp_vec),
256 Err(_) => return Err(anyhow::anyhow!("Metadata encryption failed")),
257 };
258
259 let c: [u8; LEN_XWING_SHAREDSECRET_ENCAPS] = match c_vec.as_slice().try_into() {
261 Ok(c) => c,
262 Err(_) => {
263 return Err(anyhow::anyhow!("Unexpected md encapsulated secret length"));
264 }
265 };
266
267 let cp = match cp_vec.as_slice().try_into() {
268 Ok(cp) => cp,
269 Err(_) => return Err(anyhow::anyhow!("Unexpected md ciphertext length")),
270 };
271
272 Ok(MetadataCiphertext { c, cp })
273}
274
275pub fn decrypt(
281 sk_r: &MetadataPrivateKey,
282 ct: &MetadataCiphertext,
283) -> Result<Vec<u8>, anyhow::Error> {
284 let mut hpke = Hpke::<HpkeLibcrux<NoRng>>::new_with_rng(
285 Mode::Base,
286 XWingDraft06,
287 HkdfSha256,
288 ChaCha20Poly1305,
289 NoRng, );
291
292 let sk_r_hpke = sk_r.0.clone().into();
293
294 hpke.open(&ct.c, &sk_r_hpke, b"", b"", &ct.cp, None, None, None)
295 .map_err(|e| anyhow::anyhow!("SD-PKE decryption failed: {:?}", e))
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301 use crate::primitives::dh_akem::DhAkemPublicKey;
302 use crate::primitives::mlkem::MLKEM768PublicKey;
303 use proptest::prelude::*;
304 use rand_chacha::ChaCha20Rng;
305 use rand_core::{SeedableRng, TryRng};
306
307 fn get_rng() -> ChaCha20Rng {
308 let mut seed = [0u8; 32];
309 getrandom::fill(&mut seed).expect("OS random source failed");
310 ChaCha20Rng::from_seed(seed)
311 }
312
313 proptest! {
314 #[test]
315 fn test_metadata_ciphertext_byte_roundtrip(
316 c in prop::collection::vec(any::<u8>(), LEN_XWING_SHAREDSECRET_ENCAPS),
317 cp in prop::collection::vec(any::<u8>(), LEN_METADATA_CIPHERTEXT)
318 .prop_map(|v| v.try_into().unwrap()),
319 ) {
320 let ct = MetadataCiphertext { c: c.try_into().expect("c length"), cp };
321 let restored = MetadataCiphertext::from_bytes(&ct.as_bytes()).expect("valid bytes");
322 prop_assert_eq!(ct.as_bytes(), restored.as_bytes());
323 }
324
325 #[test]
326 fn test_metadata_encrypt_decrypt_roundtrip(m in proptest::collection::vec(any::<u8>(), 0..200)) {
327 let mut rng = get_rng();
328 let kp = keygen(&mut rng).expect("KGen failed");
329
330 let mut fake_key_bytes: [u8; DhAkemPublicKey::LEN + MLKEM768PublicKey::LEN] = [0u8; DhAkemPublicKey::LEN + MLKEM768PublicKey::LEN];
331 rng.try_fill_bytes(&mut fake_key_bytes);
332
333 let m = MessagePublicKey::from_bytes(&fake_key_bytes).unwrap();
334
335 let ct = encrypt(&mut rng, kp.public_key(), &m);
336 let decrypted = decrypt(kp.private_key(), &ct.unwrap()).expect("Decryption failed");
337
338 prop_assert_eq!(m.as_bytes(), decrypted);
339 }
340 }
341
342 #[test]
343 fn test_metadata_decrypt_wrong_key_fails() {
344 let mut rng = get_rng();
345 let kp = keygen(&mut rng).expect("KGen failed");
346 let wrong_kp = keygen(&mut rng).expect("KGen failed");
347 let mut fake_key_bytes: [u8; DhAkemPublicKey::LEN + MLKEM768PublicKey::LEN] =
348 [0u8; DhAkemPublicKey::LEN + MLKEM768PublicKey::LEN];
349 rng.try_fill_bytes(&mut fake_key_bytes);
350
351 let m = MessagePublicKey::from_bytes(&fake_key_bytes).unwrap();
352
353 let ct = encrypt(&mut rng, kp.public_key(), &m);
354 assert!(decrypt(wrong_kp.private_key(), &ct.unwrap()).is_err());
355 }
356}