use std::{future::Future, pin::Pin, sync::Arc};
use async_trait::async_trait;
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use better_auth_core::{AuthResult, SecretKey};
use rand::{Rng, distributions::Alphanumeric};
use sha2::{Digest, Sha256};
pub type TwoFactorHasher =
Arc<dyn Fn(String) -> Pin<Box<dyn Future<Output = AuthResult<String>> + Send>> + Send + Sync>;
#[async_trait]
pub trait TwoFactorCipher: Send + Sync {
async fn encrypt(&self, plaintext: &str) -> AuthResult<String>;
async fn decrypt(&self, ciphertext: &str) -> AuthResult<String>;
}
#[derive(Clone, Default)]
pub enum TwoFactorOtpStorage {
#[default]
Plain,
Hashed,
Encrypted,
CustomHash(TwoFactorHasher),
CustomEncryption(Arc<dyn TwoFactorCipher>),
}
impl TwoFactorOtpStorage {
pub(super) async fn encode(&self, value: &str, secret: SecretKey<'_>) -> AuthResult<String> {
match self {
Self::Plain => Ok(value.to_owned()),
Self::Hashed => Ok(URL_SAFE_NO_PAD.encode(Sha256::digest(value.as_bytes()))),
Self::Encrypted => crate::plugins::symmetric::encrypt(secret, value),
Self::CustomHash(hash) => hash(value.to_owned()).await,
Self::CustomEncryption(cipher) => cipher.encrypt(value).await,
}
}
pub(super) async fn verify(
&self,
stored: &str,
input: &str,
secret: SecretKey<'_>,
) -> AuthResult<bool> {
let (stored, input) = match self {
Self::Encrypted => (
crate::plugins::symmetric::decrypt(secret, stored)?,
input.to_owned(),
),
Self::CustomEncryption(cipher) => (cipher.decrypt(stored).await?, input.to_owned()),
_ => (stored.to_owned(), self.encode(input, secret).await?),
};
Ok(stored.len() == input.len() && openssl::memcmp::eq(stored.as_bytes(), input.as_bytes()))
}
}
#[derive(Clone, Default)]
pub enum BackupCodeStorage {
Plain,
#[default]
Encrypted,
Custom(Arc<dyn TwoFactorCipher>),
}
impl BackupCodeStorage {
pub(super) async fn encode(
&self,
codes: &[String],
secret: SecretKey<'_>,
) -> AuthResult<String> {
let json = serde_json::to_string(codes)?;
match self {
Self::Plain => Ok(json),
Self::Encrypted => crate::plugins::symmetric::encrypt(secret, &json),
Self::Custom(cipher) => cipher.encrypt(&json).await,
}
}
pub(super) async fn decode(
&self,
stored: &str,
secret: SecretKey<'_>,
) -> AuthResult<Option<Vec<String>>> {
let json = match self {
Self::Plain => stored.to_owned(),
Self::Encrypted => crate::plugins::symmetric::decrypt(secret, stored)?,
Self::Custom(cipher) => cipher.decrypt(stored).await?,
};
Ok(serde_json::from_str(&json).ok())
}
}
#[derive(Clone)]
pub struct BackupCodeOptions {
pub amount: usize,
pub length: usize,
pub generate: Option<Arc<dyn Fn() -> Vec<String> + Send + Sync>>,
pub storage: BackupCodeStorage,
pub allow_passwordless: Option<bool>,
}
impl Default for BackupCodeOptions {
fn default() -> Self {
Self {
amount: 10,
length: 10,
generate: None,
storage: BackupCodeStorage::Encrypted,
allow_passwordless: None,
}
}
}
impl BackupCodeOptions {
pub(super) fn generate(&self) -> Vec<String> {
if let Some(generate) = &self.generate {
return generate();
}
(0..self.amount)
.map(|_| {
let code: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(self.length)
.map(char::from)
.collect();
let (prefix, suffix) = code.split_at(code.len().min(5));
format!("{prefix}-{suffix}")
})
.collect()
}
}