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