dipper 0.5.3

An out-of-the-box modular dependency injection web application framework.
Documentation
use serde::{Deserialize, Serialize};
use time::{Duration, OffsetDateTime};

use crate::{
    fnd::{
        aes::*,
        authn::{Authn, UserHandle, UserTotpInfo},
        captcha::{Captcha, CaptchaName, Difficulty},
        identifier::{Uid, UserIdentifier},
        totp::TOTP,
    },
    prelude::*,
};

#[dipper]
impl<U> Authn<U>
where
    U: UserHandle,
{
    /// Obtain the user's TOTP QR code.
    #[dipper(handler)]
    pub async fn get_user_totp_qr_goal(&self, depot: &mut Depot, res: &mut Response) -> ApiResponse<Vec<u8>> {
        self.inner_user_totp_qr_goal(depot, res, false).await.into()
    }

    /// Refresh the user's TOTP QR code.
    #[dipper(handler)]
    pub async fn refresh_user_totp_qr_goal(&self, depot: &mut Depot, res: &mut Response) -> ApiResponse<Vec<u8>> {
        self.inner_user_totp_qr_goal(depot, res, true).await.into()
    }

    pub async fn inner_user_totp_qr_goal(
        &self,
        depot: &mut Depot,
        res: &mut Response,
        is_refresh: bool,
    ) -> Result<Vec<u8>, ApiError> {
        if let Some(data) = self.obtain_jwt_claims(depot) {
            let UserTotpInfo {
                totp_secret, username, ..
            } = if is_refresh {
                self.user_handle()
                    .get_totp_info(depot, &UserIdentifier::id(data.claims.uid.clone()))
                    .await?
            } else {
                self.user_handle()
                    .set_totp_secret(depot, data.claims.uid.clone(), TOTP::gen_secret_base32())
                    .await?
            };
            #[allow(clippy::unwrap_used)]
            res.headers_mut().insert("content-type", "image/png".parse().unwrap());
            self.new_totp_qr_png(TotpData::otp(totp_secret), username)
        } else {
            Err(api_err!(
                ET_ACCESS_AUTHZ,
                "There is a problem with the authentication hoop.",
                &ERR_PATH_AUTHN
            ))
        }
    }

    /// Generate TOTP QR
    pub fn new_totp_qr_base64(&self, secret_base32: TotpData<String>, username: String) -> Result<String, ApiError> {
        secret_base32.with_totp(|totp, secret| {
            totp.qr_base64(secret, self.app_name().clone(), username)
                .map_err(|_| api_err!(ET_SYS_ERR, "Failed to generate OTP QR.", &ERR_PATH_AUTHN))
        })
    }

    /// Generate TOTP QR
    pub fn new_totp_qr_png(&self, secret_base32: TotpData<String>, username: String) -> Result<Vec<u8>, ApiError> {
        secret_base32.with_totp(|totp, secret| {
            totp.qr_png(secret, self.app_name().clone(), username)
                .map_err(|_| api_err!(ET_SYS_ERR, "Failed to generate OTP QR.", &ERR_PATH_AUTHN))
        })
    }

    /// Obtain the public current TOTP token.
    #[inline(always)]
    pub async fn obtain_pub_totp_secret(&self) -> String {
        TOTP::secret_from_root(self.obtain_jwt_secret_key().await)
    }

    /// Obtain the public current TOTP token.
    #[inline(always)]
    pub async fn obtain_pub_totp_token(&self, args: TotpData<()>) -> anyhow::Result<String> {
        let secret = self.obtain_pub_totp_secret().await;
        args.with_totp(|totp, _| totp.gen_current(secret))
    }

    /// Verify the public current TOTP token.
    #[inline(always)]
    pub async fn verify_pub_totp_token(&self, totp_token: TotpData<&str>) -> Result<bool, ApiError> {
        self.verify_totp_token(self.obtain_pub_totp_secret().await, totp_token)
    }

    /// Obtain the current TOTP token.
    #[inline(always)]
    pub fn obtain_totp_token(&self, totp_secret: TotpData<String>) -> anyhow::Result<String> {
        totp_secret.with_totp(|totp, totp_secret| totp.gen_current(totp_secret))
    }

    /// Verify the current TOTP token.
    #[inline(always)]
    pub fn verify_totp_token(&self, totp_secret: String, totp_token: TotpData<&str>) -> Result<bool, ApiError> {
        let matched = totp_token.with_totp(|totp, otp_token| totp.check_current(totp_secret, otp_token));
        matched.map_err(|err| api_err!(ET_SYS_ERR, &ERR_PATH_AUTHN).with_source(err.into_error(), true))
    }

    /// Verify the user TOTP token and return the user ID.
    pub async fn verify_user_totp_token(
        &self,
        depot: &mut Depot,
        user_ident: &UserIdentifier,
        totp_token: TotpData<&str>,
    ) -> Result<Uid, ApiError> {
        let user_totp_info = self.user_handle.get_totp_info(depot, user_ident).await?;
        let matched = self.verify_totp_token(user_totp_info.totp_secret, totp_token)?;
        if !matched {
            return Err(api_err!(
                ET_USER_VERIF_CODE,
                "OTP token is not matched.",
                &ERR_PATH_AUTHN
            ));
        }
        Ok(user_totp_info.user_id)
    }

    /// Obtain a captcha.
    #[dipper(handler)]
    pub async fn get_captcha_goal(&self) -> Json<CaptchaPair> {
        Json(self.obtain_captcha().await)
    }

    async fn obtain_captcha_secret(&self) -> String {
        #[allow(clippy::string_slice)]
        self.obtain_pub_totp_secret().await[0..16].to_owned()
    }

    fn new_captcha_args(x: &String) -> [u8; 16] {
        let mut iv: [u8; 16] = [0x24; 16];
        for (i, val) in x.as_bytes().iter().enumerate() {
            if i < iv.len() {
                iv[i] = *val;
            } else {
                break;
            }
        }
        iv
    }

    #[inline(always)]
    pub async fn obtain_captcha(&self) -> CaptchaPair {
        let (chars, png_base64) = Captcha::by_name(Difficulty::Medium, CaptchaName::Mila)
            .as_base64()
            .expect("unreachable!");
        let key = self.obtain_captcha_secret().await;
        let exp = OffsetDateTime::now_utc() + Duration::minutes(5);
        let plaintext = format!("{chars}@{}", exp.unix_timestamp());
        let ciphertext_base64 = Aes128Cbc::encrypt(
            plaintext.as_bytes(),
            &Self::new_captcha_args(&key),
            &Self::new_captcha_args(&chars),
        );
        CaptchaPair {
            captcha: png_base64,
            captcha_sign: ciphertext_base64,
        }
    }

    #[inline(always)]
    pub async fn verify_captcha(&self, captcha: &CaptchaPair) -> Result<(), ApiError> {
        let CaptchaPair {
            captcha: chars,
            captcha_sign: ciphertext_base64,
        } = &captcha;
        let key = self.obtain_captcha_secret().await;
        match Aes128Cbc::decrypt(
            ciphertext_base64,
            &Self::new_captcha_args(&key),
            &Self::new_captcha_args(chars),
        ) {
            Ok(plaintext) => {
                if let Ok(plaintext) = String::from_utf8(plaintext) {
                    if let Some((chars2, exp)) = plaintext.split_once("@") {
                        if chars2 == chars {
                            if let Ok(exp) = exp.parse::<i64>() {
                                if exp >= OffsetDateTime::now_utc().unix_timestamp() {
                                    return Ok(());
                                }
                            }
                        }
                    }
                }
            }
            Err(e) => {
                println!("verify_captcha: captcha={captcha:?}, error={e:?}");
            }
        }
        Err(api_err!(ET_USER_VERIF_CODE, "Captcha is not matched.", &ERR_PATH_AUTHN))
    }
}

