password length

This commit is contained in:
Aravinth Manivannan 2021-03-04 20:41:50 +05:30
parent 9fbfd359b3
commit 769bf21c61
Signed by: realaravinth
GPG key ID: AD9F0F08E855ED88
4 changed files with 106 additions and 36 deletions

View file

@ -1,14 +1,19 @@
//To gain fine-grained control over how credentials are managed, consider using ConfigBuilder: //To gain fine-grained control over how credentials are managed, consider using ConfigBuilder:
use argon2_creds::{Config, ConfigBuilder}; use argon2_creds::{Config, ConfigBuilder, PasswordPolicyBuilder};
fn main() { fn main() {
let config = ConfigBuilder::default() let config = ConfigBuilder::default()
.salt_length(32)
.username_case_mapped(false) .username_case_mapped(false)
.profanity(true) .profanity(true)
.blacklist(false) .blacklist(false)
.argon2(argon2::Config::default()) .password_policy(
PasswordPolicyBuilder::default()
.min(12)
.max(80)
.build()
.unwrap(),
)
.build() .build()
.unwrap(); .unwrap();

View file

@ -5,13 +5,54 @@ use validator_derive::Validate;
use crate::errors::*; use crate::errors::*;
use crate::filters::{beep, filter, forbidden}; use crate::filters::{beep, filter, forbidden};
/// Credential management configuration
#[derive(Clone, Builder)] #[derive(Clone, Builder)]
pub struct Config { pub struct Config {
/// activates profanity filter. Default `false`
#[builder(default = "false")]
profanity: bool, profanity: bool,
/// activates blacklist filter. Default `true`
#[builder(default = "true")]
blacklist: bool, blacklist: bool,
/// activates username_case_mapped filter. Default `true`
#[builder(default = "true")]
username_case_mapped: bool, username_case_mapped: bool,
salt_length: usize, /// activates profanity filter. Default `false`
#[builder(default = "PasswordPolicyBuilder::default().build().unwrap()")]
password_policy: PasswordPolicy,
}
impl PasswordPolicyBuilder {
fn validate(&self) -> Result<(), String> {
if self.min > self.max {
Err("Configuration error: Password max length shorter than min length".to_string())
} else {
Ok(())
}
}
}
#[derive(Clone, Builder)]
#[builder(build_fn(validate = "Self::validate"))]
pub struct PasswordPolicy {
/// See [argon2 config][argon2::Config]
#[builder(default = "argon2::Config::default()")]
argon2: argon2::Config<'static>, argon2: argon2::Config<'static>,
/// minium password length
#[builder(default = "8")]
min: usize,
/// maximum password length(to protect against DoS attacks)
#[builder(default = "64")]
max: usize,
/// salt length in password hashing
#[builder(default = "32")]
salt_length: usize,
}
impl Default for PasswordPolicy {
fn default() -> Self {
PasswordPolicyBuilder::default().build().unwrap()
}
} }
#[derive(Validate)] #[derive(Validate)]
@ -22,23 +63,12 @@ struct Email {
impl Default for Config { impl Default for Config {
fn default() -> Self { fn default() -> Self {
Config { ConfigBuilder::default().build().unwrap()
/// 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 Config { impl Config {
/// process username /// Mormalises, converts to lowercase and applies filters to the username
pub fn username(&self, username: &str) -> CredsResult<String> { pub fn username(&self, username: &str) -> CredsResult<String> {
use ammonia::clean; use ammonia::clean;
use unicode_normalization::UnicodeNormalization; use unicode_normalization::UnicodeNormalization;
@ -53,7 +83,7 @@ impl Config {
Ok(clean_username) Ok(clean_username)
} }
/// process email /// Checks if input is an email
pub fn email(&self, email: Option<&str>) -> CredsResult<()> { pub fn email(&self, email: Option<&str>) -> CredsResult<()> {
if let Some(email) = email { if let Some(email) = email {
let email = Email { let email = Email {
@ -77,27 +107,37 @@ impl Config {
Ok(()) Ok(())
} }
/// generate hash for password /// Generate hash for passsword
pub fn password(&self, password: &str) -> CredsResult<String> { pub fn password(&self, password: &str) -> CredsResult<String> {
use argon2::hash_encoded; use argon2::hash_encoded;
use rand::distributions::Alphanumeric; use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
let length = password.len();
if self.password_policy.min > length {
return Err(CredsError::PasswordTooShort);
}
if self.password_policy.max < length {
return Err(CredsError::PasswordTooLong);
}
let mut rng = thread_rng(); let mut rng = thread_rng();
let salt: String = std::iter::repeat(()) let salt: String = std::iter::repeat(())
.map(|()| rng.sample(Alphanumeric)) .map(|()| rng.sample(Alphanumeric))
.map(char::from) .map(char::from)
.take(self.salt_length) .take(self.password_policy.salt_length)
.collect(); .collect();
Ok(hash_encoded( Ok(hash_encoded(
password.as_bytes(), password.as_bytes(),
salt.as_bytes(), salt.as_bytes(),
&self.argon2, &self.password_policy.argon2,
)?) )?)
} }
/// verify password against hash /// Verify password against hash
pub fn verify(hash: &str, password: &str) -> CredsResult<bool> { pub fn verify(hash: &str, password: &str) -> CredsResult<bool> {
let status = argon2::verify_encoded(hash, password.as_bytes())?; let status = argon2::verify_encoded(hash, password.as_bytes())?;
Ok(status) Ok(status)
@ -114,33 +154,28 @@ mod tests {
assert!(!config.profanity); assert!(!config.profanity);
assert!(config.blacklist); assert!(config.blacklist);
assert!(config.username_case_mapped); assert!(config.username_case_mapped);
assert_eq!(config.salt_length, 32); assert_eq!(config.password_policy.salt_length, 32);
let new_length = 50;
let config = ConfigBuilder::default() let config = ConfigBuilder::default()
.salt_length(new_length)
.username_case_mapped(false) .username_case_mapped(false)
.profanity(true) .profanity(true)
.blacklist(false) .blacklist(false)
.argon2(argon2::Config::default()) .password_policy(PasswordPolicy::default())
.build() .build()
.unwrap(); .unwrap();
assert!(config.profanity); assert!(config.profanity);
assert!(!config.blacklist); assert!(!config.blacklist);
assert!(!config.username_case_mapped); assert!(!config.username_case_mapped);
assert_eq!(config.salt_length, new_length);
} }
#[test] #[test]
fn creds_email_err() { fn creds_email_err() {
let config = ConfigBuilder::default() let config = ConfigBuilder::default()
.salt_length(50)
.username_case_mapped(false) .username_case_mapped(false)
.profanity(true) .profanity(true)
.blacklist(false) .blacklist(false)
.argon2(argon2::Config::default()) .password_policy(PasswordPolicy::default())
.build() .build()
.unwrap(); .unwrap();
@ -167,11 +202,10 @@ mod tests {
#[test] #[test]
fn utils_create_new_profane_organisation() { fn utils_create_new_profane_organisation() {
let config = ConfigBuilder::default() let config = ConfigBuilder::default()
.salt_length(50)
.username_case_mapped(false) .username_case_mapped(false)
.profanity(true) .profanity(true)
.blacklist(false) .blacklist(false)
.argon2(argon2::Config::default()) .password_policy(PasswordPolicy::default())
.build() .build()
.unwrap(); .unwrap();
@ -187,4 +221,28 @@ mod tests {
assert_eq!(forbidden_err, Err(CredsError::BlacklistError)); assert_eq!(forbidden_err, Err(CredsError::BlacklistError));
} }
#[test]
fn password_length_check() {
let min_max_error = PasswordPolicyBuilder::default().min(50).max(10).build();
assert!(min_max_error.is_err());
let config = ConfigBuilder::default()
.password_policy(
PasswordPolicyBuilder::default()
.min(5)
.max(10)
.build()
.unwrap(),
)
.build()
.unwrap();
let too_short_err = config.password("a");
let too_long_err = config.password("asdfasdfasdf");
assert_eq!(too_short_err, Err(CredsError::PasswordTooShort));
assert_eq!(too_long_err, Err(CredsError::PasswordTooLong));
}
} }

View file

@ -26,6 +26,14 @@ pub enum CredsError {
#[display(fmt = "The value passed in not an email")] #[display(fmt = "The value passed in not an email")]
NotAnEmail, NotAnEmail,
/// password too short
#[display(fmt = "Password too short")]
PasswordTooShort,
/// password too long
#[display(fmt = "Password too long")]
PasswordTooLong,
/// Errors from argon2 /// Errors from argon2
#[display(fmt = "{}", _0)] #[display(fmt = "{}", _0)]
Argon2Error(argon2::Error), Argon2Error(argon2::Error),

View file

@ -33,15 +33,14 @@
//! [ConfigBuilder]: //! [ConfigBuilder]:
//! //!
//!```rust //!```rust
//! use argon2_creds::{ConfigBuilder, Config}; //! use argon2_creds::{ConfigBuilder, PasswordPolicy, Config};
//! //!
//! fn main() { //! fn main() {
//! let config = ConfigBuilder::default() //! let config = ConfigBuilder::default()
//! .salt_length(32)
//! .username_case_mapped(false) //! .username_case_mapped(false)
//! .profanity(true) //! .profanity(true)
//! .blacklist(false) //! .blacklist(false)
//! .argon2(argon2::Config::default()) //! .password_policy(PasswordPolicy::default())
//! .build() //! .build()
//! .unwrap(); //! .unwrap();
//! //!
@ -90,5 +89,5 @@ pub mod config;
pub mod errors; pub mod errors;
mod filters; mod filters;
pub use crate::config::{Config, ConfigBuilder}; pub use crate::config::{Config, ConfigBuilder, PasswordPolicy, PasswordPolicyBuilder};
pub use crate::errors::{CredsError, CredsResult}; pub use crate::errors::{CredsError, CredsResult};