config and tests

This commit is contained in:
Aravinth Manivannan 2021-01-29 10:31:36 +05:30
parent d62201d186
commit 1023ad24dc
Signed by: realaravinth
GPG key ID: AD9F0F08E855ED88
4 changed files with 245 additions and 172 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
/target
Cargo.lock
tarpaulin-report.html

View file

@ -27,3 +27,4 @@ validator_derive = "0.12.0"
lazy_static = "1.4.0"
regex = { version = "1.3.9", features = [ "perf-inline", "perf-dfa", "perf-literal", "perf-cache", "perf"]}
rand = "0.8.0"
derive_builder = "0.9"

190
src/config.rs Normal file
View file

@ -0,0 +1,190 @@
use derive_builder::Builder;
use validator::Validate;
use validator_derive::Validate;
use crate::errors::*;
use crate::filters::{beep, filter, forbidden};
#[derive(Clone, Builder)]
pub struct Config<'a> {
profanity: bool,
blacklist: bool,
username_case_mapped: bool,
salt_length: usize,
argon2: argon2::Config<'a>,
}
#[derive(Validate)]
struct Email {
#[validate(email)]
pub email: String,
}
impl<'a> Default for Config<'a> {
fn default() -> Self {
Config {
/// profanity filter
profanity: false,
/// blacklist filter
blacklist: true,
/// UsernameCaseMapped filter
username_case_mapped: true,
/// salt length
salt_length: 32,
/// argon2 configuration, see argon2::Processor for more information
argon2: argon2::Config::default(),
}
}
}
impl<'a> Config<'a> {
/// process username
pub fn username(&'a self, username: &str) -> CredsResult<String> {
use ammonia::clean;
use unicode_normalization::UnicodeNormalization;
let clean_username = clean(username)
.to_lowercase()
.nfc()
.collect::<String>()
.trim()
.to_owned();
self.validate_username(&clean_username)?;
Ok(clean_username)
}
/// process email
pub fn email(&self, email: Option<&'a str>) -> CredsResult<()> {
if let Some(email) = email {
let email = Email {
email: email.trim().to_owned(),
};
email.validate()?;
}
Ok(())
}
fn validate_username(&self, username: &'a str) -> CredsResult<()> {
if self.username_case_mapped {
filter(&username)?;
}
if self.blacklist {
forbidden(&username)?;
}
if self.profanity {
beep(&username)?;
}
Ok(())
}
/// generate hash for password
pub fn password(&'a self, password: &str) -> CredsResult<String> {
use argon2::hash_encoded;
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
let mut rng = thread_rng();
let salt: String = std::iter::repeat(())
.map(|()| rng.sample(Alphanumeric))
.map(char::from)
.take(self.salt_length)
.collect();
Ok(hash_encoded(
password.as_bytes(),
salt.as_bytes(),
&self.argon2,
)?)
}
/// verify password against hash
pub fn verify(hash: &str, password: &str) -> CredsResult<bool> {
let status = argon2::verify_encoded(hash, password.as_bytes())?;
Ok(status)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_works() {
let config = Config::default();
assert!(!config.profanity);
assert!(config.blacklist);
assert!(config.username_case_mapped);
assert_eq!(config.salt_length, 32);
let new_length = 50;
let config = ConfigBuilder::default()
.salt_length(new_length)
.username_case_mapped(false)
.profanity(true)
.blacklist(false)
.argon2(argon2::Config::default())
.build()
.unwrap();
assert!(config.profanity);
assert!(!config.blacklist);
assert!(!config.username_case_mapped);
assert_eq!(config.salt_length, new_length);
}
#[test]
fn creds_email_err() {
let config = ConfigBuilder::default()
.salt_length(50)
.username_case_mapped(false)
.profanity(true)
.blacklist(false)
.argon2(argon2::Config::default())
.build()
.unwrap();
assert_eq!(
config.email(Some("sdfasdf".into())),
Err(CredsError::NotAnEmail)
);
}
#[test]
fn utils_create_new_organisation() {
let password = "somepassword";
let config = Config::default();
config.email(Some("batman@we.net")).unwrap();
let username = config.username("Realaravinth").unwrap();
let hash = config.password(password).unwrap();
assert_eq!(username, "realaravinth");
assert!(Config::verify(&hash, password).unwrap(), "verify hahsing");
}
#[test]
fn utils_create_new_profane_organisation() {
let config = ConfigBuilder::default()
.salt_length(50)
.username_case_mapped(false)
.profanity(true)
.blacklist(false)
.argon2(argon2::Config::default())
.build()
.unwrap();
let username_err = config.username("fuck");
assert_eq!(username_err, Err(CredsError::ProfainityError));
}
#[test]
fn utils_create_new_forbidden_organisation() {
let config = Config::default();
let forbidden_err = config.username("htaccessasnc");
assert_eq!(forbidden_err, Err(CredsError::BlacklistError));
}
}

View file

@ -1,175 +1,56 @@
//! Argon2-Creds provides abstractions over credential management and cuts down on boilerplate code
//! required to implement authenticatin
//!
//! ## Example
//!
//! ```rust
//! use argon2_creds::Config;
//!
//! fn main() {
//! let username = "iamBatman";
//! let password = "ironmansucks";
//! let email = "iambatman@wayne.org";
//!
//! let config = Config::default();
//! let hash = config.password(password).unwrap();
//! config.email(Some("batman@we.net")).unwrap();
//! let username = config.username("Realaravinth").unwrap();
//! let hash = config.password(password).unwrap();
//! assert_eq!(username, "realaravinth");
//! assert!(Config::verify(&hash, password).unwrap(), "verify hahsing");
//!
//! }
//! ```
//!
//! ## Documentation & Community Resources
//!
//! In addition to this API documentation, other resources are available:
//!
//! * [Examples](https://github.com/realaravinth/argon2-creds/)
//!
//! To get started navigating the API docs, you may consider looking at the following pages first:
//!
//! * [Config]: This struct is the entry point to `argon2_creds`
//!
//! * [User]: This struct represents a fully processed credentials
//!
//! * [Error]: This module provides essential types for errors that can occour during
//! credential processing
//!
//! ## Features
//!
//! * [rust-argon2](https://crates.io/rust-argon2)-based password hashing
//! * PRECIS Framework [UsernameCaseMapped](https://tools.ietf.org/html/rfc8265#page-7)
//! * Keep-alive and slow requests handling
//! * Profanity filter based off of
//! [List-of-Dirty-Naughty-Obscene-and-Otherwise-Bad-Words](https://github.com/LDNOOBW/List-of-Dirty-Naughty-Obscene-and-Otherwise-Bad-Words)
//! * Problamatic usernames filter based off of
//! [The-Big-Username-Blacklist](https://github.com/marteinn/The-Big-Username-Blacklist)
//! * Email validation using [validator](https://crates.io/validator)
pub mod config;
pub mod errors;
mod filters;
use ammonia::clean;
use argon2::{self, Config, ThreadMode, Variant, Version};
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use unicode_normalization::UnicodeNormalization;
use validator::Validate;
use validator_derive::Validate;
use errors::*;
pub use filters::{beep, filter, forbidden};
#[derive(Debug, Clone, PartialEq)]
pub struct UnvalidatedRegisterCreds {
pub username: String,
pub email_id: Option<String>,
pub password: String,
}
#[derive(Debug, Default, Clone, PartialEq, Validate)]
pub struct RegisterCreds {
pub username: String,
#[validate(email)]
pub email_id: Option<String>,
pub password: String,
}
impl UnvalidatedRegisterCreds {
pub fn process(&self) -> CredsResult<RegisterCreds> {
let creds = RegisterCreds::default()
.set_email(&self.email_id)?
.set_username(&self.username)
.validate_fields()?
.set_password(&self.password)?
.build();
Ok(creds)
}
}
impl RegisterCreds {
pub fn set_username<'a>(&'a mut self, username: &str) -> &'a mut Self {
self.username = clean(username)
.to_lowercase()
.nfc()
.collect::<String>()
.trim()
.to_owned();
self
}
pub fn set_email<'a>(&'a mut self, email_id: &Option<String>) -> CredsResult<&'a mut Self> {
if let Some(email) = email_id {
self.email_id = Some(email.trim().to_owned());
self.validate()?;
}
Ok(self)
}
pub fn validate_fields<'a>(&'a mut self) -> CredsResult<&'a mut Self> {
filter(&self.username)?;
forbidden(&self.username)?;
beep(&self.username)?;
Ok(self)
}
pub fn set_password<'a>(&'a mut self, password: &str) -> CredsResult<&'a mut Self> {
// let config = Config {
// variant: Variant::Argon2i,
// version: Version::Version13,
// mem_cost: SETTINGS.password_difficulty.mem_cost,
// time_cost: SETTINGS.password_difficulty.time_cost,
// lanes: SETTINGS.password_difficulty.lanes,
// thread_mode: ThreadMode::Parallel,
// secret: &[],
// ad: &[],
// hash_length: 32,
// };
let config = Config::default();
let mut rng = thread_rng();
let salt: String = std::iter::repeat(())
.map(|()| rng.sample(Alphanumeric))
.map(char::from)
.take(32)
.collect();
self.password = argon2::hash_encoded(password.as_bytes(), salt.as_bytes(), &config)?;
Ok(self)
}
pub fn build(&mut self) -> Self {
self.to_owned()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn utils_register_builer() {
let registered_creds = RegisterCreds::default()
.set_password("password")
.unwrap()
.set_username("realaravinth")
.set_email(&Some("batman@we.net".into()))
.unwrap()
.validate_fields()
.unwrap()
.build();
assert_eq!(registered_creds.username, "realaravinth");
assert_eq!(registered_creds.email_id, Some("batman@we.net".into()));
}
#[test]
fn utils_register_email_err() {
let mut email_err = RegisterCreds::default()
.set_password("password")
.unwrap()
.set_username("realaravinth")
.build();
assert_eq!(
email_err.set_email(&Some("sdfasdf".into())),
Err(CredsError::NotAnEmail)
);
}
#[test]
fn utils_create_new_organisation() {
let password = "somepassword";
let org = RegisterCreds::default()
.set_email(&Some("batman@we.net".into()))
.unwrap()
.set_username("Realaravinth")
.validate_fields()
.unwrap()
.set_password(password)
.unwrap()
.build();
assert_eq!(org.username, "realaravinth");
assert!(
argon2::verify_encoded(&org.password, password.as_bytes()).unwrap(),
"verify hahsing"
);
}
#[test]
fn utils_create_new_profane_organisation() {
let mut profane_org = RegisterCreds::default();
profane_org.set_username("fuck");
assert_eq!(
profane_org.validate_fields(),
Err(CredsError::ProfainityError)
);
}
#[test]
fn utils_create_new_forbidden_organisation() {
let mut forbidden_org = RegisterCreds::default()
.set_username("htaccessasnc")
.build();
assert_eq!(
forbidden_org.validate_fields(),
Err(CredsError::BlacklistError)
);
}
}
pub use crate::config::Config;
pub use crate::errors::{CredsError, CredsResult};