Browse Source

Remove dependencies to unmatained crats

Remove dependency csrf 0.3 as unmatained and can't compile on arm8
Add ring to do required cryptography
Remove dependency to rand as ring provid CPRNG
dependabot/cargo/ring-approx-0.14.0
Trinity Pointard 2 years ago
parent
commit
9eb7b3f98b
5 changed files with 140 additions and 45 deletions
  1. +1
    -2
      Cargo.toml
  2. +99
    -0
      src/crypto.rs
  3. +22
    -22
      src/csrf_fairing.rs
  4. +12
    -19
      src/csrf_token.rs
  5. +6
    -2
      src/lib.rs

+ 1
- 2
Cargo.toml View File

@@ -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"

+ 99
- 0
src/crypto.rs View File

@@ -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<CsrfCookie<'a>, 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<CsrfToken<'a>, 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<CsrfCookie>, 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,
}

+ 22
- 22
src/csrf_fairing.rs View File

@@ -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<Rocket, Rocket> {
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::<State<(AesGcmCsrfProtection, i64)>>()
.guard::<State<(CsrfProtection, u64)>>()
.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},


+ 12
- 19
src/csrf_token.rs View File

@@ -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<Self, ()> {
let (csrf_engine, duration) = request
.guard::<State<(AesGcmCsrfProtection, i64)>>()
.guard::<State<(CsrfProtection, u64)>>()
.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, ())),


+ 6
- 2
src/lib.rs View File

@@ -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 {


Loading…
Cancel
Save