Skip to main content

securedrop_protocol_minimal/
encrypt_decrypt.rs

1use crate::message::MessagePublicKey;
2use crate::metadata;
3use crate::primitives::provider::constants::{LEN_KMID, LEN_MESSAGE_ID};
4use crate::primitives::ristretto255::{
5    DHPublicKey, dh_shared_secret, generate_dh_keypair, generate_random_scalar,
6    random_dh_public_key,
7};
8use crate::primitives::xwing::XWING_PUBLIC_KEY_LEN;
9use crate::primitives::{decrypt_message_id, encrypt_message_id};
10use crate::{Envelope, FetchResponse, MessageKeyBundle, Plaintext, UserPublic, UserSecret};
11use alloc::vec::Vec;
12use rand_core::{CryptoRng, RngCore};
13use uuid::Uuid;
14
15// Mock Newsroom ID
16const NR_ID: &[u8] = b"MOCK_NEWSROOM_ID";
17
18/// Encrypt a message from a sender to a recipient (step 6).
19///
20/// Produces an [`Envelope`] containing:
21/// - `ct^APKE`: SD-APKE ciphertext (encrypted message)
22/// - `ct^PKE`: SD-PKE ciphertext (encrypted sender APKE public key)
23/// - `(X, Z)`: hint for privacy-preserving message fetching
24#[cfg_attr(hax, hax_lib::fstar::verification_status(lax))]
25pub fn encrypt<R, Sender, Recipient>(
26    rng: &mut R,
27    sender: &Sender,
28    plaintext: &Plaintext,
29    recipient: &Recipient,
30) -> Envelope
31where
32    R: RngCore + CryptoRng,
33    Sender: UserSecret + ?Sized,
34    Recipient: UserPublic + ?Sized,
35{
36    // spec: sk_S^APKE - sender's long-term APKE private key
37    let keypair_s = sender.message_auth_keypair();
38    // spec: pk_R^APKE - recipient's APKE public key
39    let pk_r = recipient.message_enc_pk();
40    // spec: pk_R^fetch
41    let pk_r_fetch = recipient.fetch_pk();
42
43    // spec: ct^APKE = SD-APKE.AuthEnc(sk_S^APKE, pk_R^APKE, pt, NR, pk_R^fetch)
44    let ct_apke = crate::message::auth_enc(
45        rng,
46        keypair_s,
47        pk_r,
48        &plaintext.to_bytes(),
49        NR_ID,
50        pk_r_fetch,
51    )
52    .expect("SD-APKE AuthEnc failed");
53
54    // Hint (X, Z): X = g^x, Z = (pk_R^fetch)^x for a fresh ephemeral scalar x
55    // spec: x (hint_esk), X (hint_epk)
56    let (hint_esk, hint_epk) = generate_dh_keypair(rng);
57    // spec: Z = (pk_R^fetch)^x
58    let hint_sharedsecret: DHPublicKey = dh_shared_secret(recipient.fetch_pk(), &hint_esk);
59
60    // spec: pk_S^APKE - sender's long-term APKE public key
61    // spec: ct^PKE = SD-PKE.Enc(pk_R^PKE, pk_S^APKE)
62    // TODO: Refactor to return Result<T, E> instead of panicking at failed metadata seal
63    let ct_pke = metadata::encrypt(recipient.message_metadata_pk(), &keypair_s.public_key())
64        .expect("Valid Keybundle should allow metadata seal");
65
66    Envelope {
67        ct_apke,                 // spec: ct^APKE
68        ct_pke,                  // spec: ct^PKE
69        mgdh_pubkey: hint_epk,   // spec: X = g^x
70        mgdh: hint_sharedsecret, // spec: Z = (pk_R^fetch)^x
71    }
72}
73
74#[cfg_attr(hax, hax_lib::fstar::verification_status(lax))]
75pub fn decrypt<U: UserSecret + ?Sized>(receiver: &U, envelope: &Envelope) -> Plaintext {
76    decrypt_with_sender(receiver, envelope).0
77}
78
79/// Decrypt like [`decrypt`], additionally returning the sender's long-term
80/// SD-APKE public key `pk_S^APKE` recovered from `ct^PKE`.
81#[cfg_attr(hax, hax_lib::fstar::verification_status(lax))]
82pub fn decrypt_with_sender<U: UserSecret + ?Sized>(
83    receiver: &U,
84    envelope: &Envelope,
85) -> (Plaintext, MessagePublicKey) {
86    // Trial-decrypt ct^PKE with each keybundle's metadata private key to find
87    // the intended recipient's bundle. There should be exactly 1 result.
88    let mut found: Option<(&MessageKeyBundle, Vec<u8>)> = None;
89    for &bundle in receiver.keybundles().iter() {
90        if let Ok(m) = metadata::decrypt(bundle.metadata_kp.private_key(), &envelope.ct_pke) {
91            found = Some((bundle, m));
92        }
93    }
94
95    // TODO: only true for test purposes!
96    let (bundle, raw_metadata) = found.expect("we should find exactly 1 result");
97
98    // spec: pk_S^APKE - reconstruct sender's APKE public key from decrypted metadata
99    let sender_pk = MessagePublicKey::from_bytes(&raw_metadata)
100        .expect("Metadata must contain valid sender APKE key tuple");
101
102    // spec: pk_R^fetch
103    let pk_r_fetch = receiver.fetch_keypair().1;
104
105    // spec: pt = SD-APKE.AuthDec(sk_R^APKE, pk_S^APKE, ct^APKE, NR, pk_R^fetch)
106    let pt = crate::message::auth_dec(
107        bundle.apke.private_key(), // spec: sk_R^APKE
108        &sender_pk,                // spec: pk_S^APKE
109        &envelope.ct_apke,         // spec: ct^APKE
110        NR_ID,                     // spec: NR
111        pk_r_fetch,                // spec: pk_R^fetch
112    )
113    .expect("SD-APKE AuthDec failed");
114
115    (Plaintext::from_bytes(&pt).unwrap(), sender_pk)
116}
117
118/// Given a set of ciphertext bundles (C, X, Z) and their associated uuid,
119/// compute a fixed-length set of "challenges" >= the number of SeverMessageStore entries.
120/// A challenge is returned as a tuple of DH agreement outputs (or random data tuples of the same length).
121/// For benchmarking purposes, supply the rng as a separable parameter, and allow the total number of expected responses to be specified as a paremeter (worst case performance
122/// when the number of items in the server store approaches num total_responses.)
123///
124/// Note this is marked lax temporaily due to the `.expect()`/`push` panic freedom requirement
125#[cfg_attr(hax, hax_lib::fstar::verification_status(lax))]
126pub fn compute_fetch_challenges<R: RngCore + CryptoRng>(
127    rng: &mut R,
128    entries: &[([u8; LEN_MESSAGE_ID], Envelope)],
129    total_responses: usize,
130) -> Vec<FetchResponse> {
131    let mut responses = Vec::with_capacity(total_responses);
132
133    for (message_id, envelope) in entries.iter() {
134        if responses.len() < total_responses {
135            // Generate ephemeral scalar r, fresh per message
136            let eph_sk = generate_random_scalar(&mut *rng);
137
138            // 3-party DH yields shared_secret used to encrypt message_id
139            let shared_secret = dh_shared_secret(&envelope.mgdh, &eph_sk);
140            let enc_mid = encrypt_message_id(&shared_secret.into_bytes(), message_id, rng).unwrap();
141
142            // `copy_from_slice` rather than `try_into()`: Core_models has no
143            // `TryInto<Vec<u8>, [u8; N]>` instance, and this is the codebase's
144            // Vec->array idiom. (Lengths must match; covered by `lax`.)
145            let mut kmid = [0u8; LEN_KMID];
146            kmid.copy_from_slice(&enc_mid);
147
148            // 2-party DH yields per-request clue (pmgdh) used by intended recipient
149            // to compute shared_secret
150            let pmgdh = dh_shared_secret(&envelope.mgdh_pubkey, &eph_sk);
151
152            responses.push(FetchResponse {
153                enc_id: kmid,
154                pmgdh,
155            });
156        }
157    }
158
159    // Pad if needed to return fixed length of responses
160    while responses.len() < total_responses {
161        let mut pad_kmid: [u8; LEN_KMID] = [0u8; LEN_KMID];
162        rng.fill_bytes(&mut pad_kmid);
163
164        responses.push(FetchResponse {
165            enc_id: pad_kmid,
166            pmgdh: random_dh_public_key(rng),
167        });
168    }
169    responses
170}
171
172/// Solve fetch challenges (encrypted message IDs) and return array of valid message_ids.
173/// TODO: For simplicity, serialize/deserialize is skipped
174#[cfg_attr(hax, hax_lib::fstar::verification_status(lax))]
175pub fn solve_fetch_challenges<S: UserSecret>(
176    recipient: &S,
177    challenges: &[FetchResponse],
178) -> Vec<Uuid> {
179    let mut message_ids: Vec<Uuid> = Vec::new();
180
181    for chall in challenges.iter() {
182        // Compute 3-party DH on the pmgdh
183        let maybe_kmid_secret = dh_shared_secret(&chall.pmgdh, recipient.fetch_keypair().0);
184
185        // Try decrypting the encrypted message id
186        // Convert to UUID (v4) format and add to message ID list on success
187        match decrypt_message_id(&maybe_kmid_secret.into_bytes(), &chall.enc_id) {
188            Ok(message_id_bytes) => {
189                let uuid = crate::primitives::provider::uuid_parse::from_slice(&message_id_bytes);
190
191                message_ids.push(uuid);
192            }
193            Err(_) => {
194                // An error in decryption is fine (may not be a valid message_id), but
195                // an error in uuid parsing isn't.
196            }
197        }
198    }
199    message_ids
200}
201
202/// Build plaintext message, including pubkeys (for replies).
203/// TODO: only sources need to attach their pubkeys (for replies),
204/// but for toy purposes, everyone builds a Plaintext message the same way
205#[cfg_attr(hax, hax_lib::fstar::verification_status(lax))]
206pub fn build_message(sender: &impl UserPublic, message: Vec<u8>) -> Plaintext {
207    let mut reply_key_pq_hybrid = [0u8; XWING_PUBLIC_KEY_LEN];
208    reply_key_pq_hybrid.copy_from_slice(sender.message_metadata_pk().as_bytes());
209
210    Plaintext {
211        sender_fetch_key: *sender.fetch_pk(),
212        sender_reply_pubkey_hybrid: reply_key_pq_hybrid,
213        msg: message,
214    }
215}
216
217// Begin unit tests
218#[cfg(test)]
219mod tests {
220    use rand_chacha::ChaCha20Rng;
221    use rand_core::SeedableRng;
222
223    use crate::primitives::ristretto255::DH_PUBLIC_KEY_LEN;
224    use crate::{Journalist, Source, SourcePublicView, storage::ServerStorage};
225
226    use super::*;
227
228    // Test purposes only!
229    fn setup_rng() -> impl rand_core::CryptoRng + rand_core::RngCore {
230        let mut seed = [0u8; 32];
231        getrandom::fill(&mut seed).expect("getrandom failed- is platform supported?");
232        ChaCha20Rng::from_seed(seed)
233    }
234
235    fn assert_encrypt_decrypt<R: CryptoRng + RngCore>(
236        rng: &mut R,
237        sender_public: &impl UserPublic,
238        sender_secret: &impl UserSecret,
239        rcvr_public: &impl UserPublic,
240        rcvr_secret: &impl UserSecret,
241        msg: Vec<u8>,
242    ) {
243        let pt = build_message(sender_public, msg);
244
245        let envelope = encrypt(rng, sender_secret, &pt, rcvr_public);
246        let decrypted = decrypt(rcvr_secret, &envelope);
247
248        let pt_ref = &pt;
249
250        assert_eq!(pt_ref.msg, decrypted.msg);
251        assert_eq!(pt_ref.len(), decrypted.to_bytes().len());
252
253        assert_eq!(
254            pt_ref.sender_fetch_key.into_bytes(),
255            sender_secret.fetch_keypair().1.clone().into_bytes()
256        );
257        assert_eq!(
258            &pt_ref.sender_reply_pubkey_hybrid,
259            sender_public.message_metadata_pk().as_bytes()
260        );
261        assert_eq!(
262            pt.len(),
263            &pt_ref.msg.len() + DH_PUBLIC_KEY_LEN + XWING_PUBLIC_KEY_LEN
264        );
265    }
266
267    #[test]
268    fn test_encrypt_decrypt_roundtrip() {
269        let mut rng = setup_rng();
270
271        let sender = Source::new(&mut rng);
272        let recipient = Journalist::new(&mut rng, 2);
273
274        let msg = b"Encrypt-decrypt-test".to_vec();
275
276        assert_encrypt_decrypt(
277            &mut rng,
278            &sender.public(),
279            &sender,
280            &recipient.public(1),
281            &recipient,
282            msg,
283        );
284    }
285
286    #[test]
287    fn test_encrypt_decrypt_sourcesource() {
288        // we don't want this, but it should work anyway
289        let mut rng = setup_rng();
290        let sender = Source::new(&mut rng);
291        let recipient = Source::new(&mut rng);
292
293        assert_encrypt_decrypt(
294            &mut rng,
295            &sender.public(),
296            &sender,
297            &recipient.public(),
298            &recipient,
299            b"Encrypt-decrypt-test".to_vec(),
300        );
301    }
302
303    #[test]
304    fn test_fetch_challenges_roundtrip() {
305        let mut rng = setup_rng();
306
307        let source = Source::new(&mut rng);
308        let journalist = Journalist::new(&mut rng, 2);
309
310        // pubkey-only capabilities (for receiver)
311        let journalist_public = journalist.public(0);
312
313        let msg = b"Fetch this message";
314        let plaintext = build_message(&source.public(), msg.to_vec());
315        let envelope = encrypt(&mut rng, &source, &plaintext, &journalist_public);
316
317        let mut store: ServerStorage = ServerStorage::new();
318        let message_id = store.deterministic_uuid(&mut rng);
319
320        store.add_message(message_id, envelope);
321
322        let entries: Vec<_> = store
323            .get_messages()
324            .iter()
325            .map(|(uuid, envelope)| (*uuid.as_bytes(), envelope.clone()))
326            .collect();
327        let challenges = compute_fetch_challenges(&mut rng, &entries, 2);
328
329        let solved_ids = solve_fetch_challenges(&journalist, &challenges);
330
331        assert_eq!(solved_ids.len(), 1);
332        assert_eq!(solved_ids[0], message_id);
333    }
334
335    #[test]
336    fn test_wrong_recipient_cannot_decrypt_challenge() {
337        let mut rng = setup_rng();
338
339        let source = Source::new(&mut rng);
340        let journalist = Journalist::new(&mut rng, 2);
341
342        let wrong_journalist = Journalist::new(&mut rng, 2);
343
344        // pubkey-only capabilities (for receiver)
345        let journalist_public = journalist.public(0);
346
347        let msg = b"Fetch this message";
348        let plaintext = build_message(&source.public(), msg.to_vec());
349        let envelope = encrypt(&mut rng, &source, &plaintext, &journalist_public);
350
351        let mut store: ServerStorage = ServerStorage::new();
352        let message_id = store.deterministic_uuid(&mut rng);
353
354        store.add_message(message_id, envelope);
355
356        let entries: Vec<_> = store
357            .get_messages()
358            .iter()
359            .map(|(uuid, envelope)| (*uuid.as_bytes(), envelope.clone()))
360            .collect();
361        let challenges = compute_fetch_challenges(&mut rng, &entries, 2);
362
363        let solved_ids = solve_fetch_challenges(&journalist, &challenges);
364
365        let solved_ids_miss = solve_fetch_challenges(&wrong_journalist, &challenges);
366
367        assert_eq!(solved_ids.len(), 1);
368        assert_eq!(solved_ids[0], message_id);
369        assert_eq!(solved_ids_miss.len(), 0);
370    }
371
372    #[test]
373    fn test_journalist_reply_to_source_roundtrip() {
374        let mut rng = setup_rng();
375
376        // Source submits to journalist.
377        let source = Source::new(&mut rng);
378        let journalist = Journalist::new(&mut rng, 2);
379        let journalist_public = journalist.public(0);
380
381        let submission = build_message(&source.public(), b"howdy".to_vec());
382        let envelope = encrypt(&mut rng, &source, &submission, &journalist_public);
383
384        // Journalist decrypts and recovers the source's reply keys.
385        let (pt, sender_apke) = decrypt_with_sender(&journalist, &envelope);
386        let reply_recipient = SourcePublicView::from_reply_keys(
387            pt.sender_fetch_key,
388            sender_apke,
389            crate::metadata::MetadataPublicKey::from_bytes(&pt.sender_reply_pubkey_hybrid)
390                .expect("recovered metadata key is valid"),
391        );
392
393        // Journalist encrypts a reply back to the source.
394        let reply_text = b"thanks dawg".to_vec();
395        let reply_pt = journalist.build_message(reply_text.clone());
396        let reply_envelope = encrypt(&mut rng, &journalist, &reply_pt, &reply_recipient);
397
398        // Source decrypts the reply.
399        let decrypted = decrypt(&source, &reply_envelope);
400        assert_eq!(decrypted.msg, reply_text);
401    }
402
403    #[test]
404    fn test_encrypt_decrypt_journalist_only() {
405        let mut rng = setup_rng();
406
407        let journalist = Journalist::new(&mut rng, 2);
408        let j2 = Journalist::new(&mut rng, 2);
409
410        let msg = "Test message".as_bytes().to_vec();
411
412        assert_encrypt_decrypt(
413            &mut rng,
414            &journalist.public(0),
415            &journalist,
416            &j2.public(0),
417            &j2,
418            msg,
419        );
420    }
421}