1use std::time::{Duration, SystemTime, UNIX_EPOCH};
9
10use base64::Engine;
11use base64::engine::general_purpose::URL_SAFE_NO_PAD;
12use serde::Serialize;
13use serde::de::DeserializeOwned;
14use sha2::{Digest, Sha256};
15
16use crate::protocol::InputResponses;
17
18const TOKEN_VERSION: &str = "v1";
19const SHA256_BLOCK_SIZE: usize = 64;
20const DEFAULT_MAX_TOKEN_BYTES: usize = 64 * 1024;
21
22#[derive(Debug, Clone, Default)]
29pub struct MrtrRequest {
30 input_responses: Option<InputResponses>,
31 request_state: Option<String>,
32}
33
34impl MrtrRequest {
35 pub(crate) fn new(
36 input_responses: Option<InputResponses>,
37 request_state: Option<String>,
38 ) -> Self {
39 Self {
40 input_responses,
41 request_state,
42 }
43 }
44
45 pub fn input_responses(&self) -> Option<&InputResponses> {
48 self.input_responses.as_ref()
49 }
50
51 pub fn request_state(&self) -> Option<&str> {
53 self.request_state.as_deref()
54 }
55
56 pub fn into_parts(self) -> (Option<InputResponses>, Option<String>) {
58 (self.input_responses, self.request_state)
59 }
60}
61
62#[derive(Debug, thiserror::Error)]
64#[non_exhaustive]
65pub enum RequestStateError {
66 #[error("request-state key must be at least 32 bytes")]
68 WeakKey,
69 #[error("request-state TTL must be greater than zero")]
71 ZeroTtl,
72 #[error("request-state token exceeds the configured maximum of {0} bytes")]
74 TooLarge(usize),
75 #[error("request-state token is malformed")]
77 Malformed,
78 #[error("unsupported request-state token version")]
80 UnsupportedVersion,
81 #[error("request-state integrity verification failed")]
83 Integrity,
84 #[error("request-state token has expired")]
86 Expired,
87 #[error("request-state token is not bound to the current subject")]
89 SubjectMismatch,
90 #[error("failed to serialize request state: {0}")]
92 Encode(#[source] serde_json::Error),
93 #[error("failed to decode request state: {0}")]
95 Decode(#[source] serde_json::Error),
96 #[error("system clock is earlier than the Unix epoch")]
98 Clock,
99}
100
101#[derive(Debug, Serialize, serde::Deserialize)]
102struct StateEnvelope<T> {
103 issued_at: u64,
104 expires_at: u64,
105 #[serde(default, skip_serializing_if = "Option::is_none")]
106 subject: Option<String>,
107 state: T,
108}
109
110#[derive(Clone)]
120pub struct RequestStateCodec {
121 key: std::sync::Arc<[u8]>,
122 ttl: Duration,
123 max_token_bytes: usize,
124}
125
126impl std::fmt::Debug for RequestStateCodec {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 f.debug_struct("RequestStateCodec")
129 .field("key", &"<redacted>")
130 .field("ttl", &self.ttl)
131 .field("max_token_bytes", &self.max_token_bytes)
132 .finish()
133 }
134}
135
136impl RequestStateCodec {
137 pub fn new(key: impl AsRef<[u8]>, ttl: Duration) -> Result<Self, RequestStateError> {
142 let key = key.as_ref();
143 if key.len() < 32 {
144 return Err(RequestStateError::WeakKey);
145 }
146 if ttl.is_zero() {
147 return Err(RequestStateError::ZeroTtl);
148 }
149 Ok(Self {
150 key: std::sync::Arc::from(key),
151 ttl,
152 max_token_bytes: DEFAULT_MAX_TOKEN_BYTES,
153 })
154 }
155
156 pub fn with_max_token_bytes(mut self, max_token_bytes: usize) -> Self {
158 self.max_token_bytes = max_token_bytes;
159 self
160 }
161
162 pub fn encode<T: Serialize>(&self, state: &T) -> Result<String, RequestStateError> {
164 self.encode_at(None, state, unix_seconds()?)
165 }
166
167 pub fn encode_for<T: Serialize>(
169 &self,
170 subject: impl Into<String>,
171 state: &T,
172 ) -> Result<String, RequestStateError> {
173 self.encode_at(Some(subject.into()), state, unix_seconds()?)
174 }
175
176 pub fn decode<T: DeserializeOwned>(&self, token: &str) -> Result<T, RequestStateError> {
178 self.decode_at(token, None, unix_seconds()?)
179 }
180
181 pub fn decode_for<T: DeserializeOwned>(
183 &self,
184 token: &str,
185 subject: &str,
186 ) -> Result<T, RequestStateError> {
187 self.decode_at(token, Some(subject), unix_seconds()?)
188 }
189
190 fn encode_at<T: Serialize>(
191 &self,
192 subject: Option<String>,
193 state: &T,
194 now: u64,
195 ) -> Result<String, RequestStateError> {
196 let ttl = self.ttl.as_secs();
197 let envelope = StateEnvelope {
198 issued_at: now,
199 expires_at: now.saturating_add(ttl),
200 subject,
201 state,
202 };
203 let payload = serde_json::to_vec(&envelope).map_err(RequestStateError::Encode)?;
204 let payload = URL_SAFE_NO_PAD.encode(payload);
205 let signed = format!("{TOKEN_VERSION}.{payload}");
206 let signature = URL_SAFE_NO_PAD.encode(hmac_sha256(&self.key, signed.as_bytes()));
207 let token = format!("{signed}.{signature}");
208 if token.len() > self.max_token_bytes {
209 return Err(RequestStateError::TooLarge(self.max_token_bytes));
210 }
211 Ok(token)
212 }
213
214 fn decode_at<T: DeserializeOwned>(
215 &self,
216 token: &str,
217 subject: Option<&str>,
218 now: u64,
219 ) -> Result<T, RequestStateError> {
220 if token.len() > self.max_token_bytes {
221 return Err(RequestStateError::TooLarge(self.max_token_bytes));
222 }
223 let mut parts = token.split('.');
224 let version = parts.next().ok_or(RequestStateError::Malformed)?;
225 let payload = parts.next().ok_or(RequestStateError::Malformed)?;
226 let signature = parts.next().ok_or(RequestStateError::Malformed)?;
227 if parts.next().is_some() {
228 return Err(RequestStateError::Malformed);
229 }
230 if version != TOKEN_VERSION {
231 return Err(RequestStateError::UnsupportedVersion);
232 }
233
234 let supplied_signature = URL_SAFE_NO_PAD
235 .decode(signature)
236 .map_err(|_| RequestStateError::Malformed)?;
237 let signed = format!("{version}.{payload}");
238 let expected_signature = hmac_sha256(&self.key, signed.as_bytes());
239 if !constant_time_eq(&supplied_signature, &expected_signature) {
240 return Err(RequestStateError::Integrity);
241 }
242
243 let payload = URL_SAFE_NO_PAD
244 .decode(payload)
245 .map_err(|_| RequestStateError::Malformed)?;
246 let envelope: StateEnvelope<T> =
247 serde_json::from_slice(&payload).map_err(RequestStateError::Decode)?;
248 if now > envelope.expires_at {
249 return Err(RequestStateError::Expired);
250 }
251 match (envelope.subject.as_deref(), subject) {
252 (None, None) => {}
253 (Some(expected), Some(actual)) if expected == actual => {}
254 _ => return Err(RequestStateError::SubjectMismatch),
255 }
256 Ok(envelope.state)
257 }
258}
259
260fn unix_seconds() -> Result<u64, RequestStateError> {
261 SystemTime::now()
262 .duration_since(UNIX_EPOCH)
263 .map(|duration| duration.as_secs())
264 .map_err(|_| RequestStateError::Clock)
265}
266
267fn hmac_sha256(key: &[u8], message: &[u8]) -> [u8; 32] {
268 let mut normalized = [0u8; SHA256_BLOCK_SIZE];
269 if key.len() > SHA256_BLOCK_SIZE {
270 normalized[..32].copy_from_slice(&Sha256::digest(key));
271 } else {
272 normalized[..key.len()].copy_from_slice(key);
273 }
274
275 let mut inner_pad = [0x36u8; SHA256_BLOCK_SIZE];
276 let mut outer_pad = [0x5cu8; SHA256_BLOCK_SIZE];
277 for ((inner, outer), key_byte) in inner_pad
278 .iter_mut()
279 .zip(outer_pad.iter_mut())
280 .zip(normalized)
281 {
282 *inner ^= key_byte;
283 *outer ^= key_byte;
284 }
285
286 let mut inner = Sha256::new();
287 inner.update(inner_pad);
288 inner.update(message);
289 let inner = inner.finalize();
290
291 let mut outer = Sha256::new();
292 outer.update(outer_pad);
293 outer.update(inner);
294 outer.finalize().into()
295}
296
297fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
298 if left.len() != right.len() {
299 return false;
300 }
301 left.iter()
302 .zip(right)
303 .fold(0u8, |difference, (left, right)| difference | (left ^ right))
304 == 0
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 const KEY: &[u8; 32] = b"0123456789abcdef0123456789abcdef";
312
313 #[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
314 struct State {
315 round: u8,
316 value: String,
317 }
318
319 #[test]
320 fn round_trips_shared_state() {
321 let first = RequestStateCodec::new(KEY, Duration::from_secs(60)).unwrap();
322 let second = RequestStateCodec::new(KEY, Duration::from_secs(60)).unwrap();
323 let state = State {
324 round: 2,
325 value: "kept".into(),
326 };
327 let token = first.encode_at(None, &state, 100).unwrap();
328 assert_eq!(second.decode_at::<State>(&token, None, 120).unwrap(), state);
329 }
330
331 #[test]
332 fn rejects_tampering_expiry_and_wrong_subject() {
333 let codec = RequestStateCodec::new(KEY, Duration::from_secs(10)).unwrap();
334 let token = codec
335 .encode_at(
336 Some("alice".into()),
337 &State {
338 round: 1,
339 value: "x".into(),
340 },
341 100,
342 )
343 .unwrap();
344
345 assert!(matches!(
346 codec.decode_at::<State>(&format!("{token}x"), Some("alice"), 101),
347 Err(RequestStateError::Integrity | RequestStateError::Malformed)
348 ));
349 assert!(matches!(
350 codec.decode_at::<State>(&token, Some("bob"), 101),
351 Err(RequestStateError::SubjectMismatch)
352 ));
353 assert!(matches!(
354 codec.decode_at::<State>(&token, Some("alice"), 111),
355 Err(RequestStateError::Expired)
356 ));
357 }
358
359 #[test]
360 fn enforces_key_ttl_and_size_limits() {
361 assert!(matches!(
362 RequestStateCodec::new(b"short", Duration::from_secs(1)),
363 Err(RequestStateError::WeakKey)
364 ));
365 assert!(matches!(
366 RequestStateCodec::new(KEY, Duration::ZERO),
367 Err(RequestStateError::ZeroTtl)
368 ));
369 let codec = RequestStateCodec::new(KEY, Duration::from_secs(1))
370 .unwrap()
371 .with_max_token_bytes(8);
372 assert!(matches!(
373 codec.encode(&"too large"),
374 Err(RequestStateError::TooLarge(8))
375 ));
376 }
377}