Skip to main content

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(("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(("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<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
146        let mut cookie = cookie.into();
147        self.sign_cookie(&mut cookie);
148        self.parent.borrow_mut().add(cookie);
149    }
150
151    /// Adds an "original" `cookie` to this jar. The cookie's value is signed
152    /// assuring integrity and authenticity. Adding an original cookie does not
153    /// affect the [`CookieJar::delta()`] computation. This method is intended
154    /// to be used to seed the cookie jar with cookies received from a client's
155    /// HTTP message.
156    ///
157    /// For accurate `delta` computations, this method should not be called
158    /// after calling `remove`.
159    ///
160    /// # Example
161    ///
162    /// ```rust
163    /// use cookie::{CookieJar, Cookie, Key};
164    ///
165    /// let key = Key::generate();
166    /// let mut jar = CookieJar::new();
167    /// jar.signed_mut(&key).add_original(("name", "value"));
168    ///
169    /// assert_eq!(jar.iter().count(), 1);
170    /// assert_eq!(jar.delta().count(), 0);
171    /// ```
172    pub fn add_original<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
173        let mut cookie = cookie.into();
174        self.sign_cookie(&mut cookie);
175        self.parent.borrow_mut().add_original(cookie);
176    }
177
178    /// Removes `cookie` from the parent jar.
179    ///
180    /// For correct removal, the passed in `cookie` must contain the same `path`
181    /// and `domain` as the cookie that was initially set.
182    ///
183    /// This is identical to [`CookieJar::remove()`]. See the method's
184    /// documentation for more details.
185    ///
186    /// # Example
187    ///
188    /// ```rust
189    /// use cookie::{CookieJar, Cookie, Key};
190    ///
191    /// let key = Key::generate();
192    /// let mut jar = CookieJar::new();
193    /// let mut signed_jar = jar.signed_mut(&key);
194    ///
195    /// signed_jar.add(("name", "value"));
196    /// assert!(signed_jar.get("name").is_some());
197    ///
198    /// signed_jar.remove("name");
199    /// assert!(signed_jar.get("name").is_none());
200    /// ```
201    pub fn remove<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
202        self.parent.borrow_mut().remove(cookie.into());
203    }
204}
205
206#[cfg(test)]
207mod test {
208    use crate::{CookieJar, Cookie, Key};
209
210    #[test]
211    fn simple() {
212        let key = Key::generate();
213        let mut jar = CookieJar::new();
214        assert_simple_behaviour!(jar, jar.signed_mut(&key));
215    }
216
217    #[test]
218    fn private() {
219        let key = Key::generate();
220        let mut jar = CookieJar::new();
221        assert_secure_behaviour!(jar, jar.signed_mut(&key));
222    }
223
224    #[test]
225    fn roundtrip() {
226        // Secret is SHA-256 hash of 'Super secret!' passed through HKDF-SHA256.
227        let key = Key::from(&[89, 202, 200, 125, 230, 90, 197, 245, 166, 249,
228            34, 169, 135, 31, 20, 197, 94, 154, 254, 79, 60, 26, 8, 143, 254,
229            24, 116, 138, 92, 225, 159, 60, 157, 41, 135, 129, 31, 226, 196, 16,
230            198, 168, 134, 4, 42, 1, 196, 24, 57, 103, 241, 147, 201, 185, 233,
231            10, 180, 170, 187, 89, 252, 137, 110, 107]);
232
233        let mut jar = CookieJar::new();
234        jar.add(Cookie::new("signed_with_ring014",
235                "3tdHXEQ2kf6fxC7dWzBGmpSLMtJenXLKrZ9cHkSsl1w=Tamper-proof"));
236        jar.add(Cookie::new("signed_with_ring016",
237                "3tdHXEQ2kf6fxC7dWzBGmpSLMtJenXLKrZ9cHkSsl1w=Tamper-proof"));
238
239        let signed = jar.signed(&key);
240        assert_eq!(signed.get("signed_with_ring014").unwrap().value(), "Tamper-proof");
241        assert_eq!(signed.get("signed_with_ring016").unwrap().value(), "Tamper-proof");
242    }
243
244    #[test]
245    fn issue_178() {
246        let data = "x=yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy£";
247        let c = Cookie::parse(data).expect("failed to parse cookie");
248        let key = Key::from(&[0u8; 64]);
249        let mut jar = CookieJar::new();
250        let signed = jar.signed_mut(&key);
251        assert!(signed.verify(c).is_none());
252    }
253}