1use chacha20poly1305::{
7 XChaCha20Poly1305, XNonce,
8 aead::{Aead, KeyInit, Payload},
9};
10use std::fmt;
11
12const MAGIC: &[u8; 8] = b"LCTXIR02";
13pub const RUNTIME_FRAME_HEADER_BYTES: usize = 12;
15pub const RUNTIME_FRAME_NONCE_BYTES: usize = 24;
17pub const RUNTIME_FRAME_TAG_BYTES: usize = 16;
19pub const MAX_RUNTIME_FRAME_PAYLOAD_BYTES: usize =
21 crate::runtime_exchange::MAX_RUNTIME_EXCHANGE_BYTES;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum RuntimeFrameDirection {
26 Request,
27 Response,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum RuntimeFrameError {
32 InvalidHeader,
33 InvalidLength,
34 EncryptionFailed,
35 AuthenticationFailed,
36}
37
38impl fmt::Display for RuntimeFrameError {
39 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
40 formatter.write_str(match self {
41 Self::InvalidHeader => "invalid runtime frame header or version",
42 Self::InvalidLength => "invalid runtime frame length",
43 Self::EncryptionFailed => "runtime frame encryption failed",
44 Self::AuthenticationFailed => "runtime frame authentication failed",
45 })
46 }
47}
48
49impl std::error::Error for RuntimeFrameError {}
50
51pub fn runtime_payload_length(header: &[u8]) -> Result<usize, RuntimeFrameError> {
53 let length_bytes: [u8; 4] = header
54 .strip_prefix(MAGIC)
55 .and_then(|bytes| bytes.try_into().ok())
56 .ok_or(RuntimeFrameError::InvalidHeader)?;
57 let length = usize::try_from(u32::from_be_bytes(length_bytes))
58 .map_err(|_| RuntimeFrameError::InvalidLength)?;
59 validate_length(length)?;
60 Ok(length)
61}
62
63pub fn encode_runtime_frame(
66 key: &[u8; 32],
67 direction: RuntimeFrameDirection,
68 nonce: &[u8; RUNTIME_FRAME_NONCE_BYTES],
69 payload: &[u8],
70) -> Result<Vec<u8>, RuntimeFrameError> {
71 let aad = associated_data(direction, payload.len())?;
72 let ciphertext = XChaCha20Poly1305::new(key.into())
73 .encrypt(
74 XNonce::from_slice(nonce),
75 Payload {
76 msg: payload,
77 aad: &aad,
78 },
79 )
80 .map_err(|_| RuntimeFrameError::EncryptionFailed)?;
81 let mut frame = Vec::with_capacity(
82 RUNTIME_FRAME_HEADER_BYTES + RUNTIME_FRAME_NONCE_BYTES + ciphertext.len(),
83 );
84 frame.extend_from_slice(&aad[..RUNTIME_FRAME_HEADER_BYTES]);
85 frame.extend_from_slice(nonce);
86 frame.extend_from_slice(&ciphertext);
87 Ok(frame)
88}
89
90pub fn decode_runtime_frame(
94 key: &[u8; 32],
95 direction: RuntimeFrameDirection,
96 frame: &[u8],
97) -> Result<Vec<u8>, RuntimeFrameError> {
98 let overhead = RUNTIME_FRAME_HEADER_BYTES + RUNTIME_FRAME_NONCE_BYTES + RUNTIME_FRAME_TAG_BYTES;
99 if frame.len() < overhead {
100 return Err(RuntimeFrameError::InvalidLength);
101 }
102 let (header, body) = frame.split_at(RUNTIME_FRAME_HEADER_BYTES);
103 let length = runtime_payload_length(header)?;
104 if frame.len() != overhead + length {
105 return Err(RuntimeFrameError::InvalidLength);
106 }
107 let (nonce, ciphertext) = body.split_at(RUNTIME_FRAME_NONCE_BYTES);
108 let aad = associated_data(direction, length)?;
109 XChaCha20Poly1305::new(key.into())
110 .decrypt(
111 XNonce::from_slice(nonce),
112 Payload {
113 msg: ciphertext,
114 aad: &aad,
115 },
116 )
117 .map_err(|_| RuntimeFrameError::AuthenticationFailed)
118}
119
120fn validate_length(length: usize) -> Result<(), RuntimeFrameError> {
121 if length == 0 || length > MAX_RUNTIME_FRAME_PAYLOAD_BYTES {
122 return Err(RuntimeFrameError::InvalidLength);
123 }
124 Ok(())
125}
126
127fn associated_data(
128 direction: RuntimeFrameDirection,
129 length: usize,
130) -> Result<Vec<u8>, RuntimeFrameError> {
131 validate_length(length)?;
132 let length = u32::try_from(length).map_err(|_| RuntimeFrameError::InvalidLength)?;
133 let mut aad = MAGIC.to_vec();
134 aad.extend_from_slice(&length.to_be_bytes());
135 aad.extend_from_slice(match direction {
136 RuntimeFrameDirection::Request => b"request\0",
137 RuntimeFrameDirection::Response => b"response\0",
138 });
139 Ok(aad)
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 const KEY: [u8; 32] = [0x0b; 32];
148 const NONCE: [u8; 24] = [0x0d; 24];
149
150 #[test]
151 fn round_trip_authenticates_every_byte_and_direction() {
152 let request = RuntimeFrameDirection::Request;
153 let payload = b"bounded projection";
154 let frame = encode_runtime_frame(&KEY, request, &NONCE, payload).unwrap();
155 assert_eq!(runtime_payload_length(&frame[..12]).unwrap(), payload.len());
156 assert_eq!(
157 decode_runtime_frame(&KEY, request, &frame).unwrap(),
158 payload
159 );
160 assert!(!frame.windows(payload.len()).any(|window| window == payload));
161 assert_ne!(
162 frame,
163 encode_runtime_frame(&KEY, request, &[0x0e; 24], payload).unwrap()
164 );
165 assert!(decode_runtime_frame(&KEY, RuntimeFrameDirection::Response, &frame).is_err());
166 assert!(decode_runtime_frame(&[0x0c; 32], request, &frame).is_err());
167 for index in 0..frame.len() {
168 let mut tampered = frame.clone();
169 tampered[index] ^= 1;
170 assert!(
171 decode_runtime_frame(&KEY, request, &tampered).is_err(),
172 "byte {index}"
173 );
174 }
175 for length in 0..frame.len() {
176 assert!(decode_runtime_frame(&KEY, request, &frame[..length]).is_err());
177 }
178 let mut trailing = frame;
179 trailing.push(0);
180 assert!(decode_runtime_frame(&KEY, request, &trailing).is_err());
181 }
182
183 #[test]
184 fn unpublished_plaintext_hmac_predecessor_is_not_a_downgrade_path() {
185 let mut old = b"LCTXIR01".to_vec();
187 old.extend_from_slice(&18_u32.to_be_bytes());
188 old.extend_from_slice(&[
189 0x22, 0x7a, 0xca, 0x2b, 0x41, 0x04, 0xc8, 0x81, 0xa1, 0xd8, 0x67, 0xfa, 0x3d, 0x9b,
190 0x8d, 0xce, 0xdf, 0xa4, 0x8d, 0x1b, 0x61, 0x28, 0x6f, 0xd2, 0xe8, 0x1c, 0xc9, 0x2b,
191 0x3a, 0x92, 0x18, 0x2c,
192 ]);
193 old.extend_from_slice(b"bounded projection");
194 assert!(decode_runtime_frame(&KEY, RuntimeFrameDirection::Request, &old).is_err());
195 }
196
197 #[test]
198 fn validates_header_before_allocation_and_rejects_oversize() {
199 for length in [0_u32, 1_048_577, u32::MAX] {
200 let mut header = MAGIC.to_vec();
201 header.extend_from_slice(&length.to_be_bytes());
202 assert!(runtime_payload_length(&header).is_err());
203 }
204 assert!(runtime_payload_length(b"LCTXIR02").is_err());
205 let direction = RuntimeFrameDirection::Response;
206 assert!(encode_runtime_frame(&KEY, direction, &NONCE, b"").is_err());
207 assert!(encode_runtime_frame(&KEY, direction, &NONCE, &vec![0; 1_048_577]).is_err());
208 let payload = vec![0; MAX_RUNTIME_FRAME_PAYLOAD_BYTES];
209 let frame = encode_runtime_frame(&KEY, direction, &NONCE, &payload).unwrap();
210 assert_eq!(
211 decode_runtime_frame(&KEY, direction, &frame).unwrap(),
212 payload
213 );
214 }
215}