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, XWING_PRIVATE_KEY_LEN, XWING_PUBLIC_KEY_LEN, XWingPrivateKey,
31 XWingPublicKey, generate_xwing_keypair,
32};
33
34pub(crate) const LEN_METADATA_CIPHERTEXT: usize = 1232;
37
38#[derive(Debug, Clone)]
40pub struct MetadataPublicKey(pub(crate) XWingPublicKey);
41
42pub struct MetadataPrivateKey(pub(crate) XWingPrivateKey);
44
45pub struct MetadataKeyPair {
47 sk: MetadataPrivateKey,
48 pk: MetadataPublicKey,
49}
50
51impl MetadataKeyPair {
52 pub fn public_key(&self) -> &MetadataPublicKey {
54 &self.pk
55 }
56
57 pub fn private_key(&self) -> &MetadataPrivateKey {
59 &self.sk
60 }
61
62 pub(crate) fn from_key_bytes(
64 sk: [u8; XWING_PRIVATE_KEY_LEN],
65 pk: [u8; XWING_PUBLIC_KEY_LEN],
66 ) -> Self {
67 Self {
68 sk: MetadataPrivateKey(XWingPrivateKey::from_bytes(sk)),
69 pk: MetadataPublicKey(XWingPublicKey::from_bytes(pk)),
70 }
71 }
72
73 pub(crate) fn secret_bytes(&self) -> &[u8; XWING_PRIVATE_KEY_LEN] {
75 self.sk.0.as_bytes()
76 }
77
78 pub(crate) fn public_bytes(&self) -> &[u8; XWING_PUBLIC_KEY_LEN] {
80 self.pk.0.as_bytes()
81 }
82}
83
84#[derive(Debug, Clone)]
87pub struct MetadataCiphertext {
88 pub(crate) c: [u8; LEN_XWING_SHAREDSECRET_ENCAPS],
90 pub(crate) cp: [u8; LEN_METADATA_CIPHERTEXT],
92}
93
94impl MetadataCiphertext {
95 pub fn len(&self) -> usize {
97 self.c.len() + self.cp.len()
98 }
99
100 pub fn as_bytes(&self) -> Vec<u8> {
102 let mut out = Vec::with_capacity(self.len());
103 out.extend_from_slice(&self.c);
104 out.extend_from_slice(&self.cp);
105 out
106 }
107
108 pub fn from_bytes(bytes: &[u8]) -> Result<Self, anyhow::Error> {
114 const TOTAL_LEN: usize = LEN_XWING_SHAREDSECRET_ENCAPS + LEN_METADATA_CIPHERTEXT;
115
116 if bytes.len() != TOTAL_LEN {
117 return Err(anyhow::anyhow!(
118 "Invalid MetadataCiphertext length: expected {}, got {}",
119 TOTAL_LEN,
120 bytes.len()
121 ));
122 }
123
124 let (c, cp) = bytes.split_at(LEN_XWING_SHAREDSECRET_ENCAPS);
125
126 Ok(Self {
127 c: c.try_into().expect("checked length"),
128 cp: cp.try_into().expect("checked length"),
129 })
130 }
131}
132
133#[cfg_attr(hax, hax_lib::exclude)]
134impl serde::Serialize for MetadataCiphertext {
135 fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
136 ser.serialize_str(&hex::encode(self.as_bytes()))
137 }
138}
139
140#[cfg_attr(hax, hax_lib::exclude)]
141impl<'de> serde::Deserialize<'de> for MetadataCiphertext {
142 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
143 let s = String::deserialize(de)?;
144 let bytes = hex::decode(s.trim()).map_err(D::Error::custom)?;
145 Self::from_bytes(&bytes).map_err(D::Error::custom)
146 }
147}
148
149pub fn keygen<R: RngCore + CryptoRng>(rng: &mut R) -> Result<MetadataKeyPair, anyhow::Error> {
155 let (sk_s, pk_s) = generate_xwing_keypair(rng)?;
156 Ok(MetadataKeyPair {
157 sk: MetadataPrivateKey(sk_s),
158 pk: MetadataPublicKey(pk_s),
159 })
160}
161
162pub(crate) fn deterministic_keygen(randomness: [u8; 32]) -> Result<MetadataKeyPair, anyhow::Error> {
171 use crate::primitives::xwing::deterministic_keygen as xwing_derand;
172 let (sk_s, pk_s) = xwing_derand(randomness)?;
173 Ok(MetadataKeyPair {
174 sk: MetadataPrivateKey(sk_s),
175 pk: MetadataPublicKey(pk_s),
176 })
177}
178
179impl MetadataPublicKey {
180 pub fn as_bytes(&self) -> &[u8] {
182 self.0.as_bytes()
183 }
184
185 pub fn from_bytes(bytes: &[u8]) -> Result<Self, anyhow::Error> {
191 let arr: [u8; XWING_PUBLIC_KEY_LEN] = bytes.try_into().map_err(|_| {
192 anyhow::anyhow!(
193 "Invalid MetadataPublicKey length: expected {}, got {}",
194 XWING_PUBLIC_KEY_LEN,
195 bytes.len()
196 )
197 })?;
198 Ok(Self(XWingPublicKey::from_bytes(arr)))
199 }
200}
201
202#[cfg_attr(hax, hax_lib::exclude)]
203impl serde::Serialize for MetadataPublicKey {
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 MetadataPublicKey {
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
218impl MetadataPrivateKey {
219 #[cfg(test)]
221 pub(crate) fn as_bytes(&self) -> &[u8] {
222 self.0.as_bytes()
223 }
224}
225
226pub(crate) fn encrypt(
230 pk_r: &MetadataPublicKey,
231 m: &MessagePublicKey,
232) -> Result<MetadataCiphertext, anyhow::Error> {
233 let mut hpke = Hpke::<HpkeLibcrux>::new(Mode::Base, XWingDraft06, HkdfSha256, ChaCha20Poly1305);
234 let pk_r_hpke = pk_r.0.clone().into();
235
236 let (c_vec, cp_vec) = match hpke.seal(&pk_r_hpke, b"", b"", &m.as_bytes(), None, None, None) {
238 Ok((c_vec, cp_vec)) => (c_vec, cp_vec),
239 Err(_) => return Err(anyhow::anyhow!("Metadata encryption failed")),
240 };
241
242 let c: [u8; LEN_XWING_SHAREDSECRET_ENCAPS] = match c_vec.as_slice().try_into() {
244 Ok(c) => c,
245 Err(_) => {
246 return Err(anyhow::anyhow!("Unexpected md encapsulated secret length"));
247 }
248 };
249
250 let cp = match cp_vec.as_slice().try_into() {
251 Ok(cp) => cp,
252 Err(_) => return Err(anyhow::anyhow!("Unexpected md ciphertext length")),
253 };
254
255 Ok(MetadataCiphertext { c, cp })
256}
257
258pub fn decrypt(
264 sk_r: &MetadataPrivateKey,
265 ct: &MetadataCiphertext,
266) -> Result<Vec<u8>, anyhow::Error> {
267 let hpke = Hpke::<HpkeLibcrux>::new(Mode::Base, XWingDraft06, HkdfSha256, ChaCha20Poly1305);
268 let sk_r_hpke = sk_r.0.clone().into();
269
270 hpke.open(&ct.c, &sk_r_hpke, b"", b"", &ct.cp, None, None, None)
271 .map_err(|e| anyhow::anyhow!("SD-PKE decryption failed: {:?}", e))
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277 use crate::primitives::dh_akem::DH_AKEM_PUBLIC_KEY_LEN;
278 use crate::primitives::mlkem::MLKEM768_PUBLIC_KEY_LEN;
279 use proptest::prelude::*;
280 use rand_chacha::ChaCha20Rng;
281 use rand_core::{SeedableRng, TryRng};
282
283 fn get_rng() -> ChaCha20Rng {
284 let mut seed = [0u8; 32];
285 getrandom::fill(&mut seed).expect("OS random source failed");
286 ChaCha20Rng::from_seed(seed)
287 }
288
289 proptest! {
290 #[test]
291 fn test_metadata_encrypt_decrypt_roundtrip(m in proptest::collection::vec(any::<u8>(), 0..200)) {
292 let mut rng = get_rng();
293 let kp = keygen(&mut rng).expect("KGen failed");
294
295 let mut fake_key_bytes: [u8; DH_AKEM_PUBLIC_KEY_LEN + MLKEM768_PUBLIC_KEY_LEN] = [0u8; DH_AKEM_PUBLIC_KEY_LEN + MLKEM768_PUBLIC_KEY_LEN];
296 rng.try_fill_bytes(&mut fake_key_bytes);
297
298 let m = MessagePublicKey::from_bytes(&fake_key_bytes).unwrap();
299
300 let ct = encrypt(kp.public_key(), &m);
301 let decrypted = decrypt(kp.private_key(), &ct.unwrap()).expect("Decryption failed");
302
303 prop_assert_eq!(m.as_bytes(), decrypted);
304 }
305 }
306
307 #[test]
308 fn test_metadata_decrypt_wrong_key_fails() {
309 let mut rng = get_rng();
310 let kp = keygen(&mut rng).expect("KGen failed");
311 let wrong_kp = keygen(&mut rng).expect("KGen failed");
312 let mut fake_key_bytes: [u8; DH_AKEM_PUBLIC_KEY_LEN + MLKEM768_PUBLIC_KEY_LEN] =
313 [0u8; DH_AKEM_PUBLIC_KEY_LEN + MLKEM768_PUBLIC_KEY_LEN];
314 rng.try_fill_bytes(&mut fake_key_bytes);
315
316 let m = MessagePublicKey::from_bytes(&fake_key_bytes).unwrap();
317
318 let ct = encrypt(kp.public_key(), &m);
319 assert!(decrypt(wrong_kp.private_key(), &ct.unwrap()).is_err());
320 }
321}