Skip to main content

alibi_api/plugins/email_otp/
storage.rs

1use 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/// Application-owned codec for custom hash/encryption storage.
7/// `retrieve` returns `None` when the representation is intentionally irreversible.
8#[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    ///
36    /// # Errors
37    /// Returns an error when validation, storage, or an application callback fails.
38    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    ///
52    /// # Errors
53    /// Returns an error when validation, storage, or an application callback fails.
54    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    ///
72    /// # Errors
73    /// Returns an error when validation, storage, or an application callback fails.
74    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    ///
91    /// # Errors
92    /// Returns an error when validation, storage, or an application callback fails.
93    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}