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

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

            
50
/// [Argon2id](https://crates.io/crates/argon2) base parameters.
51
2605
#[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
2620
    pub fn default_for(system: &System, hashers: u32) -> Self {
66
2620
        ArgonParams::Timed(TimedArgonParams::default_for(system, hashers))
67
2620
    }
68
}
69

            
70
/// Automatic configuration based on execution time. This is measured during the
71
/// first `set_password`
72
2605
#[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: 4,
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 64MB but will change as the [minimum recommendations][rfc] are
108
    /// changed.
109
    ///
110
    /// [rfc]: https://www.rfc-editor.org/rfc/rfc9106.html#name-parameter-choice
111
    pub const MINIMUM_RAM_PER_HASHER: u32 = 64 * 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 4.
127
    ///
128
    /// - `minimum_duration`: defaults to 1 second. The [RFC][rfc] suggests 0.5
129
    ///   seconds, but many in the community recommend 1 second.
130
    ///
131
    /// The strength of Argon2 is derived largely by the amount of RAM dedicated
132
    /// to it, so the largest value acceptable should be chosen for
133
    /// `ram_per_hasher`. For more guidance on parameter selection, see [RFC
134
    /// 9106, section 4 "Parameter Choice"][rfc].
135
    ///
136
    /// ## Debug Mode
137
    ///
138
    /// When running with `debug_assertions` the `ram_per_hasher` will be set to
139
    /// 32kb. This is due to how slow debug mode is for the hashing algorithm.
140
    /// These settings should not be used in production.
141
    ///
142
    /// [rfc]: https://www.rfc-editor.org/rfc/rfc9106.html#name-parameter-choice
143
2620
    pub fn default_for(system: &System, hashers: u32) -> Self {
144
2620
        let total_memory = u32::try_from(system.total_memory()).unwrap_or(u32::MAX);
145
2620
        let max_memory = total_memory / 32;
146

            
147
2620
        let ram_per_hasher = if cfg!(debug_assertions) {
148
2620
            32 * 1024
149
        } else {
150
            (max_memory / hashers).max(Self::MINIMUM_RAM_PER_HASHER)
151
        };
152

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