1#![forbid(unsafe_code)]
2
3use oxicrypto_core::{CryptoError, PasswordHash as PasswordHashTrait, PasswordHashParams};
9use scrypt::{scrypt, Params as RcScryptParams};
10
11#[derive(Debug, Clone, Copy)]
22pub struct ScryptParams {
23 pub log_n: u8,
25 pub r: u32,
27 pub p: u32,
29}
30
31impl ScryptParams {
32 #[must_use = "ScryptParams creation result must be checked"]
34 pub fn new(log_n: u8, r: u32, p: u32) -> Result<Self, CryptoError> {
35 RcScryptParams::new(log_n, r, p).map_err(|_| CryptoError::BadInput)?;
37 Ok(Self { log_n, r, p })
38 }
39
40 #[must_use]
45 pub fn interactive() -> Self {
46 Self {
47 log_n: 15,
48 r: 8,
49 p: 1,
50 }
51 }
52
53 #[must_use]
58 pub fn moderate() -> Self {
59 Self {
60 log_n: 17,
61 r: 8,
62 p: 1,
63 }
64 }
65
66 #[must_use]
71 pub fn sensitive() -> Self {
72 Self {
73 log_n: 20,
74 r: 8,
75 p: 1,
76 }
77 }
78}
79
80impl PasswordHashParams for ScryptParams {
81 fn memory_cost(&self) -> Option<u32> {
83 let n: u64 = 1u64 << self.log_n;
85 let kib = 128u64.saturating_mul(n).saturating_mul(self.r as u64) / 1024;
86 u32::try_from(kib).ok()
87 }
88
89 fn time_cost(&self) -> Option<u32> {
90 None
92 }
93
94 fn parallelism(&self) -> Option<u32> {
95 Some(self.p)
96 }
97}
98
99#[must_use = "scrypt derive result must be checked"]
114pub fn scrypt_derive(
115 password: &[u8],
116 salt: &[u8],
117 log_n: u8,
118 r: u32,
119 p: u32,
120 out: &mut [u8],
121) -> Result<(), CryptoError> {
122 if out.is_empty() {
123 return Err(CryptoError::BadInput);
124 }
125 let params = RcScryptParams::new(log_n, r, p).map_err(|_| CryptoError::BadInput)?;
126 scrypt(password, salt, ¶ms, out).map_err(|_| CryptoError::Internal("scrypt failed"))
127}
128
129#[derive(Debug, Clone, Copy)]
146pub struct ScryptHasher {
147 pub params: ScryptParams,
149}
150
151impl ScryptHasher {
152 pub fn new(params: ScryptParams) -> Result<Self, CryptoError> {
156 RcScryptParams::new(params.log_n, params.r, params.p).map_err(|_| CryptoError::BadInput)?;
158 Ok(Self { params })
159 }
160
161 pub fn new_checked(params: ScryptParams) -> Self {
165 Self::new(params).expect("invalid ScryptParams")
166 }
167
168 #[must_use]
170 pub fn interactive() -> Self {
171 Self {
172 params: ScryptParams::interactive(),
173 }
174 }
175
176 #[must_use]
178 pub fn moderate() -> Self {
179 Self {
180 params: ScryptParams::moderate(),
181 }
182 }
183
184 #[must_use]
186 pub fn sensitive() -> Self {
187 Self {
188 params: ScryptParams::sensitive(),
189 }
190 }
191}
192
193impl PasswordHashTrait for ScryptHasher {
194 fn name(&self) -> &'static str {
195 "scrypt"
196 }
197
198 fn hash_password(
199 &self,
200 password: &[u8],
201 salt: &[u8],
202 _params: &dyn PasswordHashParams,
203 out: &mut [u8],
204 ) -> Result<(), CryptoError> {
205 scrypt_derive(
206 password,
207 salt,
208 self.params.log_n,
209 self.params.r,
210 self.params.p,
211 out,
212 )
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 fn test_params() -> ScryptParams {
222 ScryptParams {
223 log_n: 1,
224 r: 1,
225 p: 1,
226 }
227 }
228
229 fn test_hasher() -> ScryptHasher {
230 ScryptHasher::new(test_params()).expect("test params valid")
231 }
232
233 const SALT: &[u8] = b"test-salt-16byte";
234
235 #[test]
236 fn scrypt_derive_deterministic() {
237 let p = test_params();
238 let mut out1 = [0u8; 32];
239 let mut out2 = [0u8; 32];
240 scrypt_derive(b"password", SALT, p.log_n, p.r, p.p, &mut out1).expect("derive 1");
241 scrypt_derive(b"password", SALT, p.log_n, p.r, p.p, &mut out2).expect("derive 2");
242 assert_eq!(out1, out2, "scrypt must be deterministic");
243 assert_ne!(out1, [0u8; 32]);
244 }
245
246 #[test]
247 fn scrypt_derive_empty_output_errors() {
248 let p = test_params();
249 let result = scrypt_derive(b"password", SALT, p.log_n, p.r, p.p, &mut []);
250 assert_eq!(result, Err(CryptoError::BadInput));
251 }
252
253 #[test]
254 fn password_hash_trait_deterministic() {
255 let hasher = test_hasher();
256 let mut out1 = [0u8; 32];
257 let mut out2 = [0u8; 32];
258 hasher
259 .hash_password(b"password", SALT, &hasher.params, &mut out1)
260 .expect("hash 1");
261 hasher
262 .hash_password(b"password", SALT, &hasher.params, &mut out2)
263 .expect("hash 2");
264 assert_eq!(out1, out2);
265 assert_ne!(out1, [0u8; 32]);
266 }
267
268 #[test]
269 fn preset_cost_ordering() {
270 let interactive = ScryptParams::interactive();
271 let moderate = ScryptParams::moderate();
272 let sensitive = ScryptParams::sensitive();
273 assert!(sensitive.log_n > moderate.log_n);
275 assert!(moderate.log_n > interactive.log_n);
276 assert!(sensitive.memory_cost() > moderate.memory_cost());
278 assert!(moderate.memory_cost() > interactive.memory_cost());
279 }
280
281 #[test]
282 fn scrypt_params_password_hash_params_impl() {
283 let p = ScryptParams::interactive();
284 assert!(p.memory_cost().is_some());
285 assert!(p.time_cost().is_none());
286 assert_eq!(p.parallelism(), Some(1));
287 }
288
289 #[test]
290 fn hasher_name() {
291 assert_eq!(test_hasher().name(), "scrypt");
292 }
293}