better_auth_api/plugins/two_factor/
options.rs1use std::{future::Future, pin::Pin, sync::Arc};
2
3use async_trait::async_trait;
4use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
5use better_auth_core::{AuthResult, SecretKey};
6use rand::{Rng, distributions::Alphanumeric};
7use sha2::{Digest, Sha256};
8
9pub type TwoFactorHasher =
11 Arc<dyn Fn(String) -> Pin<Box<dyn Future<Output = AuthResult<String>> + Send>> + Send + Sync>;
12
13#[async_trait]
15pub trait TwoFactorCipher: Send + Sync {
16 async fn encrypt(&self, plaintext: &str) -> AuthResult<String>;
18 async fn decrypt(&self, ciphertext: &str) -> AuthResult<String>;
20}
21
22#[derive(Clone, Default)]
24pub enum TwoFactorOtpStorage {
25 #[default]
27 Plain,
28 Hashed,
30 Encrypted,
32 CustomHash(TwoFactorHasher),
34 CustomEncryption(Arc<dyn TwoFactorCipher>),
36}
37
38impl TwoFactorOtpStorage {
39 pub(super) async fn encode(&self, value: &str, secret: SecretKey<'_>) -> AuthResult<String> {
40 match self {
41 Self::Plain => Ok(value.to_owned()),
42 Self::Hashed => Ok(URL_SAFE_NO_PAD.encode(Sha256::digest(value.as_bytes()))),
43 Self::Encrypted => crate::plugins::symmetric::encrypt(secret, value),
44 Self::CustomHash(hash) => hash(value.to_owned()).await,
45 Self::CustomEncryption(cipher) => cipher.encrypt(value).await,
46 }
47 }
48
49 pub(super) async fn verify(
50 &self,
51 stored: &str,
52 input: &str,
53 secret: SecretKey<'_>,
54 ) -> AuthResult<bool> {
55 let (stored, input) = match self {
56 Self::Encrypted => (
57 crate::plugins::symmetric::decrypt(secret, stored)?,
58 input.to_owned(),
59 ),
60 Self::CustomEncryption(cipher) => (cipher.decrypt(stored).await?, input.to_owned()),
61 _ => (stored.to_owned(), self.encode(input, secret).await?),
62 };
63 Ok(stored.len() == input.len() && openssl::memcmp::eq(stored.as_bytes(), input.as_bytes()))
64 }
65}
66
67#[derive(Clone, Default)]
69pub enum BackupCodeStorage {
70 Plain,
72 #[default]
74 Encrypted,
75 Custom(Arc<dyn TwoFactorCipher>),
77}
78
79impl BackupCodeStorage {
80 pub(super) async fn encode(
81 &self,
82 codes: &[String],
83 secret: SecretKey<'_>,
84 ) -> AuthResult<String> {
85 let json = serde_json::to_string(codes)?;
86 match self {
87 Self::Plain => Ok(json),
88 Self::Encrypted => crate::plugins::symmetric::encrypt(secret, &json),
89 Self::Custom(cipher) => cipher.encrypt(&json).await,
90 }
91 }
92
93 pub(super) async fn decode(
94 &self,
95 stored: &str,
96 secret: SecretKey<'_>,
97 ) -> AuthResult<Option<Vec<String>>> {
98 let json = match self {
99 Self::Plain => stored.to_owned(),
100 Self::Encrypted => crate::plugins::symmetric::decrypt(secret, stored)?,
101 Self::Custom(cipher) => cipher.decrypt(stored).await?,
102 };
103 Ok(serde_json::from_str(&json).ok())
105 }
106}
107
108#[derive(Clone)]
110pub struct BackupCodeOptions {
111 pub amount: usize,
113 pub length: usize,
115 pub generate: Option<Arc<dyn Fn() -> Vec<String> + Send + Sync>>,
117 pub storage: BackupCodeStorage,
119 pub allow_passwordless: Option<bool>,
121}
122
123impl Default for BackupCodeOptions {
124 fn default() -> Self {
125 Self {
126 amount: 10,
127 length: 10,
128 generate: None,
129 storage: BackupCodeStorage::Encrypted,
130 allow_passwordless: None,
131 }
132 }
133}
134
135impl BackupCodeOptions {
136 pub(super) fn generate(&self) -> Vec<String> {
137 if let Some(generate) = &self.generate {
138 return generate();
139 }
140 (0..self.amount)
141 .map(|_| {
142 let code: String = rand::thread_rng()
143 .sample_iter(&Alphanumeric)
144 .take(self.length)
145 .map(char::from)
146 .collect();
147 let (prefix, suffix) = code.split_at(code.len().min(5));
148 format!("{prefix}-{suffix}")
149 })
150 .collect()
151 }
152}