1use crate::ProcessorConfig;
2use crate::crypto::Key;
3use crate::crypto::encryption::EncryptionKey;
4use crate::crypto::signing::SigningKey;
5use crate::encoding::encode;
6use crate::{RequestCookie, ResponseCookie, config};
7use percent_encoding::percent_decode;
8use std::collections::HashMap;
9use std::str::Utf8Error;
10
11#[derive(Debug, Clone)]
43pub struct Processor {
44 percent_encode: bool,
45 rules: HashMap<String, Rule>,
46}
47
48impl From<ProcessorConfig> for Processor {
49 fn from(value: ProcessorConfig) -> Self {
50 let mut processor = Processor {
51 percent_encode: value.percent_encode,
52 rules: HashMap::new(),
53 };
54
55 for rule in value.crypto_rules.into_iter() {
56 let primary = CryptoConfig::new(&rule.key, rule.algorithm.into());
57 let fallbacks: Vec<_> = rule
58 .fallbacks
59 .into_iter()
60 .map(|config| CryptoConfig::new(&config.key, config.algorithm.into()))
61 .collect();
62 for name in rule.cookie_names {
63 processor.rules.insert(
64 name,
65 Rule {
66 primary,
67 fallbacks: fallbacks.clone(),
68 },
69 );
70 }
71 }
72 processor
73 }
74}
75
76impl Processor {
77 pub fn process_outgoing<'c>(&self, mut cookie: ResponseCookie<'c>) -> ResponseCookie<'c> {
79 if self.percent_encode {
80 let name = encode(&cookie.name).to_string();
81 cookie.name = name.into();
82 }
83 if let Some(rule) = self.rules.get(cookie.name.as_ref()) {
84 let value = rule.primary.process_outgoing(&cookie.name, &cookie.value);
85 cookie.value = value.into();
86 } else {
87 if self.percent_encode {
91 let value = encode(&cookie.value).to_string();
92 cookie.value = value.into();
93 }
94 }
95
96 cookie
97 }
98
99 pub fn will_encrypt(&self, name: &str) -> bool {
104 self.rules
105 .get(name)
106 .is_some_and(|rule| matches!(rule.primary, CryptoConfig::Encryption { .. }))
107 }
108
109 pub fn will_sign(&self, name: &str) -> bool {
114 self.rules
115 .get(name)
116 .is_some_and(|rule| matches!(rule.primary, CryptoConfig::Signing { .. }))
117 }
118
119 pub fn process_incoming<'c>(
123 &self,
124 name: &'c str,
125 value: &'c str,
126 ) -> Result<RequestCookie<'c>, ProcessIncomingError> {
127 let mut cookie = RequestCookie {
128 name: name.into(),
129 value: value.into(),
130 };
131
132 let mut decode_value = false;
133
134 if let Some(rule) = self.rules.get(name) {
135 let configs = std::iter::once(rule.primary).chain(rule.fallbacks.iter().copied());
136 let value = 'outer: {
137 let mut error = None;
138 for config in configs {
139 let outcome = config.process_incoming(name, value);
140 match outcome {
141 Ok(value) => {
142 break 'outer value;
143 }
144 Err(e) => {
145 if error.is_none() {
146 error = Some(e);
148 }
149 }
150 }
151 }
152 return Err(error.unwrap().into());
154 };
155 cookie.value = value.into();
156 } else {
157 decode_value = true;
158 }
159 if self.percent_encode {
160 cookie.name =
161 percent_decode(name.as_bytes())
162 .decode_utf8()
163 .map_err(|e| DecodingError {
164 invalid_part: InvalidCookiePart::Name {
165 raw_value: name.to_string(),
166 },
167 source: e,
168 })?;
169 }
170
171 if self.percent_encode && decode_value {
172 cookie.value = percent_decode(value.as_bytes())
173 .decode_utf8()
174 .map_err(|e| DecodingError {
175 invalid_part: InvalidCookiePart::Value {
176 cookie_name: cookie.name.clone().into_owned(),
177 raw_value: value.to_string(),
178 },
179 source: e,
180 })?;
181 }
182
183 Ok(cookie)
184 }
185}
186
187#[derive(Debug)]
188#[non_exhaustive]
189pub enum ProcessIncomingError {
191 Crypto(CryptoError),
192 Decoding(DecodingError),
193}
194
195impl std::fmt::Display for ProcessIncomingError {
196 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197 match self {
198 ProcessIncomingError::Crypto(e) => e.fmt(f),
199 ProcessIncomingError::Decoding(e) => e.fmt(f),
200 }
201 }
202}
203
204impl std::error::Error for ProcessIncomingError {
205 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
206 match self {
207 ProcessIncomingError::Crypto(e) => Some(e),
208 ProcessIncomingError::Decoding(e) => Some(e),
209 }
210 }
211}
212
213impl From<CryptoError> for ProcessIncomingError {
214 fn from(value: CryptoError) -> Self {
215 ProcessIncomingError::Crypto(value)
216 }
217}
218
219impl From<DecodingError> for ProcessIncomingError {
220 fn from(value: DecodingError) -> Self {
221 ProcessIncomingError::Decoding(value)
222 }
223}
224
225#[derive(Debug)]
226pub struct CryptoError {
230 name: String,
231 r#type: CryptoAlgorithm,
232 source: anyhow::Error,
233}
234
235impl std::fmt::Display for CryptoError {
236 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237 let t = match self.r#type {
238 CryptoAlgorithm::Encryption => "an encrypted",
239 CryptoAlgorithm::Signing => "a signed",
240 };
241 write!(f, "Failed to process `{}` as {t} request cookie", self.name)
242 }
243}
244
245impl std::error::Error for CryptoError {
246 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
247 Some(self.source.as_ref())
248 }
249}
250
251#[derive(Debug)]
252pub struct DecodingError {
256 invalid_part: InvalidCookiePart,
257 source: Utf8Error,
258}
259
260impl std::fmt::Display for DecodingError {
261 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262 match &self.invalid_part {
263 InvalidCookiePart::Name { raw_value } => {
264 write!(
265 f,
266 "Failed to percent-decode the name of a cookie: `{raw_value}`",
267 )
268 }
269 InvalidCookiePart::Value {
270 cookie_name,
271 raw_value,
272 } => {
273 write!(
274 f,
275 "Failed to percent-decode the value of the `{cookie_name}` cookie: `{raw_value}`",
276 )
277 }
278 }
279 }
280}
281
282impl std::error::Error for DecodingError {
283 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
284 Some(&self.source)
285 }
286}
287
288#[derive(Debug, Eq, PartialEq)]
289pub(crate) enum InvalidCookiePart {
290 Name {
291 raw_value: String,
292 },
293 Value {
294 cookie_name: String,
295 raw_value: String,
296 },
297}
298
299#[derive(Debug, Clone)]
300struct Rule {
301 primary: CryptoConfig,
302 fallbacks: Vec<CryptoConfig>,
303}
304
305#[derive(Debug, Clone, Copy)]
306enum CryptoConfig {
307 Encryption { key: EncryptionKey },
308 Signing { key: SigningKey },
309}
310
311impl CryptoConfig {
312 pub fn new(master_key: &Key, crypto_algorithm: CryptoAlgorithm) -> Self {
313 match crypto_algorithm {
314 CryptoAlgorithm::Encryption => {
315 let key = EncryptionKey::derive(master_key);
316 CryptoConfig::Encryption { key }
317 }
318 CryptoAlgorithm::Signing => {
319 let key = SigningKey::derive(master_key);
320 CryptoConfig::Signing { key }
321 }
322 }
323 }
324
325 fn process_incoming(&self, name: &str, value: &str) -> Result<String, CryptoError> {
327 match self {
328 Self::Encryption { key } => {
329 key.decrypt(name.as_bytes(), value.as_bytes())
330 .map_err(|e| CryptoError {
331 name: name.to_owned(),
332 r#type: CryptoAlgorithm::Encryption,
333 source: e,
334 })
335 }
336 Self::Signing { key } => key.verify(name, value).map_err(|e| CryptoError {
337 name: name.to_owned(),
338 r#type: CryptoAlgorithm::Signing,
339 source: e,
340 }),
341 }
342 }
343
344 fn process_outgoing(&self, name: &str, value: &str) -> String {
346 match self {
347 Self::Encryption { key } => key.encrypt(name.as_bytes(), value.as_bytes()),
348 Self::Signing { key } => key.sign(name, value),
349 }
350 }
351}
352
353#[derive(Debug, Clone, Copy)]
354enum CryptoAlgorithm {
355 Encryption,
356 Signing,
357}
358
359impl std::fmt::Display for CryptoAlgorithm {
360 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
361 match self {
362 CryptoAlgorithm::Encryption => write!(f, "encryption"),
363 CryptoAlgorithm::Signing => write!(f, "signing"),
364 }
365 }
366}
367
368impl From<config::CryptoAlgorithm> for CryptoAlgorithm {
369 fn from(value: config::CryptoAlgorithm) -> Self {
370 match value {
371 config::CryptoAlgorithm::Encryption => CryptoAlgorithm::Encryption,
372 config::CryptoAlgorithm::Signing => CryptoAlgorithm::Signing,
373 }
374 }
375}
376
377#[cfg(test)]
378mod tests {
379 use crate::config::{CryptoAlgorithm, CryptoRule, FallbackConfig};
380 use crate::encoding::encode;
381 use crate::{Key, Processor, ProcessorConfig, RequestCookies, ResponseCookie};
382 use std::error::Error;
383
384 #[test]
385 fn roundtrip_encryption() {
386 let name = "encrypted";
387 let unencrypted_value = "tamper-proof";
388 let processor: Processor = ProcessorConfig {
389 crypto_rules: vec![CryptoRule {
390 cookie_names: vec![name.to_string()],
391 algorithm: CryptoAlgorithm::Encryption,
392 key: Key::generate(),
393 fallbacks: vec![],
394 }],
395 ..Default::default()
396 }
397 .into();
398
399 assert!(processor.will_encrypt(name));
400 assert!(!processor.will_sign(name));
401
402 let cookie = ResponseCookie::new(name, unencrypted_value);
403 let encrypted_cookie = processor.process_outgoing(cookie);
404 assert_ne!(encrypted_cookie.value(), unencrypted_value);
405 assert_eq!(
407 encode(encrypted_cookie.value()).to_string(),
408 encrypted_cookie.value()
409 );
410
411 let header = format!("{}={}", encrypted_cookie.name(), encrypted_cookie.value());
412 let request_cookies = RequestCookies::parse_header(&header, &processor)
413 .expect("Failed to parse the encrypted cookie");
414 let decrypted_cookie = request_cookies
415 .get(name)
416 .expect("Failed to get the decrypted cookie");
417
418 assert_eq!(decrypted_cookie.name(), name);
419 assert_eq!(decrypted_cookie.value(), unencrypted_value);
420 }
421
422 #[test]
423 fn roundtrip_signing() {
424 let name = "signed";
425 let value = "tamper-proof";
426 let processor: Processor = ProcessorConfig {
427 crypto_rules: vec![CryptoRule {
428 cookie_names: vec![name.to_string()],
429 algorithm: CryptoAlgorithm::Signing,
430 key: Key::generate(),
431 fallbacks: vec![],
432 }],
433 ..Default::default()
434 }
435 .into();
436
437 assert!(!processor.will_encrypt(name));
438 assert!(processor.will_sign(name));
439
440 let cookie = ResponseCookie::new(name, value);
441 let signed_cookie = processor.process_outgoing(cookie);
442 assert_ne!(signed_cookie.value(), value);
443
444 let header = format!("{}={}", signed_cookie.name(), signed_cookie.value());
445 let request_cookies = RequestCookies::parse_header(&header, &processor)
446 .expect("Failed to parse the signed cookie");
447 let verified_cookie = request_cookies
448 .get(name)
449 .expect("Failed to get the signed cookie");
450
451 assert_eq!(verified_cookie.name(), name);
452 assert_eq!(verified_cookie.value(), value);
453 }
454
455 #[test]
456 fn roundtrip_encoded() {
457 let name = "to be encoded";
458 let value = "a bunch of % very special ! # characters ;";
459 let processor: Processor = ProcessorConfig::default().into();
460
461 let cookie = ResponseCookie::new(name, value);
462 let encoded_cookie = processor.process_outgoing(cookie);
463 assert_ne!(encoded_cookie.name(), name);
464 assert_ne!(encoded_cookie.value(), value);
465
466 let header = format!("{}={}", encoded_cookie.name(), encoded_cookie.value());
467 let request_cookies = RequestCookies::parse_header(&header, &processor)
468 .expect("Failed to parse the decoded cookie");
469 let decoded_cookie = request_cookies
470 .get(name)
471 .expect("Failed to get the decoded cookie");
472
473 assert_eq!(decoded_cookie.name(), name);
474 assert_eq!(decoded_cookie.value(), value);
475 }
476
477 #[test]
478 fn unsigned_is_rejected() {
479 let name = "session";
480 let value = "a-value-that-should-be-signed-but-is-not";
481 let header = format!("{name}={value}");
482
483 let processor: Processor = ProcessorConfig {
484 crypto_rules: vec![CryptoRule {
485 cookie_names: vec![name.to_string()],
486 algorithm: CryptoAlgorithm::Signing,
487 key: Key::generate(),
488 fallbacks: vec![],
489 }],
490 ..Default::default()
491 }
492 .into();
493 let err = RequestCookies::parse_header(&header, &processor)
494 .expect_err("A non-signed cookie passed verification, bad!");
495 assert_eq!(
496 err.to_string(),
497 "Failed to parse cookies out of a header value"
498 );
499 assert_eq!(
500 err.source().unwrap().to_string(),
501 "Failed to process `session` as a signed request cookie"
502 );
503 }
504
505 #[test]
506 fn unencrypted_is_rejected() {
507 let name = "session";
508 let value = "a-value-that-should-be-encrypted-but-is-not";
509 let header = format!("{name}={value}");
510
511 let processor: Processor = ProcessorConfig {
512 crypto_rules: vec![CryptoRule {
513 cookie_names: vec![name.to_string()],
514 algorithm: CryptoAlgorithm::Encryption,
515 key: Key::generate(),
516 fallbacks: vec![],
517 }],
518 ..Default::default()
519 }
520 .into();
521 let err = RequestCookies::parse_header(&header, &processor)
522 .expect_err("A non-encrypted cookie passed, bad!");
523 assert_eq!(
524 err.to_string(),
525 "Failed to parse cookies out of a header value"
526 );
527 assert_eq!(
528 err.source().unwrap().to_string(),
529 "Failed to process `session` as an encrypted request cookie"
530 );
531 }
532
533 #[test]
534 fn signed_with_secondary_is_fine() {
535 let name = "signed";
536 let value = "tamper-proof";
537 let primary_key = Key::generate();
538 let fallbacks = vec![
539 FallbackConfig {
540 key: Key::generate(),
541 algorithm: CryptoAlgorithm::Signing,
542 },
543 FallbackConfig {
544 key: Key::generate(),
545 algorithm: CryptoAlgorithm::Signing,
546 },
547 FallbackConfig {
548 key: Key::generate(),
549 algorithm: CryptoAlgorithm::Signing,
550 },
551 ];
552 let fallback = fallbacks[1].clone();
553
554 let processor: Processor = ProcessorConfig {
555 crypto_rules: vec![CryptoRule {
556 cookie_names: vec![name.to_string()],
557 algorithm: fallback.algorithm,
558 key: fallback.key.clone(),
559 fallbacks: vec![],
560 }],
561 ..Default::default()
562 }
563 .into();
564 let cookie = ResponseCookie::new(name, value);
565 let secured_cookie = processor.process_outgoing(cookie);
567 assert_ne!(secured_cookie.value(), value);
568
569 let header = format!("{}={}", secured_cookie.name(), secured_cookie.value());
570 let processor: Processor = ProcessorConfig {
571 crypto_rules: vec![CryptoRule {
572 cookie_names: vec![name.to_string()],
573 algorithm: CryptoAlgorithm::Signing,
574 key: primary_key.clone(),
576 fallbacks,
577 }],
578 ..Default::default()
579 }
580 .into();
581 let request_cookies = RequestCookies::parse_header(&header, &processor)
582 .expect("Failed to parse the signed cookie");
583 let verified_cookie = request_cookies
584 .get(name)
585 .expect("Failed to get the signed cookie");
586
587 assert_eq!(verified_cookie.name(), name);
588 assert_eq!(verified_cookie.value(), value);
589 }
590
591 #[test]
592 fn encrypted_with_secondary_is_fine() {
593 let name = "encrypted";
594 let value = "tamper-proof";
595 let primary_key = Key::generate();
596 let fallbacks = vec![
597 FallbackConfig {
598 key: Key::generate(),
599 algorithm: CryptoAlgorithm::Signing,
600 },
601 FallbackConfig {
602 key: Key::generate(),
603 algorithm: CryptoAlgorithm::Signing,
604 },
605 FallbackConfig {
606 key: Key::generate(),
607 algorithm: CryptoAlgorithm::Signing,
608 },
609 ];
610 let fallback = fallbacks[1].clone();
611
612 let processor: Processor = ProcessorConfig {
613 crypto_rules: vec![CryptoRule {
614 cookie_names: vec![name.to_string()],
615 algorithm: fallback.algorithm,
616 key: fallback.key.clone(),
617 fallbacks: vec![],
618 }],
619 ..Default::default()
620 }
621 .into();
622 let cookie = ResponseCookie::new(name, value);
623 let secured_cookie = processor.process_outgoing(cookie);
625 assert_ne!(secured_cookie.value(), value);
626
627 let header = format!("{}={}", secured_cookie.name(), secured_cookie.value());
628 let processor: Processor = ProcessorConfig {
629 crypto_rules: vec![CryptoRule {
630 cookie_names: vec![name.to_string()],
631 algorithm: CryptoAlgorithm::Encryption,
632 key: primary_key.clone(),
634 fallbacks,
635 }],
636 ..Default::default()
637 }
638 .into();
639 let request_cookies = RequestCookies::parse_header(&header, &processor)
640 .expect("Failed to parse the encrypted cookie");
641 let decrypted_cookie = request_cookies
642 .get(name)
643 .expect("Failed to get the encrypted cookie");
644
645 assert_eq!(decrypted_cookie.name(), name);
646 assert_eq!(decrypted_cookie.value(), value);
647 }
648}