1use crate::cookies::Cookie;
28use crate::error::{Error, Result};
29use crate::request::Request;
30use crate::response::Response;
31use base64::engine::general_purpose::URL_SAFE_NO_PAD;
32use base64::Engine;
33use hmac::{Hmac, KeyInit, Mac};
34use sha2::Sha256;
35
36type HmacSha256 = Hmac<Sha256>;
37
38#[derive(Clone)]
40pub struct CookieSigner {
41 key: Vec<u8>,
42 previous: Vec<Vec<u8>>,
43}
44
45impl std::fmt::Debug for CookieSigner {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 f.debug_struct("CookieSigner").finish_non_exhaustive()
48 }
49}
50
51impl CookieSigner {
52 pub fn new(key: &[u8]) -> Result<Self> {
55 if key.len() < 32 {
56 return Err(Error::internal(
57 "Cookie signing key must be at least 32 bytes",
58 ));
59 }
60 Ok(Self {
61 key: key.to_vec(),
62 previous: Vec::new(),
63 })
64 }
65
66 pub fn previous_key(mut self, key: &[u8]) -> Result<Self> {
68 if key.len() < 32 {
69 return Err(Error::internal(
70 "Cookie signing key must be at least 32 bytes",
71 ));
72 }
73 self.previous.push(key.to_vec());
74 Ok(self)
75 }
76
77 fn mac(key: &[u8], name: &str, value: &str) -> HmacSha256 {
78 let mut m = HmacSha256::new_from_slice(key).expect("HMAC takes any key length");
80 m.update(name.as_bytes());
81 m.update(&[0]); m.update(value.as_bytes());
83 m
84 }
85
86 pub fn sign(&self, name: &str, value: &str) -> String {
88 let tag = Self::mac(&self.key, name, value).finalize().into_bytes();
89 format!("{}.{}", value, URL_SAFE_NO_PAD.encode(tag))
90 }
91
92 pub fn verify(&self, name: &str, signed: &str) -> Option<String> {
94 let (value, tag) = signed.rsplit_once('.')?;
95 let tag = URL_SAFE_NO_PAD.decode(tag).ok()?;
96 std::iter::once(&self.key)
97 .chain(&self.previous)
98 .any(|key| Self::mac(key, name, value).verify_slice(&tag).is_ok())
99 .then(|| value.to_string())
100 }
101}
102
103impl Response {
104 pub fn signed_cookie(self, signer: &CookieSigner, cookie: Cookie) -> Self {
106 let signed = signer.sign(cookie.name(), cookie.value());
107 self.cookie(cookie.with_value(signed))
108 }
109}
110
111impl Request {
112 pub fn signed_cookie(&self, signer: &CookieSigner, name: &str) -> Option<String> {
115 signer.verify(name, self.cookies().get(name)?)
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 const KEY: &[u8] = b"0123456789abcdef0123456789abcdef";
124
125 #[test]
126 fn roundtrip_and_tampering() {
127 let s = CookieSigner::new(KEY).unwrap();
128 let signed = s.sign("c", "v.with.dots");
129 assert_eq!(s.verify("c", &signed).as_deref(), Some("v.with.dots"));
130 assert_eq!(s.verify("c", &signed.replace('v', "w")), None);
131 assert_eq!(s.verify("other", &signed), None);
132 assert_eq!(s.verify("c", "nosignature"), None);
133 assert_eq!(s.verify("c", "value.!!!"), None);
134 assert_eq!(s.verify("c", ""), None);
135 let other = CookieSigner::new(b"ffffffffffffffffffffffffffffffff").unwrap();
137 assert_eq!(other.verify("c", &signed), None);
138 }
139
140 #[test]
141 fn name_and_value_boundaries_are_unambiguous() {
142 let s = CookieSigner::new(KEY).unwrap();
143 let signed = s.sign("ab", "c");
145 let tag = signed.rsplit_once('.').unwrap().1;
146 assert_eq!(s.verify("a", &format!("bc.{tag}")), None);
147 }
148
149 #[test]
150 fn short_keys_are_refused() {
151 assert!(CookieSigner::new(b"short").is_err());
152 assert!(CookieSigner::new(KEY)
153 .unwrap()
154 .previous_key(b"short")
155 .is_err());
156 }
157
158 #[test]
159 fn rotation_verifies_old_cookies_and_signs_with_the_new_key() {
160 let old = CookieSigner::new(KEY).unwrap();
161 let old_cookie = old.sign("c", "v");
162 let new_key = b"ffffffffffffffffffffffffffffffff";
163 let rotated = CookieSigner::new(new_key)
164 .unwrap()
165 .previous_key(KEY)
166 .unwrap();
167 assert_eq!(rotated.verify("c", &old_cookie).as_deref(), Some("v"));
168 let fresh = rotated.sign("c", "v");
169 assert_ne!(fresh, old_cookie);
170 let only_new = CookieSigner::new(new_key).unwrap();
172 assert_eq!(only_new.verify("c", &old_cookie), None);
173 assert_eq!(only_new.verify("c", &fresh).as_deref(), Some("v"));
174 }
175}