securedrop_protocol_minimal/wire/
core.rs1use crate::message::MessageCiphertext;
2use crate::message::MessagePublicKey;
3use crate::metadata::MetadataCiphertext;
4use crate::primitives::provider::constants::LEN_KMID;
5use crate::primitives::ristretto255::DHPublicKey;
6use crate::sign::{
7 FpfOnNewsroom, JournalistLongTermKey, NewsroomOnJournalist, Signature, VerifyingKey,
8};
9use crate::wire::bundle::{SignedKeyBundlePublic, SignedLongtermKeyBundle};
10use alloc::vec::Vec;
11use serde::{Deserialize, Serialize};
12use uuid::Uuid;
13
14#[cfg_attr(not(hax), derive(Serialize, Deserialize))]
21pub struct JournalistLongTermView {
22 pub vk: VerifyingKey,
23 pub signed_longterm_key_bundle: SignedLongtermKeyBundle,
24 pub nr_signature: Signature<NewsroomOnJournalist>,
25}
26
27#[cfg_attr(not(hax), derive(Serialize, Deserialize))]
31pub struct WelcomeBundle {
32 pub newsroom_verifying_key: VerifyingKey,
33 pub fpf_sig: Signature<FpfOnNewsroom>,
34 pub journalists: Vec<JournalistLongTermView>,
35}
36
37#[cfg_attr(not(hax), derive(Serialize, Deserialize))]
40pub struct JournalistEphemeralKeys {
41 pub vk: VerifyingKey,
42 pub ephemeral: SignedKeyBundlePublic,
43}
44
45pub struct MessageChallengeFetchRequest {}
49
50#[cfg_attr(not(hax), derive(Serialize, Deserialize))]
54pub struct MessageChallengeFetchResponse {
55 pub count: usize,
58 pub messages: Vec<FetchResponse>,
60}
61
62pub struct MessageFetchRequest {
66 pub message_id: Uuid,
68}
69
70#[cfg(not(hax))]
72mod hex_array {
73 use alloc::string::String;
74 use serde::de::Error as _;
75 use serde::{Deserialize, Deserializer, Serializer};
76
77 pub fn serialize<const N: usize, S: Serializer>(
78 bytes: &[u8; N],
79 ser: S,
80 ) -> Result<S::Ok, S::Error> {
81 ser.serialize_str(&hex::encode(bytes))
82 }
83
84 pub fn deserialize<'de, const N: usize, D: Deserializer<'de>>(
85 de: D,
86 ) -> Result<[u8; N], D::Error> {
87 let s = String::deserialize(de)?;
88 let mut out = [0u8; N];
89 hex::decode_to_slice(s.trim(), &mut out).map_err(D::Error::custom)?;
90 Ok(out)
91 }
92}
93
94#[derive(Clone, Debug)]
95#[cfg_attr(not(hax), derive(Serialize, Deserialize))]
96pub struct FetchResponse {
97 #[cfg_attr(not(hax), serde(with = "hex_array"))]
98 pub(crate) enc_id: [u8; LEN_KMID], pub(crate) pmgdh: DHPublicKey,
101}
102
103impl FetchResponse {
104 pub fn new(enc_id: [u8; LEN_KMID], pmgdh: DHPublicKey) -> Self {
105 Self { enc_id, pmgdh }
106 }
107}
108
109#[derive(Debug, Clone)]
117#[cfg_attr(not(hax), derive(Serialize, Deserialize))]
118pub struct Envelope {
119 pub(crate) ct_apke: MessageCiphertext,
121
122 pub(crate) ct_pke: MetadataCiphertext,
124
125 pub(crate) mgdh_pubkey: DHPublicKey,
127
128 pub(crate) mgdh: DHPublicKey,
130}
131
132impl Envelope {
133 #[cfg_attr(hax, hax_lib::fstar::verification_status(lax))]
137 pub fn size_hint(&self) -> usize {
138 self.ct_apke.len() + self.ct_pke.len()
139 }
140
141 pub fn cmessage_len(&self) -> usize {
142 self.ct_apke.len()
143 }
144
145 pub fn cmetadata_len(&self) -> usize {
147 self.ct_pke.len()
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use crate::metadata::LEN_METADATA_CIPHERTEXT;
155 use crate::primitives::dh_akem::DH_AKEM_ENCAPS_SECRET_LEN;
156 use crate::primitives::mlkem::LEN_MLKEM_SHAREDSECRET_ENCAPS;
157 use crate::primitives::ristretto255::random_dh_public_key;
158 use crate::primitives::xwing::LEN_XWING_SHAREDSECRET_ENCAPS;
159 use proptest::prelude::*;
160 use rand_chacha::ChaCha20Rng;
161 use rand_core::SeedableRng;
162
163 fn message_ct(c1: Vec<u8>, cp: Vec<u8>, c2: Vec<u8>) -> MessageCiphertext {
164 MessageCiphertext {
165 c1: c1.try_into().expect("c1 length"),
166 cp,
167 c2: c2.try_into().expect("c2 length"),
168 }
169 }
170
171 proptest! {
172 #[test]
173 fn test_envelope_serde_roundtrip(
174 c1 in prop::collection::vec(any::<u8>(), DH_AKEM_ENCAPS_SECRET_LEN),
175 cp_a in prop::collection::vec(any::<u8>(), 0..128),
176 c2 in prop::collection::vec(any::<u8>(), LEN_MLKEM_SHAREDSECRET_ENCAPS),
177 c in prop::collection::vec(any::<u8>(), LEN_XWING_SHAREDSECRET_ENCAPS),
178 cp_b in prop::collection::vec(any::<u8>(), LEN_METADATA_CIPHERTEXT)
179 .prop_map(|v| v.try_into().unwrap()),
180 rng_seed: u64,
181 ) {
182 let mut rng = ChaCha20Rng::seed_from_u64(rng_seed);
184 let mgdh_pubkey = random_dh_public_key(&mut rng);
185 let mgdh = random_dh_public_key(&mut rng);
186
187 let env = Envelope {
188 ct_apke: message_ct(c1, cp_a, c2),
189 ct_pke: MetadataCiphertext { c: c.try_into().expect("c length"), cp: cp_b },
190 mgdh_pubkey,
191 mgdh,
192 };
193 let json = serde_json::to_string(&env).expect("serialize");
194 let restored: Envelope = serde_json::from_str(&json).expect("deserialize");
195 prop_assert_eq!(env.ct_apke.as_bytes(), restored.ct_apke.as_bytes());
196 prop_assert_eq!(env.ct_pke.as_bytes(), restored.ct_pke.as_bytes());
197 prop_assert_eq!(env.mgdh_pubkey.into_bytes(), restored.mgdh_pubkey.into_bytes());
198 prop_assert_eq!(env.mgdh.into_bytes(), restored.mgdh.into_bytes());
199 }
200
201 #[test]
202 fn test_fetch_challenge_response_serde_roundtrip(
203 enc_ids in prop::collection::vec(
204 prop::collection::vec(any::<u8>(), LEN_KMID), 0..6),
205 rng_seed: u64,
206 ) {
207 let mut rng = ChaCha20Rng::seed_from_u64(rng_seed);
209 let messages: Vec<FetchResponse> = enc_ids
210 .iter()
211 .map(|enc_id| FetchResponse {
212 enc_id: enc_id.clone().try_into().expect("enc_id length"),
213 pmgdh: random_dh_public_key(&mut rng),
214 })
215 .collect();
216 let n = messages.len();
217 let resp = crate::wire::core::MessageChallengeFetchResponse { count: n, messages };
218
219 let json = serde_json::to_string(&resp).expect("serialize");
220 let restored: crate::wire::core::MessageChallengeFetchResponse =
221 serde_json::from_str(&json).expect("deserialize");
222
223 prop_assert_eq!(restored.count, resp.count);
224 prop_assert_eq!(restored.messages.len(), resp.messages.len());
225 for (a, b) in resp.messages.iter().zip(restored.messages.iter()) {
226 prop_assert_eq!(a.enc_id, b.enc_id);
227 prop_assert_eq!(a.pmgdh.into_bytes(), b.pmgdh.into_bytes());
228 }
229 }
230 }
231}