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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
use std::time::Duration;

use argon2::Algorithm;
use sysinfo::{System, SystemExt};

use crate::config::SystemDefault;

/// Password hashing configuration.
///
/// BonsaiDb uses [`argon2`](https://crates.io/crates/argon2) for its password hashing.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ArgonConfiguration {
    /// The number of concurrent hashing operations that are allowed to take place.
    pub hashers: u32,
    /// The algorithm variation to use. Most users should select
    /// [`Algorithm::Argon2id`].
    pub algorithm: Algorithm,
    /// The parameters for each hasher.
    pub params: ArgonParams,
}

impl SystemDefault for ArgonConfiguration {
    fn default_for(system: &System) -> Self {
        let cpu_count = u32::try_from(
            system
                .physical_core_count()
                .unwrap_or_else(|| system.cpus().len()),
        )
        .expect("cpu count returned unexpectedly large value");
        let mut hashers = (cpu_count + 3) / 4;
        let max_hashers = u32::try_from(
            system.total_memory() / u64::from(TimedArgonParams::MINIMUM_RAM_PER_HASHER),
        )
        .unwrap_or(u32::MAX);
        // total_memory() can return 0, so we need to ensure max_hashers isn't
        // 0.
        if max_hashers > 0 && hashers > max_hashers {
            hashers = max_hashers;
        }

        ArgonConfiguration {
            hashers,
            algorithm: Algorithm::Argon2id,
            params: ArgonParams::default_for(system, hashers),
        }
    }
}

/// [Argon2id](https://crates.io/crates/argon2) base parameters.
#[derive(Debug, Clone)]
#[non_exhaustive]
#[must_use]
pub enum ArgonParams {
    /// Specific argon2 parameters.
    Params(argon2::ParamsBuilder),
    /// Automatic configuration based on execution time. This is measured during
    /// the first `set_password` operation.
    Timed(TimedArgonParams),
}

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

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

impl Default for TimedArgonParams {
    /// ## Default Values
    ///
    /// When using `TimedArgonParams::default()`, the settings are 4 lanes,
    /// [`Self::MINIMUM_RAM_PER_HASHER`] of RAM per hasher, and a minimum
    /// duration of 1 second.
    ///
    /// The strength of Argon2 is derived largely by the amount of RAM dedicated
    /// to it, so the largest value acceptable should be chosen for
    /// `ram_per_hasher`. For more guidance on parameter selection, see [RFC
    /// 9106, section 4 "Parameter Choice"][rfc].
    ///
    /// [rfc]: https://www.rfc-editor.org/rfc/rfc9106.html#name-parameter-choice
    fn default() -> Self {
        Self {
            lanes: 1,
            ram_per_hasher: Self::MINIMUM_RAM_PER_HASHER,
            minimum_duration: Duration::from_secs(1),
        }
    }
}

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

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

        let ram_per_hasher = if cfg!(debug_assertions) {
            Self::MINIMUM_RAM_PER_HASHER
        } else {
            (max_memory / hashers).max(Self::MINIMUM_RAM_PER_HASHER)
        };

        // Hypothetical Configurations used to determine these numbers:
        //
        // 1cpu, 512mb ram: 1 thread, 1 hasher, 32mb ram
        // 2cpus, 1GB ram: 1 thread, 1 hasher, 64mb ram
        // 16cpus, 16GB ram: 4 threads, 4 hashers, 64mb ram
        // 96cpus, 192GB ram: 24 threads, 24 hashers, ~510mb ram
        Self {
            lanes: 4,
            ram_per_hasher,
            minimum_duration: Duration::from_secs(1),
        }
    }
}