diff --git a/src/lib.rs b/src/lib.rs index f666d2c3..62e7aa5b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,22 +29,29 @@ //! //! * **`signed`** //! -//! Enables _signed_ cookies via [`CookieJar::signed()`]. +//! Enables _signed_ cookies via [`Cookie::sign()`], [`Cookie::verify()`], and +//! [`CookieJar::signed()`]. //! -//! When this feature is enabled, the [`CookieJar::signed()`] method, -//! [`SignedJar`] type, and [`Key`] type are available. The jar acts as "child +//! When this feature is enabled, the type [`Key`] and the methods +//! [`Cookie::sign()`] and [`Cookie::verify()`] are available, which can be +//! used to sign and verify cookies. Additionally, the type [`SignedJar`] and +//! the method [`CookieJar::signed()`] is available. The jar acts as "child //! jar"; operations on the jar automatically sign and verify cookies as they //! are added and retrieved from the parent jar. //! //! * **`private`** //! //! Enables _private_ (authenticated, encrypted) cookies via +//! [`Cookie::encrypt()`], [`Cookie::decrypt()`], and //! [`CookieJar::private()`]. //! -//! When this feature is enabled, the [`CookieJar::private()`] method, -//! [`PrivateJar`] type, and [`Key`] type are available. The jar acts as "child -//! jar"; operations on the jar automatically encrypt and decrypt/authenticate -//! cookies as they are added and retrieved from the parent jar. +//! When this feature is enabled, the type [`Key`] and the methods +//! [`Cookie::encrypt()`] and [`Cookie::decrypt()`] are available, which can +//! be used to encrypt and decrypt/authenticate cookies. Additionally, the +//! type [`PrivateJar`] and the method [`CookieJar::private()`] is available. +//! The jar acts as "child jar"; operations on the jar automatically encrypt +//! and decrypt/authenticate cookies as they are added and retrieved from the +//! parent jar. //! //! * **`key-expansion`** //! @@ -1415,6 +1422,88 @@ assert_eq!(&c.stripped().encoded().to_string(), "key%3F=value"); pub fn stripped<'a>(&'a self) -> Display<'a, 'c> { Display::new_stripped(self) } + + /// Encrypts and signs this cookie using authenticated encryption with the + /// provided key and returns the encrypted cookie. + /// + /// # Example + /// + /// ```rust + /// use cookie::{Cookie, Key}; + /// + /// let key = Key::generate(); + /// let plain = Cookie::from(("name", "value")); + /// let encrypted = plain.encrypt(&key); + /// ``` + #[cfg(feature = "private")] + #[cfg_attr(all(nightly, doc), doc(cfg(feature = "private")))] + pub fn encrypt(&self, key: &Key) -> Cookie<'static> { + let mut cookie = self.clone().into_owned(); + secure::encrypt_cookie(&mut cookie, key.encryption()); + cookie + } + + /// Authenticates and decrypts this cookie with the provided key and returns + /// the plaintext version if decryption succeeds or `None` otherwise. + /// + /// # Example + /// + /// ```rust + /// use cookie::{Cookie, Key}; + /// + /// let key = Key::generate(); + /// let plain = Cookie::from(("name", "value")); + /// let encrypted = plain.encrypt(&key); + /// + /// let decrypted = encrypted.decrypt(&key).unwrap(); + /// assert_eq!(decrypted.value(), "value"); + /// ``` + #[cfg(feature = "private")] + #[cfg_attr(all(nightly, doc), doc(cfg(feature = "private")))] + pub fn decrypt(&self, key: &Key) -> Option> { + secure::decrypt_cookie(self, key.encryption()) + } + + /// Signs this cookie with the provided key and returns the signed cookie. + /// + /// # Example + /// + /// ```rust + /// use cookie::{Cookie, Key}; + /// + /// let key = Key::generate(); + /// let plain = Cookie::from(("name", "value")); + /// let signed = plain.sign(&key); + /// ``` + #[cfg(feature = "signed")] + #[cfg_attr(all(nightly, doc), doc(cfg(feature = "signed")))] + pub fn sign(&self, key: &Key) -> Cookie<'static> { + let mut cookie = self.clone().into_owned(); + secure::sign_cookie(&mut cookie, key.signing()); + cookie + } + + /// Verifies the signature of this cookie with the provided key and returns + /// the plaintext version if verification succeeds or `None` otherwise. + /// + /// # Example + /// + /// ```rust + /// use cookie::{Cookie, Key}; + /// + /// let key = Key::generate(); + /// let plain = Cookie::from(("name", "value")); + /// let signed = plain.sign(&key); + /// + /// let verified = signed.verify(&key).unwrap(); + /// assert_eq!(verified.value(), "value"); + /// ``` + #[cfg(feature = "signed")] + #[cfg_attr(all(nightly, doc), doc(cfg(feature = "signed")))] + pub fn verify(&self, key: &Key) -> Option> { + secure::verify_cookie(self, key.signing()) + } + } /// An iterator over cookie parse `Result`s: `Result`. diff --git a/src/secure/private.rs b/src/secure/private.rs index 8caf86e5..f30d3a25 100644 --- a/src/secure/private.rs +++ b/src/secure/private.rs @@ -37,56 +37,6 @@ impl PrivateJar { 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 { - 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 @@ -112,13 +62,8 @@ impl PrivateJar { /// let plain = Cookie::new("plaintext", "hello"); /// assert!(jar.private(&key).decrypt(plain).is_none()); /// ``` - pub fn decrypt(&self, mut cookie: Cookie<'static>) -> Option> { - if let Ok(value) = self.unseal(cookie.name(), cookie.value()) { - cookie.set_value(value); - return Some(cookie); - } - - None + pub fn decrypt(&self, cookie: Cookie<'static>) -> Option> { + decrypt_cookie(&cookie, &self.key) } } @@ -143,7 +88,7 @@ impl> PrivateJar { /// assert_eq!(private_jar.get("name").unwrap().value(), "value"); /// ``` pub fn get(&self, name: &str) -> Option> { - self.parent.borrow().get(name).and_then(|c| self.decrypt(c.clone())) + self.parent.borrow().get(name).and_then(|c| decrypt_cookie(c, &self.key)) } } @@ -166,7 +111,7 @@ impl> PrivateJar { /// ``` pub fn add>>(&mut self, cookie: C) { let mut cookie = cookie.into(); - self.encrypt_cookie(&mut cookie); + encrypt_cookie(&mut cookie, &self.key); self.parent.borrow_mut().add(cookie); } @@ -194,7 +139,7 @@ impl> PrivateJar { /// ``` pub fn add_original>>(&mut self, cookie: C) { let mut cookie = cookie.into(); - self.encrypt_cookie(&mut cookie); + encrypt_cookie(&mut cookie, &self.key); self.parent.borrow_mut().add_original(cookie); } @@ -226,6 +171,67 @@ impl> PrivateJar { } } +/// Encrypts `cookie` in-place using authenticated encryption with the provided +/// key `key`. +pub(crate) fn encrypt_cookie(cookie: &mut Cookie<'static>, key: &[u8]) { + // 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 = 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(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]. + let new_value = base64::encode(&data); + + // Return encrypted cookie. + cookie.set_value(new_value); +} + +/// Authenticates and decrypts `cookie` using the provided key `key` and returns +/// the plaintext version if decryption succeeds, otherwise `None`. +pub(crate) fn decrypt_cookie<'a>(cookie: &Cookie<'a>, key: &[u8]) -> Option> { + if let Ok(value) = decrypt_cookie_impl(cookie, key) { + let mut cookie = cookie.clone().into_owned(); + cookie.set_value(value); + return Some(cookie); + } + + None +} + +fn decrypt_cookie_impl<'a>(cookie: &Cookie<'a>, key: &[u8]) -> Result { + let data = base64::decode(cookie.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: cookie.name().as_bytes() }; + + let aead = Aes256Gcm::new(GenericArray::from_slice(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")) +} + #[cfg(test)] mod test { use crate::{CookieJar, Cookie, Key}; diff --git a/src/secure/signed.rs b/src/secure/signed.rs index 9dbb732f..ec6a2f45 100644 --- a/src/secure/signed.rs +++ b/src/secure/signed.rs @@ -33,38 +33,6 @@ impl SignedJar { 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::::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 { - 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::::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 @@ -90,13 +58,8 @@ impl SignedJar { /// let plain = Cookie::new("plaintext", "hello"); /// assert!(jar.signed(&key).verify(plain).is_none()); /// ``` - pub fn verify(&self, mut cookie: Cookie<'static>) -> Option> { - if let Ok(value) = self._verify(cookie.value()) { - cookie.set_value(value); - return Some(cookie); - } - - None + pub fn verify(&self, cookie: Cookie<'static>) -> Option> { + verify_cookie(&cookie, &self.key) } } @@ -121,7 +84,7 @@ impl> SignedJar { /// assert_eq!(signed_jar.get("name").unwrap().value(), "value"); /// ``` pub fn get(&self, name: &str) -> Option> { - self.parent.borrow().get(name).and_then(|c| self.verify(c.clone())) + self.parent.borrow().get(name).and_then(|c| verify_cookie(&c, &self.key)) } } @@ -144,7 +107,7 @@ impl> SignedJar { /// ``` pub fn add>>(&mut self, cookie: C) { let mut cookie = cookie.into(); - self.sign_cookie(&mut cookie); + sign_cookie(&mut cookie, &self.key); self.parent.borrow_mut().add(cookie); } @@ -171,7 +134,7 @@ impl> SignedJar { /// ``` pub fn add_original>>(&mut self, cookie: C) { let mut cookie = cookie.into(); - self.sign_cookie(&mut cookie); + sign_cookie(&mut cookie, &self.key); self.parent.borrow_mut().add_original(cookie); } @@ -203,6 +166,48 @@ impl> SignedJar { } } +/// Sign `cookie` in-place using the provided key `key`. +pub(crate) fn sign_cookie(cookie: &mut Cookie<'static>, key: &[u8]) { + // Compute HMAC-SHA256 of the cookie's value. + let mut mac = Hmac::::new_from_slice(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); +} + +/// Verifies the signature of `cookie` using the provided key `key` and returns +/// the plaintext version if decryption succeeds, otherwise `None`. +pub(crate) fn verify_cookie<'a>(cookie: &Cookie<'a>, key: &[u8]) -> Option> { + if let Ok(value) = verify_cookie_impl(cookie, key) { + let mut cookie = cookie.clone().into_owned(); + cookie.set_value(value); + return Some(cookie); + } + + None +} + +fn verify_cookie_impl<'a>(cookie: &Cookie<'a>, key: &[u8]) -> Result { + let cookie_value = cookie.value(); + 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::::new_from_slice(key).expect("good key"); + mac.update(value.as_bytes()); + mac.verify_slice(&digest) + .map(|_| value.to_string()) + .map_err(|_| "value did not verify") +} + #[cfg(test)] mod test { use crate::{CookieJar, Cookie, Key};