alibi_api/plugins/email_otp/
storage.rs1use crate::plugins::token_crypto::{decrypt_with_config, encrypt_with_config, hash_token};
2use alibi_core::{AuthError, AuthResult};
3use async_trait::async_trait;
4use std::sync::Arc;
5
6#[async_trait]
9pub trait EmailOtpCodec: Send + Sync {
10 async fn store(&self, otp: &str) -> AuthResult<String>;
11 async fn verify(&self, stored: &str, otp: &str) -> AuthResult<bool>;
12 async fn retrieve(&self, stored: &str) -> AuthResult<Option<String>>;
13}
14
15#[derive(Clone)]
16pub enum EmailOtpStorage {
17 Plain,
18 Hashed,
19 Encrypted,
20 Custom(Arc<dyn EmailOtpCodec>),
21}
22
23impl std::fmt::Debug for EmailOtpStorage {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 match self {
26 Self::Plain => f.write_str("EmailOtpStorage::Plain"),
27 Self::Hashed => f.write_str("EmailOtpStorage::Hashed"),
28 Self::Encrypted => f.write_str("EmailOtpStorage::Encrypted"),
29 Self::Custom(..) => f.write_str("EmailOtpStorage::Custom"),
30 }
31 }
32}
33
34impl EmailOtpStorage {
35 pub(super) async fn store(
39 &self,
40 otp: &str,
41 secret: &alibi_core::AuthConfig,
42 ) -> AuthResult<String> {
43 match self {
44 Self::Plain => Ok(otp.to_owned()),
45 Self::Hashed => Ok(hash_token(otp)),
46 Self::Encrypted => encrypt_with_config(otp, secret),
47 Self::Custom(codec) => codec.store(otp).await,
48 }
49 }
50
51 pub(super) async fn verify(
55 &self,
56 stored: &str,
57 otp: &str,
58 secret: &alibi_core::AuthConfig,
59 ) -> AuthResult<bool> {
60 match self {
61 Self::Plain => Ok(constant_time_equal(stored, otp)),
62 Self::Hashed => Ok(constant_time_equal(stored, &hash_token(otp))),
63 Self::Encrypted => Ok(constant_time_equal(
64 &decrypt_with_config(stored, secret)?,
65 otp,
66 )),
67 Self::Custom(codec) => codec.verify(stored, otp).await,
68 }
69 }
70
71 pub(super) async fn retrieve(
75 &self,
76 stored: &str,
77 secret: &alibi_core::AuthConfig,
78 ) -> AuthResult<Option<String>> {
79 let plain = match self {
80 Self::Plain => Some(stored.to_owned()),
81 Self::Hashed => None,
82 Self::Encrypted => Some(decrypt_with_config(stored, secret)?),
83 Self::Custom(codec) => codec.retrieve(stored).await?,
84 };
85 plain.map(Some).ok_or_else(|| {
86 AuthError::bad_request("OTP is hashed, cannot return the plain text OTP")
87 })
88 }
89
90 pub(super) async fn reusable(
94 &self,
95 stored: &str,
96 secret: &alibi_core::AuthConfig,
97 ) -> AuthResult<Option<String>> {
98 match self {
99 Self::Hashed => Ok(None),
100 Self::Custom(codec) => codec.retrieve(stored).await,
101 Self::Plain | Self::Encrypted => self.retrieve(stored, secret).await,
102 }
103 }
104}
105
106fn constant_time_equal(left: &str, right: &str) -> bool {
107 if left.len() != right.len() {
108 return false;
109 }
110 left.bytes()
111 .zip(right.bytes())
112 .fold(0u8, |difference, (a, b)| difference | (a ^ b))
113 == 0
114}