diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /third_party/rust/cookie/src/secure | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/cookie/src/secure')
-rw-r--r-- | third_party/rust/cookie/src/secure/key.rs | 301 | ||||
-rw-r--r-- | third_party/rust/cookie/src/secure/macros.rs | 49 | ||||
-rw-r--r-- | third_party/rust/cookie/src/secure/mod.rs | 14 | ||||
-rw-r--r-- | third_party/rust/cookie/src/secure/private.rs | 264 | ||||
-rw-r--r-- | third_party/rust/cookie/src/secure/signed.rs | 251 |
5 files changed, 879 insertions, 0 deletions
diff --git a/third_party/rust/cookie/src/secure/key.rs b/third_party/rust/cookie/src/secure/key.rs new file mode 100644 index 0000000000..9c2228eb3a --- /dev/null +++ b/third_party/rust/cookie/src/secure/key.rs @@ -0,0 +1,301 @@ +use std::convert::TryFrom; + +const SIGNING_KEY_LEN: usize = 32; +const ENCRYPTION_KEY_LEN: usize = 32; +const COMBINED_KEY_LENGTH: usize = SIGNING_KEY_LEN + ENCRYPTION_KEY_LEN; + +// Statically ensure the numbers above are in-sync. +#[cfg(feature = "signed")] +const_assert!(crate::secure::signed::KEY_LEN == SIGNING_KEY_LEN); +#[cfg(feature = "private")] +const_assert!(crate::secure::private::KEY_LEN == ENCRYPTION_KEY_LEN); + +/// A cryptographic master key for use with `Signed` and/or `Private` jars. +/// +/// This structure encapsulates secure, cryptographic keys for use with both +/// [`PrivateJar`](crate::PrivateJar) and [`SignedJar`](crate::SignedJar). A +/// single instance of a `Key` can be used for both a `PrivateJar` and a +/// `SignedJar` simultaneously with no notable security implications. +#[cfg_attr(all(nightly, doc), doc(cfg(any(feature = "private", feature = "signed"))))] +#[derive(Clone)] +pub struct Key([u8; COMBINED_KEY_LENGTH /* SIGNING | ENCRYPTION */]); + +impl PartialEq for Key { + fn eq(&self, other: &Self) -> bool { + use subtle::ConstantTimeEq; + + self.0.ct_eq(&other.0).into() + } +} + +impl Key { + // An empty key structure, to be filled. + const fn zero() -> Self { + Key([0; COMBINED_KEY_LENGTH]) + } + + /// Creates a new `Key` from a 512-bit cryptographically random string. + /// + /// The supplied key must be at least 512-bits (64 bytes). For security, the + /// master key _must_ be cryptographically random. + /// + /// # Panics + /// + /// Panics if `key` is less than 64 bytes in length. + /// + /// For a non-panicking version, use [`Key::try_from()`] or generate a key with + /// [`Key::generate()`] or [`Key::try_generate()`]. + /// + /// # Example + /// + /// ```rust + /// use cookie::Key; + /// + /// # /* + /// let key = { /* a cryptographically random key >= 64 bytes */ }; + /// # */ + /// # let key: &Vec<u8> = &(0..64).collect(); + /// + /// let key = Key::from(key); + /// ``` + #[inline] + pub fn from(key: &[u8]) -> Key { + Key::try_from(key).unwrap() + } + + /// Derives new signing/encryption keys from a master key. + /// + /// The master key must be at least 256-bits (32 bytes). For security, the + /// master key _must_ be cryptographically random. The keys are derived + /// deterministically from the master key. + /// + /// # Panics + /// + /// Panics if `key` is less than 32 bytes in length. + /// + /// # Example + /// + /// ```rust + /// use cookie::Key; + /// + /// # /* + /// let master_key = { /* a cryptographically random key >= 32 bytes */ }; + /// # */ + /// # let master_key: &Vec<u8> = &(0..32).collect(); + /// + /// let key = Key::derive_from(master_key); + /// ``` + #[cfg(feature = "key-expansion")] + #[cfg_attr(all(nightly, doc), doc(cfg(feature = "key-expansion")))] + pub fn derive_from(master_key: &[u8]) -> Self { + if master_key.len() < 32 { + panic!("bad master key length: expected >= 32 bytes, found {}", master_key.len()); + } + + // Expand the master key into two HKDF generated keys. + const KEYS_INFO: &[u8] = b"COOKIE;SIGNED:HMAC-SHA256;PRIVATE:AEAD-AES-256-GCM"; + let mut both_keys = [0; COMBINED_KEY_LENGTH]; + let hk = hkdf::Hkdf::<sha2::Sha256>::from_prk(master_key).expect("key length prechecked"); + hk.expand(KEYS_INFO, &mut both_keys).expect("expand into keys"); + Key::from(&both_keys) + } + + /// Generates signing/encryption keys from a secure, random source. Keys are + /// generated nondeterministically. + /// + /// # Panics + /// + /// Panics if randomness cannot be retrieved from the operating system. See + /// [`Key::try_generate()`] for a non-panicking version. + /// + /// # Example + /// + /// ```rust + /// use cookie::Key; + /// + /// let key = Key::generate(); + /// ``` + pub fn generate() -> Key { + Self::try_generate().expect("failed to generate `Key` from randomness") + } + + /// Attempts to generate signing/encryption keys from a secure, random + /// source. Keys are generated nondeterministically. If randomness cannot be + /// retrieved from the underlying operating system, returns `None`. + /// + /// # Example + /// + /// ```rust + /// use cookie::Key; + /// + /// let key = Key::try_generate(); + /// ``` + pub fn try_generate() -> Option<Key> { + use crate::secure::rand::RngCore; + + let mut rng = crate::secure::rand::thread_rng(); + let mut key = Key::zero(); + rng.try_fill_bytes(&mut key.0).ok()?; + Some(key) + } + + /// Returns the raw bytes of a key suitable for signing cookies. Guaranteed + /// to be at least 32 bytes. + /// + /// # Example + /// + /// ```rust + /// use cookie::Key; + /// + /// let key = Key::generate(); + /// let signing_key = key.signing(); + /// ``` + pub fn signing(&self) -> &[u8] { + &self.0[..SIGNING_KEY_LEN] + } + + /// Returns the raw bytes of a key suitable for encrypting cookies. + /// Guaranteed to be at least 32 bytes. + /// + /// # Example + /// + /// ```rust + /// use cookie::Key; + /// + /// let key = Key::generate(); + /// let encryption_key = key.encryption(); + /// ``` + pub fn encryption(&self) -> &[u8] { + &self.0[SIGNING_KEY_LEN..] + } + + /// Returns the raw bytes of the master key. Guaranteed to be at least 64 + /// bytes. + /// + /// # Example + /// + /// ```rust + /// use cookie::Key; + /// + /// let key = Key::generate(); + /// let master_key = key.master(); + /// ``` + pub fn master(&self) -> &[u8] { + &self.0 + } +} + +/// An error indicating an issue with generating or constructing a key. +#[cfg_attr(all(nightly, doc), doc(cfg(any(feature = "private", feature = "signed"))))] +#[derive(Debug)] +#[non_exhaustive] +pub enum KeyError { + /// Too few bytes (`.0`) were provided to generate a key. + /// + /// See [`Key::from()`] for minimum requirements. + TooShort(usize), +} + +impl std::error::Error for KeyError { } + +impl std::fmt::Display for KeyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + KeyError::TooShort(n) => { + write!(f, "key material is too short: expected >= {} bytes, got {} bytes", + COMBINED_KEY_LENGTH, n) + } + } + } +} + +impl TryFrom<&[u8]> for Key { + type Error = KeyError; + + /// A fallible version of [`Key::from()`]. + /// + /// Succeeds when [`Key::from()`] succeds and returns an error where + /// [`Key::from()`] panics, namely, if `key` is too short. + /// + /// # Example + /// + /// ```rust + /// # use std::convert::TryFrom; + /// use cookie::Key; + /// + /// # /* + /// let key = { /* a cryptographically random key >= 64 bytes */ }; + /// # */ + /// # let key: &Vec<u8> = &(0..64).collect(); + /// # let key: &[u8] = &key[..]; + /// assert!(Key::try_from(key).is_ok()); + /// + /// // A key that's far too short to use. + /// let key = &[1, 2, 3, 4][..]; + /// assert!(Key::try_from(key).is_err()); + /// ``` + fn try_from(key: &[u8]) -> Result<Self, Self::Error> { + if key.len() < COMBINED_KEY_LENGTH { + Err(KeyError::TooShort(key.len())) + } else { + let mut output = Key::zero(); + output.0.copy_from_slice(&key[..COMBINED_KEY_LENGTH]); + Ok(output) + } + } +} + +#[cfg(test)] +mod test { + use super::Key; + + #[test] + fn from_works() { + let key = Key::from(&(0..64).collect::<Vec<_>>()); + + let signing: Vec<u8> = (0..32).collect(); + assert_eq!(key.signing(), &*signing); + + let encryption: Vec<u8> = (32..64).collect(); + assert_eq!(key.encryption(), &*encryption); + } + + #[test] + fn try_from_works() { + use core::convert::TryInto; + let data = (0..64).collect::<Vec<_>>(); + let key_res: Result<Key, _> = data[0..63].try_into(); + assert!(key_res.is_err()); + + let key_res: Result<Key, _> = data.as_slice().try_into(); + assert!(key_res.is_ok()); + } + + #[test] + #[cfg(feature = "key-expansion")] + fn deterministic_derive() { + let master_key: Vec<u8> = (0..32).collect(); + + let key_a = Key::derive_from(&master_key); + let key_b = Key::derive_from(&master_key); + + assert_eq!(key_a.signing(), key_b.signing()); + assert_eq!(key_a.encryption(), key_b.encryption()); + assert_ne!(key_a.encryption(), key_a.signing()); + + let master_key_2: Vec<u8> = (32..64).collect(); + let key_2 = Key::derive_from(&master_key_2); + + assert_ne!(key_2.signing(), key_a.signing()); + assert_ne!(key_2.encryption(), key_a.encryption()); + } + + #[test] + fn non_deterministic_generate() { + let key_a = Key::generate(); + let key_b = Key::generate(); + + assert_ne!(key_a.signing(), key_b.signing()); + assert_ne!(key_a.encryption(), key_b.encryption()); + } +} diff --git a/third_party/rust/cookie/src/secure/macros.rs b/third_party/rust/cookie/src/secure/macros.rs new file mode 100644 index 0000000000..dfbddcf928 --- /dev/null +++ b/third_party/rust/cookie/src/secure/macros.rs @@ -0,0 +1,49 @@ +#[cfg(test)] +macro_rules! assert_simple_behaviour { + ($clear:expr, $secure:expr) => ({ + assert_eq!($clear.iter().count(), 0); + + $secure.add(Cookie::new("name", "val")); + assert_eq!($clear.iter().count(), 1); + assert_eq!($secure.get("name").unwrap().value(), "val"); + assert_ne!($clear.get("name").unwrap().value(), "val"); + + $secure.add(Cookie::new("another", "two")); + assert_eq!($clear.iter().count(), 2); + + $clear.remove(Cookie::named("another")); + assert_eq!($clear.iter().count(), 1); + + $secure.remove(Cookie::named("name")); + assert_eq!($clear.iter().count(), 0); + }) +} + +#[cfg(test)] +macro_rules! assert_secure_behaviour { + ($clear:expr, $secure:expr) => ({ + $secure.add(Cookie::new("secure", "secure")); + assert!($clear.get("secure").unwrap().value() != "secure"); + assert!($secure.get("secure").unwrap().value() == "secure"); + + let mut cookie = $clear.get("secure").unwrap().clone(); + let new_val = format!("{}l", cookie.value()); + cookie.set_value(new_val); + $clear.add(cookie); + assert!($secure.get("secure").is_none()); + + let mut cookie = $clear.get("secure").unwrap().clone(); + cookie.set_value("foobar"); + $clear.add(cookie); + assert!($secure.get("secure").is_none()); + }) +} + +// This is courtesty of `static_assertions`. That library is Copyright (c) 2017 +// Nikolai Vazquez. See https://github.com/nvzqz/static-assertions-rs for more. +macro_rules! const_assert { + ($x:expr $(,)?) => { + #[allow(unknown_lints, clippy::eq_op)] + const _: [(); 0 - !{ const ASSERT: bool = $x; ASSERT } as usize] = []; + }; +} diff --git a/third_party/rust/cookie/src/secure/mod.rs b/third_party/rust/cookie/src/secure/mod.rs new file mode 100644 index 0000000000..ca066888e7 --- /dev/null +++ b/third_party/rust/cookie/src/secure/mod.rs @@ -0,0 +1,14 @@ +extern crate rand; +extern crate base64; + +#[macro_use] +mod macros; +mod key; + +pub use self::key::*; + +#[cfg(feature = "private")] mod private; +#[cfg(feature = "private")] pub use self::private::*; + +#[cfg(feature = "signed")] mod signed; +#[cfg(feature = "signed")] pub use self::signed::*; diff --git a/third_party/rust/cookie/src/secure/private.rs b/third_party/rust/cookie/src/secure/private.rs new file mode 100644 index 0000000000..1264c3983c --- /dev/null +++ b/third_party/rust/cookie/src/secure/private.rs @@ -0,0 +1,264 @@ +extern crate aes_gcm; + +use std::convert::TryInto; +use std::borrow::{Borrow, BorrowMut}; + +use crate::secure::{base64, rand, Key}; +use crate::{Cookie, CookieJar}; + +use self::aes_gcm::aead::{generic_array::GenericArray, Aead, AeadInPlace, KeyInit, Payload}; +use self::aes_gcm::Aes256Gcm; +use self::rand::RngCore; + +// Keep these in sync, and keep the key len synced with the `private` docs as +// well as the `KEYS_INFO` const in secure::Key. +pub(crate) const NONCE_LEN: usize = 12; +pub(crate) const TAG_LEN: usize = 16; +pub(crate) const KEY_LEN: usize = 32; + +/// A child cookie jar that provides authenticated encryption for its cookies. +/// +/// A _private_ child jar signs and encrypts all the cookies added to it and +/// verifies and decrypts cookies retrieved from it. Any cookies stored in a +/// `PrivateJar` are simultaneously assured confidentiality, integrity, and +/// authenticity. In other words, clients cannot discover nor tamper with the +/// contents of a cookie, nor can they fabricate cookie data. +#[cfg_attr(all(nightly, doc), doc(cfg(feature = "private")))] +pub struct PrivateJar<J> { + parent: J, + key: [u8; KEY_LEN] +} + +impl<J> PrivateJar<J> { + /// Creates a new child `PrivateJar` with parent `parent` and key `key`. + /// This method is typically called indirectly via the `signed` method of + /// `CookieJar`. + pub(crate) fn new(parent: J, key: &Key) -> PrivateJar<J> { + PrivateJar { parent, key: key.encryption().try_into().expect("enc key len") } + } + + /// Encrypts the cookie's value with authenticated encryption providing + /// confidentiality, integrity, and authenticity. + fn encrypt_cookie(&self, cookie: &mut Cookie) { + // Create a vec to hold the [nonce | cookie value | tag]. + let cookie_val = cookie.value().as_bytes(); + let mut data = vec![0; NONCE_LEN + cookie_val.len() + TAG_LEN]; + + // Split data into three: nonce, input/output, tag. Copy input. + let (nonce, in_out) = data.split_at_mut(NONCE_LEN); + let (in_out, tag) = in_out.split_at_mut(cookie_val.len()); + in_out.copy_from_slice(cookie_val); + + // Fill nonce piece with random data. + let mut rng = self::rand::thread_rng(); + rng.try_fill_bytes(nonce).expect("couldn't random fill nonce"); + let nonce = GenericArray::clone_from_slice(nonce); + + // Perform the actual sealing operation, using the cookie's name as + // associated data to prevent value swapping. + let aad = cookie.name().as_bytes(); + let aead = Aes256Gcm::new(GenericArray::from_slice(&self.key)); + let aad_tag = aead.encrypt_in_place_detached(&nonce, aad, in_out) + .expect("encryption failure!"); + + // Copy the tag into the tag piece. + tag.copy_from_slice(&aad_tag); + + // Base64 encode [nonce | encrypted value | tag]. + cookie.set_value(base64::encode(&data)); + } + + /// Given a sealed value `str` and a key name `name`, where the nonce is + /// prepended to the original value and then both are Base64 encoded, + /// verifies and decrypts the sealed value and returns it. If there's a + /// problem, returns an `Err` with a string describing the issue. + fn unseal(&self, name: &str, value: &str) -> Result<String, &'static str> { + let data = base64::decode(value).map_err(|_| "bad base64 value")?; + if data.len() <= NONCE_LEN { + return Err("length of decoded data is <= NONCE_LEN"); + } + + let (nonce, cipher) = data.split_at(NONCE_LEN); + let payload = Payload { msg: cipher, aad: name.as_bytes() }; + + let aead = Aes256Gcm::new(GenericArray::from_slice(&self.key)); + aead.decrypt(GenericArray::from_slice(nonce), payload) + .map_err(|_| "invalid key/nonce/value: bad seal") + .and_then(|s| String::from_utf8(s).map_err(|_| "bad unsealed utf8")) + } + + /// Authenticates and decrypts `cookie`, returning the plaintext version if + /// decryption succeeds or `None` otherwise. Authenticatation and decryption + /// _always_ succeeds if `cookie` was generated by a `PrivateJar` with the + /// same key as `self`. + /// + /// # Example + /// + /// ```rust + /// use cookie::{CookieJar, Cookie, Key}; + /// + /// let key = Key::generate(); + /// let mut jar = CookieJar::new(); + /// assert!(jar.private(&key).get("name").is_none()); + /// + /// jar.private_mut(&key).add(Cookie::new("name", "value")); + /// assert_eq!(jar.private(&key).get("name").unwrap().value(), "value"); + /// + /// let plain = jar.get("name").cloned().unwrap(); + /// assert_ne!(plain.value(), "value"); + /// let decrypted = jar.private(&key).decrypt(plain).unwrap(); + /// assert_eq!(decrypted.value(), "value"); + /// + /// let plain = Cookie::new("plaintext", "hello"); + /// assert!(jar.private(&key).decrypt(plain).is_none()); + /// ``` + pub fn decrypt(&self, mut cookie: Cookie<'static>) -> Option<Cookie<'static>> { + if let Ok(value) = self.unseal(cookie.name(), cookie.value()) { + cookie.set_value(value); + return Some(cookie); + } + + None + } +} + +impl<J: Borrow<CookieJar>> PrivateJar<J> { + /// Returns a reference to the `Cookie` inside this jar with the name `name` + /// and authenticates and decrypts the cookie's value, returning a `Cookie` + /// with the decrypted value. If the cookie cannot be found, or the cookie + /// fails to authenticate or decrypt, `None` is returned. + /// + /// # Example + /// + /// ```rust + /// use cookie::{CookieJar, Cookie, Key}; + /// + /// let key = Key::generate(); + /// let jar = CookieJar::new(); + /// assert!(jar.private(&key).get("name").is_none()); + /// + /// let mut jar = jar; + /// let mut private_jar = jar.private_mut(&key); + /// private_jar.add(Cookie::new("name", "value")); + /// assert_eq!(private_jar.get("name").unwrap().value(), "value"); + /// ``` + pub fn get(&self, name: &str) -> Option<Cookie<'static>> { + self.parent.borrow().get(name).and_then(|c| self.decrypt(c.clone())) + } +} + +impl<J: BorrowMut<CookieJar>> PrivateJar<J> { + /// Adds `cookie` to the parent jar. The cookie's value is encrypted with + /// authenticated encryption assuring confidentiality, integrity, and + /// authenticity. + /// + /// # Example + /// + /// ```rust + /// use cookie::{CookieJar, Cookie, Key}; + /// + /// let key = Key::generate(); + /// let mut jar = CookieJar::new(); + /// jar.private_mut(&key).add(Cookie::new("name", "value")); + /// + /// assert_ne!(jar.get("name").unwrap().value(), "value"); + /// assert_eq!(jar.private(&key).get("name").unwrap().value(), "value"); + /// ``` + pub fn add(&mut self, mut cookie: Cookie<'static>) { + self.encrypt_cookie(&mut cookie); + self.parent.borrow_mut().add(cookie); + } + + /// Adds an "original" `cookie` to parent jar. The cookie's value is + /// encrypted with authenticated encryption assuring confidentiality, + /// integrity, and authenticity. Adding an original cookie does not affect + /// the [`CookieJar::delta()`] computation. This method is intended to be + /// used to seed the cookie jar with cookies received from a client's HTTP + /// message. + /// + /// For accurate `delta` computations, this method should not be called + /// after calling `remove`. + /// + /// # Example + /// + /// ```rust + /// use cookie::{CookieJar, Cookie, Key}; + /// + /// let key = Key::generate(); + /// let mut jar = CookieJar::new(); + /// jar.private_mut(&key).add_original(Cookie::new("name", "value")); + /// + /// assert_eq!(jar.iter().count(), 1); + /// assert_eq!(jar.delta().count(), 0); + /// ``` + pub fn add_original(&mut self, mut cookie: Cookie<'static>) { + self.encrypt_cookie(&mut cookie); + self.parent.borrow_mut().add_original(cookie); + } + + /// Removes `cookie` from the parent jar. + /// + /// For correct removal, the passed in `cookie` must contain the same `path` + /// and `domain` as the cookie that was initially set. + /// + /// This is identical to [`CookieJar::remove()`]. See the method's + /// documentation for more details. + /// + /// # Example + /// + /// ```rust + /// use cookie::{CookieJar, Cookie, Key}; + /// + /// let key = Key::generate(); + /// let mut jar = CookieJar::new(); + /// let mut private_jar = jar.private_mut(&key); + /// + /// private_jar.add(Cookie::new("name", "value")); + /// assert!(private_jar.get("name").is_some()); + /// + /// private_jar.remove(Cookie::named("name")); + /// assert!(private_jar.get("name").is_none()); + /// ``` + pub fn remove(&mut self, cookie: Cookie<'static>) { + self.parent.borrow_mut().remove(cookie); + } +} + +#[cfg(test)] +mod test { + use crate::{CookieJar, Cookie, Key}; + + #[test] + fn simple() { + let key = Key::generate(); + let mut jar = CookieJar::new(); + assert_simple_behaviour!(jar, jar.private_mut(&key)); + } + + #[test] + fn secure() { + let key = Key::generate(); + let mut jar = CookieJar::new(); + assert_secure_behaviour!(jar, jar.private_mut(&key)); + } + + #[test] + fn roundtrip() { + // Secret is SHA-256 hash of 'Super secret!' passed through HKDF-SHA256. + let key = Key::from(&[89, 202, 200, 125, 230, 90, 197, 245, 166, 249, + 34, 169, 135, 31, 20, 197, 94, 154, 254, 79, 60, 26, 8, 143, 254, + 24, 116, 138, 92, 225, 159, 60, 157, 41, 135, 129, 31, 226, 196, 16, + 198, 168, 134, 4, 42, 1, 196, 24, 57, 103, 241, 147, 201, 185, 233, + 10, 180, 170, 187, 89, 252, 137, 110, 107]); + + let mut jar = CookieJar::new(); + jar.add(Cookie::new("encrypted_with_ring014", + "lObeZJorGVyeSWUA8khTO/8UCzFVBY9g0MGU6/J3NN1R5x11dn2JIA==")); + jar.add(Cookie::new("encrypted_with_ring016", + "SU1ujceILyMBg3fReqRmA9HUtAIoSPZceOM/CUpObROHEujXIjonkA==")); + + let private = jar.private(&key); + assert_eq!(private.get("encrypted_with_ring014").unwrap().value(), "Tamper-proof"); + assert_eq!(private.get("encrypted_with_ring016").unwrap().value(), "Tamper-proof"); + } +} diff --git a/third_party/rust/cookie/src/secure/signed.rs b/third_party/rust/cookie/src/secure/signed.rs new file mode 100644 index 0000000000..b46fae4e80 --- /dev/null +++ b/third_party/rust/cookie/src/secure/signed.rs @@ -0,0 +1,251 @@ +use std::convert::TryInto; +use std::borrow::{Borrow, BorrowMut}; + +use sha2::Sha256; +use hmac::{Hmac, Mac}; + +use crate::secure::{base64, Key}; +use crate::{Cookie, CookieJar}; + +// Keep these in sync, and keep the key len synced with the `signed` docs as +// well as the `KEYS_INFO` const in secure::Key. +pub(crate) const BASE64_DIGEST_LEN: usize = 44; +pub(crate) const KEY_LEN: usize = 32; + +/// A child cookie jar that authenticates its cookies. +/// +/// A _signed_ child jar signs all the cookies added to it and verifies cookies +/// retrieved from it. Any cookies stored in a `SignedJar` are provided +/// integrity and authenticity. In other words, clients cannot tamper with the +/// contents of a cookie nor can they fabricate cookie values, but the data is +/// visible in plaintext. +#[cfg_attr(all(nightly, doc), doc(cfg(feature = "signed")))] +pub struct SignedJar<J> { + parent: J, + key: [u8; KEY_LEN], +} + +impl<J> SignedJar<J> { + /// Creates a new child `SignedJar` with parent `parent` and key `key`. This + /// method is typically called indirectly via the `signed{_mut}` methods of + /// `CookieJar`. + pub(crate) fn new(parent: J, key: &Key) -> SignedJar<J> { + SignedJar { parent, key: key.signing().try_into().expect("sign key len") } + } + + /// Signs the cookie's value providing integrity and authenticity. + fn sign_cookie(&self, cookie: &mut Cookie) { + // Compute HMAC-SHA256 of the cookie's value. + let mut mac = Hmac::<Sha256>::new_from_slice(&self.key).expect("good key"); + mac.update(cookie.value().as_bytes()); + + // Cookie's new value is [MAC | original-value]. + let mut new_value = base64::encode(&mac.finalize().into_bytes()); + new_value.push_str(cookie.value()); + cookie.set_value(new_value); + } + + /// Given a signed value `str` where the signature is prepended to `value`, + /// verifies the signed value and returns it. If there's a problem, returns + /// an `Err` with a string describing the issue. + fn _verify(&self, cookie_value: &str) -> Result<String, &'static str> { + if !cookie_value.is_char_boundary(BASE64_DIGEST_LEN) { + return Err("missing or invalid digest"); + } + + // Split [MAC | original-value] into its two parts. + let (digest_str, value) = cookie_value.split_at(BASE64_DIGEST_LEN); + let digest = base64::decode(digest_str).map_err(|_| "bad base64 digest")?; + + // Perform the verification. + let mut mac = Hmac::<Sha256>::new_from_slice(&self.key).expect("good key"); + mac.update(value.as_bytes()); + mac.verify_slice(&digest) + .map(|_| value.to_string()) + .map_err(|_| "value did not verify") + } + + /// Verifies the authenticity and integrity of `cookie`, returning the + /// plaintext version if verification succeeds or `None` otherwise. + /// Verification _always_ succeeds if `cookie` was generated by a + /// `SignedJar` with the same key as `self`. + /// + /// # Example + /// + /// ```rust + /// use cookie::{CookieJar, Cookie, Key}; + /// + /// let key = Key::generate(); + /// let mut jar = CookieJar::new(); + /// assert!(jar.signed(&key).get("name").is_none()); + /// + /// jar.signed_mut(&key).add(Cookie::new("name", "value")); + /// assert_eq!(jar.signed(&key).get("name").unwrap().value(), "value"); + /// + /// let plain = jar.get("name").cloned().unwrap(); + /// assert_ne!(plain.value(), "value"); + /// let verified = jar.signed(&key).verify(plain).unwrap(); + /// assert_eq!(verified.value(), "value"); + /// + /// let plain = Cookie::new("plaintext", "hello"); + /// assert!(jar.signed(&key).verify(plain).is_none()); + /// ``` + pub fn verify(&self, mut cookie: Cookie<'static>) -> Option<Cookie<'static>> { + if let Ok(value) = self._verify(cookie.value()) { + cookie.set_value(value); + return Some(cookie); + } + + None + } +} + +impl<J: Borrow<CookieJar>> SignedJar<J> { + /// Returns a reference to the `Cookie` inside this jar with the name `name` + /// and verifies the authenticity and integrity of the cookie's value, + /// returning a `Cookie` with the authenticated value. If the cookie cannot + /// be found, or the cookie fails to verify, `None` is returned. + /// + /// # Example + /// + /// ```rust + /// use cookie::{CookieJar, Cookie, Key}; + /// + /// let key = Key::generate(); + /// let jar = CookieJar::new(); + /// assert!(jar.signed(&key).get("name").is_none()); + /// + /// let mut jar = jar; + /// let mut signed_jar = jar.signed_mut(&key); + /// signed_jar.add(Cookie::new("name", "value")); + /// assert_eq!(signed_jar.get("name").unwrap().value(), "value"); + /// ``` + pub fn get(&self, name: &str) -> Option<Cookie<'static>> { + self.parent.borrow().get(name).and_then(|c| self.verify(c.clone())) + } +} + +impl<J: BorrowMut<CookieJar>> SignedJar<J> { + /// Adds `cookie` to the parent jar. The cookie's value is signed assuring + /// integrity and authenticity. + /// + /// # Example + /// + /// ```rust + /// use cookie::{CookieJar, Cookie, Key}; + /// + /// let key = Key::generate(); + /// let mut jar = CookieJar::new(); + /// jar.signed_mut(&key).add(Cookie::new("name", "value")); + /// + /// assert_ne!(jar.get("name").unwrap().value(), "value"); + /// assert!(jar.get("name").unwrap().value().contains("value")); + /// assert_eq!(jar.signed(&key).get("name").unwrap().value(), "value"); + /// ``` + pub fn add(&mut self, mut cookie: Cookie<'static>) { + self.sign_cookie(&mut cookie); + self.parent.borrow_mut().add(cookie); + } + + /// Adds an "original" `cookie` to this jar. The cookie's value is signed + /// assuring integrity and authenticity. Adding an original cookie does not + /// affect the [`CookieJar::delta()`] computation. This method is intended + /// to be used to seed the cookie jar with cookies received from a client's + /// HTTP message. + /// + /// For accurate `delta` computations, this method should not be called + /// after calling `remove`. + /// + /// # Example + /// + /// ```rust + /// use cookie::{CookieJar, Cookie, Key}; + /// + /// let key = Key::generate(); + /// let mut jar = CookieJar::new(); + /// jar.signed_mut(&key).add_original(Cookie::new("name", "value")); + /// + /// assert_eq!(jar.iter().count(), 1); + /// assert_eq!(jar.delta().count(), 0); + /// ``` + pub fn add_original(&mut self, mut cookie: Cookie<'static>) { + self.sign_cookie(&mut cookie); + self.parent.borrow_mut().add_original(cookie); + } + + /// Removes `cookie` from the parent jar. + /// + /// For correct removal, the passed in `cookie` must contain the same `path` + /// and `domain` as the cookie that was initially set. + /// + /// This is identical to [`CookieJar::remove()`]. See the method's + /// documentation for more details. + /// + /// # Example + /// + /// ```rust + /// use cookie::{CookieJar, Cookie, Key}; + /// + /// let key = Key::generate(); + /// let mut jar = CookieJar::new(); + /// let mut signed_jar = jar.signed_mut(&key); + /// + /// signed_jar.add(Cookie::new("name", "value")); + /// assert!(signed_jar.get("name").is_some()); + /// + /// signed_jar.remove(Cookie::named("name")); + /// assert!(signed_jar.get("name").is_none()); + /// ``` + pub fn remove(&mut self, cookie: Cookie<'static>) { + self.parent.borrow_mut().remove(cookie); + } +} + +#[cfg(test)] +mod test { + use crate::{CookieJar, Cookie, Key}; + + #[test] + fn simple() { + let key = Key::generate(); + let mut jar = CookieJar::new(); + assert_simple_behaviour!(jar, jar.signed_mut(&key)); + } + + #[test] + fn private() { + let key = Key::generate(); + let mut jar = CookieJar::new(); + assert_secure_behaviour!(jar, jar.signed_mut(&key)); + } + + #[test] + fn roundtrip() { + // Secret is SHA-256 hash of 'Super secret!' passed through HKDF-SHA256. + let key = Key::from(&[89, 202, 200, 125, 230, 90, 197, 245, 166, 249, + 34, 169, 135, 31, 20, 197, 94, 154, 254, 79, 60, 26, 8, 143, 254, + 24, 116, 138, 92, 225, 159, 60, 157, 41, 135, 129, 31, 226, 196, 16, + 198, 168, 134, 4, 42, 1, 196, 24, 57, 103, 241, 147, 201, 185, 233, + 10, 180, 170, 187, 89, 252, 137, 110, 107]); + + let mut jar = CookieJar::new(); + jar.add(Cookie::new("signed_with_ring014", + "3tdHXEQ2kf6fxC7dWzBGmpSLMtJenXLKrZ9cHkSsl1w=Tamper-proof")); + jar.add(Cookie::new("signed_with_ring016", + "3tdHXEQ2kf6fxC7dWzBGmpSLMtJenXLKrZ9cHkSsl1w=Tamper-proof")); + + let signed = jar.signed(&key); + assert_eq!(signed.get("signed_with_ring014").unwrap().value(), "Tamper-proof"); + assert_eq!(signed.get("signed_with_ring016").unwrap().value(), "Tamper-proof"); + } + + #[test] + fn issue_178() { + let data = "x=yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy£"; + let c = Cookie::parse(data).expect("failed to parse cookie"); + let key = Key::from(&[0u8; 64]); + let mut jar = CookieJar::new(); + let signed = jar.signed_mut(&key); + assert!(signed.verify(c).is_none()); + } +} |