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::XWingPublicKey;
9use crate::primitives::{decrypt_message_id, derive_challenge_key, 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 key = derive_challenge_key(&shared_secret, NR_ID).unwrap();
141            let enc_mid = encrypt_message_id(&key, message_id).unwrap();
142
143            // `copy_from_slice` rather than `try_into()`: Core_models has no
144            // `TryInto<Vec<u8>, [u8; N]>` instance, and this is the codebase's
145            // Vec->array idiom. (Lengths must match; covered by `lax`.)
146            let mut kmid = [0u8; LEN_KMID];
147            kmid.copy_from_slice(&enc_mid);
148
149            // 2-party DH yields per-request clue (pmgdh) used by intended recipient
150            // to compute shared_secret
151            let pmgdh = dh_shared_secret(&envelope.mgdh_pubkey, &eph_sk);
152
153            responses.push(FetchResponse {
154                enc_id: kmid,
155                pmgdh,
156            });
157        }
158    }
159
160    // Pad if needed to return fixed length of responses
161    while responses.len() < total_responses {
162        let mut pad_kmid: [u8; LEN_KMID] = [0u8; LEN_KMID];
163        rng.fill_bytes(&mut pad_kmid);
164
165        responses.push(FetchResponse {
166            enc_id: pad_kmid,
167            pmgdh: random_dh_public_key(rng),
168        });
169    }
170    responses
171}
172
173/// Solve fetch challenges (encrypted message IDs) and return array of valid message_ids.
174/// TODO: For simplicity, serialize/deserialize is skipped
175#[cfg_attr(hax, hax_lib::fstar::verification_status(lax))]
176pub fn solve_fetch_challenges<S: UserSecret>(
177    recipient: &S,
178    challenges: &[FetchResponse],
179) -> Vec<Uuid> {
180    let mut message_ids: Vec<Uuid> = Vec::new();
181
182    for chall in challenges.iter() {
183        // Compute 3-party DH on the pmgdh
184        let maybe_kmid_secret = dh_shared_secret(&chall.pmgdh, recipient.fetch_keypair().0);
185        let challenge_key = derive_challenge_key(&maybe_kmid_secret, NR_ID).unwrap();
186
187        // Try decrypting the encrypted message id
188        // Convert to UUID (v4) format and add to message ID list on success
189        match decrypt_message_id(&challenge_key, &chall.enc_id) {
190            Ok(message_id_bytes) => {
191                let uuid = crate::primitives::provider::uuid_parse::from_slice(&message_id_bytes);
192
193                message_ids.push(uuid);
194            }
195            Err(_) => {
196                // An error in decryption is fine (may not be a valid message_id), but
197                // an error in uuid parsing isn't.
198            }
199        }
200    }
201    message_ids
202}
203
204/// Build plaintext message, including pubkeys (for replies).
205/// TODO: only sources need to attach their pubkeys (for replies),
206/// but for toy purposes, everyone builds a Plaintext message the same way
207#[cfg_attr(hax, hax_lib::fstar::verification_status(lax))]
208pub fn build_message(sender: &impl UserPublic, message: Vec<u8>) -> Plaintext {
209    let mut reply_key_pq_hybrid = [0u8; XWingPublicKey::LEN];
210    reply_key_pq_hybrid.copy_from_slice(sender.message_metadata_pk().as_bytes());
211
212    Plaintext {
213        sender_fetch_key: *sender.fetch_pk(),
214        sender_reply_pubkey_hybrid: reply_key_pq_hybrid,
215        msg: message,
216    }
217}
218
219// Begin unit tests
220#[cfg(test)]
221mod tests {
222    use rand_chacha::ChaCha20Rng;
223    use rand_core::SeedableRng;
224
225    use crate::{Journalist, Source, SourcePublicView, storage::ServerStorage};
226
227    use super::*;
228
229    // Test purposes only!
230    fn setup_rng() -> impl rand_core::CryptoRng + rand_core::RngCore {
231        let mut seed = [0u8; 32];
232        getrandom::fill(&mut seed).expect("getrandom failed- is platform supported?");
233        ChaCha20Rng::from_seed(seed)
234    }
235
236    fn assert_encrypt_decrypt<R: CryptoRng + RngCore>(
237        rng: &mut R,
238        sender_public: &impl UserPublic,
239        sender_secret: &impl UserSecret,
240        rcvr_public: &impl UserPublic,
241        rcvr_secret: &impl UserSecret,
242        msg: Vec<u8>,
243    ) {
244        let pt = build_message(sender_public, msg);
245
246        let envelope = encrypt(rng, sender_secret, &pt, rcvr_public);
247        let decrypted = decrypt(rcvr_secret, &envelope);
248
249        let pt_ref = &pt;
250
251        assert_eq!(pt_ref.msg, decrypted.msg);
252        assert_eq!(pt_ref.len(), decrypted.to_bytes().len());
253
254        assert_eq!(
255            pt_ref.sender_fetch_key.into_bytes(),
256            sender_secret.fetch_keypair().1.clone().into_bytes()
257        );
258        assert_eq!(
259            &pt_ref.sender_reply_pubkey_hybrid,
260            sender_public.message_metadata_pk().as_bytes()
261        );
262        assert_eq!(
263            pt.len(),
264            &pt_ref.msg.len() + DHPublicKey::LEN + XWingPublicKey::LEN
265        );
266    }
267
268    #[test]
269    fn test_encrypt_decrypt_roundtrip() {
270        let mut rng = setup_rng();
271
272        let sender = Source::new(&mut rng);
273        let recipient = Journalist::new(&mut rng, 2);
274
275        let msg = b"Encrypt-decrypt-test".to_vec();
276
277        assert_encrypt_decrypt(
278            &mut rng,
279            &sender.public(),
280            &sender,
281            &recipient.public(1),
282            &recipient,
283            msg,
284        );
285    }
286
287    #[test]
288    fn test_encrypt_decrypt_sourcesource() {
289        // we don't want this, but it should work anyway
290        let mut rng = setup_rng();
291        let sender = Source::new(&mut rng);
292        let recipient = Source::new(&mut rng);
293
294        assert_encrypt_decrypt(
295            &mut rng,
296            &sender.public(),
297            &sender,
298            &recipient.public(),
299            &recipient,
300            b"Encrypt-decrypt-test".to_vec(),
301        );
302    }
303
304    #[test]
305    fn test_fetch_challenges_roundtrip() {
306        let mut rng = setup_rng();
307
308        let source = Source::new(&mut rng);
309        let journalist = Journalist::new(&mut rng, 2);
310
311        // pubkey-only capabilities (for receiver)
312        let journalist_public = journalist.public(0);
313
314        let msg = b"Fetch this message";
315        let plaintext = build_message(&source.public(), msg.to_vec());
316        let envelope = encrypt(&mut rng, &source, &plaintext, &journalist_public);
317
318        let mut store: ServerStorage = ServerStorage::new();
319        let message_id = store.deterministic_uuid(&mut rng);
320
321        store.add_message(message_id, envelope);
322
323        let entries: Vec<_> = store
324            .get_messages()
325            .iter()
326            .map(|(uuid, envelope)| (*uuid.as_bytes(), envelope.clone()))
327            .collect();
328        let challenges = compute_fetch_challenges(&mut rng, &entries, 2);
329
330        let solved_ids = solve_fetch_challenges(&journalist, &challenges);
331
332        assert_eq!(solved_ids.len(), 1);
333        assert_eq!(solved_ids[0], message_id);
334    }
335
336    #[test]
337    fn test_wrong_recipient_cannot_decrypt_challenge() {
338        let mut rng = setup_rng();
339
340        let source = Source::new(&mut rng);
341        let journalist = Journalist::new(&mut rng, 2);
342
343        let wrong_journalist = Journalist::new(&mut rng, 2);
344
345        // pubkey-only capabilities (for receiver)
346        let journalist_public = journalist.public(0);
347
348        let msg = b"Fetch this message";
349        let plaintext = build_message(&source.public(), msg.to_vec());
350        let envelope = encrypt(&mut rng, &source, &plaintext, &journalist_public);
351
352        let mut store: ServerStorage = ServerStorage::new();
353        let message_id = store.deterministic_uuid(&mut rng);
354
355        store.add_message(message_id, envelope);
356
357        let entries: Vec<_> = store
358            .get_messages()
359            .iter()
360            .map(|(uuid, envelope)| (*uuid.as_bytes(), envelope.clone()))
361            .collect();
362        let challenges = compute_fetch_challenges(&mut rng, &entries, 2);
363
364        let solved_ids = solve_fetch_challenges(&journalist, &challenges);
365
366        let solved_ids_miss = solve_fetch_challenges(&wrong_journalist, &challenges);
367
368        assert_eq!(solved_ids.len(), 1);
369        assert_eq!(solved_ids[0], message_id);
370        assert_eq!(solved_ids_miss.len(), 0);
371    }
372
373    #[test]
374    fn test_journalist_reply_to_source_roundtrip() {
375        let mut rng = setup_rng();
376
377        // Source submits to journalist.
378        let source = Source::new(&mut rng);
379        let journalist = Journalist::new(&mut rng, 2);
380        let journalist_public = journalist.public(0);
381
382        let submission = build_message(&source.public(), b"howdy".to_vec());
383        let envelope = encrypt(&mut rng, &source, &submission, &journalist_public);
384
385        // Journalist decrypts and recovers the source's reply keys.
386        let (pt, sender_apke) = decrypt_with_sender(&journalist, &envelope);
387        let reply_recipient = SourcePublicView::from_reply_keys(
388            pt.sender_fetch_key,
389            sender_apke,
390            crate::metadata::MetadataPublicKey::from_bytes(&pt.sender_reply_pubkey_hybrid)
391                .expect("recovered metadata key is valid"),
392        );
393
394        // Journalist encrypts a reply back to the source.
395        let reply_text = b"thanks dawg".to_vec();
396        let reply_pt = journalist.build_message(reply_text.clone());
397        let reply_envelope = encrypt(&mut rng, &journalist, &reply_pt, &reply_recipient);
398
399        // Source decrypts the reply.
400        let decrypted = decrypt(&source, &reply_envelope);
401        assert_eq!(decrypted.msg, reply_text);
402    }
403
404    #[test]
405    fn test_encrypt_decrypt_journalist_only() {
406        let mut rng = setup_rng();
407
408        let journalist = Journalist::new(&mut rng, 2);
409        let j2 = Journalist::new(&mut rng, 2);
410
411        let msg = "Test message".as_bytes().to_vec();
412
413        assert_encrypt_decrypt(
414            &mut rng,
415            &journalist.public(0),
416            &journalist,
417            &j2.public(0),
418            &j2,
419            msg,
420        );
421    }
422}