zino_orm/
helper.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
use super::Schema;
use std::fmt::Display;
use zino_core::{
    crypto,
    encoding::base64,
    error::Error,
    extension::{JsonObjectExt, TomlTableExt},
    state::State,
    warn, LazyLock, Map,
};

/// Helper utilities for models.
pub trait ModelHelper<K>: Schema<PrimaryKey = K>
where
    K: Default + Display + PartialEq,
{
    /// Returns the secret key for the model.
    /// It should have at least 64 bytes.
    ///
    /// # Note
    ///
    /// This should only be used for internal services. Do not expose it to external users.
    #[inline]
    fn secret_key() -> &'static [u8] {
        SECRET_KEY.as_slice()
    }

    /// Encrypts the password for the model.
    fn encrypt_password(password: &str) -> Result<String, Error> {
        let key = Self::secret_key();
        let password = password.as_bytes();
        if base64::decode(password).is_ok_and(|bytes| bytes.len() == 256) {
            crypto::encrypt_hashed_password(password, key)
                .map_err(|err| warn!("fail to encrypt hashed password: {}", err.message()))
        } else {
            crypto::encrypt_raw_password(password, key)
                .map_err(|err| warn!("fail to encrypt raw password: {}", err.message()))
        }
    }

    /// Verifies the password for the model.
    fn verify_password(password: &str, encrypted_password: &str) -> Result<bool, Error> {
        let key = Self::secret_key();
        let password = password.as_bytes();
        let encrypted_password = encrypted_password.as_bytes();
        if base64::decode(password).is_ok_and(|bytes| bytes.len() == 256) {
            crypto::verify_hashed_password(password, encrypted_password, key)
                .map_err(|err| warn!("fail to verify hashed password: {}", err.message()))
        } else {
            crypto::verify_raw_password(password, encrypted_password, key)
                .map_err(|err| warn!("fail to verify raw password: {}", err.message()))
        }
    }

    /// Translates the model data.
    fn translate_model(model: &mut Map) {
        #[cfg(feature = "openapi")]
        zino_openapi::translate_model_entry(model, Self::model_name());
        for col in Self::columns() {
            if let Some(translated_field) = col.extra().get_str("translate_as") {
                let field = [col.name(), "_translated"].concat();
                if let Some(value) = model.remove(&field) {
                    model.upsert(translated_field, value);
                }
            }
        }
    }
}

impl<M, K> ModelHelper<K> for M
where
    M: Schema<PrimaryKey = K>,
    K: Default + Display + PartialEq,
{
}

/// Secret key.
static SECRET_KEY: LazyLock<[u8; 64]> = LazyLock::new(|| {
    let app_config = State::shared().config();
    let config = app_config.get_table("database").unwrap_or(app_config);
    let checksum: [u8; 32] = config
        .get_str("checksum")
        .and_then(|checksum| checksum.as_bytes().try_into().ok())
        .unwrap_or_else(|| {
            let secret = config
                .get_str("secret")
                .map(|s| s.to_owned())
                .unwrap_or_else(|| {
                    tracing::warn!("auto-generated `secret` is used for deriving a secret key");
                    format!("{}{}", *super::TABLE_PREFIX, super::DRIVER_NAME)
                });
            crypto::digest(secret.as_bytes())
        });
    let info = config.get_str("info").unwrap_or("ZINO:ORM");
    crypto::derive_key(info, &checksum)
});