Skip to main content

scrypt/
params.rs

1use crate::errors::InvalidParams;
2
3#[cfg(feature = "phc")]
4use password_hash::{Error, phc};
5
6#[cfg(all(feature = "phc", doc))]
7use password_hash::PasswordHasher;
8
9/// The Scrypt parameter values.
10#[derive(Clone, Copy, Debug, PartialEq)]
11pub struct Params {
12    pub(crate) log_n: u8,
13    pub(crate) r: u32,
14    pub(crate) p: u32,
15    #[cfg(feature = "password-hash")]
16    pub(crate) len: Option<usize>,
17}
18
19impl Params {
20    /// Recommended log₂ of the Scrypt parameter `N`: CPU/memory cost.
21    pub const RECOMMENDED_LOG_N: u8 = 17;
22
23    /// Recommended Scrypt parameter `r`: block size.
24    pub const RECOMMENDED_R: u32 = 8;
25
26    /// Recommended Scrypt parameter `p`: parallelism.
27    pub const RECOMMENDED_P: u32 = 1;
28
29    /// Recommended Scrypt parameter `Key length`.
30    pub const RECOMMENDED_LEN: usize = 32;
31
32    /// Recommended values according to the [OWASP cheat sheet].
33    /// - `log_n = 17` (`n = 131072`)
34    /// - `r = 8`
35    /// - `p = 1`
36    ///
37    /// [OWASP cheat sheet]: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#scrypt
38    pub const RECOMMENDED: Self = Self {
39        log_n: Self::RECOMMENDED_LOG_N,
40        r: Self::RECOMMENDED_R,
41        p: Self::RECOMMENDED_P,
42        #[cfg(feature = "password-hash")]
43        len: None,
44    };
45
46    /// Create a new instance of [`Params`].
47    ///
48    /// # Arguments
49    /// - `log_n` - The log₂ of the Scrypt parameter `N`
50    /// - `r` - The Scrypt parameter `r`
51    /// - `p` - The Scrypt parameter `p`
52    ///
53    /// # Conditions
54    /// - `log_n` must be less than `64`
55    /// - `r` must be greater than `0` and less than or equal to `4294967295`
56    /// - `p` must be greater than `0` and less than `4294967295`
57    ///
58    /// # Errors
59    /// Returns [`InvalidParams`] if one of the parameters is incorrect.
60    pub fn new(log_n: u8, r: u32, p: u32) -> Result<Params, InvalidParams> {
61        let cond1 = (log_n as usize) < usize::BITS as usize;
62        let cond2 = size_of::<usize>() >= size_of::<u32>();
63        let cond3 = usize::try_from(r).is_ok() && usize::try_from(p).is_ok();
64        if !(r > 0 && p > 0 && cond1 && (cond2 || cond3)) {
65            return Err(InvalidParams);
66        }
67
68        let n = 1usize << log_n;
69
70        // check that r * 128 doesn't overflow
71        let r128 = usize::try_from(r)
72            .map_err(|_| InvalidParams)?
73            .checked_mul(128)
74            .ok_or(InvalidParams)?;
75
76        // check that n * r * 128 doesn't overflow
77        r128.checked_mul(n).ok_or(InvalidParams)?;
78
79        // check that p * r * 128 doesn't overflow
80        usize::try_from(p)
81            .ok()
82            .and_then(|p| r128.checked_mul(p))
83            .ok_or(InvalidParams)?;
84
85        // Note: RFC 7914 requires `n < 2^(128 * r / 8)`, i.e. `log_n < r * 16`,
86        // but this upper bound is based on an error in the RFC where a bit count
87        // was treated as a byte count. The correct bound from the original scrypt
88        // paper (Percival, 2009) is `N < 2^(128*r)`, i.e. `log_n < r * 128`,
89        // which far exceeds any practical parameter value.
90        // We intentionally omit this check, consistent with the Tarsnap reference
91        // implementation and Go's x/crypto/scrypt.
92        // See: https://github.com/RustCrypto/password-hashes/issues/866
93        // See: https://www.rfc-editor.org/errata/eid5971
94
95        // This check required by Scrypt:
96        // check: p <= ((2^32-1) * 32) / (128 * r)
97        // It takes a bit of re-arranging to get the check above into this form,
98        // but it is indeed the same.
99        if r * p >= 0x4000_0000 {
100            return Err(InvalidParams);
101        }
102
103        Ok(Params {
104            log_n,
105            r,
106            p,
107            #[cfg(feature = "password-hash")]
108            len: None,
109        })
110    }
111
112    /// Create a new instance of [`Params`], overriding the output length.
113    ///
114    /// Note that this length is only intended for use with the [`PasswordHasher`] API, and not with
115    /// the low-level [`scrypt::scrypt`][`crate::scrypt`] API, which determines the output length
116    /// using the size of the `output` slice.
117    ///
118    /// The allowed values for `len` are between 10 bytes (80 bits) and 64 bytes inclusive.
119    /// These lengths come from the [PHC string format specification](https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md)
120    /// because they are intended for use with password hash strings.
121    ///
122    /// # Errors
123    /// Returns [`InvalidParams`] if one of the parameters is incorrect.
124    #[cfg(feature = "phc")]
125    pub fn new_with_output_len(
126        log_n: u8,
127        r: u32,
128        p: u32,
129        len: usize,
130    ) -> Result<Params, InvalidParams> {
131        if !(phc::Output::MIN_LENGTH..=phc::Output::MAX_LENGTH).contains(&len) {
132            return Err(InvalidParams);
133        }
134
135        let mut ret = Self::new(log_n, r, p)?;
136        ret.len = Some(len);
137        Ok(ret)
138    }
139
140    /// Deprecated: recommended values according to the OWASP cheat sheet.
141    #[deprecated(since = "0.12.0", note = "use Params::RECOMMENDED instead")]
142    #[must_use]
143    pub const fn recommended() -> Params {
144        Self::RECOMMENDED
145    }
146
147    /// log₂ of the Scrypt parameter `N`, the work factor.
148    ///
149    /// Memory and CPU usage scale linearly with `N`. If you need `N`, use
150    /// [`Params::n`] instead.
151    #[must_use]
152    pub const fn log_n(&self) -> u8 {
153        self.log_n
154    }
155
156    /// `N` parameter: the work factor.
157    ///
158    /// This method returns 2 to the power of [`Params::log_n`]. Memory and CPU
159    /// usage scale linearly with `N`.
160    #[must_use]
161    pub const fn n(&self) -> u64 {
162        1 << self.log_n
163    }
164
165    /// `r` parameter: resource usage.
166    ///
167    /// scrypt iterates 2*r times. Memory and CPU time scale linearly
168    /// with this parameter.
169    #[must_use]
170    pub const fn r(&self) -> u32 {
171        self.r
172    }
173
174    /// `p` parameter: parallelization.
175    #[must_use]
176    pub const fn p(&self) -> u32 {
177        self.p
178    }
179}
180
181impl Default for Params {
182    fn default() -> Params {
183        Params::RECOMMENDED
184    }
185}
186
187#[cfg(feature = "phc")]
188impl TryFrom<&phc::ParamsString> for Params {
189    type Error = Error;
190
191    fn try_from(params: &phc::ParamsString) -> password_hash::Result<Self> {
192        let mut log_n = Self::RECOMMENDED_LOG_N;
193        let mut r = Self::RECOMMENDED_R;
194        let mut p = Self::RECOMMENDED_P;
195
196        for (ident, value) in params.iter() {
197            match ident.as_str() {
198                "ln" => {
199                    log_n = value
200                        .decimal()
201                        .ok()
202                        .and_then(|dec| dec.try_into().ok())
203                        .ok_or(Error::ParamInvalid { name: "ln" })?;
204                }
205                "r" => {
206                    r = value
207                        .decimal()
208                        .map_err(|_| Error::ParamInvalid { name: "r" })?;
209                }
210                "p" => {
211                    p = value
212                        .decimal()
213                        .map_err(|_| Error::ParamInvalid { name: "p" })?;
214                }
215                _ => return Err(Error::ParamsInvalid),
216            }
217        }
218
219        Params::new(log_n, r, p).map_err(|_| Error::ParamsInvalid)
220    }
221}
222
223#[cfg(feature = "phc")]
224impl TryFrom<&phc::PasswordHash> for Params {
225    type Error = Error;
226
227    fn try_from(hash: &phc::PasswordHash) -> password_hash::Result<Self> {
228        if hash.version.is_some() {
229            return Err(Error::Version);
230        }
231
232        let mut params = Params::try_from(&hash.params)?;
233
234        params.len = Some(hash.hash.map_or(Self::RECOMMENDED_LEN, |out| out.len()));
235
236        Ok(params)
237    }
238}
239
240#[cfg(feature = "phc")]
241impl TryFrom<Params> for phc::ParamsString {
242    type Error = Error;
243
244    fn try_from(params: Params) -> Result<phc::ParamsString, Error> {
245        Self::try_from(&params)
246    }
247}
248
249#[cfg(feature = "phc")]
250impl TryFrom<&Params> for phc::ParamsString {
251    type Error = Error;
252
253    fn try_from(input: &Params) -> Result<phc::ParamsString, Error> {
254        let mut output = phc::ParamsString::new();
255
256        for (name, value) in [
257            ("ln", phc::Decimal::from(input.log_n)),
258            ("r", input.r),
259            ("p", input.p),
260        ] {
261            output
262                .add_decimal(name, value)
263                .map_err(|_| Error::ParamInvalid { name })?;
264        }
265
266        Ok(output)
267    }
268}