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)]
|
2021-03-04 16:35:40 +05:30
|
|
|
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()")]
|
2021-03-04 16:35:40 +05:30
|
|
|
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
|
|
|
}
|
|
|
|
|
2021-03-04 16:35:40 +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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-04 16:35:40 +05:30
|
|
|
impl Config {
|
2021-12-17 10:42:22 +05:30
|
|
|
/// Normalises, converts to lowercase and applies filters to the username
|
2021-03-04 16:35:40 +05:30
|
|
|
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-06-29 19:21:15 +05:30
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2021-03-04 16:35:40 +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
|
2021-03-04 16:35:40 +05:30
|
|
|
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
|
|
|
}
|
|
|
|
|
2021-06-29 19:21:15 +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();
|
2021-06-29 19:21:15 +05:30
|
|
|
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
|
|
|
}
|