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