Skip to main content

securedrop_protocol_minimal/schemes/
challenge.rs

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