config and tests
This commit is contained in:
parent
d62201d186
commit
1023ad24dc
4 changed files with 245 additions and 172 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
|||
/target
|
||||
Cargo.lock
|
||||
tarpaulin-report.html
|
||||
|
|
|
@ -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
190
src/config.rs
Normal 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));
|
||||
}
|
||||
}
|
225
src/lib.rs
225
src/lib.rs
|
@ -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};
|
||||
|
|
Loading…
Reference in a new issue