use std::borrow::{Borrow, BorrowMut};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use cookie::{Cookie, CookieJar, Key};
pub(crate) const BASE64_DIGEST_LEN: usize = 44;
pub(crate) const KEY_LEN: usize = 32;
use base64::{prelude::BASE64_STANDARD, DecodeError, Engine};
use tokio::task;
pub(crate) fn encode<T: AsRef<[u8]>>(input: T) -> String {
BASE64_STANDARD.encode(input)
}
pub(crate) fn decode<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>, DecodeError> {
BASE64_STANDARD.decode(input)
}
pub trait CookiesAdditionJar {
fn message_signed<'a>(&'a self, key: &Key, message: String) -> AdditionalSignedJar<&'a Self>;
fn message_signed_mut<'a>(
&'a mut self,
key: &Key,
message: String,
) -> AdditionalSignedJar<&'a mut Self>;
}
impl CookiesAdditionJar for CookieJar {
fn message_signed<'a>(&'a self, key: &Key, message: String) -> AdditionalSignedJar<&'a Self> {
AdditionalSignedJar::new(self, key, message)
}
fn message_signed_mut<'a>(
&'a mut self,
key: &Key,
message: String,
) -> AdditionalSignedJar<&'a mut Self> {
AdditionalSignedJar::new(self, key, message)
}
}
pub struct AdditionalSignedJar<J> {
parent: J,
key: [u8; KEY_LEN],
message: String,
}
impl<J> AdditionalSignedJar<J> {
pub(crate) fn new(parent: J, key: &Key, message: String) -> AdditionalSignedJar<J> {
AdditionalSignedJar {
parent,
key: key.signing().try_into().expect("sign key len"),
message,
}
}
async fn sign_cookie(&self, cookie: &mut Cookie<'_>) {
let key = self.key.to_owned();
let message = self.message.to_owned();
let value = cookie.value().to_owned();
let new_value = task::spawn_blocking(move || {
let mut mac = match Hmac::<Sha256>::new_from_slice(&key) {
Ok(v) => v,
Err(err) => {
tracing::error!(err = %err, "key is invalid." );
return None;
}
};
let message = format!("{}{}", &value, &message);
mac.update(message.as_bytes());
let mut new_value = encode(mac.finalize().into_bytes());
new_value.push_str(&value);
Some(new_value)
})
.await;
if let Ok(Some(value)) = new_value {
cookie.set_value(value);
}
}
async 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");
}
let key = self.key.to_owned();
let message = self.message.to_owned();
let cookie_value = cookie_value.to_owned();
task::spawn_blocking(move || {
let (digest_str, value) = cookie_value.split_at(BASE64_DIGEST_LEN);
let digest = decode(digest_str).map_err(|_| "bad base64 digest")?;
let mut mac = Hmac::<Sha256>::new_from_slice(&key).map_err(|_| "key is invalid.")?;
let message = format!("{}{}", value, &message);
mac.update(message.as_bytes());
mac.verify_slice(&digest)
.map(|_| value.to_string())
.map_err(|_| "value did not verify")
})
.await
.map_err(|_| "thread join in _verify was unsuccessful")?
}
pub async fn verify(&self, mut cookie: Cookie<'static>) -> Option<Cookie<'static>> {
match self._verify(cookie.value()).await {
Ok(value) => {
cookie.set_value(value);
Some(cookie)
}
Err(err) => {
tracing::warn!(
err = %err,
"possibly suspicious activity: Verification failed for Cookie. cookie validation string was: {}",
self.message
);
None
}
}
}
}
impl<J: Borrow<CookieJar>> AdditionalSignedJar<J> {
pub async fn get(&self, name: &str) -> Option<Cookie<'static>> {
match self.parent.borrow().get(name) {
Some(c) => self.verify(c.clone()).await,
None => None,
}
}
}
impl<J: BorrowMut<CookieJar>> AdditionalSignedJar<J> {
pub async fn add<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
let mut cookie = cookie.into();
self.sign_cookie(&mut cookie).await;
self.parent.borrow_mut().add(cookie);
}
pub async fn add_original<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
let mut cookie = cookie.into();
self.sign_cookie(&mut cookie).await;
self.parent.borrow_mut().add_original(cookie);
}
pub fn remove<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
self.parent.borrow_mut().remove(cookie.into());
}
}
pub(crate) async fn sign_header(
value: &str,
key: &Key,
message: &str,
) -> Result<String, &'static str> {
let value = value.to_owned();
let message = message.to_owned();
let key = key.to_owned();
task::spawn_blocking(move || {
let mut mac =
Hmac::<Sha256>::new_from_slice(key.signing()).map_err(|_| "Key was invalid.")?;
let message = format!("{value}{message}");
mac.update(message.as_bytes());
let mut new_value = encode(mac.finalize().into_bytes());
new_value.push_str(&value);
Ok(new_value)
})
.await
.map_err(|_| "thread join in _verify was unsuccessful")?
}
pub(crate) async fn verify_header(
header_value: &str,
key: &Key,
message: &str,
) -> Result<String, &'static str> {
let header_value = header_value.to_owned();
let message = message.to_owned();
let key = key.to_owned();
task::spawn_blocking(move || {
if !header_value.is_char_boundary(BASE64_DIGEST_LEN) {
return Err("missing or invalid digest");
}
let (digest_str, value) = header_value.split_at(BASE64_DIGEST_LEN);
let digest = decode(digest_str).map_err(|_| "bad base64 digest")?;
let mut mac =
Hmac::<Sha256>::new_from_slice(key.signing()).map_err(|_| "Key was invalid.")?;
let message = format!("{value}{message}");
mac.update(message.as_bytes());
mac.verify_slice(&digest)
.map(|_| value.to_string())
.map_err(|_| "value did not verify")
})
.await
.map_err(|_| "thread join in _verify was unsuccessful")?
}
#[cfg(test)]
mod test {
use crate::sec::signed::CookiesAdditionJar;
use cookie::{Cookie, CookieJar, Key};
#[tokio::test]
async fn roundtrip() {
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.message_signed(&key, "".to_owned());
assert_eq!(
signed.get("signed_with_ring014").await.unwrap().value(),
"Tamper-proof"
);
assert_eq!(
signed.get("signed_with_ring016").await.unwrap().value(),
"Tamper-proof"
);
}
}