securedrop_protocol_minimal/
metadata.rs1use crate::{
19 message::MessagePublicKey,
20 primitives::provider::hpke_rs::{
21 ChaCha20Poly1305, HkdfSha256, Hpke, HpkeLibcrux, Mode, XWingDraft06,
22 },
23};
24use alloc::string::String;
25use alloc::vec::Vec;
26use rand_core::{CryptoRng, RngCore};
27use serde::de::Error as _;
28
29use crate::primitives::xwing::{
30 LEN_XWING_SHAREDSECRET_ENCAPS, XWingPrivateKey, XWingPublicKey, generate_xwing_keypair,
31};
32
33pub(crate) const LEN_METADATA_CIPHERTEXT: usize = 1232;
36
37#[derive(Debug, Clone)]
39pub struct MetadataPublicKey(pub(crate) XWingPublicKey);
40
41pub struct MetadataPrivateKey(pub(crate) XWingPrivateKey);
43
44pub struct MetadataKeyPair {
46 sk: MetadataPrivateKey,
47 pk: MetadataPublicKey,
48}
49
50impl MetadataKeyPair {
51 pub fn public_key(&self) -> &MetadataPublicKey {
53 &self.pk
54 }
55
56 pub fn private_key(&self) -> &MetadataPrivateKey {
58 &self.sk
59 }
60
61 pub(crate) fn from_key_bytes(
63 sk: [u8; XWingPrivateKey::LEN],
64 pk: [u8; XWingPublicKey::LEN],
65 ) -> Self {
66 Self {
67 sk: MetadataPrivateKey(XWingPrivateKey::from_bytes(sk)),
68 pk: MetadataPublicKey(XWingPublicKey::from_bytes(pk)),
69 }
70 }
71
72 pub(crate) fn secret_bytes(&self) -> &[u8; XWingPrivateKey::LEN] {
74 self.sk.0.as_bytes()
75 }
76
77 pub(crate) fn public_bytes(&self) -> &[u8; XWingPublicKey::LEN] {
79 self.pk.0.as_bytes()
80 }
81}
82
83#[derive(Debug, Clone)]
86pub struct MetadataCiphertext {
87 pub(crate) c: [u8; LEN_XWING_SHAREDSECRET_ENCAPS],
89 pub(crate) cp: [u8; LEN_METADATA_CIPHERTEXT],
91}
92
93impl MetadataCiphertext {
94 pub fn len(&self) -> usize {
96 self.c.len() + self.cp.len()
97 }
98
99 pub fn as_bytes(&self) -> Vec<u8> {
101 let mut out = Vec::with_capacity(self.len());
102 out.extend_from_slice(&self.c);
103 out.extend_from_slice(&self.cp);
104 out
105 }
106
107 pub fn from_bytes(bytes: &[u8]) -> Result<Self, anyhow::Error> {
113 const TOTAL_LEN: usize = LEN_XWING_SHAREDSECRET_ENCAPS + LEN_METADATA_CIPHERTEXT;
114
115 if bytes.len() != TOTAL_LEN {
116 return Err(anyhow::anyhow!(
117 "Invalid MetadataCiphertext length: expected {}, got {}",
118 TOTAL_LEN,
119 bytes.len()
120 ));
121 }
122
123 let (c, cp) = bytes.split_at(LEN_XWING_SHAREDSECRET_ENCAPS);
124
125 Ok(Self {
126 c: c.try_into().expect("checked length"),
127 cp: cp.try_into().expect("checked length"),
128 })
129 }
130}
131
132#[cfg_attr(hax, hax_lib::exclude)]
133impl serde::Serialize for MetadataCiphertext {
134 fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
135 ser.serialize_str(&hex::encode(self.as_bytes()))
136 }
137}
138
139#[cfg_attr(hax, hax_lib::exclude)]
140impl<'de> serde::Deserialize<'de> for MetadataCiphertext {
141 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
142 let s = String::deserialize(de)?;
143 let bytes = hex::decode(s.trim()).map_err(D::Error::custom)?;
144 Self::from_bytes(&bytes).map_err(D::Error::custom)
145 }
146}
147
148pub fn keygen<R: RngCore + CryptoRng>(rng: &mut R) -> Result<MetadataKeyPair, anyhow::Error> {
154 let (sk_s, pk_s) = generate_xwing_keypair(rng)?;
155 Ok(MetadataKeyPair {
156 sk: MetadataPrivateKey(sk_s),
157 pk: MetadataPublicKey(pk_s),
158 })
159}
160
161pub(crate) fn deterministic_keygen(
170 randomness: [u8; XWingPrivateKey::SEED_LEN],
171) -> Result<MetadataKeyPair, anyhow::Error> {
172 use crate::primitives::xwing::deterministic_keygen as xwing_derand;
173 let (sk_s, pk_s) = xwing_derand(randomness)?;
174 Ok(MetadataKeyPair {
175 sk: MetadataPrivateKey(sk_s),
176 pk: MetadataPublicKey(pk_s),
177 })
178}
179
180impl MetadataPublicKey {
181 pub const LEN: usize = XWingPublicKey::LEN;
182
183 pub fn as_bytes(&self) -> &[u8] {
185 self.0.as_bytes()
186 }
187
188 pub fn from_bytes(bytes: &[u8]) -> Result<Self, anyhow::Error> {
194 let arr: [u8; XWingPublicKey::LEN] = bytes.try_into().map_err(|_| {
195 anyhow::anyhow!(
196 "Invalid MetadataPublicKey length: expected {}, got {}",
197 XWingPublicKey::LEN,
198 bytes.len()
199 )
200 })?;
201 Ok(Self(XWingPublicKey::from_bytes(arr)))
202 }
203}
204
205#[cfg_attr(hax, hax_lib::exclude)]
206impl serde::Serialize for MetadataPublicKey {
207 fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
208 ser.serialize_str(&hex::encode(self.as_bytes()))
209 }
210}
211
212#[cfg_attr(hax, hax_lib::exclude)]
213impl<'de> serde::Deserialize<'de> for MetadataPublicKey {
214 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
215 let s = String::deserialize(de)?;
216 let bytes = hex::decode(s.trim()).map_err(D::Error::custom)?;
217 Self::from_bytes(&bytes).map_err(D::Error::custom)
218 }
219}
220
221impl MetadataPrivateKey {
222 #[cfg(test)]
224 pub(crate) fn as_bytes(&self) -> &[u8] {
225 self.0.as_bytes()
226 }
227}
228
229pub(crate) fn encrypt(
233 pk_r: &MetadataPublicKey,
234 m: &MessagePublicKey,
235) -> Result<MetadataCiphertext, anyhow::Error> {
236 let mut hpke = Hpke::<HpkeLibcrux>::new(Mode::Base, XWingDraft06, HkdfSha256, ChaCha20Poly1305);
237 let pk_r_hpke = pk_r.0.clone().into();
238
239 let (c_vec, cp_vec) = match hpke.seal(&pk_r_hpke, b"", b"", &m.as_bytes(), None, None, None) {
241 Ok((c_vec, cp_vec)) => (c_vec, cp_vec),
242 Err(_) => return Err(anyhow::anyhow!("Metadata encryption failed")),
243 };
244
245 let c: [u8; LEN_XWING_SHAREDSECRET_ENCAPS] = match c_vec.as_slice().try_into() {
247 Ok(c) => c,
248 Err(_) => {
249 return Err(anyhow::anyhow!("Unexpected md encapsulated secret length"));
250 }
251 };
252
253 let cp = match cp_vec.as_slice().try_into() {
254 Ok(cp) => cp,
255 Err(_) => return Err(anyhow::anyhow!("Unexpected md ciphertext length")),
256 };
257
258 Ok(MetadataCiphertext { c, cp })
259}
260
261pub fn decrypt(
267 sk_r: &MetadataPrivateKey,
268 ct: &MetadataCiphertext,
269) -> Result<Vec<u8>, anyhow::Error> {
270 let hpke = Hpke::<HpkeLibcrux>::new(Mode::Base, XWingDraft06, HkdfSha256, ChaCha20Poly1305);
271 let sk_r_hpke = sk_r.0.clone().into();
272
273 hpke.open(&ct.c, &sk_r_hpke, b"", b"", &ct.cp, None, None, None)
274 .map_err(|e| anyhow::anyhow!("SD-PKE decryption failed: {:?}", e))
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 use crate::primitives::dh_akem::DhAkemPublicKey;
281 use crate::primitives::mlkem::MLKEM768PublicKey;
282 use proptest::prelude::*;
283 use rand_chacha::ChaCha20Rng;
284 use rand_core::{SeedableRng, TryRng};
285
286 fn get_rng() -> ChaCha20Rng {
287 let mut seed = [0u8; 32];
288 getrandom::fill(&mut seed).expect("OS random source failed");
289 ChaCha20Rng::from_seed(seed)
290 }
291
292 proptest! {
293 #[test]
294 fn test_metadata_encrypt_decrypt_roundtrip(m in proptest::collection::vec(any::<u8>(), 0..200)) {
295 let mut rng = get_rng();
296 let kp = keygen(&mut rng).expect("KGen failed");
297
298 let mut fake_key_bytes: [u8; DhAkemPublicKey::LEN + MLKEM768PublicKey::LEN] = [0u8; DhAkemPublicKey::LEN + MLKEM768PublicKey::LEN];
299 rng.try_fill_bytes(&mut fake_key_bytes);
300
301 let m = MessagePublicKey::from_bytes(&fake_key_bytes).unwrap();
302
303 let ct = encrypt(kp.public_key(), &m);
304 let decrypted = decrypt(kp.private_key(), &ct.unwrap()).expect("Decryption failed");
305
306 prop_assert_eq!(m.as_bytes(), decrypted);
307 }
308 }
309
310 #[test]
311 fn test_metadata_decrypt_wrong_key_fails() {
312 let mut rng = get_rng();
313 let kp = keygen(&mut rng).expect("KGen failed");
314 let wrong_kp = keygen(&mut rng).expect("KGen failed");
315 let mut fake_key_bytes: [u8; DhAkemPublicKey::LEN + MLKEM768PublicKey::LEN] =
316 [0u8; DhAkemPublicKey::LEN + MLKEM768PublicKey::LEN];
317 rng.try_fill_bytes(&mut fake_key_bytes);
318
319 let m = MessagePublicKey::from_bytes(&fake_key_bytes).unwrap();
320
321 let ct = encrypt(kp.public_key(), &m);
322 assert!(decrypt(wrong_kp.private_key(), &ct.unwrap()).is_err());
323 }
324}