#![allow(dead_code)]
#![warn(missing_docs)]
use base64::prelude::*;
use pbkdf2::pbkdf2_hmac_array;
use sha1::Sha1;
use sha2::Sha256;
use std::{env, fmt::Display};
use aes_gcm::{
aead::{Aead, KeyInit},
Aes256Gcm, Key, Nonce,
};
#[derive(Clone, Debug, PartialEq)]
pub enum ParseCookieError {
CookieDecodingError(base64::DecodeError),
CookieFormatError(usize),
ResultEncodingError(std::string::FromUtf8Error),
AesError(aes_gcm::Error),
}
impl Display for ParseCookieError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ParseCookieError::CookieDecodingError(e) =>
write!(f, "error decoding base64: {}", e.to_string()),
ParseCookieError::CookieFormatError(e) =>
write!(f, "wrong cookie format, expecting `3` parts, found `{}`", e),
ParseCookieError::ResultEncodingError(e) =>
write!(f, "error parsing string as UTF-8: {}", e.to_string()),
ParseCookieError::AesError(e) =>
write!(f, "error decrypting aes-gcm: {}", e.to_string())
}
}
}
pub enum HashDigest {
Sha1,
Sha256,
}
pub struct RailsCookieParser {
key: [u8; 32],
}
impl Default for RailsCookieParser {
fn default() -> Self {
let default_key_base = match env::var("SECRET_KEY_BASE") {
Ok(env_key_base) => env_key_base,
Err(_) => {
eprintln!("rails-cookie-parser: No ENV set for SECRET_KEY_BASE");
"".to_owned()
}
};
let default_key_salt = match env::var("SECRET_KEY_SALT") {
Ok(env_key_salt) => env_key_salt,
Err(_) => RailsCookieParser::DEFAULT_KEY_SALT.to_owned(),
};
Self::new(
&default_key_base,
&default_key_salt,
1000,
HashDigest::Sha256,
)
}
}
impl RailsCookieParser {
const DEFAULT_KEY_SALT: &'static str = "authenticated encrypted cookie";
pub fn default_rails5() -> Self {
Self::default_rails6()
}
pub fn default_rails6() -> Self {
let default_key_base = match env::var("SECRET_KEY_BASE") {
Ok(env_key_base) => env_key_base,
Err(_) => {
eprintln!("rails-cookie-parser: No ENV set for SECRET_KEY_BASE");
"".to_owned()
}
};
let default_key_salt = match env::var("SECRET_KEY_SALT") {
Ok(env_key_salt) => env_key_salt,
Err(_) => RailsCookieParser::DEFAULT_KEY_SALT.to_owned(),
};
Self::new(&default_key_base, &default_key_salt, 1000, HashDigest::Sha1)
}
pub fn default_rails7() -> Self {
Self::default()
}
pub fn new(key_base: &str, key_salt: &str, iterations: u32, key_hash: HashDigest) -> Self {
let key_base = key_base.as_bytes().to_vec();
let key_salt = key_salt.as_bytes().to_vec();
let key = match key_hash {
HashDigest::Sha1 => pbkdf2_hmac_array::<Sha1, 32>(&key_base, &key_salt, iterations),
HashDigest::Sha256 => pbkdf2_hmac_array::<Sha256, 32>(&key_base, &key_salt, iterations),
};
RailsCookieParser { key }
}
fn decipher_aes(
&self,
payload: &[u8],
iv: &[u8],
auth_tag: &[u8],
) -> Result<Vec<u8>, ParseCookieError> {
let decipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&self.key));
let nonce = Nonce::from_slice(iv);
let new_payload = [payload, auth_tag].concat();
match decipher.decrypt(&nonce, new_payload.as_ref()) {
Ok(result) => Ok(result),
Err(x) => Err(ParseCookieError::AesError(x)),
}
}
pub fn decipher_cookie(&self, cookie: &str) -> Result<String, ParseCookieError> {
let parts: Result<Vec<_>, _> = cookie
.split("--")
.map(|part| BASE64_STANDARD.decode(part))
.collect();
let Ok(parts) = parts else {
return Err(ParseCookieError::CookieDecodingError(parts.err().unwrap()));
};
if parts.len() != 3 {
return Err(ParseCookieError::CookieFormatError(parts.len()));
}
let deciphered_cookie = match self.decipher_aes(&parts[0], &parts[1], &parts[2]) {
Ok(result) => result,
Err(err) => return Err(err),
};
match String::from_utf8(deciphered_cookie) {
Ok(result) => Ok(result),
Err(err) => return Err(ParseCookieError::ResultEncodingError(err.into()))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_fails_on_non_base64_cookies() {
let rails_key = RailsCookieParser::default();
let result = rails_key.decipher_cookie("%test--foo--bar");
assert_eq!(result, Err(ParseCookieError::CookieDecodingError(base64::DecodeError::InvalidByte(0, 37))));
}
#[test]
fn it_fails_on_invalid_cookies() {
let rails_key = RailsCookieParser::default();
let result = rails_key.decipher_cookie("test--");
assert_eq!(result, Err(ParseCookieError::CookieFormatError(2)));
}
#[test]
fn it_works_for_rails_v6() {
let rails_key = RailsCookieParser::new(
"10b8683351f3a680391ba9b4735285900b6a7745ed5791437f001a938cc4dfac997363195051a28be31ba64d7a0098c2efc41aed4ef206fb18f373339a44bd2f",
"authenticated encrypted cookie",
1000,
HashDigest::Sha1
);
let result = rails_key.decipher_cookie("FaxbWcVc2y/48LYms/BrNb5r2MUXZcfZLpfzYOR0lVoQvoKz5R3IQwoNVL3VXgcudYp4oWYCuxX4IID70mjmFBcDK5DQTvykD1JAKgcFsbQcxDR5E/PBKTBwS5L5pEruWIB72Lu9o6BXreK6VZeNrAp9xBASiz+a/X33XAMrZFPPC/TGotfkkeLjwTx24wVg5OoET/Y3DkDhlXd9H0sho6lUEJLxxLyhNB+zqCc3i/sB2nyqRXf7J/3FsALeiwrPSCjF8ivBFyeD4pRercRBTNbLP6+Z--7FdUaoy4iwX6C+GX--2bq4Qvv1UDTUHv+O4pRxBg==");
assert_eq!(result.unwrap(), "{\"_rails\":{\"message\":\"eyJzZXNzaW9uX2lkIjoiMDMzZmY0MTBiYjY4MTcyMGJmZDRkMDM5NjIxMTFiMzkiLCJjdXJyZW50X3VzZXJfdXVpZCI6Ijk1ZDczYmY4LTZlZGYtNGE2Ny1iMWY5LTIyNzM5OGJkZDhhZiJ9\",\"exp\":\"2030-11-27T11:58:33.210Z\",\"pur\":null}}");
}
#[test]
fn it_works_for_default_rails() {
let rails_key = RailsCookieParser::new(
"5ac471dc7dc882a9d8367253dcdebd086be029cad10f681725fad25e8b425d241854a054ea06b08d9ac36e03439948eddd2e93b1310b1c5c9843f6f54a562286",
"authenticated encrypted cookie",
1000,
HashDigest::Sha256
);
let result = rails_key.decipher_cookie("ClX3OHg9XV03KMYDOUJGB8u1wTq4qnahW1GS9nwbX0Z0eOsuIqWo6l0AVenz1wN61BPg79Bifwr2zGwKwyH9JhFpO75wPlh6llTJ4/dOzmucMsZIRpFDvLoDLjVkeuxSdIRE9JURM9/sD92jOby4qFdR4bkCHMGmnS+T4hbactT88X0uDOpyeifEUVHUi+Mmmui4qzpRbaR86lvqnudVKHYlC53Sb5EQJX0IK1oE/8tl/hXXAd0fQCP+Ho0pqz6LtH4+PPa7H7PXJFOxJ1epDqotmUI9XuYJp7Cq6GZ+NoE2t4WAl+SHqxjjAwE6vzfajA553x4=--5hm+u0xu/mHYUHIh--YiXje6ZO08vnyf/hY41dgQ==");
assert_eq!(result.unwrap(), "{\"_rails\":{\"message\":\"eyJzZXNzaW9uX2lkIjoiYjJjM2RmNTdhYmZlZGU4M2JiOWUwZGIzNmFjMzBmMGUiLCJmb28iOiJiYXIiLCJfY3NyZl90b2tlbiI6IjFjWWN6a3lvVGpXYnVlMVphR3F2TE9uWmVwOTkycmM5alFiX21XRTBfNzgiLCJjb3VudCI6MTJ9\",\"exp\":null,\"pur\":\"cookie._your_app_session\"}}");
}
}