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