Skip to main content

securedrop_protocol_minimal/
source.rs

1use 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::vec::Vec;
9use argon2::{Algorithm, Argon2, Params, Version};
10use rand_core::{CryptoRng, RngCore};
11
12use crate::ciphertext::Plaintext;
13use crate::keys::*;
14use crate::primitives::x25519::DH_PUBLIC_KEY_LEN;
15use crate::primitives::xwing::XWING_PUBLIC_KEY_LEN;
16use crate::traits::{UserPublic, UserSecret};
17
18// do not re-export!
19use crate::sealed;
20impl sealed::Sealed for Source {}
21
22/// Fixed public salt for Argon2id. Argon2id requires a salt; since source
23/// keys must be deterministic from the passphrase alone, we use a fixed
24/// application-specific value rather than a random one.
25const SOURCE_PBKDF_SALT: &[u8] = b"securedrop-source-v1";
26
27/// A source and their long-term key material (step 4).
28///
29/// A source's keys are fully determined by their passphrase: the fetch key,
30/// APKE key, and PKE key are all derived from a master key via Argon2id and
31/// a domain-separated KDF. Returning sources reconstruct the same keys by
32/// calling [`Source::from_passphrase`] with the same passphrase.
33pub struct Source {
34    fetch_key: DhFetchKeyPair,
35    message_keys: MessageKeyBundle,
36    passphrase: Vec<u8>,
37    session: SessionStorage,
38}
39
40impl core::fmt::Debug for Source {
41    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
42        // Using non-exhaustive to avoid leaking source keys.
43        f.debug_struct("Source").finish_non_exhaustive()
44    }
45}
46
47/// The public key material of a source, used by journalists to send replies.
48#[derive(Debug, Clone)]
49pub struct SourcePublicView {
50    fetch_pk: DHPublicKey,
51    apke_pk: MessagePublicKey,
52    message_pks: KeyBundlePublic,
53}
54
55impl UserPublic for SourcePublicView {
56    fn fetch_pk(&self) -> &DHPublicKey {
57        &self.fetch_pk
58    }
59
60    fn message_auth_pk(&self) -> &MessagePublicKey {
61        &self.apke_pk
62    }
63
64    fn message_metadata_pk(&self) -> &MetadataPublicKey {
65        &self.message_pks.metadata_pk
66    }
67
68    fn message_enc_pk(&self) -> &MessagePublicKey {
69        &self.message_pks.apke_pk
70    }
71}
72
73impl Client for Source {
74    fn newsroom_verifying_key(&self) -> Option<&VerifyingKey> {
75        self.session.nr_key.as_ref()
76    }
77
78    fn set_newsroom_verifying_key(&mut self, key: VerifyingKey) {
79        self.session.nr_key = Some(key);
80    }
81}
82
83/// Private, common to all users, implemented for sources
84impl UserSecret for Source {
85    fn num_bundles(&self) -> usize {
86        1
87    }
88
89    fn fetch_keypair(&self) -> (&DHPrivateKey, &DHPublicKey) {
90        (&self.fetch_key.sk, &self.fetch_key.pk)
91    }
92
93    fn message_auth_key(&self) -> &crate::message::MessagePrivateKey {
94        self.message_keys.apke.private_key()
95    }
96
97    fn message_auth_pk(&self) -> &crate::message::MessagePublicKey {
98        self.message_keys.apke.public_key()
99    }
100
101    fn build_message(&self, message: Vec<u8>) -> Plaintext {
102        let mut fetch_pk = [0u8; DH_PUBLIC_KEY_LEN];
103        fetch_pk.copy_from_slice(&self.fetch_key.pk.into_bytes());
104
105        let mut reply_key_pq_hybrid = [0u8; XWING_PUBLIC_KEY_LEN];
106        reply_key_pq_hybrid.copy_from_slice(self.message_keys.metadata_kp.public_key().as_bytes());
107
108        Plaintext {
109            sender_fetch_key: fetch_pk,
110            sender_reply_pubkey_hybrid: reply_key_pq_hybrid,
111            msg: message,
112        }
113    }
114
115    fn keybundles(&self) -> Vec<&MessageKeyBundle> {
116        alloc::vec![&self.message_keys]
117    }
118}
119
120impl Source {
121    /// Create a new source with a randomly generated passphrase.
122    ///
123    /// TODO / For testing only - in production the passphrase must be a mnemonic
124    /// of sufficient entropy generated and displayed to the source.
125    pub fn new<R: RngCore + CryptoRng>(mut rng: R) -> Self {
126        let mut passphrase = [0u8; 32];
127        rng.fill_bytes(&mut passphrase);
128        Self::from_passphrase(&passphrase)
129    }
130
131    /// Returns the source's passphrase.
132    ///
133    /// # Security
134    ///
135    /// The passphrase is the root secret from which all source keys are
136    /// derived. It MUST be stored and transmitted only over secure channels.
137    pub fn passphrase(&self) -> &[u8] {
138        &self.passphrase
139    }
140
141    /// Derive the master key from a passphrase using Argon2id (step 4).
142    ///
143    /// Uses a fixed, public, domain-specific salt. The security of the master
144    /// key rests entirely on the entropy of the passphrase.
145    fn derive_master_key(passphrase: &[u8]) -> [u8; 64] {
146        // OWASP minimum recommended parameters for Argon2id from here:
147        // https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id
148        let params = Params::new(19456, 2, 1, Some(64)).expect("valid Argon2id params");
149        let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
150
151        let mut mk = [0u8; 64];
152        argon2
153            .hash_password_into(passphrase, SOURCE_PBKDF_SALT, &mut mk)
154            .expect("Argon2id master key derivation failed");
155        mk
156    }
157
158    /// Reconstruct source keys from a passphrase (step 4).
159    ///
160    /// Derives a master key via [`Source::derive_master_key`], then derives
161    /// each private key from the master key using a domain-separated KDF.
162    pub fn from_passphrase(passphrase: &[u8]) -> Self {
163        use blake2::{Blake2b, Digest};
164
165        let mk = Self::derive_master_key(passphrase);
166
167        let mut fetch_hasher = Blake2b::<blake2::digest::typenum::U32>::new();
168        fetch_hasher.update(b"sourcefetchkey");
169        fetch_hasher.update(mk);
170        let fetch_result = fetch_hasher.finalize();
171
172        // sk_S^APKE is a hybrid key requiring two sub-derivations:
173        // the DH-AKEM and ML-KEM components are each derived with their own
174        // label under the "sourceAPKEkey" namespace.
175        let mut dh_hasher = Blake2b::<blake2::digest::typenum::U32>::new();
176        dh_hasher.update(b"sourceAPKEkey-dh");
177        dh_hasher.update(mk);
178        let dh_result = dh_hasher.finalize();
179
180        let mut kem_hasher = Blake2b::<blake2::digest::typenum::U64>::new();
181        kem_hasher.update(b"sourceAPKEkey-mlkem");
182        kem_hasher.update(mk);
183        let kem_result = kem_hasher.finalize();
184
185        let mut pke_hasher = Blake2b::<blake2::digest::typenum::U32>::new();
186        pke_hasher.update(b"sourcePKEkey");
187        pke_hasher.update(mk);
188        let pke_result = pke_hasher.finalize();
189
190        // Create key pairs
191        let (fetch_sk, fetch_pk): (DHPrivateKey, DHPublicKey) =
192            deterministic_dh_keygen(fetch_result.into()).expect("Need Fetch keygen");
193
194        let message_kp = kgen_deterministic_message(dh_result.into(), kem_result.into())
195            .expect("Need SD-APKE keygen");
196
197        let metadata_kp =
198            kgen_deterministic_metadata(pke_result.into()).expect("Need X-Wing keygen");
199
200        let session = SessionStorage {
201            fpf_key: None,
202            nr_key: None,
203            fpf_signature: None,
204        };
205
206        Self {
207            fetch_key: KeyPair {
208                sk: fetch_sk,
209                pk: fetch_pk,
210            },
211            message_keys: MessageKeyBundle::new(message_kp, metadata_kp),
212            passphrase: passphrase.to_vec(),
213            session,
214        }
215    }
216
217    /// Returns the public key material for this source.
218    pub fn public(&self) -> SourcePublicView {
219        SourcePublicView {
220            fetch_pk: self.fetch_key.pk,
221            apke_pk: self.message_keys.apke.public_key().clone(),
222            message_pks: self.message_keys.public(),
223        }
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230    use crate::primitives::xwing::XWING_PRIVATE_KEY_LEN;
231    use rand_chacha::ChaCha20Rng;
232    use rand_core::SeedableRng;
233
234    #[test]
235    fn test_initialize_with_passphrase() {
236        // Fixed seed RNG
237        let mut rng = ChaCha20Rng::seed_from_u64(666);
238
239        let mut passphrase_bytes: [u8; 32] = [0u8; 32];
240        let _ = &rng.fill_bytes(&mut passphrase_bytes);
241
242        let source1 = Source::from_passphrase(&passphrase_bytes.clone());
243        let source2 = Source::from_passphrase(&passphrase_bytes);
244
245        assert_eq!(
246            source1.passphrase, source2.passphrase,
247            "Expected identical passphrase"
248        );
249
250        // SD-APKE keys (pk^APKE = (pk1, pk2))
251        assert_eq!(
252            source1.message_keys.apke.public_key().as_bytes(),
253            source2.message_keys.apke.public_key().as_bytes(),
254            "SD-APKE public key should be identical"
255        );
256
257        // Metadata keys
258        assert_eq!(
259            source1.message_keys.metadata_kp.public_key().as_bytes(),
260            source2.message_keys.metadata_kp.public_key().as_bytes(),
261            "XWING Encaps Key should be identical"
262        );
263        assert_eq!(
264            source1.message_keys.metadata_kp.private_key().as_bytes(),
265            source2.message_keys.metadata_kp.private_key().as_bytes(),
266            "XWING Decaps Key should be identical"
267        );
268        assert_ne!(
269            source1.message_keys.metadata_kp.private_key().as_bytes(),
270            &[0u8; XWING_PRIVATE_KEY_LEN]
271        );
272    }
273}