Skip to main content

securedrop_protocol_minimal/
encrypt_decrypt.rs

1use crate::constants::{LEN_DH_ITEM, LEN_KMID, LEN_XWING_ENCAPS_KEY};
2use crate::message::MessagePublicKey;
3use crate::metadata;
4use crate::primitives::x25519::DHPublicKey;
5use crate::primitives::x25519::DHSharedSecret;
6use crate::primitives::x25519::dh_shared_secret;
7use crate::primitives::x25519::generate_dh_keypair;
8use crate::primitives::x25519::generate_random_scalar;
9use crate::primitives::{decrypt_message_id, encrypt_message_id};
10use crate::storage::ServerMessageStore;
11use crate::{Envelope, FetchResponse, MessageKeyBundle, Plaintext, UserPublic, UserSecret};
12use alloc::format;
13use alloc::vec::Vec;
14use rand_core::{CryptoRng, RngCore};
15use uuid::Uuid;
16
17// Mock Newsroom ID
18const NR_ID: &[u8] = b"MOCK_NEWSROOM_ID";
19
20/// Encrypt a message from a sender to a recipient (step 6).
21///
22/// Produces an [`Envelope`] containing:
23/// - `ct^APKE`: SD-APKE ciphertext (encrypted message)
24/// - `ct^PKE`: SD-PKE ciphertext (encrypted sender APKE public key)
25/// - `(X, Z)`: hint for privacy-preserving message fetching
26pub fn encrypt<R, Sender, Recipient>(
27    rng: &mut R,
28    sender: &Sender,
29    plaintext: &Plaintext,
30    recipient: &Recipient,
31) -> Envelope
32where
33    R: RngCore + CryptoRng,
34    Sender: UserSecret + ?Sized,
35    Recipient: UserPublic + ?Sized,
36{
37    // spec: sk_S^APKE - sender's long-term APKE private key
38    let sk_s = sender.message_auth_key();
39    // spec: pk_R^APKE - recipient's APKE public key
40    let pk_r = recipient.message_enc_pk();
41    // spec: pk_R^fetch
42    let pk_r_fetch = recipient.fetch_pk().into_bytes();
43
44    // spec: ct^APKE = SD-APKE.AuthEnc(sk_S^APKE, pk_R^APKE, pt, NR, pk_R^fetch)
45    let ct_apke =
46        crate::message::auth_enc(rng, sk_s, pk_r, &plaintext.to_bytes(), NR_ID, &pk_r_fetch)
47            .expect("SD-APKE AuthEnc failed");
48
49    // Hint (X, Z): X = g^x, Z = (pk_R^fetch)^x for a fresh ephemeral scalar x
50    // spec: x (hint_esk), X (hint_epk)
51    let (hint_esk, hint_epk) = generate_dh_keypair(rng).expect("DH Keygen (hint) failed");
52    // spec: Z = (pk_R^fetch)^x
53    let hint_sharedsecret: DHSharedSecret =
54        dh_shared_secret(recipient.fetch_pk(), hint_esk.into_bytes())
55            .expect("Failed to generate shared secret");
56
57    // spec: pk_S^APKE - sender's long-term APKE public key
58    let sender_apke_bytes = sender.message_auth_pk().as_bytes();
59
60    // spec: ct^PKE = SD-PKE.Enc(pk_R^PKE, pk_S^APKE)
61    let ct_pke = metadata::encrypt(recipient.message_metadata_pk(), &sender_apke_bytes);
62
63    Envelope {
64        ct_apke,                              // spec: ct^APKE
65        ct_pke,                               // spec: ct^PKE
66        mgdh_pubkey: hint_epk.into_bytes(),   // spec: X = g^x
67        mgdh: hint_sharedsecret.into_bytes(), // spec: Z = (pk_R^fetch)^x
68    }
69}
70
71pub fn decrypt<U: UserSecret + ?Sized>(receiver: &U, envelope: &Envelope) -> Plaintext {
72    // Trial-decrypt ct^PKE with each keybundle's metadata private key to find
73    // the intended recipient's bundle. There should be exactly 1 result.
74    let mut results: Vec<(&MessageKeyBundle, Vec<u8>)> = Vec::new();
75
76    // hax doesn't support FnMut closures (cryspen/hax/issues/1060), so avoid filter_map() etc
77    for bundle in receiver.keybundles() {
78        if let Ok(m) = metadata::decrypt(bundle.metadata_kp.private_key(), &envelope.ct_pke) {
79            results.push((bundle, m));
80        }
81    }
82
83    // TODO: only true for test purposes!
84    let (bundle, raw_metadata) = results.first().expect("we should find exactly 1 result");
85
86    // spec: pk_S^APKE - reconstruct sender's APKE public key from decrypted metadata
87    let sender_pk = MessagePublicKey::from_bytes(raw_metadata)
88        .expect("Metadata must contain valid sender APKE key tuple");
89
90    // spec: pk_R^fetch
91    let pk_r_fetch = receiver.fetch_keypair().1.into_bytes();
92
93    // spec: pt = SD-APKE.AuthDec(sk_R^APKE, pk_S^APKE, ct^APKE, NR, pk_R^fetch)
94    let pt = crate::message::auth_dec(
95        bundle.apke.private_key(), // spec: sk_R^APKE
96        &sender_pk,                // spec: pk_S^APKE
97        &envelope.ct_apke,         // spec: ct^APKE
98        NR_ID,                     // spec: NR
99        &pk_r_fetch,               // spec: pk_R^fetch
100    )
101    .expect("SD-APKE AuthDec failed");
102
103    Plaintext::from_bytes(&pt).unwrap()
104}
105
106/// Given a set of ciphertext bundles (C, X, Z) and their associated uuid,
107/// compute a fixed-length set of "challenges" >= the number of SeverMessageStore entries.
108/// A challenge is returned as a tuple of DH agreement outputs (or random data tuples of the same length).
109/// 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
110/// when the number of items in the server store approaches num total_responses.)
111pub fn compute_fetch_challenges<R: RngCore + CryptoRng>(
112    rng: &mut R,
113    store: &ServerMessageStore,
114    total_responses: usize,
115) -> Vec<FetchResponse> {
116    let mut responses = Vec::with_capacity(total_responses);
117
118    // Generate ephemeral (per request) scalar (don't need full keypair)
119    let eph_sk = generate_random_scalar(&mut *rng).expect("Want dh scalar");
120
121    for entry in store.keys() {
122        let message_id = entry.as_bytes();
123        let envelope = store.get(entry).expect("missing message for this uuid");
124
125        // 3-party DH yields shared_secret used to encrypt message_id
126        let shared_secret = dh_shared_secret(&DHPublicKey::from_bytes(envelope.mgdh), eph_sk)
127            .expect("Need 3-party dh shared secret");
128        let enc_mid = encrypt_message_id(&shared_secret.into_bytes(), message_id, rng).unwrap();
129
130        let kmid = enc_mid
131            .try_into()
132            .expect(&format!("Need {} bytes", LEN_KMID));
133
134        // 2-party DH yields per-request clue (pmgdh) used by intended recipient
135        // to compute shared_secret
136        let pmgdh = dh_shared_secret(&DHPublicKey::from_bytes(envelope.mgdh_pubkey), eph_sk)
137            .expect("Need pmgdh");
138
139        responses.push(FetchResponse {
140            enc_id: kmid,
141            pmgdh: pmgdh.into_bytes(),
142        });
143
144        // Are we done?
145        if responses.len() == total_responses {
146            break;
147        }
148    }
149
150    // Pad if needed to return fixed length of responses
151    while responses.len() < total_responses {
152        let mut pad_kmid: [u8; LEN_KMID] = [0u8; LEN_KMID];
153        rng.fill_bytes(&mut pad_kmid);
154
155        let mut pad_pmgdh: [u8; LEN_DH_ITEM] = [0u8; LEN_DH_ITEM];
156        rng.fill_bytes(&mut pad_pmgdh);
157
158        responses.push(FetchResponse {
159            enc_id: pad_kmid,
160            pmgdh: pad_pmgdh,
161        });
162    }
163    responses
164}
165
166/// Solve fetch challenges (encrypted message IDs) and return array of valid message_ids.
167/// TODO: For simplicity, serialize/deserialize is skipped
168pub fn solve_fetch_challenges<S: UserSecret>(
169    recipient: &S,
170    challenges: &[FetchResponse],
171) -> Vec<Uuid> {
172    let mut message_ids: Vec<Uuid> = Vec::new();
173
174    for chall in challenges.iter() {
175        // Compute 3-party DH on the pmgdh
176        let maybe_kmid_secret = dh_shared_secret(
177            &DHPublicKey::from_bytes(chall.pmgdh),
178            recipient.fetch_keypair().0.clone().into_bytes(),
179        )
180        .expect("Need 3-party DH (scalarmult) on pmgdh");
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 = Uuid::from_slice(&message_id_bytes)
187                    // TODO: return Result<Vec<Uuid>, Error> instead of panic
188                    // (will change wasm stuff too so deferring for now)
189                    .expect("Need uuid from decrypted 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
205pub fn build_message(sender: &impl UserPublic, message: Vec<u8>) -> Plaintext {
206    let mut fetch_pk = [0u8; LEN_DH_ITEM];
207    fetch_pk.copy_from_slice(&sender.fetch_pk().clone().into_bytes());
208
209    let mut reply_key_pq_hybrid = [0u8; LEN_XWING_ENCAPS_KEY];
210    reply_key_pq_hybrid.copy_from_slice(sender.message_metadata_pk().as_bytes());
211
212    Plaintext {
213        sender_fetch_key: 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, 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        mut rng: 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(&mut 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,
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() + LEN_DH_ITEM + LEN_XWING_ENCAPS_KEY
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            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            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 challenges = compute_fetch_challenges(&mut rng, &store.get_messages(), 2);
324
325        let solved_ids = solve_fetch_challenges(&journalist, &challenges);
326
327        assert_eq!(solved_ids.len(), 1);
328        assert_eq!(solved_ids[0], message_id);
329    }
330
331    #[test]
332    fn test_wrong_recipient_cannot_decrypt_challenge() {
333        let mut rng = setup_rng();
334
335        let source = Source::new(&mut rng);
336        let journalist = Journalist::new(&mut rng, 2);
337
338        let wrong_journalist = Journalist::new(&mut rng, 2);
339
340        // pubkey-only capabilities (for receiver)
341        let journalist_public = journalist.public(0);
342
343        let msg = b"Fetch this message";
344        let plaintext = build_message(&source.public(), msg.to_vec());
345        let envelope = encrypt(&mut rng, &source, &plaintext, &journalist_public);
346
347        let mut store: ServerStorage = ServerStorage::new();
348        let message_id = store.deterministic_uuid(&mut rng);
349
350        store.add_message(message_id, envelope);
351
352        let challenges = compute_fetch_challenges(&mut rng, &store.get_messages(), 2);
353
354        let solved_ids = solve_fetch_challenges(&journalist, &challenges);
355
356        let solved_ids_miss = solve_fetch_challenges(&wrong_journalist, &challenges);
357
358        assert_eq!(solved_ids.len(), 1);
359        assert_eq!(solved_ids[0], message_id);
360        assert_eq!(solved_ids_miss.len(), 0);
361    }
362
363    #[test]
364    fn test_encrypt_decrypt_journalist_only() {
365        let mut rng = setup_rng();
366
367        let journalist = Journalist::new(&mut rng, 2);
368        let j2 = Journalist::new(&mut rng, 2);
369
370        let msg = "Test message".as_bytes().to_vec();
371
372        assert_encrypt_decrypt(
373            rng,
374            &journalist.public(0),
375            &journalist,
376            &j2.public(0),
377            &j2,
378            msg,
379        );
380    }
381}