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 alloc::vec::Vec;
19use anyhow::Error;
20use hpke_rs::{
21    Hpke, Mode, hpke_types::AeadAlgorithm::Aes256Gcm, hpke_types::KdfAlgorithm::HkdfSha256,
22    hpke_types::KemAlgorithm::XWingDraft06, libcrux::HpkeLibcrux,
23};
24use rand_core::{CryptoRng, RngCore};
25
26use crate::constants::LEN_XWING_SHAREDSECRET_ENCAPS;
27use crate::primitives::xwing::{XWingPrivateKey, XWingPublicKey, generate_xwing_keypair};
28
29/// The recipient's metadata public key (`pk_R^PKE` in the spec).
30#[derive(Debug, Clone)]
31pub struct MetadataPublicKey(pub(crate) XWingPublicKey);
32
33/// The recipient's metadata private key (`sk_R^PKE` in the spec).
34pub struct MetadataPrivateKey(pub(crate) XWingPrivateKey);
35
36/// A `(MetadataPrivateKey, MetadataPublicKey)` SD-PKE keypair.
37pub struct MetadataKeyPair {
38    sk: MetadataPrivateKey,
39    pk: MetadataPublicKey,
40}
41
42impl MetadataKeyPair {
43    /// Returns the public key.
44    pub fn public_key(&self) -> &MetadataPublicKey {
45        &self.pk
46    }
47
48    /// Returns the private key.
49    pub fn private_key(&self) -> &MetadataPrivateKey {
50        &self.sk
51    }
52}
53
54/// SD-PKE ciphertext `(c, c')`: X-Wing encapsulation `c` together with HPKE
55/// ciphertext `c'`.
56#[derive(Debug, Clone)]
57pub struct MetadataCiphertext {
58    /// HPKE encapsulation output (`c` in the spec)
59    pub(crate) c: [u8; LEN_XWING_SHAREDSECRET_ENCAPS],
60    /// HPKE AEAD ciphertext (`c'` / `cp` in the spec)
61    pub(crate) cp: Vec<u8>,
62}
63
64impl MetadataCiphertext {
65    /// Total byte length of the ciphertext: encapsulation `c` + AEAD ciphertext `c'`.
66    pub fn len(&self) -> usize {
67        self.c.len() + self.cp.len()
68    }
69}
70
71/// SD-PKE.KGen: generate a `MetadataKeyPair`.
72///
73/// # Errors
74///
75/// Returns an error if X-Wing key generation fails.
76pub fn keygen<R: RngCore + CryptoRng>(rng: &mut R) -> Result<MetadataKeyPair, anyhow::Error> {
77    let (sk_s, pk_s) = generate_xwing_keypair(rng)?;
78    Ok(MetadataKeyPair {
79        sk: MetadataPrivateKey(sk_s),
80        pk: MetadataPublicKey(pk_s),
81    })
82}
83
84/// SD-PKE.KGen (deterministic): derive a `MetadataKeyPair` from 32 bytes of seed material.
85///
86/// For use in passphrase-derived key generation only; do not use with random bytes
87/// from a live RNG (use [`keygen`] instead).
88///
89/// # Errors
90///
91/// Returns an error if X-Wing key generation fails.
92pub(crate) fn deterministic_keygen(randomness: [u8; 32]) -> Result<MetadataKeyPair, anyhow::Error> {
93    use crate::primitives::xwing::deterministic_keygen as xwing_derand;
94    let (sk_s, pk_s) = xwing_derand(randomness)?;
95    Ok(MetadataKeyPair {
96        sk: MetadataPrivateKey(sk_s),
97        pk: MetadataPublicKey(pk_s),
98    })
99}
100
101impl MetadataPublicKey {
102    /// Returns the public key as bytes.
103    pub fn as_bytes(&self) -> &[u8] {
104        self.0.as_bytes()
105    }
106}
107
108impl MetadataPrivateKey {
109    /// Returns the private key as bytes.
110    #[cfg(test)]
111    pub(crate) fn as_bytes(&self) -> &[u8] {
112        self.0.as_bytes()
113    }
114}
115
116/// SD-PKE.Enc: encrypt message `m` to recipient key `pk_r`, returning `(c, c')`.
117///
118/// `m` is the sender's serialized long-term APKE public key.
119pub fn encrypt(pk_r: &MetadataPublicKey, m: &[u8]) -> MetadataCiphertext {
120    let mut hpke = Hpke::<HpkeLibcrux>::new(Mode::Base, XWingDraft06, HkdfSha256, Aes256Gcm);
121    let pk_r_hpke = pk_r.0.clone().into();
122
123    // MetadataPublicKey always holds a valid XWing key, so seal cannot fail.
124    let (c_vec, cp) = hpke
125        .seal(&pk_r_hpke, b"", b"", m, None, None, None)
126        .expect("SD-PKE encryption failed");
127
128    // XWing will always produce this length ciphertext, so this .expect is fine.
129    let c: [u8; LEN_XWING_SHAREDSECRET_ENCAPS] = c_vec
130        .try_into()
131        .expect("X-Wing encapsulation output has unexpected length");
132
133    MetadataCiphertext { c, cp }
134}
135
136/// SD-PKE.Dec: decrypt `(c, c')` using recipient key `sk_r`, returning message `m`.
137///
138/// # Errors
139///
140/// Returns an error if HPKE decryption fails.
141pub fn decrypt(sk_r: &MetadataPrivateKey, ct: &MetadataCiphertext) -> Result<Vec<u8>, Error> {
142    let hpke = Hpke::<HpkeLibcrux>::new(Mode::Base, XWingDraft06, HkdfSha256, Aes256Gcm);
143    let sk_r_hpke = sk_r.0.clone().into();
144
145    hpke.open(&ct.c, &sk_r_hpke, b"", b"", &ct.cp, None, None, None)
146        .map_err(|e| anyhow::anyhow!("SD-PKE decryption failed: {:?}", e))
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use proptest::prelude::*;
153    use rand_chacha::ChaCha20Rng;
154    use rand_core::SeedableRng;
155
156    fn get_rng() -> ChaCha20Rng {
157        let mut seed = [0u8; 32];
158        getrandom::fill(&mut seed).expect("OS random source failed");
159        ChaCha20Rng::from_seed(seed)
160    }
161
162    proptest! {
163        #[test]
164        fn test_metadata_encrypt_decrypt_roundtrip(m in proptest::collection::vec(any::<u8>(), 0..200)) {
165            let mut rng = get_rng();
166            let kp = keygen(&mut rng).expect("KGen failed");
167
168            let ct = encrypt(kp.public_key(), &m);
169            let decrypted = decrypt(kp.private_key(), &ct).expect("Decryption failed");
170
171            prop_assert_eq!(m, decrypted);
172        }
173    }
174
175    #[test]
176    fn test_metadata_decrypt_wrong_key_fails() {
177        let mut rng = get_rng();
178        let kp = keygen(&mut rng).expect("KGen failed");
179        let wrong_kp = keygen(&mut rng).expect("KGen failed");
180
181        let ct = encrypt(kp.public_key(), b"some sender apke key bytes");
182        assert!(decrypt(wrong_kp.private_key(), &ct).is_err());
183    }
184}