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