diff --git a/Cargo.toml b/Cargo.toml index 27f082ba..4ed473d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,9 +9,8 @@ license = "GPL-3.0" keywords = ["rocket", "csrf", "security"] [dependencies] -csrf = "~0.3.0" data-encoding = "~2.1.1" -rand = "~0.6.1" +ring = "~0.13.5" rocket = "0.4.0-rc.1" serde = "~1.0" time = "~0.1.40" diff --git a/src/crypto.rs b/src/crypto.rs new file mode 100644 index 00000000..86432f1a --- /dev/null +++ b/src/crypto.rs @@ -0,0 +1,99 @@ +use ring::aead::{CHACHA20_POLY1305, OpeningKey, open_in_place, SealingKey, seal_in_place}; +use ring::rand::{SecureRandom, SystemRandom}; +use std::time::SystemTime; + +pub struct CsrfProtection { + aead_key: [u8; 32],// 256b +} + +impl CsrfProtection { + pub fn from_key(aead_key: [u8; 32]) -> Self { + CsrfProtection { aead_key } + } + + pub fn parse_cookie<'a>(&self, cookie: &'a mut [u8]) -> Result, CsrfError> { + let key = OpeningKey::new(&CHACHA20_POLY1305, &self.aead_key).map_err(|_| CsrfError::UnknownError)?; + if cookie.len() < 12 { + return Err(CsrfError::ValidationError);// cookie is too short to be valid + } + let (nonce, token) = cookie.split_at_mut(12);// 96b + let token = open_in_place(&key, nonce, &[], 0, token).map_err(|_| CsrfError::ValidationError)?; + if token.len() < 8 {// shorter than a timestamp, must be invalid + return Err(CsrfError::ValidationError); + } + let mut expires = [0;8]; + expires.copy_from_slice(&token[..8]); + let expires = u64::from_be_bytes(expires); + let token = &token[8..]; + Ok(CsrfCookie{ + token, + expires, + }) + } + + pub fn parse_token<'a>(&self, token: &'a mut [u8]) -> Result, CsrfError> { + let key = OpeningKey::new(&CHACHA20_POLY1305, &self.aead_key).map_err(|_| CsrfError::UnknownError)?; + if token.len() < 12 { + return Err(CsrfError::ValidationError);// cookie is too short to be valid + } + let (nonce, token) = token.split_at_mut(12);// 96b + let token = open_in_place(&key, nonce, &[], 0, token).map_err(|_| CsrfError::ValidationError)?; + Ok(CsrfToken{ + token, + }) + } + + pub fn verify_token_pair(&self, token: CsrfToken, cookie: CsrfCookie) -> bool { + let token_ok = &token.token == &cookie.token; + let not_expired = cookie.time_left() > 0; + + token_ok && not_expired + } + + pub fn generate_token_pair<'a>(&self, previous_token: Option, ttl_seconds: u64, source_buffer: &'a mut[u8; 64*2+16*2+12*2+8]) -> Result<(&'a[u8], &'a[u8]), CsrfError> { + let key = SealingKey::new(&CHACHA20_POLY1305, &self.aead_key).map_err(|_| CsrfError::UnknownError)?; + let (token, cookie) = source_buffer.split_at_mut(64+16+12); + let expire = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).map(|d| d.as_secs() + ttl_seconds).map_err(|_| CsrfError::UnknownError)?; + cookie[12..8+12].copy_from_slice(&expire.to_be_bytes()); + + let rand = SystemRandom::new(); + if let Some(previous_token) = previous_token { + cookie[20..64+20].copy_from_slice(previous_token.token); + token[12..64+12].copy_from_slice(previous_token.token); + } else { + rand.fill(&mut token[12..64+12]).map_err(|_| CsrfError::UnknownError)?; + cookie[20..64+20].copy_from_slice(&token[12..64+12]); + } + + let mut nonce = [0;12]; + + rand.fill(&mut nonce).map_err(|_| CsrfError::UnknownError)?; + token[..12].copy_from_slice(&nonce); + seal_in_place(&key, &nonce, &[], &mut token[12..], CHACHA20_POLY1305.tag_len()).map_err(|_| CsrfError::UnknownError)?; + rand.fill(&mut nonce).map_err(|_| CsrfError::UnknownError)?; + cookie[..12].copy_from_slice(&nonce); + seal_in_place(&key, &nonce, &[], &mut cookie[12..], CHACHA20_POLY1305.tag_len()).map_err(|_| CsrfError::UnknownError)?; + + return Ok((token, cookie)); + } +} + +pub struct CsrfToken<'a> { + token: &'a[u8], +} + +pub struct CsrfCookie<'a> { + token: &'a[u8], + expires: u64 +} + +impl<'a> CsrfCookie<'a> { + pub fn time_left(&self) -> u64 { + SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).ok().and_then(|now| self.expires.checked_sub(now.as_secs())).unwrap_or(0) + } +} + +pub enum CsrfError { + ValidationError, + UnknownError, +} diff --git a/src/csrf_fairing.rs b/src/csrf_fairing.rs index ef8a7745..7e10c17b 100644 --- a/src/csrf_fairing.rs +++ b/src/csrf_fairing.rs @@ -1,7 +1,5 @@ -use csrf::{AesGcmCsrfProtection, CsrfProtection, CSRF_COOKIE_NAME, CSRF_FORM_FIELD}; use data_encoding::{BASE64, BASE64URL_NOPAD}; -use rand::prelude::thread_rng; -use rand::Rng; +use ring::rand::{SecureRandom, SystemRandom}; use rocket::fairing::{Fairing, Info, Kind}; use rocket::http::uri::{Origin, Uri}; use rocket::http::Cookie; @@ -15,12 +13,12 @@ use std::io::{Cursor, Read}; use std::str::from_utf8; use time::Duration; +use crypto::CsrfProtection; use csrf_proxy::CsrfProxy; use csrf_token::CsrfToken; use path::Path; use utils::parse_args; - -const CSRF_FORM_FIELD_MULTIPART: &[u8] = b"Content-Disposition: form-data; name=\"csrf-token\""; +use {CSRF_COOKIE_NAME, CSRF_FORM_FIELD, CSRF_FORM_FIELD_MULTIPART}; /// Builder for [CsrfFairing](struct.CsrfFairing.html) /// @@ -57,7 +55,7 @@ const CSRF_FORM_FIELD_MULTIPART: &[u8] = b"Content-Disposition: form-data; name= /// ``` pub struct CsrfFairingBuilder { - duration: i64, + duration: u64, default_target: (String, Method), exceptions: Vec<(String, String, Method)>, secret: Option<[u8; 32]>, @@ -70,7 +68,7 @@ impl CsrfFairingBuilder { /// Create a new builder with default values. pub fn new() -> Self { CsrfFairingBuilder { - duration: 60 * 60, + duration: 60 * 60 * 12, default_target: (String::from("/"), Get), exceptions: Vec::new(), secret: None, @@ -81,8 +79,8 @@ impl CsrfFairingBuilder { } /// Set the timeout (in seconds) of CSRF tokens generated by the final Fairing. Default timeout - /// is one hour. - pub fn set_timeout(mut self, timeout: i64) -> Self { + /// is twelve hour. + pub fn set_timeout(mut self, timeout: u64) -> Self { self.duration = timeout; self } @@ -107,7 +105,6 @@ impl CsrfFairingBuilder { /// //add your routes, other fairings... /// .launch(); /// } - pub fn set_default_target(mut self, default_target: String, method: Method) -> Self { self.default_target = (default_target, method); self @@ -232,7 +229,10 @@ impl CsrfFairingBuilder { })//else get secret environment variable .unwrap_or_else(|| { eprintln!("[rocket_csrf] No secret key was found, you should consider set one to keep token validity across application restart"); - thread_rng().gen() + let rand = SystemRandom::new(); + let mut array = [0;32]; + rand.fill(&mut array).unwrap(); + array }) //if environment variable is not set, generate a random secret and print a warning }); @@ -272,7 +272,7 @@ impl Default for CsrfFairingBuilder { /// /// [`CsrfFairingBuilder`]: /rocket_csrf/struct.CsrfFairing.html pub struct CsrfFairing { - duration: i64, + duration: u64, default_target: (Path, Method), exceptions: Vec<(Path, Path, Method)>, secret: [u8; 32], @@ -297,7 +297,7 @@ impl Fairing for CsrfFairing { } fn on_attach(&self, rocket: Rocket) -> Result { - Ok(rocket.manage((AesGcmCsrfProtection::from_key(self.secret), self.duration))) //add the Csrf engine to Rocket's managed state + Ok(rocket.manage((CsrfProtection::from_key(self.secret), self.duration))) //add the Csrf engine to Rocket's managed state } fn on_request(&self, request: &mut Request, data: &Data) { @@ -316,17 +316,17 @@ impl Fairing for CsrfFairing { } let (csrf_engine, _) = request - .guard::>() + .guard::>() .unwrap() .inner(); - let cookie = request + let mut cookie = request .cookies() .get(CSRF_COOKIE_NAME) - .and_then(|cookie| BASE64URL_NOPAD.decode(cookie.value().as_bytes()).ok()) - .and_then(|cookie| csrf_engine.parse_cookie(&cookie).ok()); //get and parse Csrf cookie + .and_then(|cookie| BASE64URL_NOPAD.decode(cookie.value().as_bytes()).ok()); + let cookie = cookie.as_mut().and_then(|c| csrf_engine.parse_cookie(&mut *c).ok()); //get and parse Csrf cookie - let token = if request + let mut token = if request .content_type() .map(|c| c.media_type()) .filter(|m| m.top() == "multipart" && m.sub() == "form-data") @@ -348,12 +348,12 @@ impl Fairing for CsrfFairing { } }) .next() - }.and_then(|token| BASE64URL_NOPAD.decode(&token).ok()) - .and_then(|token| csrf_engine.parse_token(&token).ok()); + }.and_then(|token| BASE64URL_NOPAD.decode(&token).ok()); + let token = token.as_mut().and_then(|token| csrf_engine.parse_token(&mut *token).ok()); if let Some(token) = token { if let Some(cookie) = cookie { - if csrf_engine.verify_token_pair(&token, &cookie) { + if csrf_engine.verify_token_pair(token, cookie) { return; //if we got both token and cookie, and they match each other, we do nothing } } @@ -452,7 +452,7 @@ impl Fairing for CsrfFairing { #[cfg(test)] mod tests { use super::*; - use csrf::{CSRF_COOKIE_NAME, CSRF_FORM_FIELD}; + use {CSRF_COOKIE_NAME, CSRF_FORM_FIELD}; use rocket::{ http::{Cookie, Header, Method}, local::{Client, LocalRequest}, diff --git a/src/csrf_token.rs b/src/csrf_token.rs index e64c3b3c..9bbf2eb7 100644 --- a/src/csrf_token.rs +++ b/src/csrf_token.rs @@ -1,4 +1,4 @@ -use csrf::{AesGcmCsrfProtection, CsrfProtection, CSRF_COOKIE_NAME}; +use CSRF_COOKIE_NAME; use data_encoding::BASE64URL_NOPAD; use rocket::http::{Cookie, SameSite, Status}; use rocket::outcome::Outcome; @@ -7,6 +7,8 @@ use rocket::{Request, State}; use serde::{Serialize, Serializer}; use time::Duration; +use crypto::CsrfProtection; + /// Csrf token to insert into pages. /// /// The `CsrfToken` type allow you to add tokens into your pages anywhere you want, and is mainly @@ -40,7 +42,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for CsrfToken { fn from_request(request: &'a Request<'r>) -> request::Outcome { let (csrf_engine, duration) = request - .guard::>() + .guard::>() .unwrap() .inner(); @@ -50,35 +52,26 @@ impl<'a, 'r> FromRequest<'a, 'r> for CsrfToken { { Outcome::Forward(()) } else { - let token_value = cookies + let mut token_value = cookies .get(CSRF_COOKIE_NAME) - .and_then(|cookie| BASE64URL_NOPAD.decode(cookie.value().as_bytes()).ok()) - .and_then(|cookie| csrf_engine.parse_cookie(&cookie).ok()) - .and_then(|cookie| { - let value = cookie.value(); - if value.len() == 64 { - let mut array = [0; 64]; - array.copy_from_slice(&value); - Some(array) - } else { - None - } - }); //when request guard is called, parse cookie to get it's encrypted secret (if there is a cookie) + .and_then(|cookie| BASE64URL_NOPAD.decode(cookie.value().as_bytes()).ok()); + let token_value = token_value.as_mut().and_then(|cookie| csrf_engine.parse_cookie(&mut *cookie).ok()); - match csrf_engine.generate_token_pair(token_value.as_ref(), *duration) { + let mut buf = [0; 192]; + match csrf_engine.generate_token_pair(token_value, *duration, &mut buf) { Ok((token, cookie)) => { let mut c = - Cookie::build(CSRF_COOKIE_NAME, BASE64URL_NOPAD.encode(cookie.value())) + Cookie::build(CSRF_COOKIE_NAME, BASE64URL_NOPAD.encode(cookie)) .http_only(true) .secure(true) .same_site(SameSite::Strict) .path("/") - .max_age(Duration::seconds(*duration)) + .max_age(Duration::seconds(*duration as i64)) .finish(); cookies.add(c); Outcome::Success(CsrfToken { - value: BASE64URL_NOPAD.encode(token.value()), + value: BASE64URL_NOPAD.encode(token), }) } Err(_) => Outcome::Failure((Status::InternalServerError, ())), diff --git a/src/lib.rs b/src/lib.rs index bdfd3b17..de9dbd99 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,9 +43,8 @@ //! You should define a route for csrf violation error, and registe it in the builder, otherwise //! errors will simply be redirected to the route matching `/` //! -extern crate csrf; extern crate data_encoding; -extern crate rand; +extern crate ring; extern crate serde; extern crate test; extern crate time; @@ -60,9 +59,14 @@ mod csrf_proxy; mod csrf_token; mod path; mod utils; +mod crypto; pub use self::csrf_fairing::{CsrfFairing, CsrfFairingBuilder}; pub use self::csrf_token::CsrfToken; +const CSRF_COOKIE_NAME: &str = "csrf"; +const CSRF_FORM_FIELD: &str = "csrf-token"; +const CSRF_FORM_FIELD_MULTIPART: &[u8] = b"Content-Disposition: form-data; name=\"csrf-token\""; + #[cfg(test)] mod tests {