Skip to main content

alibi_api/plugins/two_factor/
backup_storage.rs

1use alibi_core::{AuthError, AuthResult};
2use async_trait::async_trait;
3use std::sync::Arc;
4
5/// Async persistence callbacks for the complete backup-code JSON string.
6///
7/// Returning [`AuthError::Internal`] represents an ordinary application throw:
8/// HTTP callers receive an empty 500. Explicit API errors retain their public
9/// status and body, even when their message matches an ordinary failure.
10#[async_trait]
11pub trait TwoFactorBackupCipher: Send + Sync {
12    async fn encrypt(&self, json: &str) -> AuthResult<String>;
13    async fn decrypt(&self, stored: &str) -> AuthResult<String>;
14}
15
16/// Backup persistence policy. Encrypted storage is the pinned default.
17#[derive(Clone, Default)]
18pub enum TwoFactorBackupStorage {
19    #[default]
20    Encrypted,
21    Plain,
22    CustomCipher(Arc<dyn TwoFactorBackupCipher>),
23}
24
25impl std::fmt::Debug for TwoFactorBackupStorage {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        f.write_str(match self {
28            Self::Encrypted => "Encrypted",
29            Self::Plain => "Plain",
30            Self::CustomCipher(_) => "CustomCipher",
31        })
32    }
33}
34
35impl TwoFactorBackupStorage {
36    ///
37    /// # Errors
38    /// Returns an error when validation, storage, or an application callback fails.
39    pub(in crate::plugins) async fn store_codes(
40        &self,
41        codes: &[String],
42        secret: &alibi_core::AuthConfig,
43    ) -> AuthResult<String> {
44        self.store_json(serde_json::to_string(codes)?, secret).await
45    }
46
47    pub(in crate::plugins) async fn store_json(
48        &self,
49        json: String,
50        secret: &alibi_core::AuthConfig,
51    ) -> AuthResult<String> {
52        match self {
53            Self::Encrypted => super::encrypt_value(secret, &json),
54            Self::Plain => Ok(json),
55            Self::CustomCipher(cipher) => cipher.encrypt(&json).await.map_err(callback_error),
56        }
57    }
58
59    // Keep the parsed value until the operation's truthiness/shape checks have
60    // run. JSON.stringify maps Infinity to null; doing that during decoding
61    // would incorrectly spend a pending attempt instead of restoring it.
62    pub(in crate::plugins) async fn load_value(
63        &self,
64        stored: &str,
65        secret: &alibi_core::AuthConfig,
66    ) -> AuthResult<Option<alibi_core::utils::json::JsValue>> {
67        let json = match self {
68            Self::Encrypted => super::decrypt_value(secret, stored)?,
69            Self::Plain => stored.to_owned(),
70            Self::CustomCipher(cipher) => cipher.decrypt(stored).await.map_err(callback_error)?,
71        };
72        Ok(alibi_core::utils::json::parse_value(&json).ok())
73    }
74}
75
76pub(super) fn json_date(value: &str) -> Option<String> {
77    alibi_core::utils::datetime::normalize_json_date(value)
78}
79
80pub(super) fn normalize_json_dates(value: &mut alibi_core::utils::json::JsValue) {
81    use alibi_core::utils::json::JsValue;
82    match value {
83        JsValue::String(text) => {
84            if let Some(date) = json_date(text) {
85                *text = date;
86            }
87        }
88        JsValue::Array(values) => values.iter_mut().for_each(normalize_json_dates),
89        JsValue::Object(values) => values.values_mut().for_each(normalize_json_dates),
90        _ => {}
91    }
92}
93
94pub(super) fn truthy(value: &alibi_core::utils::json::JsValue) -> bool {
95    use alibi_core::utils::json::JsValue;
96    match value {
97        JsValue::Null | JsValue::Bool(false) => false,
98        JsValue::Number(number) => *number != 0.0 && !number.is_nan(),
99        JsValue::String(text) => !text.is_empty(),
100        _ => true,
101    }
102}
103
104fn callback_error(error: AuthError) -> AuthError {
105    match error {
106        AuthError::Internal(_) => AuthError::CallbackFailure(Box::new(error)),
107        error => error,
108    }
109}