Skip to main content

alibi_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    pub(crate) async fn store_codes(
37        &self,
38        codes: &[String],
39        secret: &alibi_core::AuthConfig,
40    ) -> AuthResult<String> {
41        self.store_json(serde_json::to_string(codes)?, secret).await
42    }
43
44    pub(crate) async fn store_json(
45        &self,
46        json: String,
47        secret: &alibi_core::AuthConfig,
48    ) -> AuthResult<String> {
49        match self {
50            Self::Encrypted => super::encrypt_value(secret, &json),
51            Self::Plain => Ok(json),
52            Self::CustomCipher(cipher) => cipher.encrypt(&json).await.map_err(callback_error),
53        }
54    }
55
56    // Keep the parsed value until the operation's truthiness/shape checks have
57    // run. JSON.stringify maps Infinity to null; doing that during decoding
58    // would incorrectly spend a pending attempt instead of restoring it.
59    pub(crate) async fn load_value(
60        &self,
61        stored: &str,
62        secret: &alibi_core::AuthConfig,
63    ) -> AuthResult<Option<alibi_core::utils::json::JsValue>> {
64        let json = match self {
65            Self::Encrypted => super::decrypt_value(secret, stored)?,
66            Self::Plain => stored.to_owned(),
67            Self::CustomCipher(cipher) => cipher.decrypt(stored).await.map_err(callback_error)?,
68        };
69        Ok(alibi_core::utils::json::parse_value(&json).ok())
70    }
71}
72
73pub(super) fn json_date(value: &str) -> Option<String> {
74    alibi_core::utils::datetime::normalize_json_date(value)
75}
76
77pub(super) fn normalize_json_dates(value: &mut alibi_core::utils::json::JsValue) {
78    use alibi_core::utils::json::JsValue;
79    match value {
80        JsValue::String(text) => {
81            if let Some(date) = json_date(text) {
82                *text = date;
83            }
84        }
85        JsValue::Array(values) => values.iter_mut().for_each(normalize_json_dates),
86        JsValue::Object(values) => values.values_mut().for_each(normalize_json_dates),
87        _ => {}
88    }
89}
90
91pub(super) fn truthy(value: &alibi_core::utils::json::JsValue) -> bool {
92    use alibi_core::utils::json::JsValue;
93    match value {
94        JsValue::Null | JsValue::Bool(false) => false,
95        JsValue::Number(number) => *number != 0.0 && !number.is_nan(),
96        JsValue::String(text) => !text.is_empty(),
97        _ => true,
98    }
99}
100
101fn callback_error(error: AuthError) -> AuthError {
102    match error {
103        AuthError::Internal(_) => AuthError::CallbackFailure(Box::new(error)),
104        error => error,
105    }
106}