tina-core 0.0.2

Tina platform
Documentation
//! 密码加密
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 {
    /// $2a
    Dollar2A,
    /// $2y
    Dollar2Y,
    /// $2b
    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(())
    }
}