Skip to main content

securedrop_protocol_minimal/schemes/
metadata.rs

1//! SD-PKE: metadata encryption
2//!
3//! Spec pseudocode:
4//! ```text
5//! def KGen():
6//!     (skS, pkS) = KEM_H.KGen()
7//!     return (skS, pkS)
8//!
9//! def Enc(pkR, m):
10//!     c, cp = HPKE.SealBase(pkR=pkR, info=None, aad=None, pt=m)
11//!     return (c, cp)
12//!
13//! def Dec(skR, c, cp):
14//!     m = HPKE.OpenBase(enc=c, skR=skR, info=None, aad=None, ct=cp)
15//!     return m
16//! ```
17
18use 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
35// TODO: maybe needs a better location
36// DHAKEM_PKLEN + MLKEM768PK_LEN + AEAD_TAG_LEN = 32 + 1184 + 16
37pub(crate) const LEN_METADATA_CIPHERTEXT: usize = 1232;
38
39/// The recipient's metadata public key (`pk_R^PKE` in the spec).
40#[derive(Debug, Clone)]
41pub struct MetadataPublicKey(pub(crate) XWingPublicKey);
42
43/// The recipient's metadata private key (`sk_R^PKE` in the spec).
44pub struct MetadataPrivateKey(pub(crate) XWingPrivateKey);
45
46/// A `(MetadataPrivateKey, MetadataPublicKey)` SD-PKE keypair.
47pub struct MetadataKeyPair {
48    sk: MetadataPrivateKey,
49    pk: MetadataPublicKey,
50}
51
52impl MetadataKeyPair {
53    /// Returns the public key.
54    pub fn public_key(&self) -> &MetadataPublicKey {
55        &self.pk
56    }
57
58    /// Returns the private key.
59    pub fn private_key(&self) -> &MetadataPrivateKey {
60        &self.sk
61    }
62
63    /// Reconstruct a keypair from the raw X-Wing secret and public key bytes
64    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    /// Raw X-Wing secret key bytes
75    pub(crate) fn secret_bytes(&self) -> &[u8; XWingPrivateKey::LEN] {
76        self.sk.0.as_bytes()
77    }
78
79    /// Raw X-Wing public key bytes
80    pub(crate) fn public_bytes(&self) -> &[u8; XWingPublicKey::LEN] {
81        self.pk.0.as_bytes()
82    }
83}
84
85/// SD-PKE ciphertext `(c, c')`: X-Wing encapsulation `c` together with HPKE
86/// ciphertext `c'`.
87#[derive(Debug, Clone)]
88pub struct MetadataCiphertext {
89    /// HPKE encapsulation output (`c` in the spec)
90    pub(crate) c: [u8; LEN_XWING_SHAREDSECRET_ENCAPS],
91    /// HPKE AEAD ciphertext (`c'` / `cp` in the spec)
92    pub(crate) cp: [u8; LEN_METADATA_CIPHERTEXT],
93}
94
95impl MetadataCiphertext {
96    /// Total byte length of the ciphertext: encapsulation `c` + AEAD ciphertext `c'`.
97    pub fn len(&self) -> usize {
98        self.c.len() + self.cp.len()
99    }
100
101    /// Wire encoding `c || cp`
102    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    /// Deserialize from the `c || cp` wire encoding.
110    ///
111    /// # Errors
112    ///
113    /// Returns an error if the byte slice is the wrong size.
114    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
150/// SD-PKE.KGen: generate a `MetadataKeyPair`.
151///
152/// # Errors
153///
154/// Returns an error if X-Wing key generation fails.
155pub 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
163/// SD-PKE.KGen (deterministic): derive a `MetadataKeyPair` from 32 bytes of seed material.
164///
165/// For use in passphrase-derived key generation only; do not use with random bytes
166/// from a live RNG (use [`keygen`] instead).
167///
168/// # Errors
169///
170/// Returns an error if X-Wing key generation fails.
171pub(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    /// Returns the public key as bytes.
186    pub fn as_bytes(&self) -> &[u8] {
187        self.0.as_bytes()
188    }
189
190    /// Deserialize from `pk_R^PKE` (X-Wing) bytes
191    ///
192    /// # Errors
193    ///
194    /// Returns an error if the byte slice has incorrect length.
195    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    /// Returns the private key as bytes.
225    #[cfg(test)]
226    pub(crate) fn as_bytes(&self) -> &[u8] {
227        self.0.as_bytes()
228    }
229}
230
231/// SD-PKE.Enc: encrypt message `m` to recipient key `pk_r`, returning `(c, c')`.
232///
233/// `m` is the sender's long-term APKE public key, which must be serializable.
234pub(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    // MetadataPublicKey always holds a valid XWing key, so seal should not fail.
254    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    // XWing will always produce same length ciphertext
260    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
275/// SD-PKE.Dec: decrypt `(c, c')` using recipient key `sk_r`, returning message `m`.
276///
277/// # Errors
278///
279/// Returns an error if HPKE decryption fails.
280pub 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, // decryption must not sample randomness
290    );
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}