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