cookie/secure/signed.rs
1use std::convert::TryInto;
2use std::borrow::{Borrow, BorrowMut};
3
4use sha2::Sha256;
5use hmac::{Hmac, Mac};
6
7use crate::secure::{base64, Key};
8use crate::{Cookie, CookieJar};
9
10// Keep these in sync, and keep the key len synced with the `signed` docs as
11// well as the `KEYS_INFO` const in secure::Key.
12pub(crate) const BASE64_DIGEST_LEN: usize = 44;
13pub(crate) const KEY_LEN: usize = 32;
14
15/// A child cookie jar that authenticates its cookies.
16///
17/// A _signed_ child jar signs all the cookies added to it and verifies cookies
18/// retrieved from it. Any cookies stored in a `SignedJar` are provided
19/// integrity and authenticity. In other words, clients cannot tamper with the
20/// contents of a cookie nor can they fabricate cookie values, but the data is
21/// visible in plaintext.
22#[cfg_attr(all(nightly, doc), doc(cfg(feature = "signed")))]
23pub struct SignedJar<J> {
24 parent: J,
25 key: [u8; KEY_LEN],
26}
27
28impl<J> SignedJar<J> {
29 /// Creates a new child `SignedJar` with parent `parent` and key `key`. This
30 /// method is typically called indirectly via the `signed{_mut}` methods of
31 /// `CookieJar`.
32 pub(crate) fn new(parent: J, key: &Key) -> SignedJar<J> {
33 SignedJar { parent, key: key.signing().try_into().expect("sign key len") }
34 }
35
36 /// Signs the cookie's value providing integrity and authenticity.
37 fn sign_cookie(&self, cookie: &mut Cookie) {
38 // Compute HMAC-SHA256 of the cookie's value.
39 let mut mac = Hmac::<Sha256>::new_from_slice(&self.key).expect("good key");
40 mac.update(cookie.value().as_bytes());
41
42 // Cookie's new value is [MAC | original-value].
43 let mut new_value = base64::encode(&mac.finalize().into_bytes());
44 new_value.push_str(cookie.value());
45 cookie.set_value(new_value);
46 }
47
48 /// Given a signed value `str` where the signature is prepended to `value`,
49 /// verifies the signed value and returns it. If there's a problem, returns
50 /// an `Err` with a string describing the issue.
51 fn _verify(&self, cookie_value: &str) -> Result<String, &'static str> {
52 if !cookie_value.is_char_boundary(BASE64_DIGEST_LEN) {
53 return Err("missing or invalid digest");
54 }
55
56 // Split [MAC | original-value] into its two parts.
57 let (digest_str, value) = cookie_value.split_at(BASE64_DIGEST_LEN);
58 let digest = base64::decode(digest_str).map_err(|_| "bad base64 digest")?;
59
60 // Perform the verification.
61 let mut mac = Hmac::<Sha256>::new_from_slice(&self.key).expect("good key");
62 mac.update(value.as_bytes());
63 mac.verify_slice(&digest)
64 .map(|_| value.to_string())
65 .map_err(|_| "value did not verify")
66 }
67
68 /// Verifies the authenticity and integrity of `cookie`, returning the
69 /// plaintext version if verification succeeds or `None` otherwise.
70 /// Verification _always_ succeeds if `cookie` was generated by a
71 /// `SignedJar` with the same key as `self`.
72 ///
73 /// # Example
74 ///
75 /// ```rust
76 /// use cookie::{CookieJar, Cookie, Key};
77 ///
78 /// let key = Key::generate();
79 /// let mut jar = CookieJar::new();
80 /// assert!(jar.signed(&key).get("name").is_none());
81 ///
82 /// jar.signed_mut(&key).add(Cookie::new("name", "value"));
83 /// assert_eq!(jar.signed(&key).get("name").unwrap().value(), "value");
84 ///
85 /// let plain = jar.get("name").cloned().unwrap();
86 /// assert_ne!(plain.value(), "value");
87 /// let verified = jar.signed(&key).verify(plain).unwrap();
88 /// assert_eq!(verified.value(), "value");
89 ///
90 /// let plain = Cookie::new("plaintext", "hello");
91 /// assert!(jar.signed(&key).verify(plain).is_none());
92 /// ```
93 pub fn verify(&self, mut cookie: Cookie<'static>) -> Option<Cookie<'static>> {
94 if let Ok(value) = self._verify(cookie.value()) {
95 cookie.set_value(value);
96 return Some(cookie);
97 }
98
99 None
100 }
101}
102
103impl<J: Borrow<CookieJar>> SignedJar<J> {
104 /// Returns a reference to the `Cookie` inside this jar with the name `name`
105 /// and verifies the authenticity and integrity of the cookie's value,
106 /// returning a `Cookie` with the authenticated value. If the cookie cannot
107 /// be found, or the cookie fails to verify, `None` is returned.
108 ///
109 /// # Example
110 ///
111 /// ```rust
112 /// use cookie::{CookieJar, Cookie, Key};
113 ///
114 /// let key = Key::generate();
115 /// let jar = CookieJar::new();
116 /// assert!(jar.signed(&key).get("name").is_none());
117 ///
118 /// let mut jar = jar;
119 /// let mut signed_jar = jar.signed_mut(&key);
120 /// signed_jar.add(Cookie::new("name", "value"));
121 /// assert_eq!(signed_jar.get("name").unwrap().value(), "value");
122 /// ```
123 pub fn get(&self, name: &str) -> Option<Cookie<'static>> {
124 self.parent.borrow().get(name).and_then(|c| self.verify(c.clone()))
125 }
126}
127
128impl<J: BorrowMut<CookieJar>> SignedJar<J> {
129 /// Adds `cookie` to the parent jar. The cookie's value is signed assuring
130 /// integrity and authenticity.
131 ///
132 /// # Example
133 ///
134 /// ```rust
135 /// use cookie::{CookieJar, Cookie, Key};
136 ///
137 /// let key = Key::generate();
138 /// let mut jar = CookieJar::new();
139 /// jar.signed_mut(&key).add(Cookie::new("name", "value"));
140 ///
141 /// assert_ne!(jar.get("name").unwrap().value(), "value");
142 /// assert!(jar.get("name").unwrap().value().contains("value"));
143 /// assert_eq!(jar.signed(&key).get("name").unwrap().value(), "value");
144 /// ```
145 pub fn add(&mut self, mut cookie: Cookie<'static>) {
146 self.sign_cookie(&mut cookie);
147 self.parent.borrow_mut().add(cookie);
148 }
149
150 /// Adds an "original" `cookie` to this jar. The cookie's value is signed
151 /// assuring integrity and authenticity. Adding an original cookie does not
152 /// affect the [`CookieJar::delta()`] computation. This method is intended
153 /// to be used to seed the cookie jar with cookies received from a client's
154 /// HTTP message.
155 ///
156 /// For accurate `delta` computations, this method should not be called
157 /// after calling `remove`.
158 ///
159 /// # Example
160 ///
161 /// ```rust
162 /// use cookie::{CookieJar, Cookie, Key};
163 ///
164 /// let key = Key::generate();
165 /// let mut jar = CookieJar::new();
166 /// jar.signed_mut(&key).add_original(Cookie::new("name", "value"));
167 ///
168 /// assert_eq!(jar.iter().count(), 1);
169 /// assert_eq!(jar.delta().count(), 0);
170 /// ```
171 pub fn add_original(&mut self, mut cookie: Cookie<'static>) {
172 self.sign_cookie(&mut cookie);
173 self.parent.borrow_mut().add_original(cookie);
174 }
175
176 /// Removes `cookie` from the parent jar.
177 ///
178 /// For correct removal, the passed in `cookie` must contain the same `path`
179 /// and `domain` as the cookie that was initially set.
180 ///
181 /// This is identical to [`CookieJar::remove()`]. See the method's
182 /// documentation for more details.
183 ///
184 /// # Example
185 ///
186 /// ```rust
187 /// use cookie::{CookieJar, Cookie, Key};
188 ///
189 /// let key = Key::generate();
190 /// let mut jar = CookieJar::new();
191 /// let mut signed_jar = jar.signed_mut(&key);
192 ///
193 /// signed_jar.add(Cookie::new("name", "value"));
194 /// assert!(signed_jar.get("name").is_some());
195 ///
196 /// signed_jar.remove(Cookie::named("name"));
197 /// assert!(signed_jar.get("name").is_none());
198 /// ```
199 pub fn remove(&mut self, cookie: Cookie<'static>) {
200 self.parent.borrow_mut().remove(cookie);
201 }
202}
203
204#[cfg(test)]
205mod test {
206 use crate::{CookieJar, Cookie, Key};
207
208 #[test]
209 fn simple() {
210 let key = Key::generate();
211 let mut jar = CookieJar::new();
212 assert_simple_behaviour!(jar, jar.signed_mut(&key));
213 }
214
215 #[test]
216 fn private() {
217 let key = Key::generate();
218 let mut jar = CookieJar::new();
219 assert_secure_behaviour!(jar, jar.signed_mut(&key));
220 }
221
222 #[test]
223 fn roundtrip() {
224 // Secret is SHA-256 hash of 'Super secret!' passed through HKDF-SHA256.
225 let key = Key::from(&[89, 202, 200, 125, 230, 90, 197, 245, 166, 249,
226 34, 169, 135, 31, 20, 197, 94, 154, 254, 79, 60, 26, 8, 143, 254,
227 24, 116, 138, 92, 225, 159, 60, 157, 41, 135, 129, 31, 226, 196, 16,
228 198, 168, 134, 4, 42, 1, 196, 24, 57, 103, 241, 147, 201, 185, 233,
229 10, 180, 170, 187, 89, 252, 137, 110, 107]);
230
231 let mut jar = CookieJar::new();
232 jar.add(Cookie::new("signed_with_ring014",
233 "3tdHXEQ2kf6fxC7dWzBGmpSLMtJenXLKrZ9cHkSsl1w=Tamper-proof"));
234 jar.add(Cookie::new("signed_with_ring016",
235 "3tdHXEQ2kf6fxC7dWzBGmpSLMtJenXLKrZ9cHkSsl1w=Tamper-proof"));
236
237 let signed = jar.signed(&key);
238 assert_eq!(signed.get("signed_with_ring014").unwrap().value(), "Tamper-proof");
239 assert_eq!(signed.get("signed_with_ring016").unwrap().value(), "Tamper-proof");
240 }
241
242 #[test]
243 fn issue_178() {
244 let data = "x=yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy£";
245 let c = Cookie::parse(data).expect("failed to parse cookie");
246 let key = Key::from(&[0u8; 64]);
247 let mut jar = CookieJar::new();
248 let signed = jar.signed_mut(&key);
249 assert!(signed.verify(c).is_none());
250 }
251}