securedrop_protocol_minimal/
metadata.rs1use crate::{
19 message::MessagePublicKey,
20 primitives::provider::hpke_rs::{Aes256Gcm, HkdfSha256, Hpke, HpkeLibcrux, Mode, XWingDraft06},
21};
22use alloc::string::String;
23use alloc::vec::Vec;
24use rand_core::{CryptoRng, RngCore};
25use serde::de::Error as _;
26
27use crate::primitives::xwing::{
28 LEN_XWING_SHAREDSECRET_ENCAPS, XWING_PRIVATE_KEY_LEN, XWING_PUBLIC_KEY_LEN, XWingPrivateKey,
29 XWingPublicKey, generate_xwing_keypair,
30};
31
32pub(crate) const LEN_METADATA_CIPHERTEXT: usize = 1232;
35
36#[derive(Debug, Clone)]
38pub struct MetadataPublicKey(pub(crate) XWingPublicKey);
39
40pub struct MetadataPrivateKey(pub(crate) XWingPrivateKey);
42
43pub struct MetadataKeyPair {
45 sk: MetadataPrivateKey,
46 pk: MetadataPublicKey,
47}
48
49impl MetadataKeyPair {
50 pub fn public_key(&self) -> &MetadataPublicKey {
52 &self.pk
53 }
54
55 pub fn private_key(&self) -> &MetadataPrivateKey {
57 &self.sk
58 }
59
60 pub(crate) fn from_key_bytes(
62 sk: [u8; XWING_PRIVATE_KEY_LEN],
63 pk: [u8; XWING_PUBLIC_KEY_LEN],
64 ) -> Self {
65 Self {
66 sk: MetadataPrivateKey(XWingPrivateKey::from_bytes(sk)),
67 pk: MetadataPublicKey(XWingPublicKey::from_bytes(pk)),
68 }
69 }
70
71 pub(crate) fn secret_bytes(&self) -> &[u8; XWING_PRIVATE_KEY_LEN] {
73 self.sk.0.as_bytes()
74 }
75
76 pub(crate) fn public_bytes(&self) -> &[u8; XWING_PUBLIC_KEY_LEN] {
78 self.pk.0.as_bytes()
79 }
80}
81
82#[derive(Debug, Clone)]
85pub struct MetadataCiphertext {
86 pub(crate) c: [u8; LEN_XWING_SHAREDSECRET_ENCAPS],
88 pub(crate) cp: [u8; LEN_METADATA_CIPHERTEXT],
90}
91
92impl MetadataCiphertext {
93 pub fn len(&self) -> usize {
95 LEN_XWING_SHAREDSECRET_ENCAPS + LEN_METADATA_CIPHERTEXT
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, Aes256Gcm);
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, Aes256Gcm);
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}