1
use std::time::Duration;
2

            
3
use argon2::Algorithm;
4
use sysinfo::{System, SystemExt};
5

            
6
use crate::config::SystemDefault;
7

            
8
/// Password hashing configuration.
9
///
10
/// BonsaiDb uses [`argon2`](https://crates.io/crates/argon2) for its password hashing.
11
12
#[derive(Debug, Clone)]
12
#[non_exhaustive]
13
pub struct ArgonConfiguration {
14
    /// The number of concurrent hashing operations that are allowed to take place.
15
    pub hashers: u32,
16
    /// The algorithm variation to use. Most users should select
17
    /// [`Algorithm::Argon2id`].
18
    pub algorithm: Algorithm,
19
    /// The parameters for each hasher.
20
    pub params: ArgonParams,
21
}
22

            
23
impl SystemDefault for ArgonConfiguration {
24
5334
    fn default_for(system: &System) -> Self {
25
5334
        let cpu_count = u32::try_from(
26
5334
            system
27
5334
                .physical_core_count()
28
5334
                .unwrap_or_else(|| system.cpus().len()),
29
5334
        )
30
5334
        .expect("cpu count returned unexpectedly large value");
31
5334
        let mut hashers = (cpu_count + 3) / 4;
32
5334
        let max_hashers = u32::try_from(
33
5334
            system.total_memory() / u64::from(TimedArgonParams::MINIMUM_RAM_PER_HASHER),
34
5334
        )
35
5334
        .unwrap_or(u32::MAX);
36
5334
        // total_memory() can return 0, so we need to ensure max_hashers isn't
37
5334
        // 0.
38
5334
        if max_hashers > 0 && hashers > max_hashers {
39
            hashers = max_hashers;
40
5334
        }
41

            
42
5334
        ArgonConfiguration {
43
5334
            hashers,
44
5334
            algorithm: Algorithm::Argon2id,
45
5334
            params: ArgonParams::default_for(system, hashers),
46
5334
        }
47
5334
    }
48
}
49

            
50
/// [Argon2id](https://crates.io/crates/argon2) base parameters.
51
5274
#[derive(Debug, Clone)]
52
#[non_exhaustive]
53
#[must_use]
54
pub enum ArgonParams {
55
    /// Specific argon2 parameters.
56
    Params(argon2::ParamsBuilder),
57
    /// Automatic configuration based on execution time. This is measured during
58
    /// the first `set_password` operation.
59
    Timed(TimedArgonParams),
60
}
61

            
62
impl ArgonParams {
63
    /// Returns the default configuration based on the system information and
64
    /// number of hashers. See [`TimedArgonParams`] for more details.
65
5334
    pub fn default_for(system: &System, hashers: u32) -> Self {
66
5334
        ArgonParams::Timed(TimedArgonParams::default_for(system, hashers))
67
5334
    }
68
}
69

            
70
/// Automatic configuration based on execution time. This is measured during the
71
/// first `set_password`
72
5274
#[derive(Debug, Clone)]
73
#[must_use]
74
pub struct TimedArgonParams {
75
    /// The number of lanes (`p`) that the argon algorithm should use.
76
    pub lanes: u32,
77
    /// The amount of ram each hashing operation should utilize.
78
    pub ram_per_hasher: u32,
79
    /// The minimum execution time that hashing a password should consume.
80
    pub minimum_duration: Duration,
81
}
82

            
83
impl Default for TimedArgonParams {
84
    /// ## Default Values
85
    ///
86
    /// When using `TimedArgonParams::default()`, the settings are 4 lanes,
87
    /// [`Self::MINIMUM_RAM_PER_HASHER`] of RAM per hasher, and a minimum
88
    /// duration of 1 second.
89
    ///
90
    /// The strength of Argon2 is derived largely by the amount of RAM dedicated
91
    /// to it, so the largest value acceptable should be chosen for
92
    /// `ram_per_hasher`. For more guidance on parameter selection, see [RFC
93
    /// 9106, section 4 "Parameter Choice"][rfc].
94
    ///
95
    /// [rfc]: https://www.rfc-editor.org/rfc/rfc9106.html#name-parameter-choice
96
    fn default() -> Self {
97
        Self {
98
            lanes: 1,
99
            ram_per_hasher: Self::MINIMUM_RAM_PER_HASHER,
100
            minimum_duration: Duration::from_secs(1),
101
        }
102
    }
103
}
104

            
105
impl TimedArgonParams {
106
    /// The minimum amount of ram to allocate per hasher. This value is
107
    /// currently 19MB but will change as the [OWASP minimum recommendations][owasp] are
108
    /// changed.
109
    ///
110
    /// [owasp]: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
111
    pub const MINIMUM_RAM_PER_HASHER: u32 = 19 * 1024 * 1024;
112

            
113
    /// Returns the default configuration based on the system information and
114
    /// number of hashers.
115
    ///
116
    /// - `ram_per_hasher`: The total amount of RAM allocated will be the total
117
    ///   system memory divided by 16. This allocated amount will be divided
118
    ///   equally between the hashers. If this number is less than
119
    ///   [`Self::MINIMUM_RAM_PER_HASHER`], [`Self::MINIMUM_RAM_PER_HASHER`]
120
    ///   will be used instead.
121
    ///
122
    ///   For example, if 4 hashers are used on a system with 16GB of RAM, a
123
    ///   total of 1GB of RAM will be used between 4 hashers, yielding a
124
    ///   `ram_per_hasher` value of 256MB.
125
    ///
126
    /// - `lanes`: defaults to 1, per the recommended `OWASP` minimum settings.
127
    ///
128
    /// - `minimum_duration`: defaults to 1 second. The [RFC][rfc] suggests 0.5
129
    ///   seconds, but many in the community recommend 1 second. When computing
130
    ///   the ideal parameters, a minimum iteration count of 2 will be used to
131
    ///   ensure compliance with minimum parameters recommended by `OWASP`.
132
    ///
133
    /// The strength of Argon2 is derived largely by the amount of RAM dedicated
134
    /// to it, so the largest value acceptable should be chosen for
135
    /// `ram_per_hasher`. For more guidance on parameter selection, see [RFC
136
    /// 9106, section 4 "Parameter Choice"][rfc] or the [`OWASP` Password
137
    /// Storage Cheetsheet][owasp]
138
    ///
139
    /// ## Debug Mode
140
    ///
141
    /// When running with `debug_assertions` the `ram_per_hasher` will be set to
142
    /// 32kb. This is due to how slow debug mode is for the hashing algorithm.
143
    /// These settings should not be used in production.
144
    ///
145
    /// [owasp]:
146
    ///     https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
147
    /// [rfc]: https://www.rfc-editor.org/rfc/rfc9106.html#name-parameter-choice
148
5334
    pub fn default_for(system: &System, hashers: u32) -> Self {
149
5334
        let total_memory = u32::try_from(system.total_memory()).unwrap_or(u32::MAX);
150
5334
        let max_memory = total_memory / 32;
151

            
152
5334
        let ram_per_hasher = if cfg!(debug_assertions) {
153
5334
            Self::MINIMUM_RAM_PER_HASHER
154
        } else {
155
            (max_memory / hashers).max(Self::MINIMUM_RAM_PER_HASHER)
156
        };
157

            
158
        // Hypothetical Configurations used to determine these numbers:
159
        //
160
        // 1cpu, 512mb ram: 1 thread, 1 hasher, 32mb ram
161
        // 2cpus, 1GB ram: 1 thread, 1 hasher, 64mb ram
162
        // 16cpus, 16GB ram: 4 threads, 4 hashers, 64mb ram
163
        // 96cpus, 192GB ram: 24 threads, 24 hashers, ~510mb ram
164
5334
        Self {
165
5334
            lanes: 4,
166
5334
            ram_per_hasher,
167
5334
            minimum_duration: Duration::from_secs(1),
168
5334
        }
169
5334
    }
170
}