#[allow(non_camel_case_types)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "factor_type", content = "factor_value")]
#[allow(clippy::exhaustive_enums)]
pub enum Factor {
    password {
        password: String,
        captcha: Option<CaptchaPair>,
    },
    totp_token {
        totp_token: String,
        captcha: Option<CaptchaPair>,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[allow(clippy::exhaustive_structs)]
pub struct CaptchaPair {
    pub captcha: String,
    pub captcha_sign: String,
}

#[allow(non_camel_case_types)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "data_type", content = "data")]
#[allow(clippy::exhaustive_enums)]
pub enum TotpData<T> {
    otp(T),
    email(T),
    sms(T),
}

impl<T> TotpData<T> {
    pub fn with_totp<D>(self, callback: impl FnOnce(TOTP, T) -> D) -> D {
        match self {
            TotpData::otp(t) => callback(TOTP::default_otp(), t),
            TotpData::email(t) => callback(TOTP::default_email(), t),
            TotpData::sms(t) => callback(TOTP::default_sms(), t),
        }
    }
    pub fn into_data(self) -> T {
        match self {
            TotpData::otp(v) => v,
            TotpData::email(v) => v,
            TotpData::sms(v) => v,
        }
    }
    pub const fn data_ref(&self) -> &T {
        match self {
            TotpData::otp(v) => v,
            TotpData::email(v) => v,
            TotpData::sms(v) => v,
        }
    }
    pub fn data_mut(&mut self) -> &mut T {
        match self {
            TotpData::otp(v) => v,
            TotpData::email(v) => v,
            TotpData::sms(v) => v,
        }
    }
    pub fn replace_data(&mut self, new_value: T) -> T {
        let old_value = match self {
            TotpData::otp(old_value) => old_value,
            TotpData::email(old_value) => old_value,
            TotpData::sms(old_value) => old_value,
        };
        std::mem::replace(old_value, new_value)
    }
}