rails_cookie_parser/
lib.rs1#![allow(dead_code)]
24#![warn(missing_docs)]
25
26use base64::prelude::*;
27use pbkdf2::pbkdf2_hmac_array;
28use sha1::Sha1;
29use sha2::Sha256;
30use std::{env, fmt::Display};
31
32use aes_gcm::{
33 aead::{Aead, KeyInit},
34 Aes256Gcm, Key, Nonce,
35};
36
37#[derive(Clone, Debug, PartialEq)]
39pub enum ParseCookieError {
40 CookieDecodingError(base64::DecodeError),
44 CookieFormatError(usize),
48 ResultEncodingError(std::string::FromUtf8Error),
52 AesError(aes_gcm::Error),
56}
57
58
59impl Display for ParseCookieError {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 match self {
69 ParseCookieError::CookieDecodingError(e) =>
70 write!(f, "error decoding base64: {}", e.to_string()),
71 ParseCookieError::CookieFormatError(e) =>
72 write!(f, "wrong cookie format, expecting `3` parts, found `{}`", e),
73 ParseCookieError::ResultEncodingError(e) =>
74 write!(f, "error parsing string as UTF-8: {}", e.to_string()),
75 ParseCookieError::AesError(e) =>
76 write!(f, "error decrypting aes-gcm: {}", e.to_string())
77 }
78 }
79}
80
81pub enum HashDigest {
88 Sha1,
90 Sha256,
92}
93
94pub struct RailsCookieParser {
96 key: [u8; 32],
97}
98
99
100impl Default for RailsCookieParser {
101 fn default() -> Self {
110 let default_key_base = match env::var("SECRET_KEY_BASE") {
111 Ok(env_key_base) => env_key_base,
112 Err(_) => {
113 eprintln!("rails-cookie-parser: No ENV set for SECRET_KEY_BASE");
114 "".to_owned()
115 }
116 };
117 let default_key_salt = match env::var("SECRET_KEY_SALT") {
118 Ok(env_key_salt) => env_key_salt,
119 Err(_) => RailsCookieParser::DEFAULT_KEY_SALT.to_owned(),
120 };
121 Self::new(
122 &default_key_base,
123 &default_key_salt,
124 1000,
125 HashDigest::Sha256,
126 )
127 }
128}
129
130impl RailsCookieParser {
131 const DEFAULT_KEY_SALT: &'static str = "authenticated encrypted cookie";
132
133 pub fn default_rails5() -> Self {
137 Self::default_rails6()
138 }
139
140 pub fn default_rails6() -> Self {
144 let default_key_base = match env::var("SECRET_KEY_BASE") {
145 Ok(env_key_base) => env_key_base,
146 Err(_) => {
147 eprintln!("rails-cookie-parser: No ENV set for SECRET_KEY_BASE");
148 "".to_owned()
149 }
150 };
151 let default_key_salt = match env::var("SECRET_KEY_SALT") {
152 Ok(env_key_salt) => env_key_salt,
153 Err(_) => RailsCookieParser::DEFAULT_KEY_SALT.to_owned(),
154 };
155 Self::new(&default_key_base, &default_key_salt, 1000, HashDigest::Sha1)
156 }
157
158 pub fn default_rails7() -> Self {
163 Self::default()
164 }
165
166 pub fn new(key_base: &str, key_salt: &str, iterations: u32, key_hash: HashDigest) -> Self {
179 let key_base = key_base.as_bytes().to_vec();
180 let key_salt = key_salt.as_bytes().to_vec();
181 let key = match key_hash {
182 HashDigest::Sha1 => pbkdf2_hmac_array::<Sha1, 32>(&key_base, &key_salt, iterations),
183 HashDigest::Sha256 => pbkdf2_hmac_array::<Sha256, 32>(&key_base, &key_salt, iterations),
184 };
185
186 RailsCookieParser { key }
187 }
188
189 fn decipher_aes(
190 &self,
191 payload: &[u8],
192 iv: &[u8],
193 auth_tag: &[u8],
194 ) -> Result<Vec<u8>, ParseCookieError> {
195 let decipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&self.key));
196 let nonce = Nonce::from_slice(iv);
197 let new_payload = [payload, auth_tag].concat();
198 match decipher.decrypt(&nonce, new_payload.as_ref()) {
199 Ok(result) => Ok(result),
200 Err(x) => Err(ParseCookieError::AesError(x)),
201 }
202 }
203
204 pub fn decipher_cookie(&self, cookie: &str) -> Result<String, ParseCookieError> {
209 let parts: Result<Vec<_>, _> = cookie
210 .split("--")
211 .map(|part| BASE64_STANDARD.decode(part))
212 .collect();
213 let Ok(parts) = parts else {
215 return Err(ParseCookieError::CookieDecodingError(parts.err().unwrap()));
216 };
217 if parts.len() != 3 {
219 return Err(ParseCookieError::CookieFormatError(parts.len()));
220 }
221 let deciphered_cookie = match self.decipher_aes(&parts[0], &parts[1], &parts[2]) {
222 Ok(result) => result,
223 Err(err) => return Err(err),
224 };
225
226 match String::from_utf8(deciphered_cookie) {
227 Ok(result) => Ok(result),
228 Err(err) => return Err(ParseCookieError::ResultEncodingError(err.into()))
229 }
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn it_fails_on_non_base64_cookies() {
239 let rails_key = RailsCookieParser::default();
240 let result = rails_key.decipher_cookie("%test--foo--bar");
241 assert_eq!(result, Err(ParseCookieError::CookieDecodingError(base64::DecodeError::InvalidByte(0, 37))));
242 }
243
244 #[test]
245 fn it_fails_on_invalid_cookies() {
246 let rails_key = RailsCookieParser::default();
247 let result = rails_key.decipher_cookie("test--");
248 assert_eq!(result, Err(ParseCookieError::CookieFormatError(2)));
249 }
250
251 #[test]
252 fn it_works_for_rails_v6() {
253 let rails_key = RailsCookieParser::new(
254 "10b8683351f3a680391ba9b4735285900b6a7745ed5791437f001a938cc4dfac997363195051a28be31ba64d7a0098c2efc41aed4ef206fb18f373339a44bd2f",
255 "authenticated encrypted cookie",
256 1000,
257 HashDigest::Sha1
258 );
259 let result = rails_key.decipher_cookie("FaxbWcVc2y/48LYms/BrNb5r2MUXZcfZLpfzYOR0lVoQvoKz5R3IQwoNVL3VXgcudYp4oWYCuxX4IID70mjmFBcDK5DQTvykD1JAKgcFsbQcxDR5E/PBKTBwS5L5pEruWIB72Lu9o6BXreK6VZeNrAp9xBASiz+a/X33XAMrZFPPC/TGotfkkeLjwTx24wVg5OoET/Y3DkDhlXd9H0sho6lUEJLxxLyhNB+zqCc3i/sB2nyqRXf7J/3FsALeiwrPSCjF8ivBFyeD4pRercRBTNbLP6+Z--7FdUaoy4iwX6C+GX--2bq4Qvv1UDTUHv+O4pRxBg==");
260
261 assert_eq!(result.unwrap(), "{\"_rails\":{\"message\":\"eyJzZXNzaW9uX2lkIjoiMDMzZmY0MTBiYjY4MTcyMGJmZDRkMDM5NjIxMTFiMzkiLCJjdXJyZW50X3VzZXJfdXVpZCI6Ijk1ZDczYmY4LTZlZGYtNGE2Ny1iMWY5LTIyNzM5OGJkZDhhZiJ9\",\"exp\":\"2030-11-27T11:58:33.210Z\",\"pur\":null}}");
262 }
263
264 #[test]
265 fn it_works_for_default_rails() {
266 let rails_key = RailsCookieParser::new(
267 "5ac471dc7dc882a9d8367253dcdebd086be029cad10f681725fad25e8b425d241854a054ea06b08d9ac36e03439948eddd2e93b1310b1c5c9843f6f54a562286",
268 "authenticated encrypted cookie",
269 1000,
270 HashDigest::Sha256
271 );
272 let result = rails_key.decipher_cookie("ClX3OHg9XV03KMYDOUJGB8u1wTq4qnahW1GS9nwbX0Z0eOsuIqWo6l0AVenz1wN61BPg79Bifwr2zGwKwyH9JhFpO75wPlh6llTJ4/dOzmucMsZIRpFDvLoDLjVkeuxSdIRE9JURM9/sD92jOby4qFdR4bkCHMGmnS+T4hbactT88X0uDOpyeifEUVHUi+Mmmui4qzpRbaR86lvqnudVKHYlC53Sb5EQJX0IK1oE/8tl/hXXAd0fQCP+Ho0pqz6LtH4+PPa7H7PXJFOxJ1epDqotmUI9XuYJp7Cq6GZ+NoE2t4WAl+SHqxjjAwE6vzfajA553x4=--5hm+u0xu/mHYUHIh--YiXje6ZO08vnyf/hY41dgQ==");
273
274 assert_eq!(result.unwrap(), "{\"_rails\":{\"message\":\"eyJzZXNzaW9uX2lkIjoiYjJjM2RmNTdhYmZlZGU4M2JiOWUwZGIzNmFjMzBmMGUiLCJmb28iOiJiYXIiLCJfY3NyZl90b2tlbiI6IjFjWWN6a3lvVGpXYnVlMVphR3F2TE9uWmVwOTkycmM5alFiX21XRTBfNzgiLCJjb3VudCI6MTJ9\",\"exp\":null,\"pur\":\"cookie._your_app_session\"}}");
275 }
276}