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,
{
#[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()
}
#[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
))
}
}
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))
})
}
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))
})
}
#[inline(always)]
pub async fn obtain_pub_totp_secret(&self) -> String {
TOTP::secret_from_root(self.obtain_jwt_secret_key().await)
}
#[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))
}
#[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)
}
#[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))
}
#[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))
}
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)
}
#[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)
}
}