Skip to main content

securedrop_protocol_minimal/client/
message.rs

1use crate::keys::NR_ID;
2use crate::message::MessagePublicKey;
3use crate::metadata;
4use crate::primitives::provider::constants::{LEN_KMID, LEN_MESSAGE_ID};
5use crate::primitives::ristretto255::{
6    DHPublicKey, dh_shared_secret, generate_dh_keypair, generate_random_scalar,
7    random_dh_public_key,
8};
9use crate::{Envelope, FetchResponse, MessageKeyBundle, Plaintext, UserPublic, UserSecret};
10use alloc::vec::Vec;
11use rand_core::{CryptoRng, RngCore};
12
13/// Encrypt a message from a sender to a recipient (step 6).
14///
15/// Produces an [`Envelope`] containing:
16/// - `ct^APKE`: SD-APKE ciphertext (encrypted message)
17/// - `ct^PKE`: SD-PKE ciphertext (encrypted sender APKE public key)
18/// - `(X, Z)`: hint for privacy-preserving message fetching
19#[cfg_attr(hax, hax_lib::fstar::verification_status(lax))]
20pub fn encrypt<R, Sender, Recipient>(
21    rng: &mut R,
22    sender: &Sender,
23    plaintext: &Plaintext,
24    recipient: &Recipient,
25) -> Envelope
26where
27    R: RngCore + CryptoRng,
28    Sender: UserSecret + ?Sized,
29    Recipient: UserPublic + ?Sized,
30{
31    // spec: sk_S^APKE - sender's long-term APKE private key
32    let keypair_s = sender.message_auth_keypair();
33    // spec: pk_R^APKE - recipient's APKE public key
34    let pk_r = recipient.message_enc_pk();
35    // spec: pk_R^fetch
36    let pk_r_fetch = recipient.fetch_pk();
37
38    // spec: ct^APKE = SD-APKE.AuthEnc(sk_S^APKE, pk_R^APKE, pt, NR, pk_R^fetch)
39    let ct_apke = crate::message::auth_enc(
40        rng,
41        keypair_s,
42        pk_r,
43        &plaintext.to_bytes(),
44        NR_ID,
45        pk_r_fetch,
46    )
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);
52    // spec: Z = (pk_R^fetch)^x
53    let hint_sharedsecret: DHPublicKey = dh_shared_secret(recipient.fetch_pk(), &hint_esk);
54
55    // spec: pk_S^APKE - sender's long-term APKE public key
56    // spec: ct^PKE = SD-PKE.Enc(pk_R^PKE, pk_S^APKE)
57    // TODO: Refactor to return Result<T, E> instead of panicking at failed metadata seal
58    let ct_pke = metadata::encrypt(
59        rng,
60        recipient.message_metadata_pk(),
61        &keypair_s.public_key(),
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,   // spec: X = g^x
69        mgdh: hint_sharedsecret, // 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;
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/// Build plaintext message, including pubkeys (for replies).
118/// TODO: only sources need to attach their pubkeys (for replies),
119/// but for toy purposes, everyone builds a Plaintext message the same way
120#[cfg_attr(hax, hax_lib::fstar::verification_status(lax))]
121pub fn build_message(sender: &impl UserPublic, message: Vec<u8>) -> Plaintext {
122    Plaintext {
123        sender_fetch_key: *sender.fetch_pk(),
124        sender_reply_pubkey_hybrid: sender.message_metadata_pk().clone(),
125        msg: message,
126    }
127}
128
129// Begin unit tests
130#[cfg(test)]
131mod tests {
132    use rand_chacha::ChaCha20Rng;
133    use rand_core::SeedableRng;
134
135    use crate::metadata::MetadataPublicKey;
136    use crate::{Journalist, Source, SourcePublicView};
137
138    use super::*;
139
140    // Test purposes only!
141    fn setup_rng() -> impl rand_core::CryptoRng + rand_core::RngCore {
142        let mut seed = [0u8; 32];
143        getrandom::fill(&mut seed).expect("getrandom failed- is platform supported?");
144        ChaCha20Rng::from_seed(seed)
145    }
146
147    fn assert_encrypt_decrypt<R: CryptoRng + RngCore>(
148        rng: &mut R,
149        sender_public: &impl UserPublic,
150        sender_secret: &impl UserSecret,
151        rcvr_public: &impl UserPublic,
152        rcvr_secret: &impl UserSecret,
153        msg: Vec<u8>,
154    ) {
155        let pt = build_message(sender_public, msg);
156
157        let envelope = encrypt(rng, sender_secret, &pt, rcvr_public);
158        let decrypted = decrypt(rcvr_secret, &envelope);
159
160        let pt_ref = &pt;
161
162        assert_eq!(pt_ref.msg, decrypted.msg);
163        assert_eq!(pt_ref.len(), decrypted.to_bytes().len());
164
165        assert_eq!(
166            pt_ref.sender_fetch_key.into_bytes(),
167            sender_secret.fetch_keypair().1.clone().into_bytes()
168        );
169        assert_eq!(
170            pt_ref.sender_reply_pubkey_hybrid.as_bytes(),
171            sender_public.message_metadata_pk().as_bytes()
172        );
173        assert_eq!(
174            pt.len(),
175            &pt_ref.msg.len() + DHPublicKey::LEN + MetadataPublicKey::LEN
176        );
177    }
178
179    #[test]
180    fn test_encrypt_decrypt_roundtrip() {
181        let mut rng = setup_rng();
182
183        let sender = Source::new(&mut rng);
184        let recipient = Journalist::new(&mut rng, 2);
185
186        let msg = b"Encrypt-decrypt-test".to_vec();
187
188        assert_encrypt_decrypt(
189            &mut rng,
190            &sender.public(),
191            &sender,
192            &recipient.public(1),
193            &recipient,
194            msg,
195        );
196    }
197
198    #[test]
199    fn test_encrypt_decrypt_sourcesource() {
200        // we don't want this, but it should work anyway
201        let mut rng = setup_rng();
202        let sender = Source::new(&mut rng);
203        let recipient = Source::new(&mut rng);
204
205        assert_encrypt_decrypt(
206            &mut rng,
207            &sender.public(),
208            &sender,
209            &recipient.public(),
210            &recipient,
211            b"Encrypt-decrypt-test".to_vec(),
212        );
213    }
214
215    #[test]
216    fn test_journalist_reply_to_source_roundtrip() {
217        let mut rng = setup_rng();
218
219        // Source submits to journalist.
220        let source = Source::new(&mut rng);
221        let journalist = Journalist::new(&mut rng, 2);
222        let journalist_public = journalist.public(0);
223
224        let submission = build_message(&source.public(), b"howdy".to_vec());
225        let envelope = encrypt(&mut rng, &source, &submission, &journalist_public);
226
227        // Journalist decrypts and recovers the source's reply keys.
228        let (pt, sender_apke) = decrypt_with_sender(&journalist, &envelope);
229        let reply_recipient = SourcePublicView::from_reply_keys(
230            pt.sender_fetch_key,
231            sender_apke,
232            pt.sender_reply_pubkey_hybrid,
233        );
234
235        // Journalist encrypts a reply back to the source.
236        let reply_text = b"thanks dawg".to_vec();
237        let reply_pt = journalist.build_message(reply_text.clone());
238        let reply_envelope = encrypt(&mut rng, &journalist, &reply_pt, &reply_recipient);
239
240        // Source decrypts the reply.
241        let decrypted = decrypt(&source, &reply_envelope);
242        assert_eq!(decrypted.msg, reply_text);
243    }
244
245    #[test]
246    fn test_encrypt_decrypt_journalist_only() {
247        let mut rng = setup_rng();
248
249        let journalist = Journalist::new(&mut rng, 2);
250        let j2 = Journalist::new(&mut rng, 2);
251
252        let msg = "Test message".as_bytes().to_vec();
253
254        assert_encrypt_decrypt(
255            &mut rng,
256            &journalist.public(0),
257            &journalist,
258            &j2.public(0),
259            &j2,
260            msg,
261        );
262    }
263}