actori_http/cookie/secure/private.rs
1use std::str;
2
3use log::warn;
4use ring::aead::{Aad, Algorithm, Nonce, AES_256_GCM};
5use ring::aead::{LessSafeKey, UnboundKey};
6use ring::rand::{SecureRandom, SystemRandom};
7
8use super::Key;
9use crate::cookie::{Cookie, CookieJar};
10
11// Keep these in sync, and keep the key len synced with the `private` docs as
12// well as the `KEYS_INFO` const in secure::Key.
13static ALGO: &Algorithm = &AES_256_GCM;
14const NONCE_LEN: usize = 12;
15pub const KEY_LEN: usize = 32;
16
17/// A child cookie jar that provides authenticated encryption for its cookies.
18///
19/// A _private_ child jar signs and encrypts all the cookies added to it and
20/// verifies and decrypts cookies retrieved from it. Any cookies stored in a
21/// `PrivateJar` are simultaneously assured confidentiality, integrity, and
22/// authenticity. In other words, clients cannot discover nor tamper with the
23/// contents of a cookie, nor can they fabricate cookie data.
24///
25/// This type is only available when the `secure` feature is enabled.
26pub struct PrivateJar<'a> {
27 parent: &'a mut CookieJar,
28 key: [u8; KEY_LEN],
29}
30
31impl<'a> PrivateJar<'a> {
32 /// Creates a new child `PrivateJar` with parent `parent` and key `key`.
33 /// This method is typically called indirectly via the `signed` method of
34 /// `CookieJar`.
35 #[doc(hidden)]
36 pub fn new(parent: &'a mut CookieJar, key: &Key) -> PrivateJar<'a> {
37 let mut key_array = [0u8; KEY_LEN];
38 key_array.copy_from_slice(key.encryption());
39 PrivateJar {
40 parent,
41 key: key_array,
42 }
43 }
44
45 /// Given a sealed value `str` and a key name `name`, where the nonce is
46 /// prepended to the original value and then both are Base64 encoded,
47 /// verifies and decrypts the sealed value and returns it. If there's a
48 /// problem, returns an `Err` with a string describing the issue.
49 fn unseal(&self, name: &str, value: &str) -> Result<String, &'static str> {
50 let mut data = base64::decode(value).map_err(|_| "bad base64 value")?;
51 if data.len() <= NONCE_LEN {
52 return Err("length of decoded data is <= NONCE_LEN");
53 }
54
55 let ad = Aad::from(name.as_bytes());
56 let key = LessSafeKey::new(
57 UnboundKey::new(&ALGO, &self.key).expect("matching key length"),
58 );
59 let (nonce, mut sealed) = data.split_at_mut(NONCE_LEN);
60 let nonce =
61 Nonce::try_assume_unique_for_key(nonce).expect("invalid length of `nonce`");
62 let unsealed = key
63 .open_in_place(nonce, ad, &mut sealed)
64 .map_err(|_| "invalid key/nonce/value: bad seal")?;
65
66 if let Ok(unsealed_utf8) = str::from_utf8(unsealed) {
67 Ok(unsealed_utf8.to_string())
68 } else {
69 warn!(
70 "Private cookie does not have utf8 content!
71It is likely the secret key used to encrypt them has been leaked.
72Please change it as soon as possible."
73 );
74 Err("bad unsealed utf8")
75 }
76 }
77
78 /// Returns a reference to the `Cookie` inside this jar with the name `name`
79 /// and authenticates and decrypts the cookie's value, returning a `Cookie`
80 /// with the decrypted value. If the cookie cannot be found, or the cookie
81 /// fails to authenticate or decrypt, `None` is returned.
82 ///
83 /// # Example
84 ///
85 /// ```rust
86 /// use actori_http::cookie::{CookieJar, Cookie, Key};
87 ///
88 /// let key = Key::generate();
89 /// let mut jar = CookieJar::new();
90 /// let mut private_jar = jar.private(&key);
91 /// assert!(private_jar.get("name").is_none());
92 ///
93 /// private_jar.add(Cookie::new("name", "value"));
94 /// assert_eq!(private_jar.get("name").unwrap().value(), "value");
95 /// ```
96 pub fn get(&self, name: &str) -> Option<Cookie<'static>> {
97 if let Some(cookie_ref) = self.parent.get(name) {
98 let mut cookie = cookie_ref.clone();
99 if let Ok(value) = self.unseal(name, cookie.value()) {
100 cookie.set_value(value);
101 return Some(cookie);
102 }
103 }
104
105 None
106 }
107
108 /// Adds `cookie` to the parent jar. The cookie's value is encrypted with
109 /// authenticated encryption assuring confidentiality, integrity, and
110 /// authenticity.
111 ///
112 /// # Example
113 ///
114 /// ```rust
115 /// use actori_http::cookie::{CookieJar, Cookie, Key};
116 ///
117 /// let key = Key::generate();
118 /// let mut jar = CookieJar::new();
119 /// jar.private(&key).add(Cookie::new("name", "value"));
120 ///
121 /// assert_ne!(jar.get("name").unwrap().value(), "value");
122 /// assert_eq!(jar.private(&key).get("name").unwrap().value(), "value");
123 /// ```
124 pub fn add(&mut self, mut cookie: Cookie<'static>) {
125 self.encrypt_cookie(&mut cookie);
126
127 // Add the sealed cookie to the parent.
128 self.parent.add(cookie);
129 }
130
131 /// Adds an "original" `cookie` to parent jar. The cookie's value is
132 /// encrypted with authenticated encryption assuring confidentiality,
133 /// integrity, and authenticity. Adding an original cookie does not affect
134 /// the [`CookieJar::delta()`](struct.CookieJar.html#method.delta)
135 /// computation. This method is intended to be used to seed the cookie jar
136 /// with cookies received from a client's HTTP message.
137 ///
138 /// For accurate `delta` computations, this method should not be called
139 /// after calling `remove`.
140 ///
141 /// # Example
142 ///
143 /// ```rust
144 /// use actori_http::cookie::{CookieJar, Cookie, Key};
145 ///
146 /// let key = Key::generate();
147 /// let mut jar = CookieJar::new();
148 /// jar.private(&key).add_original(Cookie::new("name", "value"));
149 ///
150 /// assert_eq!(jar.iter().count(), 1);
151 /// assert_eq!(jar.delta().count(), 0);
152 /// ```
153 pub fn add_original(&mut self, mut cookie: Cookie<'static>) {
154 self.encrypt_cookie(&mut cookie);
155
156 // Add the sealed cookie to the parent.
157 self.parent.add_original(cookie);
158 }
159
160 /// Encrypts the cookie's value with
161 /// authenticated encryption assuring confidentiality, integrity, and authenticity.
162 fn encrypt_cookie(&self, cookie: &mut Cookie<'_>) {
163 let name = cookie.name().as_bytes();
164 let value = cookie.value().as_bytes();
165 let data = encrypt_name_value(name, value, &self.key);
166
167 // Base64 encode the nonce and encrypted value.
168 let sealed_value = base64::encode(&data);
169 cookie.set_value(sealed_value);
170 }
171
172 /// Removes `cookie` from the parent jar.
173 ///
174 /// For correct removal, the passed in `cookie` must contain the same `path`
175 /// and `domain` as the cookie that was initially set.
176 ///
177 /// See [CookieJar::remove](struct.CookieJar.html#method.remove) for more
178 /// details.
179 ///
180 /// # Example
181 ///
182 /// ```rust
183 /// use actori_http::cookie::{CookieJar, Cookie, Key};
184 ///
185 /// let key = Key::generate();
186 /// let mut jar = CookieJar::new();
187 /// let mut private_jar = jar.private(&key);
188 ///
189 /// private_jar.add(Cookie::new("name", "value"));
190 /// assert!(private_jar.get("name").is_some());
191 ///
192 /// private_jar.remove(Cookie::named("name"));
193 /// assert!(private_jar.get("name").is_none());
194 /// ```
195 pub fn remove(&mut self, cookie: Cookie<'static>) {
196 self.parent.remove(cookie);
197 }
198}
199
200fn encrypt_name_value(name: &[u8], value: &[u8], key: &[u8]) -> Vec<u8> {
201 // Create the `SealingKey` structure.
202 let unbound = UnboundKey::new(&ALGO, key).expect("matching key length");
203 let key = LessSafeKey::new(unbound);
204
205 // Create a vec to hold the [nonce | cookie value | overhead].
206 let mut data = vec![0; NONCE_LEN + value.len() + ALGO.tag_len()];
207
208 // Randomly generate the nonce, then copy the cookie value as input.
209 let (nonce, in_out) = data.split_at_mut(NONCE_LEN);
210 let (in_out, tag) = in_out.split_at_mut(value.len());
211 in_out.copy_from_slice(value);
212
213 // Randomly generate the nonce into the nonce piece.
214 SystemRandom::new()
215 .fill(nonce)
216 .expect("couldn't random fill nonce");
217 let nonce = Nonce::try_assume_unique_for_key(nonce).expect("invalid `nonce` length");
218
219 // Use cookie's name as associated data to prevent value swapping.
220 let ad = Aad::from(name);
221 let ad_tag = key
222 .seal_in_place_separate_tag(nonce, ad, in_out)
223 .expect("in-place seal");
224
225 // Copy the tag into the tag piece.
226 tag.copy_from_slice(ad_tag.as_ref());
227
228 // Remove the overhead and return the sealed content.
229 data
230}
231
232#[cfg(test)]
233mod test {
234 use super::{encrypt_name_value, Cookie, CookieJar, Key};
235
236 #[test]
237 fn simple() {
238 let key = Key::generate();
239 let mut jar = CookieJar::new();
240 assert_simple_behaviour!(jar, jar.private(&key));
241 }
242
243 #[test]
244 fn private() {
245 let key = Key::generate();
246 let mut jar = CookieJar::new();
247 assert_secure_behaviour!(jar, jar.private(&key));
248 }
249
250 #[test]
251 fn non_utf8() {
252 let key = Key::generate();
253 let mut jar = CookieJar::new();
254
255 let name = "malicious";
256 let mut assert_non_utf8 = |value: &[u8]| {
257 let sealed = encrypt_name_value(name.as_bytes(), value, &key.encryption());
258 let encoded = base64::encode(&sealed);
259 assert_eq!(
260 jar.private(&key).unseal(name, &encoded),
261 Err("bad unsealed utf8")
262 );
263 jar.add(Cookie::new(name, encoded));
264 assert_eq!(jar.private(&key).get(name), None);
265 };
266
267 assert_non_utf8(&[0x72, 0xfb, 0xdf, 0x74]); // rûst in ISO/IEC 8859-1
268
269 let mut malicious =
270 String::from(r#"{"id":"abc123??%X","admin":true}"#).into_bytes();
271 malicious[8] |= 0b1100_0000;
272 malicious[9] |= 0b1100_0000;
273 assert_non_utf8(&malicious);
274 }
275}