password length
This commit is contained in:
parent
9fbfd359b3
commit
769bf21c61
4 changed files with 106 additions and 36 deletions
|
@ -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();
|
||||||
|
|
||||||
|
|
116
src/config.rs
116
src/config.rs
|
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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};
|
||||||
|
|
Loading…
Reference in a new issue