requiem_http/cookie/secure/
signed.rs

1use ring::hmac::{self, sign, verify};
2
3use super::Key;
4use crate::cookie::{Cookie, CookieJar};
5
6// Keep these in sync, and keep the key len synced with the `signed` docs as
7// well as the `KEYS_INFO` const in secure::Key.
8static HMAC_DIGEST: hmac::Algorithm = hmac::HMAC_SHA256;
9const BASE64_DIGEST_LEN: usize = 44;
10pub const KEY_LEN: usize = 32;
11
12/// A child cookie jar that authenticates its cookies.
13///
14/// A _signed_ child jar signs all the cookies added to it and verifies cookies
15/// retrieved from it. Any cookies stored in a `SignedJar` are assured integrity
16/// and authenticity. In other words, clients cannot tamper with the contents of
17/// a cookie nor can they fabricate cookie values, but the data is visible in
18/// plaintext.
19///
20/// This type is only available when the `secure` feature is enabled.
21pub struct SignedJar<'a> {
22    parent: &'a mut CookieJar,
23    key: hmac::Key,
24}
25
26impl<'a> SignedJar<'a> {
27    /// Creates a new child `SignedJar` with parent `parent` and key `key`. This
28    /// method is typically called indirectly via the `signed` method of
29    /// `CookieJar`.
30    #[doc(hidden)]
31    pub fn new(parent: &'a mut CookieJar, key: &Key) -> SignedJar<'a> {
32        SignedJar {
33            parent,
34            key: hmac::Key::new(HMAC_DIGEST, key.signing()),
35        }
36    }
37
38    /// Given a signed value `str` where the signature is prepended to `value`,
39    /// verifies the signed value and returns it. If there's a problem, returns
40    /// an `Err` with a string describing the issue.
41    fn verify(&self, cookie_value: &str) -> Result<String, &'static str> {
42        if cookie_value.len() < BASE64_DIGEST_LEN {
43            return Err("length of value is <= BASE64_DIGEST_LEN");
44        }
45
46        let (digest_str, value) = cookie_value.split_at(BASE64_DIGEST_LEN);
47        let sig = base64::decode(digest_str).map_err(|_| "bad base64 digest")?;
48
49        verify(&self.key, value.as_bytes(), &sig)
50            .map(|_| value.to_string())
51            .map_err(|_| "value did not verify")
52    }
53
54    /// Returns a reference to the `Cookie` inside this jar with the name `name`
55    /// and verifies the authenticity and integrity of the cookie's value,
56    /// returning a `Cookie` with the authenticated value. If the cookie cannot
57    /// be found, or the cookie fails to verify, `None` is returned.
58    ///
59    /// # Example
60    ///
61    /// ```rust
62    /// use requiem_http::cookie::{CookieJar, Cookie, Key};
63    ///
64    /// let key = Key::generate();
65    /// let mut jar = CookieJar::new();
66    /// let mut signed_jar = jar.signed(&key);
67    /// assert!(signed_jar.get("name").is_none());
68    ///
69    /// signed_jar.add(Cookie::new("name", "value"));
70    /// assert_eq!(signed_jar.get("name").unwrap().value(), "value");
71    /// ```
72    pub fn get(&self, name: &str) -> Option<Cookie<'static>> {
73        if let Some(cookie_ref) = self.parent.get(name) {
74            let mut cookie = cookie_ref.clone();
75            if let Ok(value) = self.verify(cookie.value()) {
76                cookie.set_value(value);
77                return Some(cookie);
78            }
79        }
80
81        None
82    }
83
84    /// Adds `cookie` to the parent jar. The cookie's value is signed assuring
85    /// integrity and authenticity.
86    ///
87    /// # Example
88    ///
89    /// ```rust
90    /// use requiem_http::cookie::{CookieJar, Cookie, Key};
91    ///
92    /// let key = Key::generate();
93    /// let mut jar = CookieJar::new();
94    /// jar.signed(&key).add(Cookie::new("name", "value"));
95    ///
96    /// assert_ne!(jar.get("name").unwrap().value(), "value");
97    /// assert!(jar.get("name").unwrap().value().contains("value"));
98    /// assert_eq!(jar.signed(&key).get("name").unwrap().value(), "value");
99    /// ```
100    pub fn add(&mut self, mut cookie: Cookie<'static>) {
101        self.sign_cookie(&mut cookie);
102        self.parent.add(cookie);
103    }
104
105    /// Adds an "original" `cookie` to this jar. The cookie's value is signed
106    /// assuring integrity and authenticity. Adding an original cookie does not
107    /// affect the [`CookieJar::delta()`](struct.CookieJar.html#method.delta)
108    /// computation. This method is intended to be used to seed the cookie jar
109    /// with cookies received from a client's HTTP message.
110    ///
111    /// For accurate `delta` computations, this method should not be called
112    /// after calling `remove`.
113    ///
114    /// # Example
115    ///
116    /// ```rust
117    /// use requiem_http::cookie::{CookieJar, Cookie, Key};
118    ///
119    /// let key = Key::generate();
120    /// let mut jar = CookieJar::new();
121    /// jar.signed(&key).add_original(Cookie::new("name", "value"));
122    ///
123    /// assert_eq!(jar.iter().count(), 1);
124    /// assert_eq!(jar.delta().count(), 0);
125    /// ```
126    pub fn add_original(&mut self, mut cookie: Cookie<'static>) {
127        self.sign_cookie(&mut cookie);
128        self.parent.add_original(cookie);
129    }
130
131    /// Signs the cookie's value assuring integrity and authenticity.
132    fn sign_cookie(&self, cookie: &mut Cookie<'_>) {
133        let digest = sign(&self.key, cookie.value().as_bytes());
134        let mut new_value = base64::encode(digest.as_ref());
135        new_value.push_str(cookie.value());
136        cookie.set_value(new_value);
137    }
138
139    /// Removes `cookie` from the parent jar.
140    ///
141    /// For correct removal, the passed in `cookie` must contain the same `path`
142    /// and `domain` as the cookie that was initially set.
143    ///
144    /// See [CookieJar::remove](struct.CookieJar.html#method.remove) for more
145    /// details.
146    ///
147    /// # Example
148    ///
149    /// ```rust
150    /// use requiem_http::cookie::{CookieJar, Cookie, Key};
151    ///
152    /// let key = Key::generate();
153    /// let mut jar = CookieJar::new();
154    /// let mut signed_jar = jar.signed(&key);
155    ///
156    /// signed_jar.add(Cookie::new("name", "value"));
157    /// assert!(signed_jar.get("name").is_some());
158    ///
159    /// signed_jar.remove(Cookie::named("name"));
160    /// assert!(signed_jar.get("name").is_none());
161    /// ```
162    pub fn remove(&mut self, cookie: Cookie<'static>) {
163        self.parent.remove(cookie);
164    }
165}
166
167#[cfg(test)]
168mod test {
169    use super::{Cookie, CookieJar, Key};
170
171    #[test]
172    fn simple() {
173        let key = Key::generate();
174        let mut jar = CookieJar::new();
175        assert_simple_behaviour!(jar, jar.signed(&key));
176    }
177
178    #[test]
179    fn private() {
180        let key = Key::generate();
181        let mut jar = CookieJar::new();
182        assert_secure_behaviour!(jar, jar.signed(&key));
183    }
184}