securedrop_protocol_minimal/
source.rs1use crate::VerifyingKey;
2use crate::api::Client;
3use crate::message::{MessagePublicKey, deterministic_keygen as kgen_deterministic_message};
4use crate::metadata::{MetadataPublicKey, deterministic_keygen as kgen_deterministic_metadata};
5use crate::primitives::ristretto255::{DHPrivateKey, DHPublicKey, deterministic_dh_keygen};
6use alloc::string::{String, ToString};
7use alloc::vec::Vec;
8use anyhow::Error;
9use bip39::{Language, Mnemonic};
10use rand_core::{CryptoRng, RngCore};
11
12use crate::ciphertext::Plaintext;
13use crate::keys::*;
14use crate::primitives::provider::hkdf;
15use crate::primitives::ristretto255::DH_SEED_LEN;
16use crate::primitives::xwing::XWING_PUBLIC_KEY_LEN;
17use crate::traits::{UserPublic, UserSecret};
18
19use crate::sealed;
21
22#[cfg(not(hax))]
23impl sealed::Sealed for Source {}
24
25const SOURCE_KDF_SALT: &[u8] = b"securedrop-source-v1";
27
28pub struct Source {
36 fetch_key: DhFetchKeyPair,
37 message_keys: MessageKeyBundle,
38 passphrase: String,
39 session: SessionStorage,
40}
41
42#[cfg_attr(hax, hax_lib::exclude)]
45impl core::fmt::Debug for Source {
46 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
47 f.debug_struct("Source").finish_non_exhaustive()
49 }
50}
51
52#[derive(Debug, Clone)]
54pub struct SourcePublicView {
55 fetch_pk: DHPublicKey,
56 apke_pk: MessagePublicKey,
57 message_pks: KeyBundlePublic,
58}
59
60impl UserPublic for SourcePublicView {
61 fn fetch_pk(&self) -> &DHPublicKey {
62 &self.fetch_pk
63 }
64
65 fn message_auth_pk(&self) -> &MessagePublicKey {
66 &self.apke_pk
67 }
68
69 fn message_metadata_pk(&self) -> &MetadataPublicKey {
70 &self.message_pks.metadata_pk
71 }
72
73 fn message_enc_pk(&self) -> &MessagePublicKey {
74 &self.message_pks.apke_pk
75 }
76}
77
78impl Client for Source {
79 fn newsroom_verifying_key(&self) -> Option<&VerifyingKey> {
80 self.session.nr_key.as_ref()
81 }
82
83 fn set_newsroom_verifying_key(&mut self, key: VerifyingKey) {
84 self.session.nr_key = Some(key);
85 }
86}
87
88impl UserSecret for Source {
90 fn num_bundles(&self) -> usize {
91 1
92 }
93
94 fn fetch_keypair(&self) -> (&DHPrivateKey, &DHPublicKey) {
95 (&self.fetch_key.sk, &self.fetch_key.pk)
96 }
97
98 fn message_auth_key(&self) -> &crate::message::MessagePrivateKey {
99 self.message_keys.apke.private_key()
100 }
101
102 fn own_message_auth_pk(&self) -> &crate::message::MessagePublicKey {
103 self.message_keys.apke.public_key()
104 }
105
106 fn build_message(&self, message: Vec<u8>) -> Plaintext {
107 let mut reply_key_pq_hybrid = [0u8; XWING_PUBLIC_KEY_LEN];
108 reply_key_pq_hybrid.copy_from_slice(self.message_keys.metadata_kp.public_key().as_bytes());
109
110 Plaintext {
111 sender_fetch_key: self.fetch_key.pk,
112 sender_reply_pubkey_hybrid: reply_key_pq_hybrid,
113 msg: message,
114 }
115 }
116
117 fn keybundles(&self) -> Vec<&MessageKeyBundle> {
118 alloc::vec![&self.message_keys]
119 }
120}
121
122impl Source {
123 #[cfg_attr(hax, hax_lib::opaque)]
125 pub fn new<R: RngCore + CryptoRng>(mut rng: R) -> Self {
126 let mut entropy = [0u8; 16];
127 rng.fill_bytes(&mut entropy);
128 let mnemonic = Mnemonic::from_entropy(&entropy).expect("16 bytes is valid BIP39 entropy");
129 Self::from_master_key(&entropy, mnemonic.to_string())
130 }
131
132 #[cfg_attr(hax, hax_lib::opaque)]
139 pub fn passphrase(&self) -> &str {
140 &self.passphrase
141 }
142
143 #[cfg_attr(hax, hax_lib::opaque)]
151 pub fn from_passphrase(passphrase: &str) -> Result<Self, Error> {
152 let mnemonic = Mnemonic::parse_in(Language::English, passphrase)
153 .map_err(|e| anyhow::anyhow!("invalid BIP39 mnemonic: {e}"))?;
154
155 let (entropy, len) = mnemonic.to_entropy_array();
156 if len != 16 {
157 return Err(anyhow::anyhow!(
158 "source passphrase must be a 12-word BIP39 mnemonic (128-bit entropy)"
159 ));
160 }
161 let mut mk = [0u8; 16];
162 mk.copy_from_slice(&entropy[..16]);
163
164 Ok(Self::from_master_key(&mk, mnemonic.to_string()))
165 }
166
167 #[cfg_attr(hax, hax_lib::opaque)]
173 fn from_master_key(mk: &[u8; 16], passphrase: String) -> Self {
174 let mut fetch_seed = [0u8; DH_SEED_LEN];
176 hkdf::sha256(&mut fetch_seed, SOURCE_KDF_SALT, mk, b"sourcefetchkey")
177 .expect("HKDF fetch key derivation failed");
178
179 let mut dh_seed = [0u8; 32];
183 hkdf::sha256(&mut dh_seed, SOURCE_KDF_SALT, mk, b"sourceAPKEkey-dh")
184 .expect("HKDF APKE DH key derivation failed");
185
186 let mut mlkem_seed = [0u8; 64];
187 hkdf::sha256(&mut mlkem_seed, SOURCE_KDF_SALT, mk, b"sourceAPKEkey-mlkem")
188 .expect("HKDF APKE ML-KEM key derivation failed");
189
190 let mut pke_seed = [0u8; 32];
191 hkdf::sha256(&mut pke_seed, SOURCE_KDF_SALT, mk, b"sourcePKEkey")
192 .expect("HKDF PKE key derivation failed");
193
194 let (fetch_sk, fetch_pk): (DHPrivateKey, DHPublicKey) = deterministic_dh_keygen(fetch_seed);
196
197 let message_kp =
198 kgen_deterministic_message(dh_seed, mlkem_seed).expect("Need SD-APKE keygen");
199
200 let metadata_kp = kgen_deterministic_metadata(pke_seed).expect("Need X-Wing keygen");
201
202 let session = SessionStorage {
203 fpf_key: None,
204 nr_key: None,
205 fpf_signature: None,
206 };
207
208 Self {
209 fetch_key: KeyPair {
210 sk: fetch_sk,
211 pk: fetch_pk,
212 },
213 message_keys: MessageKeyBundle::new(message_kp, metadata_kp),
214 passphrase,
215 session,
216 }
217 }
218
219 pub fn public(&self) -> SourcePublicView {
221 SourcePublicView {
222 fetch_pk: self.fetch_key.pk,
223 apke_pk: self.message_keys.apke.public_key().clone(),
224 message_pks: self.message_keys.public(),
225 }
226 }
227}
228
229impl SourcePublicView {
230 pub fn from_reply_keys(
233 fetch_pk: DHPublicKey,
234 apke: MessagePublicKey,
235 metadata_pk: MetadataPublicKey,
236 ) -> Self {
237 SourcePublicView {
238 fetch_pk,
239 apke_pk: apke.clone(),
240 message_pks: KeyBundlePublic {
241 apke_pk: apke,
242 metadata_pk,
243 },
244 }
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251 use crate::primitives::xwing::XWING_PRIVATE_KEY_LEN;
252 use proptest::prelude::*;
253 use rand_chacha::ChaCha20Rng;
254 use rand_core::{Rng, SeedableRng};
255
256 const TEST_MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
258
259 #[test]
260 fn test_initialize_with_passphrase() {
261 let source1 = Source::from_passphrase(TEST_MNEMONIC).expect("valid mnemonic");
262 let source2 = Source::from_passphrase(TEST_MNEMONIC).expect("valid mnemonic");
263
264 assert_eq!(
265 source1.passphrase, source2.passphrase,
266 "Expected identical passphrase"
267 );
268
269 assert_eq!(
271 source1.message_keys.apke.public_key().as_bytes(),
272 source2.message_keys.apke.public_key().as_bytes(),
273 "SD-APKE public key should be identical"
274 );
275
276 assert_eq!(
278 source1.message_keys.metadata_kp.public_key().as_bytes(),
279 source2.message_keys.metadata_kp.public_key().as_bytes(),
280 "XWING Encaps Key should be identical"
281 );
282 assert_eq!(
283 source1.message_keys.metadata_kp.private_key().as_bytes(),
284 source2.message_keys.metadata_kp.private_key().as_bytes(),
285 "XWING Decaps Key should be identical"
286 );
287 assert_ne!(
288 source1.message_keys.metadata_kp.private_key().as_bytes(),
289 &[0u8; XWING_PRIVATE_KEY_LEN]
290 );
291 }
292
293 proptest! {
294 #[test]
295 fn test_new_source_roundtrips_through_passphrase(seed in any::<u64>()) {
296 let source = Source::new(ChaCha20Rng::seed_from_u64(seed));
297
298 let restored = Source::from_passphrase(source.passphrase())
299 .expect("generated mnemonic is valid");
300
301 prop_assert_eq!(source.passphrase(), restored.passphrase());
302 prop_assert_eq!(
303 source.message_keys.apke.public_key().as_bytes(),
304 restored.message_keys.apke.public_key().as_bytes(),
305 );
306 }
307 }
308
309 #[test]
310 fn test_invalid_mnemonic_is_rejected() {
311 let bad_checksum = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon";
312 assert!(Source::from_passphrase(bad_checksum).is_err());
313
314 assert!(Source::from_passphrase("hello world").is_err());
315 }
316}