argon2-creds/src/config.rs

290 lines
8.1 KiB
Rust
Raw Normal View History

2023-10-14 17:08:33 +05:30
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
2021-03-05 14:52:18 +05:30
//! Credential processor and configuration
2021-01-29 10:31:36 +05:30
use derive_builder::Builder;
2021-06-13 13:22:36 +05:30
use lazy_static::initialize;
2021-01-29 10:31:36 +05:30
use validator::Validate;
use crate::errors::*;
use crate::filters::{beep, filter, forbidden};
2021-06-13 13:22:36 +05:30
use crate::filters::{
blacklist::RE_BLACKLIST, profainity::RE_PROFAINITY, user_case_mapped::RE_USERNAME_CASE_MAPPED,
};
2021-01-29 10:31:36 +05:30
2021-03-04 20:41:50 +05:30
/// Credential management configuration
2021-01-29 10:31:36 +05:30
#[derive(Clone, Builder)]
pub struct Config {
2021-03-04 20:41:50 +05:30
/// activates profanity filter. Default `false`
#[builder(default = "false")]
2021-01-29 10:31:36 +05:30
profanity: bool,
2021-03-04 20:41:50 +05:30
/// activates blacklist filter. Default `true`
#[builder(default = "true")]
2021-01-29 10:31:36 +05:30
blacklist: bool,
2021-03-04 20:41:50 +05:30
/// activates username_case_mapped filter. Default `true`
#[builder(default = "true")]
2021-01-29 10:31:36 +05:30
username_case_mapped: bool,
2021-03-04 20:41:50 +05:30
/// 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>,
2021-12-17 10:42:22 +05:30
/// minimum password length
2021-03-04 20:41:50 +05:30
#[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()
}
2021-01-29 10:31:36 +05:30
}
#[derive(Validate)]
2021-04-13 23:11:34 +05:30
struct Email<'a> {
2021-01-29 10:31:36 +05:30
#[validate(email)]
2021-04-13 23:11:34 +05:30
pub email: &'a str,
2021-01-29 10:31:36 +05:30
}
impl Default for Config {
2021-01-29 10:31:36 +05:30
fn default() -> Self {
2021-03-04 20:41:50 +05:30
ConfigBuilder::default().build().unwrap()
2021-01-29 10:31:36 +05:30
}
}
impl Config {
2021-12-17 10:42:22 +05:30
/// Normalises, converts to lowercase and applies filters to the username
pub fn username(&self, username: &str) -> CredsResult<String> {
2021-01-29 10:31:36 +05:30
use ammonia::clean;
use unicode_normalization::UnicodeNormalization;
let clean_username = clean(username)
.to_lowercase()
.nfc()
.collect::<String>()
.trim()
.to_owned();
2021-01-29 10:31:36 +05:30
self.validate_username(&clean_username)?;
Ok(clean_username)
}
2021-03-04 20:41:50 +05:30
/// Checks if input is an email
2021-04-13 23:11:34 +05:30
pub fn email(&self, email: &str) -> CredsResult<()> {
let email = Email {
email: email.trim(),
};
Ok(email.validate()?)
2021-01-29 10:31:36 +05:30
}
fn validate_username(&self, username: &str) -> CredsResult<()> {
2021-01-29 10:31:36 +05:30
if self.username_case_mapped {
filter(&username)?;
}
if self.blacklist {
forbidden(&username)?;
}
if self.profanity {
beep(&username)?;
}
Ok(())
}
2021-03-04 20:41:50 +05:30
/// Generate hash for passsword
pub fn password(&self, password: &str) -> CredsResult<String> {
2021-01-29 10:31:36 +05:30
use argon2::hash_encoded;
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
2021-03-04 20:41:50 +05:30
let length = password.len();
if self.password_policy.min > length {
return Err(CredsError::PasswordTooShort);
}
if self.password_policy.max < length {
return Err(CredsError::PasswordTooLong);
}
2021-01-29 10:31:36 +05:30
let mut rng = thread_rng();
let salt: String = std::iter::repeat(())
.map(|()| rng.sample(Alphanumeric))
.map(char::from)
2021-03-04 20:41:50 +05:30
.take(self.password_policy.salt_length)
2021-01-29 10:31:36 +05:30
.collect();
Ok(hash_encoded(
password.as_bytes(),
salt.as_bytes(),
2021-03-04 20:41:50 +05:30
&self.password_policy.argon2,
2021-01-29 10:31:36 +05:30
)?)
}
2021-03-04 20:41:50 +05:30
/// Verify password against hash
2021-01-29 10:31:36 +05:30
pub fn verify(hash: &str, password: &str) -> CredsResult<bool> {
let status = argon2::verify_encoded(hash, password.as_bytes())?;
Ok(status)
}
2021-06-13 13:22:36 +05:30
2021-12-17 10:42:22 +05:30
/// Initialize filters according to configuration.
2021-06-13 13:22:36 +05:30
///
/// Filters are lazy initialized so there's a slight delay during the very first use of
/// filter. By calling this method during the early stages of program execution,
/// that delay can be avoided.
pub fn init(&self) {
if self.username_case_mapped {
initialize(&RE_USERNAME_CASE_MAPPED);
}
if self.blacklist {
initialize(&RE_BLACKLIST);
}
if self.profanity {
initialize(&RE_PROFAINITY);
}
}
2021-01-29 10:31:36 +05:30
}
#[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);
2021-03-04 20:41:50 +05:30
assert_eq!(config.password_policy.salt_length, 32);
2021-01-29 10:31:36 +05:30
let config = ConfigBuilder::default()
.username_case_mapped(false)
.profanity(true)
.blacklist(false)
2021-03-04 20:41:50 +05:30
.password_policy(PasswordPolicy::default())
2021-01-29 10:31:36 +05:30
.build()
.unwrap();
assert!(config.profanity);
assert!(!config.blacklist);
assert!(!config.username_case_mapped);
}
#[test]
fn creds_email_err() {
let config = ConfigBuilder::default()
.username_case_mapped(false)
.profanity(true)
.blacklist(false)
2021-03-04 20:41:50 +05:30
.password_policy(PasswordPolicy::default())
2021-01-29 10:31:36 +05:30
.build()
.unwrap();
2021-06-13 13:22:36 +05:30
config.init();
2021-01-29 10:31:36 +05:30
2021-06-13 13:22:36 +05:30
assert_eq!(config.email("sdfasdf"), Err(CredsError::NotAnEmail));
2021-01-29 10:31:36 +05:30
}
#[test]
fn utils_create_new_organisation() {
let password = "somepassword";
let config = Config::default();
2021-06-13 13:22:36 +05:30
config.init();
2021-01-29 10:31:36 +05:30
2021-04-13 23:11:34 +05:30
config.email("batman@we.net").unwrap();
2021-01-29 10:31:36 +05:30
let username = config.username("Realaravinth").unwrap();
let hash = config.password(password).unwrap();
assert_eq!(username, "realaravinth");
2021-12-17 10:42:22 +05:30
assert!(Config::verify(&hash, password).unwrap(), "verify hashing");
2021-01-29 10:31:36 +05:30
}
#[test]
fn username_case_mapped_org() {
let config = ConfigBuilder::default()
.username_case_mapped(true)
.profanity(true)
.blacklist(false)
.password_policy(PasswordPolicy::default())
.build()
.unwrap();
config.init();
let username_err = config.username("a@test.com");
assert_eq!(username_err, Err(CredsError::UsernameCaseMappedError));
}
2021-01-29 10:31:36 +05:30
#[test]
fn utils_create_new_profane_organisation() {
let config = ConfigBuilder::default()
.username_case_mapped(false)
.profanity(true)
.blacklist(false)
2021-03-04 20:41:50 +05:30
.password_policy(PasswordPolicy::default())
2021-01-29 10:31:36 +05:30
.build()
.unwrap();
2021-06-13 13:22:36 +05:30
config.init();
2021-01-29 10:31:36 +05:30
let username_err = config.username("fuck");
assert_eq!(username_err, Err(CredsError::ProfainityError));
}
#[test]
fn utils_create_new_forbidden_organisation() {
let config = Config::default();
2021-06-13 13:22:36 +05:30
config.init();
let forbidden_err = config.username("webmaster");
2021-01-29 10:31:36 +05:30
assert_eq!(forbidden_err, Err(CredsError::BlacklistError));
}
2021-03-04 20:41:50 +05:30
#[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();
2021-06-13 13:22:36 +05:30
config.init();
2021-03-04 20:41:50 +05:30
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));
}
2021-01-29 10:31:36 +05:30
}