Skip to main content

securedrop_protocol_minimal/
server.rs

1//! Server-side protocol implementation
2//!
3//! This module implements the server-side handling of SecureDrop protocol steps 5-10.
4
5use alloc::vec::Vec;
6use anyhow::Error;
7use rand_core::{CryptoRng, RngCore};
8use uuid::Uuid;
9
10use crate::encrypt_decrypt::compute_fetch_challenges;
11use crate::keys::NewsroomKeyPair;
12use crate::primitives;
13use crate::sign::{FpfOnNewsroom, NewsroomOnJournalist, Signature, VerifyingKey};
14use crate::storage::ServerStorage;
15use crate::wire::core::{
16    MessageChallengeFetchRequest, MessageChallengeFetchResponse, MessageFetchRequest,
17    SourceJournalistKeyRequest, SourceJournalistKeyResponse, SourceNewsroomKeyRequest,
18    SourceNewsroomKeyResponse,
19};
20use crate::wire::setup::{
21    JournalistEphemeralKeyRequest, JournalistSetupRequest, JournalistSetupResponse,
22    NewsroomSetupRequest,
23};
24use crate::{Envelope, JournalistPublicView};
25
26/// Server session for handling source requests
27#[derive(Default)]
28pub struct Server {
29    storage: ServerStorage,
30    newsroom_keys: Option<NewsroomKeyPair>,
31    /// Signature from FPF over the newsroom keys
32    signature: Option<Signature<FpfOnNewsroom>>,
33}
34
35impl Server {
36    /// Create a new server session
37    ///
38    /// TODO: Load newsroom keys from storage if they exist.
39    pub fn new() -> Self {
40        Self::default()
41    }
42
43    /// Generate a new newsroom setup request.
44    ///
45    /// This creates a newsroom key pair, stores it in the server storage,
46    /// and returns a setup request that can be sent to FPF for signing.
47    ///
48    /// TODO: The caller should persist these keys to disk.
49    pub fn create_newsroom_setup_request<R: RngCore + CryptoRng>(
50        &mut self,
51        mut rng: R,
52    ) -> Result<NewsroomSetupRequest, Error> {
53        let newsroom_keys = NewsroomKeyPair::new(&mut rng)?;
54        let newsroom_vk = newsroom_keys.verifying_key();
55
56        // Store the newsroom keys in the session for later use (e.g., signing journalist keys)
57        self.newsroom_keys = Some(newsroom_keys);
58
59        Ok(NewsroomSetupRequest {
60            newsroom_verifying_key: newsroom_vk,
61        })
62    }
63
64    /// Setup a journalist. This corresponds to step 3.1 in the spec.
65    ///
66    /// The newsroom then signs the bundle of journalist public keys.
67    ///
68    /// TODO: There is a manual verification step here, so the caller should
69    /// instruct the user to stop, verify the fingerprint out of band, and
70    /// then proceed. The caller should also persist the fingerprint and signature
71    /// in its local data store.
72    ///
73    /// TODO(later): How to handle signing when offline? (Not relevant for benchmarking)
74    pub fn setup_journalist(
75        &mut self,
76        request: JournalistSetupRequest,
77    ) -> Result<JournalistSetupResponse, Error> {
78        // Get enrollment key from the request
79        let journalist_signing_key = request.enrollment.keys.0;
80
81        // Verify journalist self-signature over their own pubkeys.
82        journalist_signing_key
83            .verify(
84                request.enrollment.bundle.as_bytes(),
85                &request.enrollment.selfsig,
86            )
87            .map_err(|_| anyhow::anyhow!("Invalid signature on longterm keys"))?;
88
89        // Sign the journalist's verifying key.
90        let verifying_key_bytes = request.enrollment.keys.0.into_bytes();
91        let newsroom_keys = self
92            .newsroom_keys
93            .as_ref()
94            .ok_or_else(|| anyhow::anyhow!("Newsroom keys not found in session"))?;
95        let newsroom_signature: Signature<NewsroomOnJournalist> =
96            newsroom_keys.sign(&verifying_key_bytes);
97
98        // Insert journalist keys into storage
99        let _journalist_id = self
100            .storage
101            .add_journalist(request.enrollment, newsroom_signature.clone());
102
103        Ok(JournalistSetupResponse {
104            sig: newsroom_signature,
105        })
106    }
107
108    /// Handle journalist ephemeral key replenishment. This corresponds to step 3.2 in the spec.
109    ///
110    /// The journalist sends ephemeral keys signed by their signing key, and the server
111    /// verifies the signature and stores the ephemeral keys.
112    ///
113    /// # Errors
114    ///
115    /// Returns an error if the journalist is not found in storage, or if any bundle
116    /// signature fails verification.
117    pub fn handle_ephemeral_key_request(
118        &mut self,
119        request: JournalistEphemeralKeyRequest,
120    ) -> Result<(), Error> {
121        // Look up the journalist by their verifying key
122        let journalist_id = self
123            .storage
124            .find_journalist_by_verifying_key(&request.verifying_key)
125            .ok_or_else(|| anyhow::anyhow!("Journalist not found in storage"))?;
126
127        // TODO: more efficient way than verifying each signature!
128        // Verify each ephemeral bundle signature.
129        request
130            .bundles
131            .iter()
132            .try_for_each(|k| request.verifying_key.verify(&k.0.as_bytes(), &k.1))
133            .map_err(|_| anyhow::anyhow!("Invalid signature on ephemeral keys"))?;
134
135        // Store the ephemeral keys for the journalist
136        self.storage
137            .add_ephemeral_keys(journalist_id, request.bundles);
138
139        Ok(())
140    }
141
142    /// Returns the newsroom verifying key, if one has been generated.
143    pub fn newsroom_verifying_key(&self) -> Option<VerifyingKey> {
144        self.newsroom_keys.as_ref().map(|keys| keys.verifying_key())
145    }
146
147    /// Set the FPF signature for the newsroom
148    pub fn set_fpf_signature(&mut self, signature: Signature<FpfOnNewsroom>) {
149        self.signature = Some(signature);
150    }
151
152    /// Get the ephemeral key count for a journalist
153    pub fn ephemeral_keys_count(&self, journalist_id: Uuid) -> usize {
154        self.storage.ephemeral_keys_count(journalist_id)
155    }
156
157    /// Check if a journalist has ephemeral keys available
158    pub fn has_ephemeral_keys(&self, journalist_id: Uuid) -> bool {
159        self.storage.has_ephemeral_keys(journalist_id)
160    }
161
162    /// Find journalist ID by verifying key
163    pub fn find_journalist_id(&self, verifying_key: &VerifyingKey) -> Option<Uuid> {
164        self.storage.find_journalist_by_verifying_key(verifying_key)
165    }
166
167    /// Check if a message exists with the given ID
168    pub fn has_message(&self, message_id: &Uuid) -> bool {
169        self.storage.get_messages().contains_key(message_id)
170    }
171
172    /// Handle source newsroom key request (step 5)
173    pub fn handle_source_newsroom_key_request(
174        &self,
175        _request: SourceNewsroomKeyRequest,
176    ) -> SourceNewsroomKeyResponse {
177        SourceNewsroomKeyResponse {
178            newsroom_verifying_key: self
179                .newsroom_keys
180                .as_ref()
181                .expect("Newsroom keys not found")
182                .verifying_key(),
183            fpf_sig: self
184                .signature
185                .as_ref()
186                .expect("FPF signature not found")
187                .clone(),
188        }
189    }
190
191    /// Handle source journalist key request (step 5)
192    pub fn handle_source_journalist_key_request<R: RngCore + CryptoRng>(
193        &mut self,
194        _request: SourceJournalistKeyRequest,
195        rng: &mut R,
196    ) -> Vec<SourceJournalistKeyResponse> {
197        let mut responses = Vec::new();
198
199        // Get all journalists and their ephemeral keys
200        let journalist_ephemeral_keys = self.storage.get_all_ephemeral_keys(rng);
201
202        for (journalist_id, ephemeral_bundle) in journalist_ephemeral_keys {
203            // Get the journalist's long-term keys
204            // TODO: Do something better than expect here
205            let (
206                signing_key,
207                fetching_key,
208                reply_apke_pk,
209                journalist_self_sig,
210                signed_pubkey_bytes,
211                newsroom_sig,
212            ) = self
213                .storage
214                .get_journalists()
215                .get(&journalist_id)
216                .expect("Journalist should exist in storage")
217                .clone();
218
219            let journo_public = JournalistPublicView::new(
220                signing_key,
221                fetching_key,
222                reply_apke_pk,
223                journalist_self_sig,
224                signed_pubkey_bytes,
225                ephemeral_bundle,
226            );
227
228            // Create response for this journalist
229            let response = SourceJournalistKeyResponse {
230                journalist: journo_public,
231                nr_signature: newsroom_sig,
232            };
233
234            responses.push(response);
235        }
236
237        responses
238    }
239
240    /// Handle message submission (step 6 for sources, step 9 for journalists)
241    pub fn handle_message_submit<R: RngCore + CryptoRng>(
242        &mut self,
243        message: Envelope,
244        rng: &mut R,
245    ) -> Result<Uuid, Error> {
246        // Generate a random message ID
247        let message_id = self.storage.deterministic_uuid(rng);
248
249        // Store the message with the generated ID
250        self.storage.add_message(message_id, message);
251
252        Ok(message_id)
253    }
254
255    /// Compute "hints"/challenges for message id fetch request (step 7)
256    pub fn handle_request_challenges<R: RngCore + CryptoRng>(
257        &self,
258        _request: MessageChallengeFetchRequest,
259        rng: &mut R,
260    ) -> Result<MessageChallengeFetchResponse, Error> {
261        let total_challenges: usize = primitives::MESSAGE_ID_FETCH_SIZE;
262        let store = self.storage.get_messages();
263        let chall = compute_fetch_challenges(rng, store, total_challenges);
264
265        Ok(MessageChallengeFetchResponse {
266            count: total_challenges,
267            messages: chall,
268        })
269    }
270
271    /// Handle message ID fetch request (step 7)
272    ///
273    /// TODO: Nothing here prevents someone from requesting messages
274    /// that aren't theirs? Should request messages have a signature?
275    // #[deprecated] // "use compute_fetch_challenges"
276    // pub fn handle_message_id_fetch<R: RngCore + CryptoRng>(
277    //     &self,
278    //     _request: MessageChallengeFetchRequest,
279    //     rng: &mut R,
280    // ) -> Result<MessageChallengeFetchResponse, Error> {
281    //     let messages = self.storage.get_messages();
282    //     let message_count = messages.len();
283    //     // Fixed response size to prevent traffic analysis
284
285    //     let mut q_entries = Vec::new();
286    //     let mut cid_entries = Vec::new();
287
288    //     // Process real messages
289    //     for (message_id, message) in messages.iter() {
290    //         let y = generate_random_scalar(rng).expect("Failed to generate random scalar");
291
292    //         // k_i = DH(Z_i, y)
293    //         let z_public_key = dh_public_key_from_scalar(
294    //             message.dh_share_z.clone().try_into().unwrap_or([0u8; 32]),
295    //         );
296    //         let k_i = dh_shared_secret(&z_public_key, y)?.into_bytes();
297
298    //         // Q_i = DH(X_i, y)
299    //         let x_public_key = dh_public_key_from_scalar(
300    //             message.dh_share_x.clone().try_into().unwrap_or([0u8; 32]),
301    //         );
302    //         let q_i = dh_shared_secret(&x_public_key, y)?.into_bytes();
303
304    //         // ID: cid_i = Enc(k_i, id_i)
305    //         let message_id_bytes = message_id.as_bytes().to_vec();
306    //         let cid_i =
307    //             encrypt_message_id(&k_i, &message_id_bytes).expect("Failed to encrypt message ID");
308
309    //         q_entries.push(q_i.to_vec());
310    //         cid_entries.push(cid_i);
311    //     }
312
313    //     // Fill remaining slots with random data
314    //     while q_entries.len() < MESSAGE_ID_FETCH_SIZE {
315    //         // Generate random Q_i that matches the structure of real Q_i
316    //         // Real Q_i = DH(X_i, y), so random Q_i should also be a DH shared secret
317    //         let random_y = generate_random_scalar(rng).expect("Failed to generate random scalar");
318    //         let random_x = generate_random_scalar(rng).expect("Failed to generate random scalar");
319    //         let random_x_pub = dh_public_key_from_scalar(random_x);
320    //         let random_q = dh_shared_secret(&random_x_pub, random_y)
321    //             .map_err(|_| anyhow!("failed to construct shared secret"))?
322    //             .into_bytes();
323
324    //         // Generate random cid by encrypting a random UUID
325    //         // This ensures indistinguishability from real cid_i
326    //         let random_uuid = Uuid::new_v4();
327    //         let random_key = generate_random_scalar(rng).expect("Failed to generate random key");
328    //         let random_cid =
329    //             crate::primitives::encrypt_message_id(&random_key, random_uuid.as_bytes())
330    //                 .expect("Failed to encrypt random UUID");
331
332    //         q_entries.push(random_q.to_vec());
333    //         cid_entries.push(random_cid);
334    //     }
335
336    //     // Shuffle the entries to hide which are real vs random
337    //     // Zip the arrays together, shuffle, then unzip
338    //     let mut pairs: Vec<_> = q_entries.into_iter().zip(cid_entries).collect();
339
340    //     let shuffled = Self::shuffle_not_for_prod(&mut pairs)
341    //         .expect("Need shuffled list")
342    //         .to_vec();
343
344    //     // Unzip back into separate arrays
345    //     let (q_entries, cid_entries): (Vec<_>, Vec<_>) = shuffled.into_iter().unzip();
346
347    //     Ok(MessageChallengeFetchResponse {
348    //         count: MESSAGE_ID_FETCH_SIZE,
349    //         messages: q_entries.into_iter().zip(cid_entries).collect(),
350    //     })
351    // }
352
353    // /// Shuffle challenges so that real and decoys are interspersed.
354    // /// Note: not a true random shuffle, toybox impl only
355    // pub fn shuffle_not_for_prod<'a>(
356    //     vec: &'a mut Vec<(Vec<u8>, Vec<u8>)>,
357    // ) -> Option<&'a mut [(Vec<u8>, Vec<u8>)]> {
358    //     if vec.is_empty() {
359    //         return None;
360    //     }
361
362    //     let len = vec.len();
363
364    //     // https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
365    //     for i in (0..len - 1).rev() {
366    //         // not for prod: modulo bias
367    //         let new_index = getrandom::u32().unwrap() as usize % i;
368    //         vec.swap(i, new_index);
369    //     }
370
371    //     Some(vec.as_mut_slice())
372    // }
373
374    /// Handle message fetch request (step 8/10)
375    pub fn handle_message_fetch(&self, request: MessageFetchRequest) -> Option<Envelope> {
376        self.storage
377            .get_messages()
378            .get(&request.message_id)
379            .cloned()
380    }
381}