Skip to main content

securedrop_protocol_minimal/
primitives.rs

1use alloc::vec::Vec;
2use anyhow::Error;
3use rand_core::{CryptoRng, RngCore};
4
5pub(crate) mod dh_akem;
6pub(crate) mod mlkem;
7pub mod pad;
8pub(crate) mod provider;
9pub mod ristretto255;
10pub(crate) mod xwing;
11
12use provider::chacha20poly1305::KEY_LEN;
13
14use crate::primitives::ristretto255::DHPublicKey;
15
16/// Fixed number of message ID entries to return in privacy-preserving fetch
17///
18/// This prevents traffic analysis by always returning the same number of entries,
19/// regardless of how many actual messages exist.
20pub const MESSAGE_ID_FETCH_SIZE: usize = 10;
21
22/// Fixed, public salt for challenge id encryption.
23const CHALLENGE_SALT: &[u8] = b"securedrop-challenge-v1";
24
25/// Derive symmetric key used for ChaCha20-Poly1305 challenge encryption
26///
27/// This is used in step 7 for encrypting message IDs with a shared secret
28///
29pub fn derive_challenge_key(
30    shared_secret: &DHPublicKey,
31    newsroom_id: &[u8],
32) -> Result<[u8; KEY_LEN], Error> {
33    use crate::primitives::provider::hkdf;
34
35    let mut challenge_key: [u8; KEY_LEN] = [0u8; KEY_LEN];
36    // Key is KDF(shared_secret, newsroom_id)
37    hkdf::sha256(
38        &mut challenge_key,
39        CHALLENGE_SALT,
40        &shared_secret.into_bytes(),
41        newsroom_id,
42    )
43    .map_err(|_| anyhow::anyhow!("HKDF challenge key derivation failed"))?;
44
45    Ok(challenge_key)
46}
47
48/// Symmetric encryption for message IDs using ChaCha20-Poly1305
49///
50/// This is used in step 7 for encrypting message IDs with a shared secret
51///
52#[cfg_attr(hax, hax_lib::requires(
53    message_id.len()
54        <= usize::MAX
55            - provider::chacha20poly1305::NONCE_LEN
56            - provider::chacha20poly1305::TAG_LEN
57))]
58pub fn encrypt_message_id(key: &[u8; KEY_LEN], message_id: &[u8]) -> Result<Vec<u8>, Error> {
59    use provider::chacha20poly1305::{NONCE_LEN, TAG_LEN};
60
61    // Use a zero-filled nonce
62    let nonce = [0u8; NONCE_LEN];
63
64    // Prepare output buffer: ciphertext + tag
65    let mut output = alloc::vec::Vec::new();
66    let mut ciphertext = alloc::vec![0u8; message_id.len() + TAG_LEN];
67
68    // Encrypt the message ID
69    match provider::chacha20poly1305::encrypt(key, message_id, &mut ciphertext, &[], &nonce) {
70        Ok(_) => {}
71        Err(e) => {
72            return Err(anyhow::anyhow!(
73                "ChaCha20-Poly1305 encryption failed: {:?}",
74                e
75            ));
76        }
77    }
78    output.extend_from_slice(&ciphertext);
79    Ok(output)
80}
81
82/// Symmetric decryption for message IDs using ChaCha20-Poly1305
83///
84/// This is used in step 7 for decrypting message IDs with a shared secret
85pub fn decrypt_message_id(key: &[u8; KEY_LEN], encrypted_data: &[u8]) -> Result<Vec<u8>, Error> {
86    use provider::chacha20poly1305::{NONCE_LEN, TAG_LEN};
87
88    if encrypted_data.len() < TAG_LEN {
89        return Err(anyhow::anyhow!("Encrypted data too short"));
90    }
91
92    // Decrypt ciphertext with zero-filled nonce
93    let nonce = [0u8; NONCE_LEN];
94
95    // Prepare output buffer
96    let mut plaintext = alloc::vec![0u8; encrypted_data.len() - TAG_LEN];
97
98    // Decrypt the message ID
99    provider::chacha20poly1305::decrypt(
100        key,
101        &mut plaintext,
102        encrypted_data,
103        &[], // empty AAD
104        &nonce,
105    )
106    .map_err(|e| anyhow::anyhow!("ChaCha20-Poly1305 decryption failed: {:?}", e))?;
107
108    Ok(plaintext)
109}