securedrop_protocol_minimal/
ciphertext.rs1use crate::message::MessageCiphertext;
2use crate::metadata::MetadataCiphertext;
3use crate::primitives::provider::constants::LEN_KMID;
4use crate::primitives::ristretto255::{DH_PUBLIC_KEY_LEN, DHPublicKey};
5use crate::primitives::xwing::XWING_PUBLIC_KEY_LEN;
6use alloc::vec::Vec;
7use anyhow::Error;
8#[cfg(not(hax))]
9use serde::{Deserialize, Serialize};
10
11#[cfg(not(hax))]
13mod hex_array {
14 use alloc::string::String;
15 use serde::de::Error as _;
16 use serde::{Deserialize, Deserializer, Serializer};
17
18 pub fn serialize<const N: usize, S: Serializer>(
19 bytes: &[u8; N],
20 ser: S,
21 ) -> Result<S::Ok, S::Error> {
22 ser.serialize_str(&hex::encode(bytes))
23 }
24
25 pub fn deserialize<'de, const N: usize, D: Deserializer<'de>>(
26 de: D,
27 ) -> Result<[u8; N], D::Error> {
28 let s = String::deserialize(de)?;
29 let mut out = [0u8; N];
30 hex::decode_to_slice(s.trim(), &mut out).map_err(D::Error::custom)?;
31 Ok(out)
32 }
33}
34
35#[derive(Debug, Clone)]
43#[cfg_attr(not(hax), derive(Serialize, Deserialize))]
44pub struct Envelope {
45 pub(crate) ct_apke: MessageCiphertext,
47
48 pub(crate) ct_pke: MetadataCiphertext,
50
51 pub(crate) mgdh_pubkey: DHPublicKey,
53
54 pub(crate) mgdh: DHPublicKey,
56}
57
58impl Envelope {
59 pub fn size_hint(&self) -> usize {
61 self.ct_apke.len() + self.ct_pke.len()
62 }
63
64 pub fn cmessage_len(&self) -> usize {
65 self.ct_apke.len()
66 }
67
68 pub fn cmetadata_len(&self) -> usize {
70 self.ct_pke.len()
71 }
72}
73
74#[derive(Debug, Clone)]
75pub struct Plaintext {
77 pub sender_fetch_key: DHPublicKey,
79 pub sender_reply_pubkey_hybrid: [u8; XWING_PUBLIC_KEY_LEN],
81 pub msg: Vec<u8>,
83}
84
85impl Plaintext {
86 pub fn to_bytes(&self) -> alloc::vec::Vec<u8> {
87 let mut buf = Vec::new();
88
89 buf.extend_from_slice(&self.sender_fetch_key.into_bytes());
90 buf.extend_from_slice(&self.sender_reply_pubkey_hybrid);
91 buf.extend_from_slice(&self.msg);
92
93 buf
94 }
95
96 pub fn len(&self) -> usize {
97 DH_PUBLIC_KEY_LEN + XWING_PUBLIC_KEY_LEN + self.msg.len()
98 }
99
100 pub fn from_bytes(pt_bytes: &[u8]) -> Result<Self, Error> {
102 let mut offset = 0;
103
104 let mut fetch_key_bytes = [0u8; DH_PUBLIC_KEY_LEN];
105 fetch_key_bytes.copy_from_slice(&pt_bytes[offset..offset + DH_PUBLIC_KEY_LEN]);
106
107 let sender_fetch_key = DHPublicKey::decode(fetch_key_bytes)?;
108 offset += DH_PUBLIC_KEY_LEN;
109
110 let mut sender_reply_pubkey_hybrid = [0u8; XWING_PUBLIC_KEY_LEN];
111 sender_reply_pubkey_hybrid
112 .copy_from_slice(&pt_bytes[offset..offset + XWING_PUBLIC_KEY_LEN]);
113 offset += XWING_PUBLIC_KEY_LEN;
114
115 let msg = pt_bytes[offset..].to_vec();
116
117 Ok(Plaintext {
118 sender_fetch_key,
119 sender_reply_pubkey_hybrid,
120 msg,
121 })
122 }
123}
124
125#[derive(Clone, Debug)]
126#[cfg_attr(not(hax), derive(Serialize, Deserialize))]
127pub struct FetchResponse {
128 #[cfg_attr(not(hax), serde(with = "hex_array"))]
129 pub(crate) enc_id: [u8; LEN_KMID], pub(crate) pmgdh: DHPublicKey,
132}
133
134impl FetchResponse {
135 pub fn new(enc_id: [u8; LEN_KMID], pmgdh: DHPublicKey) -> Self {
136 Self { enc_id, pmgdh }
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use crate::metadata::LEN_METADATA_CIPHERTEXT;
144 use crate::primitives::dh_akem::DH_AKEM_ENCAPS_SECRET_LEN;
145 use crate::primitives::mlkem::LEN_MLKEM_SHAREDSECRET_ENCAPS;
146 use crate::primitives::ristretto255::random_dh_public_key;
147 use crate::primitives::xwing::LEN_XWING_SHAREDSECRET_ENCAPS;
148 use proptest::prelude::*;
149 use rand_chacha::ChaCha20Rng;
150 use rand_core::SeedableRng;
151
152 fn message_ct(c1: Vec<u8>, cp: Vec<u8>, c2: Vec<u8>) -> MessageCiphertext {
153 MessageCiphertext {
154 c1: c1.try_into().expect("c1 length"),
155 cp,
156 c2: c2.try_into().expect("c2 length"),
157 }
158 }
159
160 proptest! {
161 #[test]
162 fn test_message_ciphertext_byte_roundtrip(
163 c1 in prop::collection::vec(any::<u8>(), DH_AKEM_ENCAPS_SECRET_LEN),
164 cp in prop::collection::vec(any::<u8>(), 0..128),
165 c2 in prop::collection::vec(any::<u8>(), LEN_MLKEM_SHAREDSECRET_ENCAPS),
166 ) {
167 let ct = message_ct(c1, cp, c2);
168 let restored = MessageCiphertext::from_bytes(&ct.as_bytes()).expect("valid bytes");
169 prop_assert_eq!(ct.as_bytes(), restored.as_bytes());
170 }
171
172 #[test]
173 fn test_metadata_ciphertext_byte_roundtrip(
174 c in prop::collection::vec(any::<u8>(), LEN_XWING_SHAREDSECRET_ENCAPS),
175 cp in prop::collection::vec(any::<u8>(), LEN_METADATA_CIPHERTEXT)
176 .prop_map(|v| v.try_into().unwrap()),
177 ) {
178 let ct = MetadataCiphertext { c: c.try_into().expect("c length"), cp };
179 let restored = MetadataCiphertext::from_bytes(&ct.as_bytes()).expect("valid bytes");
180 prop_assert_eq!(ct.as_bytes(), restored.as_bytes());
181 }
182
183 #[test]
184 fn test_envelope_serde_roundtrip(
185 c1 in prop::collection::vec(any::<u8>(), DH_AKEM_ENCAPS_SECRET_LEN),
186 cp_a in prop::collection::vec(any::<u8>(), 0..128),
187 c2 in prop::collection::vec(any::<u8>(), LEN_MLKEM_SHAREDSECRET_ENCAPS),
188 c in prop::collection::vec(any::<u8>(), LEN_XWING_SHAREDSECRET_ENCAPS),
189 cp_b in prop::collection::vec(any::<u8>(), LEN_METADATA_CIPHERTEXT)
190 .prop_map(|v| v.try_into().unwrap()),
191 rng_seed: u64,
192 ) {
193 let mut rng = ChaCha20Rng::seed_from_u64(rng_seed);
195 let mgdh_pubkey = random_dh_public_key(&mut rng);
196 let mgdh = random_dh_public_key(&mut rng);
197
198 let env = Envelope {
199 ct_apke: message_ct(c1, cp_a, c2),
200 ct_pke: MetadataCiphertext { c: c.try_into().expect("c length"), cp: cp_b },
201 mgdh_pubkey,
202 mgdh,
203 };
204 let json = serde_json::to_string(&env).expect("serialize");
205 let restored: Envelope = serde_json::from_str(&json).expect("deserialize");
206 prop_assert_eq!(env.ct_apke.as_bytes(), restored.ct_apke.as_bytes());
207 prop_assert_eq!(env.ct_pke.as_bytes(), restored.ct_pke.as_bytes());
208 prop_assert_eq!(env.mgdh_pubkey.into_bytes(), restored.mgdh_pubkey.into_bytes());
209 prop_assert_eq!(env.mgdh.into_bytes(), restored.mgdh.into_bytes());
210 }
211
212 #[test]
213 fn test_fetch_challenge_response_serde_roundtrip(
214 enc_ids in prop::collection::vec(
215 prop::collection::vec(any::<u8>(), LEN_KMID), 0..6),
216 rng_seed: u64,
217 ) {
218 let mut rng = ChaCha20Rng::seed_from_u64(rng_seed);
220 let messages: Vec<FetchResponse> = enc_ids
221 .iter()
222 .map(|enc_id| FetchResponse {
223 enc_id: enc_id.clone().try_into().expect("enc_id length"),
224 pmgdh: random_dh_public_key(&mut rng),
225 })
226 .collect();
227 let n = messages.len();
228 let resp = crate::wire::core::MessageChallengeFetchResponse { count: n, messages };
229
230 let json = serde_json::to_string(&resp).expect("serialize");
231 let restored: crate::wire::core::MessageChallengeFetchResponse =
232 serde_json::from_str(&json).expect("deserialize");
233
234 prop_assert_eq!(restored.count, resp.count);
235 prop_assert_eq!(restored.messages.len(), resp.messages.len());
236 for (a, b) in resp.messages.iter().zip(restored.messages.iter()) {
237 prop_assert_eq!(a.enc_id, b.enc_id);
238 prop_assert_eq!(a.pmgdh.into_bytes(), b.pmgdh.into_bytes());
239 }
240 }
241 }
242}