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}