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}