Skip to main content

securedrop_protocol_minimal/
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::{
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
33// TODO: maybe needs a better location
34// DHAKEM_PKLEN + MLKEM768PK_LEN + AEAD_TAG_LEN = 32 + 1184 + 16
35pub(crate) const LEN_METADATA_CIPHERTEXT: usize = 1232;
36
37/// The recipient's metadata public key (`pk_R^PKE` in the spec).
38#[derive(Debug, Clone)]
39pub struct MetadataPublicKey(pub(crate) XWingPublicKey);
40
41/// The recipient's metadata private key (`sk_R^PKE` in the spec).
42pub struct MetadataPrivateKey(pub(crate) XWingPrivateKey);
43
44/// A `(MetadataPrivateKey, MetadataPublicKey)` SD-PKE keypair.
45pub struct MetadataKeyPair {
46    sk: MetadataPrivateKey,
47    pk: MetadataPublicKey,
48}
49
50impl MetadataKeyPair {
51    /// Returns the public key.
52    pub fn public_key(&self) -> &MetadataPublicKey {
53        &self.pk
54    }
55
56    /// Returns the private key.
57    pub fn private_key(&self) -> &MetadataPrivateKey {
58        &self.sk
59    }
60
61    /// Reconstruct a keypair from the raw X-Wing secret and public key bytes
62    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    /// Raw X-Wing secret key bytes
73    pub(crate) fn secret_bytes(&self) -> &[u8; XWingPrivateKey::LEN] {
74        self.sk.0.as_bytes()
75    }
76
77    /// Raw X-Wing public key bytes
78    pub(crate) fn public_bytes(&self) -> &[u8; XWingPublicKey::LEN] {
79        self.pk.0.as_bytes()
80    }
81}
82
83/// SD-PKE ciphertext `(c, c')`: X-Wing encapsulation `c` together with HPKE
84/// ciphertext `c'`.
85#[derive(Debug, Clone)]
86pub struct MetadataCiphertext {
87    /// HPKE encapsulation output (`c` in the spec)
88    pub(crate) c: [u8; LEN_XWING_SHAREDSECRET_ENCAPS],
89    /// HPKE AEAD ciphertext (`c'` / `cp` in the spec)
90    pub(crate) cp: [u8; LEN_METADATA_CIPHERTEXT],
91}
92
93impl MetadataCiphertext {
94    /// Total byte length of the ciphertext: encapsulation `c` + AEAD ciphertext `c'`.
95    pub fn len(&self) -> usize {
96        self.c.len() + self.cp.len()
97    }
98
99    /// Wire encoding `c || cp`
100    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    /// Deserialize from the `c || cp` wire encoding.
108    ///
109    /// # Errors
110    ///
111    /// Returns an error if the byte slice is the wrong size.
112    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
148/// SD-PKE.KGen: generate a `MetadataKeyPair`.
149///
150/// # Errors
151///
152/// Returns an error if X-Wing key generation fails.
153pub 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
161/// SD-PKE.KGen (deterministic): derive a `MetadataKeyPair` from 32 bytes of seed material.
162///
163/// For use in passphrase-derived key generation only; do not use with random bytes
164/// from a live RNG (use [`keygen`] instead).
165///
166/// # Errors
167///
168/// Returns an error if X-Wing key generation fails.
169pub(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    /// Returns the public key as bytes.
184    pub fn as_bytes(&self) -> &[u8] {
185        self.0.as_bytes()
186    }
187
188    /// Deserialize from `pk_R^PKE` (X-Wing) bytes
189    ///
190    /// # Errors
191    ///
192    /// Returns an error if the byte slice has incorrect length.
193    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    /// Returns the private key as bytes.
223    #[cfg(test)]
224    pub(crate) fn as_bytes(&self) -> &[u8] {
225        self.0.as_bytes()
226    }
227}
228
229/// SD-PKE.Enc: encrypt message `m` to recipient key `pk_r`, returning `(c, c')`.
230///
231/// `m` is the sender's long-term APKE public key, which must be serializable.
232pub(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    // MetadataPublicKey always holds a valid XWing key, so seal should not fail.
240    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    // XWing will always produce same length ciphertext
246    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
261/// SD-PKE.Dec: decrypt `(c, c')` using recipient key `sk_r`, returning message `m`.
262///
263/// # Errors
264///
265/// Returns an error if HPKE decryption fails.
266pub 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}