1use chacha20poly1305::{
9 aead::{Aead, KeyInit},
10 XChaCha20Poly1305, XNonce,
11};
12
13pub const APPLICATION_ENVELOPE_MAGIC: &[u8; 6] = b"ORTCE1";
14pub const APPLICATION_ENVELOPE_VERSION_REPLAY: u8 = 2;
15pub const APPLICATION_REPLAY_SEQUENCE_BYTES: usize = 8;
16pub const APPLICATION_KEY_BYTES: usize = 32;
17pub const APPLICATION_NONCE_BYTES: usize = 24;
18pub const RAW_STREAM_TYPE_ID: u8 = 3;
19pub const DEFAULT_MAX_RAW_FRAME_BYTES: usize = 16 * 1024 * 1024;
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum CryptoError {
23 InvalidKeyLength,
24 InvalidNonceLength,
25 RandomNonceFailed,
26 PlaintextRejected,
27 InvalidEnvelope,
28 DecryptFailed,
29 TypeMismatch,
30}
31
32fn constant_time_prefix_eq(input: &[u8], prefix: &[u8]) -> bool {
33 if input.len() < prefix.len() {
34 return false;
35 }
36 let mut diff = 0u8;
37 for (left, right) in input.iter().zip(prefix.iter()) {
38 diff |= left ^ right;
39 }
40 diff == 0
41}
42
43fn envelope_version(payload: &[u8]) -> Option<u8> {
44 if !constant_time_prefix_eq(payload, APPLICATION_ENVELOPE_MAGIC) {
45 return None;
46 }
47 let version = payload[APPLICATION_ENVELOPE_MAGIC.len()];
48 if version == APPLICATION_ENVELOPE_VERSION_REPLAY {
49 Some(version)
50 } else {
51 None
52 }
53}
54
55pub fn is_encrypted_payload(payload: &[u8]) -> bool {
56 envelope_version(payload).is_some_and(|_| {
57 payload.len()
58 > APPLICATION_ENVELOPE_MAGIC.len()
59 + 1
60 + APPLICATION_REPLAY_SEQUENCE_BYTES
61 + APPLICATION_NONCE_BYTES
62 })
63}
64
65pub fn protect_payload(key: &[u8], type_id: u8, payload: &[u8]) -> Result<Vec<u8>, CryptoError> {
66 let mut nonce = [0u8; APPLICATION_NONCE_BYTES];
67 getrandom::getrandom(&mut nonce).map_err(|_| CryptoError::RandomNonceFailed)?;
68 protect_payload_with_nonce(key, type_id, payload, &nonce)
69}
70
71pub(crate) fn protect_payload_with_nonce(
72 key: &[u8],
73 type_id: u8,
74 payload: &[u8],
75 nonce: &[u8],
76) -> Result<Vec<u8>, CryptoError> {
77 protect_payload_with_nonce_and_sequence(key, type_id, payload, nonce, 0)
78}
79
80pub(crate) fn protect_payload_with_nonce_and_sequence(
81 key: &[u8],
82 type_id: u8,
83 payload: &[u8],
84 nonce: &[u8],
85 sequence: u64,
86) -> Result<Vec<u8>, CryptoError> {
87 if key.len() != APPLICATION_KEY_BYTES {
88 return Err(CryptoError::InvalidKeyLength);
89 }
90 if nonce.len() != APPLICATION_NONCE_BYTES {
91 return Err(CryptoError::InvalidNonceLength);
92 }
93
94 let cipher =
95 XChaCha20Poly1305::new_from_slice(key).map_err(|_| CryptoError::InvalidKeyLength)?;
96 let nonce = XNonce::from_slice(nonce);
97 let mut plaintext = Vec::with_capacity(1 + payload.len());
98 plaintext.push(type_id);
99 plaintext.extend_from_slice(payload);
100 let ciphertext = cipher
101 .encrypt(nonce, plaintext.as_slice())
102 .map_err(|_| CryptoError::DecryptFailed)?;
103
104 let mut envelope = Vec::with_capacity(
105 APPLICATION_ENVELOPE_MAGIC.len()
106 + 1
107 + APPLICATION_REPLAY_SEQUENCE_BYTES
108 + APPLICATION_NONCE_BYTES
109 + ciphertext.len(),
110 );
111 envelope.extend_from_slice(APPLICATION_ENVELOPE_MAGIC);
112 envelope.push(APPLICATION_ENVELOPE_VERSION_REPLAY);
113 envelope.extend_from_slice(&sequence.to_be_bytes());
114 envelope.extend_from_slice(nonce);
115 envelope.extend_from_slice(&ciphertext);
116 Ok(envelope)
117}
118
119pub fn open_payload(
120 key: &[u8],
121 expected_type_id: u8,
122 payload: &[u8],
123 require_encrypted: bool,
124) -> Result<Vec<u8>, CryptoError> {
125 if key.len() != APPLICATION_KEY_BYTES {
126 return Err(CryptoError::InvalidKeyLength);
127 }
128 if !is_encrypted_payload(payload) {
129 return if require_encrypted {
130 Err(CryptoError::PlaintextRejected)
131 } else {
132 Ok(payload.to_vec())
133 };
134 }
135
136 let version = envelope_version(payload).ok_or(CryptoError::InvalidEnvelope)?;
137 if version != APPLICATION_ENVELOPE_VERSION_REPLAY {
138 return Err(CryptoError::InvalidEnvelope);
139 }
140 let nonce_offset = APPLICATION_ENVELOPE_MAGIC.len() + 1 + APPLICATION_REPLAY_SEQUENCE_BYTES;
141 let cipher_offset = nonce_offset + APPLICATION_NONCE_BYTES;
142 if payload.len() <= cipher_offset {
143 return Err(CryptoError::InvalidEnvelope);
144 }
145
146 let cipher =
147 XChaCha20Poly1305::new_from_slice(key).map_err(|_| CryptoError::InvalidKeyLength)?;
148 let nonce = XNonce::from_slice(&payload[nonce_offset..cipher_offset]);
149 let plaintext = cipher
150 .decrypt(nonce, &payload[cipher_offset..])
151 .map_err(|_| CryptoError::DecryptFailed)?;
152 let Some((&type_id, body)) = plaintext.split_first() else {
153 return Err(CryptoError::InvalidEnvelope);
154 };
155 if type_id != expected_type_id {
156 return Err(CryptoError::TypeMismatch);
157 }
158 Ok(body.to_vec())
159}
160
161pub fn protect_frame(key: &[u8], payload: &[u8]) -> Result<Vec<u8>, CryptoError> {
162 let encrypted = protect_payload(key, RAW_STREAM_TYPE_ID, payload)?;
163 Ok(frame_payload(&encrypted))
164}
165
166pub fn protect_with_nonce(
167 key: &[u8],
168 payload: &[u8],
169 nonce: &[u8],
170) -> Result<Vec<u8>, CryptoError> {
171 let encrypted = protect_payload_with_nonce(key, RAW_STREAM_TYPE_ID, payload, nonce)?;
172 Ok(frame_payload(&encrypted))
173}
174
175pub fn frame_payload(payload: &[u8]) -> Vec<u8> {
176 let mut frame = Vec::with_capacity(4 + payload.len());
177 frame.extend_from_slice(&(payload.len() as u32).to_be_bytes());
178 frame.extend_from_slice(payload);
179 frame
180}
181
182#[derive(Debug, Clone, PartialEq, Eq)]
183pub enum CryptoStreamError {
184 InvalidFrameLength,
185 FrameTooLarge,
186 AuthenticationFailed(CryptoError),
187 IncompleteFrame,
188}
189
190#[derive(Debug, Clone)]
191pub struct CryptoFrameDecoder {
192 key: Vec<u8>,
193 max_frame_bytes: usize,
194 buffer: Vec<u8>,
195}
196
197impl CryptoFrameDecoder {
198 pub fn new(key: &[u8]) -> Result<Self, CryptoError> {
199 Self::with_max_frame_bytes(key, DEFAULT_MAX_RAW_FRAME_BYTES)
200 }
201
202 pub fn with_max_frame_bytes(key: &[u8], max_frame_bytes: usize) -> Result<Self, CryptoError> {
203 if key.len() != APPLICATION_KEY_BYTES {
204 return Err(CryptoError::InvalidKeyLength);
205 }
206 Ok(Self {
207 key: key.to_vec(),
208 max_frame_bytes,
209 buffer: Vec::new(),
210 })
211 }
212
213 pub fn push(&mut self, chunk: &[u8]) -> Result<Vec<Vec<u8>>, CryptoStreamError> {
214 self.buffer.extend_from_slice(chunk);
215 let mut opened = Vec::new();
216
217 loop {
218 if self.buffer.len() < 4 {
219 break;
220 }
221 let frame_len = u32::from_be_bytes([
222 self.buffer[0],
223 self.buffer[1],
224 self.buffer[2],
225 self.buffer[3],
226 ]) as usize;
227 if frame_len == 0 {
228 return Err(CryptoStreamError::InvalidFrameLength);
229 }
230 if frame_len > self.max_frame_bytes {
231 return Err(CryptoStreamError::FrameTooLarge);
232 }
233 if self.buffer.len() < 4 + frame_len {
234 break;
235 }
236
237 let encrypted = self.buffer[4..4 + frame_len].to_vec();
238 self.buffer.drain(..4 + frame_len);
239 let payload = open_payload(&self.key, RAW_STREAM_TYPE_ID, &encrypted, true)
240 .map_err(CryptoStreamError::AuthenticationFailed)?;
241 opened.push(payload);
242 }
243
244 Ok(opened)
245 }
246
247 pub fn finish(&self) -> Result<(), CryptoStreamError> {
248 if self.buffer.is_empty() {
249 Ok(())
250 } else {
251 Err(CryptoStreamError::IncompleteFrame)
252 }
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 fn key() -> [u8; APPLICATION_KEY_BYTES] {
261 [0x42; APPLICATION_KEY_BYTES]
262 }
263
264 fn nonce() -> [u8; APPLICATION_NONCE_BYTES] {
265 [0x24; APPLICATION_NONCE_BYTES]
266 }
267
268 #[test]
269 fn application_payload_envelope_round_trips_without_plaintext() {
270 let plaintext = b"native-super-secret-payload";
271 let envelope = protect_payload_with_nonce(&key(), 0, plaintext, &nonce()).unwrap();
272
273 assert!(is_encrypted_payload(&envelope));
274 assert!(!String::from_utf8_lossy(&envelope).contains("native-super-secret-payload"));
275
276 let opened = open_payload(&key(), 0, &envelope, true).unwrap();
277 assert_eq!(opened, plaintext);
278 }
279
280 #[test]
281 fn application_payload_envelope_matches_typescript_xchacha_fixture() {
282 let key = [0x11; APPLICATION_KEY_BYTES];
283 let nonce = [0x22; APPLICATION_NONCE_BYTES];
284 let envelope =
285 protect_payload_with_nonce(&key, 0, b"interop-secret-payload", &nonce).unwrap();
286
287 assert_eq!(
288 hex::encode(envelope),
289 "4f5254434531020000000000000000222222222222222222222222222222222222222222222222830e897839cb8be4c83e71a8cf1cd5cf42f6bab87aff9e8eb208926635a43db794781e4d3047a2",
290 );
291 }
292
293 #[test]
294 fn application_payload_envelope_rejects_tampering() {
295 let plaintext = b"native-super-secret-payload";
296 let mut envelope = protect_payload_with_nonce(&key(), 0, plaintext, &nonce()).unwrap();
297 let last = envelope.len() - 1;
298 envelope[last] ^= 0x55;
299
300 assert_eq!(
301 open_payload(&key(), 0, &envelope, true),
302 Err(CryptoError::DecryptFailed),
303 );
304 }
305
306 #[test]
307 fn application_payload_envelope_rejects_wrong_type() {
308 let envelope = protect_payload_with_nonce(&key(), 1, b"video payload", &nonce()).unwrap();
309
310 assert_eq!(
311 open_payload(&key(), 2, &envelope, true),
312 Err(CryptoError::TypeMismatch),
313 );
314 }
315
316 #[test]
317 fn application_payload_envelope_rejects_foreign_key() {
318 let attacker_key = [0x11; APPLICATION_KEY_BYTES];
321 let victim_key = [0x22; APPLICATION_KEY_BYTES];
322 let envelope =
323 protect_payload_with_nonce(&attacker_key, 0, b"cross-key-forgery", &nonce()).unwrap();
324
325 assert_eq!(
326 open_payload(&victim_key, 0, &envelope, true),
327 Err(CryptoError::DecryptFailed),
328 );
329 }
330
331 #[test]
332 fn raw_stream_decoder_rejects_foreign_key_frames() {
333 let attacker_key = [0x33; APPLICATION_KEY_BYTES];
334 let victim_key = [0x44; APPLICATION_KEY_BYTES];
335 let frame = protect_with_nonce(&attacker_key, b"foreign raw frame", &nonce()).unwrap();
336 let mut decoder = CryptoFrameDecoder::new(&victim_key).unwrap();
337
338 assert_eq!(
339 decoder.push(&frame),
340 Err(CryptoStreamError::AuthenticationFailed(
341 CryptoError::DecryptFailed
342 )),
343 );
344 }
345
346 #[test]
347 fn application_payload_envelope_rejects_plaintext_when_required() {
348 assert_eq!(
349 open_payload(&key(), 0, b"plaintext-downgrade", true),
350 Err(CryptoError::PlaintextRejected),
351 );
352 }
353
354 #[test]
355 fn application_payload_envelope_can_allow_legacy_plaintext_when_requested() {
356 assert_eq!(
357 open_payload(&key(), 0, b"legacy plaintext", false).unwrap(),
358 b"legacy plaintext",
359 );
360 }
361
362 #[test]
363 fn raw_stream_frame_encrypts_and_decodes_split_frames() {
364 let plaintext = b"native raw stream secret";
365 let frame = protect_with_nonce(&key(), plaintext, &nonce()).unwrap();
366
367 assert!(!String::from_utf8_lossy(&frame).contains("native raw stream secret"));
368
369 let split = frame.len() / 2;
370 let mut decoder = CryptoFrameDecoder::new(&key()).unwrap();
371 assert!(decoder.push(&frame[..split]).unwrap().is_empty());
372 let opened = decoder.push(&frame[split..]).unwrap();
373
374 assert_eq!(opened, vec![plaintext.to_vec()]);
375 assert_eq!(decoder.finish(), Ok(()));
376 }
377
378 #[test]
379 fn raw_stream_decoder_opens_multiple_frames_from_one_chunk() {
380 let first = protect_with_nonce(&key(), b"first chunk", &nonce()).unwrap();
381 let mut second_nonce = nonce();
382 second_nonce[0] ^= 0x01;
383 let second = protect_with_nonce(&key(), b"second chunk", &second_nonce).unwrap();
384 let mut combined = first;
385 combined.extend_from_slice(&second);
386
387 let mut decoder = CryptoFrameDecoder::new(&key()).unwrap();
388 let opened = decoder.push(&combined).unwrap();
389
390 assert_eq!(
391 opened,
392 vec![b"first chunk".to_vec(), b"second chunk".to_vec()]
393 );
394 assert_eq!(decoder.finish(), Ok(()));
395 }
396
397 #[test]
398 fn raw_stream_decoder_rejects_plaintext_downgrade_frame() {
399 let plaintext_frame = frame_payload(b"plaintext raw stream");
400 let mut decoder = CryptoFrameDecoder::new(&key()).unwrap();
401
402 assert_eq!(
403 decoder.push(&plaintext_frame),
404 Err(CryptoStreamError::AuthenticationFailed(
405 CryptoError::PlaintextRejected
406 )),
407 );
408 }
409
410 #[test]
411 fn raw_stream_decoder_rejects_tampered_frame() {
412 let mut frame = protect_with_nonce(&key(), b"tamper me", &nonce()).unwrap();
413 let last = frame.len() - 1;
414 frame[last] ^= 0x7f;
415 let mut decoder = CryptoFrameDecoder::new(&key()).unwrap();
416
417 assert_eq!(
418 decoder.push(&frame),
419 Err(CryptoStreamError::AuthenticationFailed(
420 CryptoError::DecryptFailed
421 )),
422 );
423 }
424
425 #[test]
426 fn raw_stream_decoder_reports_incomplete_frame_on_finish() {
427 let frame = protect_with_nonce(&key(), b"partial", &nonce()).unwrap();
428 let mut decoder = CryptoFrameDecoder::new(&key()).unwrap();
429 assert!(decoder.push(&frame[..frame.len() - 1]).unwrap().is_empty());
430
431 assert_eq!(decoder.finish(), Err(CryptoStreamError::IncompleteFrame),);
432 }
433
434 #[test]
435 fn raw_stream_decoder_rejects_invalid_lengths() {
436 let mut decoder = CryptoFrameDecoder::with_max_frame_bytes(&key(), 8).unwrap();
437
438 assert_eq!(
439 decoder.push(&0u32.to_be_bytes()),
440 Err(CryptoStreamError::InvalidFrameLength),
441 );
442
443 let oversized = 9u32.to_be_bytes();
444 let mut decoder = CryptoFrameDecoder::with_max_frame_bytes(&key(), 8).unwrap();
445 assert_eq!(
446 decoder.push(&oversized),
447 Err(CryptoStreamError::FrameTooLarge),
448 );
449 }
450}