use std::str::FromStr;
use crate::tina::data::AppResult;
use crate::tina::security::bcrypt::{BCrypt, MAX_LOG_ROUNDS, MIN_LOG_ROUNDS};
use crate::{app_system_error, tina::data::app_error::AppError};
use once_cell::sync::Lazy;
use regex::{Regex, RegexBuilder};
use tracing::warn;
static BCRYPT_PATTERN: Lazy<Regex> =
Lazy::new(|| RegexBuilder::new("\\A\\$2(a|y|b)?\\$(\\d\\d)\\$[./0-9A-Za-z]{53}").build().expect("build BCRYPT_PATTERN failed"));
#[derive(Debug, Copy, Clone)]
pub enum BCryptVersion {
Dollar2A,
Dollar2Y,
Dollar2B,
}
impl FromStr for BCryptVersion {
type Err = AppError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"$2a" => Ok(BCryptVersion::Dollar2A),
"$2y" => Ok(BCryptVersion::Dollar2Y),
"$2b" => Ok(BCryptVersion::Dollar2B),
_ => Err(app_system_error!("invalid BCryptVersion")),
}
}
}
impl ToString for BCryptVersion {
fn to_string(&self) -> String {
match self {
BCryptVersion::Dollar2A => "$2a".to_string(),
BCryptVersion::Dollar2Y => "$2y".to_string(),
BCryptVersion::Dollar2B => "$2b".to_string(),
}
}
}
pub struct PasswordEncoder {
strength: u32,
version: BCryptVersion,
}
impl PasswordEncoder {
pub fn try_new() -> AppResult<Self> {
Self::try_new_strength(0)
}
pub fn try_new_version(version: BCryptVersion) -> AppResult<Self> {
Self::try_new_all(0, version)
}
pub fn try_new_strength(strength: u32) -> AppResult<Self> {
Self::try_new_all(strength, BCryptVersion::Dollar2A)
}
pub fn try_new_all(strength: u32, version: BCryptVersion) -> AppResult<Self> {
if strength != 0 && (strength < MIN_LOG_ROUNDS || strength > MAX_LOG_ROUNDS) {
return Err(app_system_error!("Bad strength"));
}
let strength = if strength == 0 { 10 } else { strength };
Ok(Self {
strength,
version,
})
}
pub fn encode(&self, raw_password: &str) -> AppResult<String> {
let salt = self.get_salt()?;
BCrypt::hashpw(raw_password, salt.as_str())
}
pub fn matches(&self, raw_password: &str, encoded_password: &str) -> AppResult<bool> {
if raw_password.is_empty() {
return Err(app_system_error!("raw_password cannot be empty"));
}
if encoded_password.is_empty() {
warn!("Empty encoded password");
return Ok(false);
}
if !BCRYPT_PATTERN.is_match(encoded_password) {
warn!("Encoded password does not look like BCrypt");
return Ok(false);
}
BCrypt::checkpw(raw_password, encoded_password)
}
pub fn upgrade_encoding(&self, encoded_password: &str) -> AppResult<bool> {
if encoded_password.is_empty() {
warn!("Empty encoded password");
return Ok(false);
}
let cap = BCRYPT_PATTERN.captures(encoded_password);
match cap {
None => Err(app_system_error!("Encoded password does not look like BCrypt: {}", encoded_password)),
Some(c) => {
let m = c.get(2).ok_or_else(|| app_system_error!("get password strength failed"))?;
let s = m.as_str();
let strength = s
.parse::<u32>()
.map_err(|err| app_system_error!("parse password strength failed: {}, reason: {:?}", encoded_password, err))?;
Ok(strength < self.strength)
}
}
}
}
impl PasswordEncoder {
fn get_salt(&self) -> AppResult<String> {
BCrypt::gensalt_with_prefix_rounds(self.version.to_string().as_str(), self.strength)
}
}
#[allow(unused)]
#[cfg(test)]
mod test {
use crate::tina::security::password_encoder::PasswordEncoder;
use std::error::Error;
#[test]
#[ignore]
fn test_encode() -> Result<(), Box<dyn Error>> {
let raw_password = "admin123";
let encoder = PasswordEncoder::try_new()?;
let encoded_password = encoder.encode(raw_password)?;
println!("{}", encoded_password);
Ok(())
}
#[test]
fn test_match() -> Result<(), Box<dyn Error>> {
let raw_password = "admin123";
let encoded_password = "$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2";
let encoder = PasswordEncoder::try_new()?;
let m = encoder.matches(raw_password, encoded_password)?;
assert!(m);
Ok(())
}
}