/rust/registry/src/index.crates.io-1949cf8c6b5b557f/bcrypt-0.18.0/src/lib.rs
Line | Count | Source |
1 | | //! Easily hash and verify passwords using bcrypt |
2 | | #![forbid(unsafe_code)] |
3 | | #![cfg_attr(not(feature = "std"), no_std)] |
4 | | |
5 | | #[cfg(any(feature = "alloc", feature = "std", test))] |
6 | | extern crate alloc; |
7 | | |
8 | | #[cfg(any(feature = "alloc", feature = "std", test))] |
9 | | use alloc::{ |
10 | | string::{String, ToString}, |
11 | | vec::Vec, |
12 | | }; |
13 | | |
14 | | #[cfg(feature = "zeroize")] |
15 | | use zeroize::Zeroize; |
16 | | |
17 | | use base64::{alphabet::BCRYPT, engine::GeneralPurpose, engine::general_purpose::NO_PAD}; |
18 | | use core::fmt; |
19 | | #[cfg(any(feature = "alloc", feature = "std"))] |
20 | | use {base64::Engine, core::convert::AsRef, core::str::FromStr}; |
21 | | |
22 | | mod bcrypt; |
23 | | mod errors; |
24 | | |
25 | | pub use crate::bcrypt::bcrypt; |
26 | | pub use crate::errors::{BcryptError, BcryptResult}; |
27 | | |
28 | | // Cost constants |
29 | | const MIN_COST: u32 = 4; |
30 | | const MAX_COST: u32 = 31; |
31 | | pub const DEFAULT_COST: u32 = 12; |
32 | | pub const BASE_64: GeneralPurpose = GeneralPurpose::new(&BCRYPT, NO_PAD); |
33 | | |
34 | | #[cfg(any(feature = "alloc", feature = "std"))] |
35 | | #[derive(Debug, PartialEq, Eq)] |
36 | | /// A bcrypt hash result before concatenating |
37 | | pub struct HashParts { |
38 | | cost: u32, |
39 | | salt: String, |
40 | | hash: String, |
41 | | } |
42 | | |
43 | | #[derive(Clone, Debug)] |
44 | | /// BCrypt hash version |
45 | | /// https://en.wikipedia.org/wiki/Bcrypt#Versioning_history |
46 | | pub enum Version { |
47 | | TwoA, |
48 | | TwoX, |
49 | | TwoY, |
50 | | TwoB, |
51 | | } |
52 | | |
53 | | #[cfg(any(feature = "alloc", feature = "std"))] |
54 | | impl HashParts { |
55 | | /// Creates the bcrypt hash string from all its parts |
56 | 0 | fn format(&self) -> String { |
57 | 0 | self.format_for_version(Version::TwoB) |
58 | 0 | } |
59 | | |
60 | | /// Get the bcrypt hash cost |
61 | 0 | pub fn get_cost(&self) -> u32 { |
62 | 0 | self.cost |
63 | 0 | } |
64 | | |
65 | | /// Get the bcrypt hash salt |
66 | 0 | pub fn get_salt(&self) -> String { |
67 | 0 | self.salt.clone() |
68 | 0 | } |
69 | | |
70 | | /// Creates the bcrypt hash string from all its part, allowing to customize the version. |
71 | 0 | pub fn format_for_version(&self, version: Version) -> String { |
72 | | // Cost need to have a length of 2 so padding with a 0 if cost < 10 |
73 | 0 | alloc::format!("${}${:02}${}{}", version, self.cost, self.salt, self.hash) |
74 | 0 | } |
75 | | } |
76 | | |
77 | | #[cfg(any(feature = "alloc", feature = "std"))] |
78 | | impl FromStr for HashParts { |
79 | | type Err = BcryptError; |
80 | | |
81 | 0 | fn from_str(s: &str) -> Result<Self, Self::Err> { |
82 | 0 | split_hash(s) |
83 | 0 | } |
84 | | } |
85 | | |
86 | | #[cfg(any(feature = "alloc", feature = "std"))] |
87 | | impl fmt::Display for HashParts { |
88 | 0 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
89 | 0 | write!(f, "{}", self.format()) |
90 | 0 | } |
91 | | } |
92 | | |
93 | | impl fmt::Display for Version { |
94 | 0 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
95 | 0 | let str = match self { |
96 | 0 | Version::TwoA => "2a", |
97 | 0 | Version::TwoB => "2b", |
98 | 0 | Version::TwoX => "2x", |
99 | 0 | Version::TwoY => "2y", |
100 | | }; |
101 | 0 | write!(f, "{}", str) |
102 | 0 | } |
103 | | } |
104 | | |
105 | | /// The main meat: actually does the hashing and does some verification with |
106 | | /// the cost to ensure it's a correct one. If err_on_truncation, this method will return |
107 | | /// `BcryptError::Truncation`; otherwise it will truncate the password. |
108 | | #[cfg(any(feature = "alloc", feature = "std"))] |
109 | 0 | fn _hash_password( |
110 | 0 | password: &[u8], |
111 | 0 | cost: u32, |
112 | 0 | salt: [u8; 16], |
113 | 0 | err_on_truncation: bool, |
114 | 0 | ) -> BcryptResult<HashParts> { |
115 | 0 | if !(MIN_COST..=MAX_COST).contains(&cost) { |
116 | 0 | return Err(BcryptError::CostNotAllowed(cost)); |
117 | 0 | } |
118 | | |
119 | | // Passwords need to be null terminated |
120 | 0 | let mut vec = Vec::with_capacity(password.len() + 1); |
121 | 0 | vec.extend_from_slice(password); |
122 | 0 | vec.push(0); |
123 | | // We only consider the first 72 chars; truncate if necessary. |
124 | | // `bcrypt` below will panic if len > 72 |
125 | 0 | let truncated = if vec.len() > 72 { |
126 | 0 | if err_on_truncation { |
127 | 0 | return Err(BcryptError::Truncation(vec.len())); |
128 | 0 | } |
129 | 0 | &vec[..72] |
130 | | } else { |
131 | 0 | &vec |
132 | | }; |
133 | | |
134 | 0 | let output = bcrypt::bcrypt(cost, salt, truncated); |
135 | | |
136 | | #[cfg(feature = "zeroize")] |
137 | 0 | vec.zeroize(); |
138 | | |
139 | 0 | Ok(HashParts { |
140 | 0 | cost, |
141 | 0 | salt: BASE_64.encode(salt), |
142 | 0 | hash: BASE_64.encode(&output[..23]), // remember to remove the last byte |
143 | 0 | }) |
144 | 0 | } |
145 | | |
146 | | /// Takes a full hash and split it into 3 parts: |
147 | | /// cost, salt and hash |
148 | | #[cfg(any(feature = "alloc", feature = "std"))] |
149 | 0 | fn split_hash(hash: &str) -> BcryptResult<HashParts> { |
150 | 0 | let mut parts = HashParts { |
151 | 0 | cost: 0, |
152 | 0 | salt: "".to_string(), |
153 | 0 | hash: "".to_string(), |
154 | 0 | }; |
155 | | |
156 | | // Should be [prefix, cost, hash] |
157 | 0 | let raw_parts: Vec<_> = hash.split('$').filter(|s| !s.is_empty()).collect(); |
158 | | |
159 | 0 | if raw_parts.len() != 3 { |
160 | 0 | return Err(BcryptError::InvalidHash("the hash format is malformed")); |
161 | 0 | } |
162 | | |
163 | 0 | if raw_parts[0] != "2y" && raw_parts[0] != "2b" && raw_parts[0] != "2a" && raw_parts[0] != "2x" |
164 | | { |
165 | 0 | return Err(BcryptError::InvalidHash( |
166 | 0 | "the hash prefix is not a bcrypt prefix", |
167 | 0 | )); |
168 | 0 | } |
169 | | |
170 | 0 | if let Ok(c) = raw_parts[1].parse::<u32>() { |
171 | 0 | parts.cost = c; |
172 | 0 | } else { |
173 | 0 | return Err(BcryptError::InvalidHash("the cost value is not a number")); |
174 | | } |
175 | | |
176 | 0 | if raw_parts[2].len() == 53 && raw_parts[2].is_char_boundary(22) { |
177 | 0 | parts.salt = raw_parts[2][..22].chars().collect(); |
178 | 0 | parts.hash = raw_parts[2][22..].chars().collect(); |
179 | 0 | } else { |
180 | 0 | return Err(BcryptError::InvalidHash("the hash format is malformed")); |
181 | | } |
182 | | |
183 | 0 | Ok(parts) |
184 | 0 | } |
185 | | |
186 | | /// Generates a password hash using the cost given. |
187 | | /// The salt is generated randomly using the OS randomness |
188 | | #[cfg(any(feature = "alloc", feature = "std"))] |
189 | 0 | pub fn hash<P: AsRef<[u8]>>(password: P, cost: u32) -> BcryptResult<String> { |
190 | 0 | hash_with_result(password, cost).map(|r| r.format()) Unexecuted instantiation: bcrypt::hash::<alloc::string::String>::{closure#0}Unexecuted instantiation: bcrypt::hash::<_>::{closure#0} |
191 | 0 | } Unexecuted instantiation: bcrypt::hash::<alloc::string::String> Unexecuted instantiation: bcrypt::hash::<_> |
192 | | |
193 | | /// Generates a password hash using the cost given. |
194 | | /// The salt is generated randomly using the OS randomness |
195 | | /// Will return BcryptError::Truncation if password is longer than 72 bytes |
196 | | #[cfg(any(feature = "alloc", feature = "std"))] |
197 | 0 | pub fn non_truncating_hash<P: AsRef<[u8]>>(password: P, cost: u32) -> BcryptResult<String> { |
198 | 0 | non_truncating_hash_with_result(password, cost).map(|r| r.format()) |
199 | 0 | } |
200 | | |
201 | | /// Generates a password hash using the cost given. |
202 | | /// The salt is generated randomly using the OS randomness. |
203 | | /// The function returns a result structure and allows to format the hash in different versions. |
204 | | #[cfg(any(feature = "alloc", feature = "std"))] |
205 | 0 | pub fn hash_with_result<P: AsRef<[u8]>>(password: P, cost: u32) -> BcryptResult<HashParts> { |
206 | 0 | let salt = { |
207 | 0 | let mut s = [0u8; 16]; |
208 | 0 | getrandom::fill(&mut s).map(|_| s) |
209 | 0 | }?; |
210 | | |
211 | 0 | _hash_password(password.as_ref(), cost, salt, false) |
212 | 0 | } Unexecuted instantiation: bcrypt::hash_with_result::<alloc::string::String> Unexecuted instantiation: bcrypt::hash_with_result::<_> |
213 | | |
214 | | /// Generates a password hash using the cost given. |
215 | | /// The salt is generated randomly using the OS randomness. |
216 | | /// The function returns a result structure and allows to format the hash in different versions. |
217 | | /// Will return BcryptError::Truncation if password is longer than 72 bytes |
218 | | #[cfg(any(feature = "alloc", feature = "std"))] |
219 | 0 | pub fn non_truncating_hash_with_result<P: AsRef<[u8]>>( |
220 | 0 | password: P, |
221 | 0 | cost: u32, |
222 | 0 | ) -> BcryptResult<HashParts> { |
223 | 0 | let salt = { |
224 | 0 | let mut s = [0u8; 16]; |
225 | 0 | getrandom::fill(&mut s).map(|_| s) |
226 | 0 | }?; |
227 | | |
228 | 0 | _hash_password(password.as_ref(), cost, salt, true) |
229 | 0 | } |
230 | | |
231 | | /// Generates a password given a hash and a cost. |
232 | | /// The function returns a result structure and allows to format the hash in different versions. |
233 | | #[cfg(any(feature = "alloc", feature = "std"))] |
234 | 0 | pub fn hash_with_salt<P: AsRef<[u8]>>( |
235 | 0 | password: P, |
236 | 0 | cost: u32, |
237 | 0 | salt: [u8; 16], |
238 | 0 | ) -> BcryptResult<HashParts> { |
239 | 0 | _hash_password(password.as_ref(), cost, salt, false) |
240 | 0 | } |
241 | | |
242 | | /// Generates a password given a hash and a cost. |
243 | | /// The function returns a result structure and allows to format the hash in different versions. |
244 | | /// Will return BcryptError::Truncation if password is longer than 72 bytes |
245 | | #[cfg(any(feature = "alloc", feature = "std"))] |
246 | 0 | pub fn non_truncating_hash_with_salt<P: AsRef<[u8]>>( |
247 | 0 | password: P, |
248 | 0 | cost: u32, |
249 | 0 | salt: [u8; 16], |
250 | 0 | ) -> BcryptResult<HashParts> { |
251 | 0 | _hash_password(password.as_ref(), cost, salt, true) |
252 | 0 | } |
253 | | |
254 | | /// Verify the password against the hash by extracting the salt from the hash and recomputing the |
255 | | /// hash from the password. If `err_on_truncation` is set to true, then this method will return |
256 | | /// `BcryptError::Truncation`. |
257 | | #[cfg(any(feature = "alloc", feature = "std"))] |
258 | 0 | fn _verify<P: AsRef<[u8]>>(password: P, hash: &str, err_on_truncation: bool) -> BcryptResult<bool> { |
259 | | use subtle::ConstantTimeEq; |
260 | | |
261 | 0 | let parts = split_hash(hash)?; |
262 | 0 | let salt = BASE_64 |
263 | 0 | .decode(&parts.salt) |
264 | 0 | .map_err(|_| BcryptError::InvalidHash("the salt part is not valid base64"))?; |
265 | 0 | let generated = _hash_password( |
266 | 0 | password.as_ref(), |
267 | 0 | parts.cost, |
268 | 0 | salt.try_into() |
269 | 0 | .map_err(|_| BcryptError::InvalidHash("the salt length is not 16 bytes"))?, |
270 | 0 | err_on_truncation, |
271 | 0 | )?; |
272 | 0 | let source_decoded = BASE_64 |
273 | 0 | .decode(parts.hash) |
274 | 0 | .map_err(|_| BcryptError::InvalidHash("the hash to verify against is not valid base64"))?; |
275 | 0 | let generated_decoded = BASE_64.decode(generated.hash).map_err(|_| { |
276 | 0 | BcryptError::InvalidHash("the generated hash for the password is not valid base64") |
277 | 0 | })?; Unexecuted instantiation: bcrypt::_verify::<alloc::string::String>::{closure#3}Unexecuted instantiation: bcrypt::_verify::<_>::{closure#3} |
278 | | |
279 | 0 | Ok(source_decoded.ct_eq(&generated_decoded).into()) |
280 | 0 | } Unexecuted instantiation: bcrypt::_verify::<alloc::string::String> Unexecuted instantiation: bcrypt::_verify::<_> |
281 | | |
282 | | /// Verify that a password is equivalent to the hash provided |
283 | | #[cfg(any(feature = "alloc", feature = "std"))] |
284 | 0 | pub fn verify<P: AsRef<[u8]>>(password: P, hash: &str) -> BcryptResult<bool> { |
285 | 0 | _verify(password, hash, false) |
286 | 0 | } Unexecuted instantiation: bcrypt::verify::<alloc::string::String> Unexecuted instantiation: bcrypt::verify::<_> |
287 | | |
288 | | /// Verify that a password is equivalent to the hash provided. |
289 | | /// Only use this if you are only using `non_truncating_hash` to generate the hash. |
290 | | /// It will return an error for inputs that will work if generated from other sources. |
291 | | #[cfg(any(feature = "alloc", feature = "std"))] |
292 | 0 | pub fn non_truncating_verify<P: AsRef<[u8]>>(password: P, hash: &str) -> BcryptResult<bool> { |
293 | 0 | _verify(password, hash, true) |
294 | 0 | } |
295 | | |
296 | | #[cfg(all(test, any(feature = "alloc", feature = "std")))] |
297 | | mod tests { |
298 | | use crate::non_truncating_hash; |
299 | | |
300 | | use super::{ |
301 | | _hash_password, BcryptError, BcryptResult, DEFAULT_COST, HashParts, Version, |
302 | | alloc::{ |
303 | | string::{String, ToString}, |
304 | | vec, |
305 | | vec::Vec, |
306 | | }, |
307 | | hash, hash_with_salt, non_truncating_verify, split_hash, verify, |
308 | | }; |
309 | | use core::convert::TryInto; |
310 | | use core::iter; |
311 | | use core::str::FromStr; |
312 | | use quickcheck::{TestResult, quickcheck}; |
313 | | |
314 | | #[test] |
315 | | fn can_split_hash() { |
316 | | let hash = "$2y$12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u"; |
317 | | let output = split_hash(hash).unwrap(); |
318 | | let expected = HashParts { |
319 | | cost: 12, |
320 | | salt: "L6Bc/AlTQHyd9liGgGEZyO".to_string(), |
321 | | hash: "FLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u".to_string(), |
322 | | }; |
323 | | assert_eq!(output, expected); |
324 | | } |
325 | | |
326 | | #[test] |
327 | | fn can_output_cost_and_salt_from_parsed_hash() { |
328 | | let hash = "$2y$12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u"; |
329 | | let parsed = HashParts::from_str(hash).unwrap(); |
330 | | assert_eq!(parsed.get_cost(), 12); |
331 | | assert_eq!(parsed.get_salt(), "L6Bc/AlTQHyd9liGgGEZyO".to_string()); |
332 | | } |
333 | | |
334 | | #[test] |
335 | | fn returns_an_error_if_a_parsed_hash_is_baddly_formated() { |
336 | | let hash1 = "$2y$12$L6Bc/AlTQHyd9lGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u"; |
337 | | assert!(HashParts::from_str(hash1).is_err()); |
338 | | |
339 | | let hash2 = "!2y$12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u"; |
340 | | assert!(HashParts::from_str(hash2).is_err()); |
341 | | |
342 | | let hash3 = "$2y$-12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u"; |
343 | | assert!(HashParts::from_str(hash3).is_err()); |
344 | | } |
345 | | |
346 | | #[test] |
347 | | fn can_verify_hash_generated_from_some_online_tool() { |
348 | | let hash = "$2a$04$UuTkLRZZ6QofpDOlMz32MuuxEHA43WOemOYHPz6.SjsVsyO1tDU96"; |
349 | | assert!(verify("password", hash).unwrap()); |
350 | | } |
351 | | |
352 | | #[test] |
353 | | fn can_verify_hash_generated_from_python() { |
354 | | let hash = "$2b$04$EGdrhbKUv8Oc9vGiXX0HQOxSg445d458Muh7DAHskb6QbtCvdxcie"; |
355 | | assert!(verify("correctbatteryhorsestapler", hash).unwrap()); |
356 | | } |
357 | | |
358 | | #[test] |
359 | | fn can_verify_hash_generated_from_node() { |
360 | | let hash = "$2a$04$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk5bektyVVa5xnIi"; |
361 | | assert!(verify("correctbatteryhorsestapler", hash).unwrap()); |
362 | | } |
363 | | |
364 | | #[test] |
365 | | fn can_verify_hash_generated_from_go() { |
366 | | /* |
367 | | package main |
368 | | import ( |
369 | | "io" |
370 | | "os" |
371 | | "golang.org/x/crypto/bcrypt" |
372 | | ) |
373 | | func main() { |
374 | | buf, err := io.ReadAll(os.Stdin) |
375 | | if err != nil { |
376 | | panic(err) |
377 | | } |
378 | | out, err := bcrypt.GenerateFromPassword(buf, bcrypt.MinCost) |
379 | | if err != nil { |
380 | | panic(err) |
381 | | } |
382 | | os.Stdout.Write(out) |
383 | | os.Stdout.Write([]byte("\n")) |
384 | | } |
385 | | */ |
386 | | let binary_input = vec![ |
387 | | 29, 225, 195, 167, 223, 236, 85, 195, 114, 227, 7, 0, 209, 239, 189, 24, 51, 105, 124, |
388 | | 168, 151, 75, 144, 64, 198, 197, 196, 4, 241, 97, 110, 135, |
389 | | ]; |
390 | | let hash = "$2a$04$tjARW6ZON3PhrAIRW2LG/u9aDw5eFdstYLR8nFCNaOQmsH9XD23w."; |
391 | | assert!(verify(binary_input, hash).unwrap()); |
392 | | } |
393 | | |
394 | | #[test] |
395 | | fn invalid_hash_does_not_panic() { |
396 | | let binary_input = vec![ |
397 | | 29, 225, 195, 167, 223, 236, 85, 195, 114, 227, 7, 0, 209, 239, 189, 24, 51, 105, 124, |
398 | | 168, 151, 75, 144, 64, 198, 197, 196, 4, 241, 97, 110, 135, |
399 | | ]; |
400 | | let hash = "$2a$04$tjARW6ZON3PhrAIRW2LG/u9a."; |
401 | | assert!(verify(binary_input, hash).is_err()); |
402 | | } |
403 | | |
404 | | #[test] |
405 | | fn a_wrong_password_is_false() { |
406 | | let hash = "$2b$04$EGdrhbKUv8Oc9vGiXX0HQOxSg445d458Muh7DAHskb6QbtCvdxcie"; |
407 | | assert!(!verify("wrong", hash).unwrap()); |
408 | | } |
409 | | |
410 | | #[test] |
411 | | fn errors_with_invalid_hash() { |
412 | | // there is another $ in the hash part |
413 | | let hash = "$2a$04$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk$5bektyVVa5xnIi"; |
414 | | assert!(verify("correctbatteryhorsestapler", hash).is_err()); |
415 | | } |
416 | | |
417 | | #[test] |
418 | | fn errors_with_non_number_cost() { |
419 | | // the cost is not a number |
420 | | let hash = "$2a$ab$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk$5bektyVVa5xnIi"; |
421 | | assert!(verify("correctbatteryhorsestapler", hash).is_err()); |
422 | | } |
423 | | |
424 | | #[test] |
425 | | fn errors_with_a_hash_too_long() { |
426 | | // the cost is not a number |
427 | | let hash = "$2a$04$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk$5bektyVVa5xnIerererereri"; |
428 | | assert!(verify("correctbatteryhorsestapler", hash).is_err()); |
429 | | } |
430 | | |
431 | | #[test] |
432 | | fn can_verify_own_generated() { |
433 | | let hashed = hash("hunter2", 4).unwrap(); |
434 | | assert_eq!(true, verify("hunter2", &hashed).unwrap()); |
435 | | } |
436 | | |
437 | | #[test] |
438 | | fn long_passwords_truncate_correctly() { |
439 | | // produced with python -c 'import bcrypt; bcrypt.hashpw(b"x"*100, b"$2a$05$...............................")' |
440 | | let hash = "$2a$05$......................YgIDy4hFBdVlc/6LHnD9mX488r9cLd2"; |
441 | | assert!(verify(iter::repeat("x").take(100).collect::<String>(), hash).unwrap()); |
442 | | } |
443 | | |
444 | | #[test] |
445 | | fn non_truncating_operations() { |
446 | | assert!(matches!( |
447 | | non_truncating_hash(iter::repeat("x").take(72).collect::<String>(), DEFAULT_COST), |
448 | | BcryptResult::Err(BcryptError::Truncation(73)) |
449 | | )); |
450 | | assert!(matches!( |
451 | | non_truncating_hash(iter::repeat("x").take(71).collect::<String>(), DEFAULT_COST), |
452 | | BcryptResult::Ok(_) |
453 | | )); |
454 | | |
455 | | let hash = "$2a$05$......................YgIDy4hFBdVlc/6LHnD9mX488r9cLd2"; |
456 | | assert!(matches!( |
457 | | non_truncating_verify(iter::repeat("x").take(100).collect::<String>(), hash), |
458 | | Err(BcryptError::Truncation(101)) |
459 | | )); |
460 | | } |
461 | | |
462 | | #[test] |
463 | | fn generate_versions() { |
464 | | let password = "hunter2".as_bytes(); |
465 | | let salt = vec![0; 16]; |
466 | | let result = |
467 | | _hash_password(password, DEFAULT_COST, salt.try_into().unwrap(), false).unwrap(); |
468 | | assert_eq!( |
469 | | "$2a$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm", |
470 | | result.format_for_version(Version::TwoA) |
471 | | ); |
472 | | assert_eq!( |
473 | | "$2b$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm", |
474 | | result.format_for_version(Version::TwoB) |
475 | | ); |
476 | | assert_eq!( |
477 | | "$2x$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm", |
478 | | result.format_for_version(Version::TwoX) |
479 | | ); |
480 | | assert_eq!( |
481 | | "$2y$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm", |
482 | | result.format_for_version(Version::TwoY) |
483 | | ); |
484 | | let hash = result.to_string(); |
485 | | assert_eq!(true, verify("hunter2", &hash).unwrap()); |
486 | | } |
487 | | |
488 | | #[test] |
489 | | fn allow_null_bytes() { |
490 | | // hash p1, check the hash against p2: |
491 | | fn hash_and_check(p1: &[u8], p2: &[u8]) -> Result<bool, BcryptError> { |
492 | | let fast_cost = 4; |
493 | | match hash(p1, fast_cost) { |
494 | | Ok(s) => verify(p2, &s), |
495 | | Err(e) => Err(e), |
496 | | } |
497 | | } |
498 | | fn assert_valid_password(p1: &[u8], p2: &[u8], expected: bool) { |
499 | | match hash_and_check(p1, p2) { |
500 | | Ok(checked) => { |
501 | | if checked != expected { |
502 | | panic!( |
503 | | "checked {:?} against {:?}, incorrect result {}", |
504 | | p1, p2, checked |
505 | | ) |
506 | | } |
507 | | } |
508 | | Err(e) => panic!("error evaluating password: {} for {:?}.", e, p1), |
509 | | } |
510 | | } |
511 | | |
512 | | // bcrypt should consider all of these distinct: |
513 | | let test_passwords = vec![ |
514 | | "\0", |
515 | | "passw0rd\0", |
516 | | "password\0with tail", |
517 | | "\0passw0rd", |
518 | | "a", |
519 | | "a\0", |
520 | | "a\0b\0", |
521 | | ]; |
522 | | |
523 | | for (i, p1) in test_passwords.iter().enumerate() { |
524 | | for (j, p2) in test_passwords.iter().enumerate() { |
525 | | assert_valid_password(p1.as_bytes(), p2.as_bytes(), i == j); |
526 | | } |
527 | | } |
528 | | |
529 | | // this is a quirk of the bcrypt algorithm: passwords that are entirely null |
530 | | // bytes hash to the same value, even if they are different lengths: |
531 | | assert_valid_password("\0\0\0\0\0\0\0\0".as_bytes(), "\0".as_bytes(), true); |
532 | | } |
533 | | |
534 | | #[test] |
535 | | fn hash_with_fixed_salt() { |
536 | | let salt = [ |
537 | | 38, 113, 212, 141, 108, 213, 195, 166, 201, 38, 20, 13, 47, 40, 104, 18, |
538 | | ]; |
539 | | let hashed = hash_with_salt("My S3cre7 P@55w0rd!", 5, salt) |
540 | | .unwrap() |
541 | | .to_string(); |
542 | | assert_eq!( |
543 | | "$2b$05$HlFShUxTu4ZHHfOLJwfmCeDj/kuKFKboanXtDJXxCC7aIPTUgxNDe", |
544 | | &hashed |
545 | | ); |
546 | | } |
547 | | |
548 | | quickcheck! { |
549 | | fn can_verify_arbitrary_own_generated(pass: Vec<u8>) -> BcryptResult<bool> { |
550 | | let mut pass = pass; |
551 | | pass.retain(|&b| b != 0); |
552 | | let hashed = hash(&pass, 4)?; |
553 | | verify(pass, &hashed) |
554 | | } |
555 | | |
556 | | fn doesnt_verify_different_passwords(a: Vec<u8>, b: Vec<u8>) -> BcryptResult<TestResult> { |
557 | | let mut a = a; |
558 | | a.retain(|&b| b != 0); |
559 | | let mut b = b; |
560 | | b.retain(|&b| b != 0); |
561 | | if a == b { |
562 | | return Ok(TestResult::discard()); |
563 | | } |
564 | | let hashed = hash(a, 4)?; |
565 | | Ok(TestResult::from_bool(!verify(b, &hashed)?)) |
566 | | } |
567 | | } |
568 | | |
569 | | #[test] |
570 | | fn does_no_error_on_char_boundary_splitting() { |
571 | | // Just checks that it does not panic |
572 | | let _ = verify( |
573 | | &[], |
574 | | "2a$$$0$OOOOOOOOOOOOOOOOOOOOO£OOOOOOOOOOOOOOOOOOOOOOOOOOOOOO", |
575 | | ); |
576 | | } |
577 | | } |