securedrop_protocol_minimal/
ciphertext.rs1use crate::message::MessageCiphertext;
2use crate::metadata::MetadataCiphertext;
3use crate::primitives::provider::constants::LEN_KMID;
4use crate::primitives::x25519::{DH_PUBLIC_KEY_LEN, DH_SHARED_SECRET_LEN};
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 #[cfg_attr(not(hax), serde(with = "hex_array"))]
53 pub(crate) mgdh_pubkey: [u8; DH_PUBLIC_KEY_LEN],
54
55 #[cfg_attr(not(hax), serde(with = "hex_array"))]
57 pub(crate) mgdh: [u8; DH_PUBLIC_KEY_LEN],
58}
59
60impl Envelope {
61 pub fn size_hint(&self) -> usize {
63 self.ct_apke.len() + self.ct_pke.len()
64 }
65
66 pub fn cmessage_len(&self) -> usize {
67 self.ct_apke.len()
68 }
69
70 pub fn cmetadata_len(&self) -> usize {
72 self.ct_pke.len()
73 }
74}
75
76#[derive(Debug, Clone)]
77pub struct Plaintext {
79 pub sender_reply_pubkey_hybrid: [u8; XWING_PUBLIC_KEY_LEN],
81 pub sender_fetch_key: [u8; DH_PUBLIC_KEY_LEN],
83 pub msg: Vec<u8>,
85}
86
87impl Plaintext {
88 pub fn to_bytes(&self) -> alloc::vec::Vec<u8> {
89 let mut buf = Vec::new();
91
92 buf.extend_from_slice(&self.sender_reply_pubkey_hybrid);
93 buf.extend_from_slice(&self.sender_fetch_key);
94 buf.extend_from_slice(&self.msg);
95
96 buf
97 }
98
99 pub fn len(&self) -> usize {
100 XWING_PUBLIC_KEY_LEN + DH_PUBLIC_KEY_LEN + self.msg.len()
101 }
102
103 pub fn from_bytes(pt_bytes: &[u8]) -> Result<Self, Error> {
105 let mut offset = 0;
106
107 let mut sender_reply_pubkey_hybrid = [0u8; XWING_PUBLIC_KEY_LEN];
108 sender_reply_pubkey_hybrid
109 .copy_from_slice(&pt_bytes[offset..offset + XWING_PUBLIC_KEY_LEN]);
110 offset += XWING_PUBLIC_KEY_LEN;
111
112 let mut sender_fetch_key = [0u8; DH_PUBLIC_KEY_LEN];
113 sender_fetch_key.copy_from_slice(&pt_bytes[offset..offset + DH_PUBLIC_KEY_LEN]);
114 offset += DH_PUBLIC_KEY_LEN;
115
116 let msg = pt_bytes[offset..].to_vec();
117
118 Ok(Plaintext {
119 sender_reply_pubkey_hybrid,
120 sender_fetch_key,
121 msg,
122 })
123 }
124}
125
126#[derive(Clone, Debug)]
127#[cfg_attr(not(hax), derive(Serialize, Deserialize))]
128pub struct FetchResponse {
129 #[cfg_attr(not(hax), serde(with = "hex_array"))]
130 pub(crate) enc_id: [u8; LEN_KMID], #[cfg_attr(not(hax), serde(with = "hex_array"))]
132 pub(crate) pmgdh: [u8; DH_SHARED_SECRET_LEN], }
134
135impl FetchResponse {
136 pub fn new(enc_id: [u8; LEN_KMID], pmgdh: [u8; DH_SHARED_SECRET_LEN]) -> Self {
137 Self { enc_id, pmgdh }
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use crate::metadata::LEN_METADATA_CIPHERTEXT;
145 use crate::primitives::dh_akem::DH_AKEM_ENCAPS_SECRET_LEN;
146 use crate::primitives::mlkem::LEN_MLKEM_SHAREDSECRET_ENCAPS;
147 use crate::primitives::xwing::LEN_XWING_SHAREDSECRET_ENCAPS;
148 use proptest::prelude::*;
149
150 fn message_ct(c1: Vec<u8>, cp: Vec<u8>, c2: Vec<u8>) -> MessageCiphertext {
151 MessageCiphertext {
152 c1: c1.try_into().expect("c1 length"),
153 cp,
154 c2: c2.try_into().expect("c2 length"),
155 }
156 }
157
158 proptest! {
159 #[test]
160 fn test_message_ciphertext_byte_roundtrip(
161 c1 in prop::collection::vec(any::<u8>(), DH_AKEM_ENCAPS_SECRET_LEN),
162 cp in prop::collection::vec(any::<u8>(), 0..128),
163 c2 in prop::collection::vec(any::<u8>(), LEN_MLKEM_SHAREDSECRET_ENCAPS),
164 ) {
165 let ct = message_ct(c1, cp, c2);
166 let restored = MessageCiphertext::from_bytes(&ct.as_bytes()).expect("valid bytes");
167 prop_assert_eq!(ct.as_bytes(), restored.as_bytes());
168 }
169
170 #[test]
171 fn test_metadata_ciphertext_byte_roundtrip(
172 c in prop::collection::vec(any::<u8>(), LEN_XWING_SHAREDSECRET_ENCAPS),
173 cp in prop::collection::vec(any::<u8>(), LEN_METADATA_CIPHERTEXT)
174 .prop_map(|v| v.try_into().unwrap()),
175 ) {
176 let ct = MetadataCiphertext { c: c.try_into().expect("c length"), cp };
177 let restored = MetadataCiphertext::from_bytes(&ct.as_bytes()).expect("valid bytes");
178 prop_assert_eq!(ct.as_bytes(), restored.as_bytes());
179 }
180
181 #[test]
182 fn test_envelope_serde_roundtrip(
183 c1 in prop::collection::vec(any::<u8>(), DH_AKEM_ENCAPS_SECRET_LEN),
184 cp_a in prop::collection::vec(any::<u8>(), 0..128),
185 c2 in prop::collection::vec(any::<u8>(), LEN_MLKEM_SHAREDSECRET_ENCAPS),
186 c in prop::collection::vec(any::<u8>(), LEN_XWING_SHAREDSECRET_ENCAPS),
187 cp_b in prop::collection::vec(any::<u8>(), LEN_METADATA_CIPHERTEXT)
188 .prop_map(|v| v.try_into().unwrap()),
189 mgdh_pubkey in prop::array::uniform32(any::<u8>()),
190 mgdh in prop::array::uniform32(any::<u8>()),
191 ) {
192 let env = Envelope {
193 ct_apke: message_ct(c1, cp_a, c2),
194 ct_pke: MetadataCiphertext { c: c.try_into().expect("c length"), cp: cp_b },
195 mgdh_pubkey,
196 mgdh,
197 };
198 let json = serde_json::to_string(&env).expect("serialize");
199 let restored: Envelope = serde_json::from_str(&json).expect("deserialize");
200 prop_assert_eq!(env.ct_apke.as_bytes(), restored.ct_apke.as_bytes());
201 prop_assert_eq!(env.ct_pke.as_bytes(), restored.ct_pke.as_bytes());
202 prop_assert_eq!(env.mgdh_pubkey, restored.mgdh_pubkey);
203 prop_assert_eq!(env.mgdh, restored.mgdh);
204 }
205
206 #[test]
207 fn test_fetch_challenge_response_serde_roundtrip(
208 enc_ids in prop::collection::vec(
209 prop::collection::vec(any::<u8>(), LEN_KMID), 0..6),
210 pmgdhs in prop::collection::vec(prop::array::uniform32(any::<u8>()), 0..6),
211 ) {
212 let n = enc_ids.len().min(pmgdhs.len());
213 let messages: Vec<FetchResponse> = (0..n)
214 .map(|i| FetchResponse {
215 enc_id: enc_ids[i].clone().try_into().expect("enc_id length"),
216 pmgdh: pmgdhs[i],
217 })
218 .collect();
219 let resp = crate::wire::core::MessageChallengeFetchResponse { count: n, messages };
220
221 let json = serde_json::to_string(&resp).expect("serialize");
222 let restored: crate::wire::core::MessageChallengeFetchResponse =
223 serde_json::from_str(&json).expect("deserialize");
224
225 prop_assert_eq!(restored.count, resp.count);
226 prop_assert_eq!(restored.messages.len(), resp.messages.len());
227 for (a, b) in resp.messages.iter().zip(restored.messages.iter()) {
228 prop_assert_eq!(a.enc_id, b.enc_id);
229 prop_assert_eq!(a.pmgdh, b.pmgdh);
230 }
231 }
232 }
233}