1#![forbid(unsafe_code)]
3#![cfg_attr(not(feature = "std"), no_std)]
4
5#[cfg(any(feature = "alloc", feature = "std", test))]
6extern crate alloc;
7
8#[cfg(any(feature = "alloc", feature = "std", test))]
9use alloc::string::String;
10
11#[cfg(feature = "zeroize")]
12use zeroize::Zeroize;
13
14use base64::{alphabet::BCRYPT, engine::GeneralPurpose, engine::general_purpose::NO_PAD};
15use core::fmt;
16#[cfg(any(feature = "alloc", feature = "std"))]
17use {base64::Engine, core::convert::AsRef, core::str::FromStr};
18
19mod bcrypt;
20mod errors;
21
22pub use crate::bcrypt::bcrypt;
23pub use crate::errors::{BcryptError, BcryptResult};
24
25const MIN_COST: u32 = 4;
27const MAX_COST: u32 = 31;
28pub const DEFAULT_COST: u32 = 12;
29pub const BASE_64: GeneralPurpose = GeneralPurpose::new(&BCRYPT, NO_PAD);
30
31#[cfg(any(feature = "alloc", feature = "std"))]
32#[derive(Debug, PartialEq, Eq)]
33pub struct HashParts {
35 cost: u32,
36 salt: [u8; 16],
37 hash: [u8; 23],
38}
39
40#[derive(Clone, Debug)]
41pub enum Version {
44 TwoA,
45 TwoX,
46 TwoY,
47 TwoB,
48}
49
50#[cfg(any(feature = "alloc", feature = "std"))]
51impl HashParts {
52 fn format(&self) -> [u8; 60] {
55 struct ByteBuf<const N: usize> {
56 buf: [u8; N],
57 pos: usize,
58 }
59 impl<const N: usize> fmt::Write for ByteBuf<N> {
60 fn write_str(&mut self, s: &str) -> fmt::Result {
61 let bytes = s.as_bytes();
62 self.buf[self.pos..self.pos + bytes.len()].copy_from_slice(bytes);
63 self.pos += bytes.len();
64 Ok(())
65 }
66 }
67 let mut w = ByteBuf {
68 buf: [0u8; 60],
69 pos: 0,
70 };
71 self.write_for_version(Version::TwoB, &mut w)
72 .expect("writing into a correctly sized buffer is infallible");
73 w.buf
74 }
75
76 pub fn get_cost(&self) -> u32 {
78 self.cost
79 }
80
81 pub fn get_salt(&self) -> String {
83 BASE_64.encode(self.salt)
84 }
85
86 pub fn get_salt_raw(&self) -> [u8; 16] {
88 self.salt
89 }
90
91 pub fn format_for_version(&self, version: Version) -> String {
93 let mut s = String::with_capacity(60);
94 self.write_for_version(version, &mut s)
95 .expect("writing into a String is infallible");
96 s
97 }
98
99 pub fn write_for_version<W: fmt::Write>(&self, version: Version, w: &mut W) -> fmt::Result {
102 let mut salt_buf = [0u8; 22];
103 let mut hash_buf = [0u8; 31];
104 BASE_64
105 .encode_slice(self.salt, &mut salt_buf)
106 .expect("salt encoding into correctly sized buffer is infallible");
107 BASE_64
108 .encode_slice(self.hash, &mut hash_buf)
109 .expect("hash encoding into correctly sized buffer is infallible");
110 write!(
111 w,
112 "${}${:02}${}{}",
113 version,
114 self.cost,
115 core::str::from_utf8(&salt_buf).expect("base64 output is always valid UTF-8"),
116 core::str::from_utf8(&hash_buf).expect("base64 output is always valid UTF-8")
117 )
118 }
119}
120
121#[cfg(any(feature = "alloc", feature = "std"))]
122impl FromStr for HashParts {
123 type Err = BcryptError;
124
125 fn from_str(s: &str) -> Result<Self, Self::Err> {
126 split_hash(s)
127 }
128}
129
130#[cfg(any(feature = "alloc", feature = "std"))]
131impl fmt::Display for HashParts {
132 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
133 self.write_for_version(Version::TwoB, f)
134 }
135}
136
137impl fmt::Display for Version {
138 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
139 let str = match self {
140 Version::TwoA => "2a",
141 Version::TwoB => "2b",
142 Version::TwoX => "2x",
143 Version::TwoY => "2y",
144 };
145 write!(f, "{}", str)
146 }
147}
148
149#[cfg(any(feature = "alloc", feature = "std"))]
153fn _hash_password(
154 password: &[u8],
155 cost: u32,
156 salt: [u8; 16],
157 err_on_truncation: bool,
158) -> BcryptResult<HashParts> {
159 if !(MIN_COST..=MAX_COST).contains(&cost) {
160 return Err(BcryptError::CostNotAllowed(cost));
161 }
162
163 let password_len = password.len();
164 if err_on_truncation && password_len >= 72 {
165 return Err(BcryptError::Truncation(password_len + 1));
166 }
167
168 let copy_len = password_len.min(72);
172 let mut pass = [0u8; 72];
173 pass[..copy_len].copy_from_slice(&password[..copy_len]);
174 let used = (copy_len + 1).min(72);
175 let truncated = &pass[..used];
176
177 let output = bcrypt::bcrypt(cost, salt, truncated);
178
179 #[cfg(feature = "zeroize")]
180 pass.zeroize();
181
182 Ok(HashParts {
183 cost,
184 salt,
185 hash: output[..23].try_into().unwrap(), })
187}
188
189#[cfg(any(feature = "alloc", feature = "std"))]
192fn split_hash(hash: &str) -> BcryptResult<HashParts> {
193 if hash.len() != 60 {
195 return Err(BcryptError::InvalidHash(
196 "the hash format is malformed; expected 60 bytes",
197 ));
198 }
199
200 let bytes = hash.as_bytes();
201 if bytes[0] != b'$' || bytes[3] != b'$' || bytes[6] != b'$' {
202 return Err(BcryptError::InvalidHash("the hash format is malformed"));
203 }
204
205 let version = &hash[1..3];
206 if version != "2y" && version != "2b" && version != "2a" && version != "2x" {
207 return Err(BcryptError::InvalidHash(
208 "the hash prefix is not a bcrypt prefix",
209 ));
210 }
211
212 let cost = hash[4..6]
213 .parse::<u32>()
214 .map_err(|_| BcryptError::InvalidHash("the cost value is not a number"))?;
215
216 let salt_and_hash = &hash[7..];
217 let mut salt = [0u8; 16];
218 let mut hash_bytes = [0u8; 23];
219 BASE_64
220 .decode_slice(&salt_and_hash[..22], &mut salt)
221 .map_err(|_| BcryptError::InvalidHash("the salt part is not valid base64"))?;
222 BASE_64
223 .decode_slice(&salt_and_hash[22..], &mut hash_bytes)
224 .map_err(|_| BcryptError::InvalidHash("the hash part is not valid base64"))?;
225
226 Ok(HashParts {
227 cost,
228 salt,
229 hash: hash_bytes,
230 })
231}
232
233#[cfg(any(feature = "alloc", feature = "std"))]
236pub fn hash<P: AsRef<[u8]>>(password: P, cost: u32) -> BcryptResult<String> {
237 hash_with_result(password, cost).map(|r| {
238 String::from(
239 core::str::from_utf8(&r.format()).expect("base64 output is always valid UTF-8"),
240 )
241 })
242}
243
244#[cfg(any(feature = "alloc", feature = "std"))]
248pub fn non_truncating_hash<P: AsRef<[u8]>>(password: P, cost: u32) -> BcryptResult<String> {
249 non_truncating_hash_with_result(password, cost).map(|r| {
250 String::from(
251 core::str::from_utf8(&r.format()).expect("base64 output is always valid UTF-8"),
252 )
253 })
254}
255
256#[cfg(any(feature = "alloc", feature = "std"))]
260pub fn hash_bytes<P: AsRef<[u8]>>(password: P, cost: u32) -> BcryptResult<[u8; 60]> {
261 hash_with_result(password, cost).map(|r| r.format())
262}
263
264#[cfg(any(feature = "alloc", feature = "std"))]
269pub fn non_truncating_hash_bytes<P: AsRef<[u8]>>(password: P, cost: u32) -> BcryptResult<[u8; 60]> {
270 non_truncating_hash_with_result(password, cost).map(|r| r.format())
271}
272
273#[cfg(any(feature = "alloc", feature = "std"))]
277pub fn hash_with_result<P: AsRef<[u8]>>(password: P, cost: u32) -> BcryptResult<HashParts> {
278 let salt = {
279 let mut s = [0u8; 16];
280 getrandom::fill(&mut s).map(|_| s)
281 }?;
282
283 _hash_password(password.as_ref(), cost, salt, false)
284}
285
286#[cfg(any(feature = "alloc", feature = "std"))]
291pub fn non_truncating_hash_with_result<P: AsRef<[u8]>>(
292 password: P,
293 cost: u32,
294) -> BcryptResult<HashParts> {
295 let salt = {
296 let mut s = [0u8; 16];
297 getrandom::fill(&mut s).map(|_| s)
298 }?;
299
300 _hash_password(password.as_ref(), cost, salt, true)
301}
302
303#[cfg(any(feature = "alloc", feature = "std"))]
306pub fn hash_with_salt<P: AsRef<[u8]>>(
307 password: P,
308 cost: u32,
309 salt: [u8; 16],
310) -> BcryptResult<HashParts> {
311 _hash_password(password.as_ref(), cost, salt, false)
312}
313
314#[cfg(any(feature = "alloc", feature = "std"))]
317pub fn hash_with_salt_bytes<P: AsRef<[u8]>>(
318 password: P,
319 cost: u32,
320 salt: [u8; 16],
321) -> BcryptResult<[u8; 60]> {
322 _hash_password(password.as_ref(), cost, salt, false).map(|r| r.format())
323}
324
325#[cfg(any(feature = "alloc", feature = "std"))]
329pub fn non_truncating_hash_with_salt<P: AsRef<[u8]>>(
330 password: P,
331 cost: u32,
332 salt: [u8; 16],
333) -> BcryptResult<HashParts> {
334 _hash_password(password.as_ref(), cost, salt, true)
335}
336
337#[cfg(any(feature = "alloc", feature = "std"))]
341pub fn non_truncating_hash_with_salt_bytes<P: AsRef<[u8]>>(
342 password: P,
343 cost: u32,
344 salt: [u8; 16],
345) -> BcryptResult<[u8; 60]> {
346 _hash_password(password.as_ref(), cost, salt, true).map(|r| r.format())
347}
348
349#[cfg(any(feature = "alloc", feature = "std"))]
353fn _verify<P: AsRef<[u8]>>(password: P, hash: &str, err_on_truncation: bool) -> BcryptResult<bool> {
354 use subtle::ConstantTimeEq;
355
356 let parts = split_hash(hash)?;
357 let generated = _hash_password(password.as_ref(), parts.cost, parts.salt, err_on_truncation)?;
358
359 Ok(parts.hash.ct_eq(&generated.hash).into())
360}
361
362#[cfg(any(feature = "alloc", feature = "std"))]
364pub fn verify<P: AsRef<[u8]>>(password: P, hash: &str) -> BcryptResult<bool> {
365 _verify(password, hash, false)
366}
367
368#[cfg(any(feature = "alloc", feature = "std"))]
372pub fn non_truncating_verify<P: AsRef<[u8]>>(password: P, hash: &str) -> BcryptResult<bool> {
373 _verify(password, hash, true)
374}
375
376#[cfg(all(test, any(feature = "alloc", feature = "std")))]
377mod tests {
378 use crate::non_truncating_hash;
379
380 use super::{
381 _hash_password, BcryptError, BcryptResult, DEFAULT_COST, HashParts, Version,
382 alloc::{
383 string::{String, ToString},
384 vec,
385 vec::Vec,
386 },
387 hash, hash_bytes, hash_with_salt, hash_with_salt_bytes, non_truncating_hash_bytes,
388 non_truncating_hash_with_salt_bytes, non_truncating_verify, split_hash, verify,
389 };
390 use base64::Engine as _;
391 use core::convert::TryInto;
392 use core::iter;
393 use core::str::FromStr;
394 use quickcheck::{TestResult, quickcheck};
395
396 #[test]
397 fn can_split_hash() {
398 let hash = "$2y$12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u";
399 let output = split_hash(hash).unwrap();
400 assert_eq!(output.get_cost(), 12);
401 assert_eq!(output.get_salt(), "L6Bc/AlTQHyd9liGgGEZyO");
402 assert_eq!(
403 output.format_for_version(Version::TwoY),
404 "$2y$12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u"
405 );
406 }
407
408 #[test]
409 fn can_output_cost_and_salt_from_parsed_hash() {
410 let hash = "$2y$12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u";
411 let parsed = HashParts::from_str(hash).unwrap();
412 assert_eq!(parsed.get_cost(), 12);
413 assert_eq!(parsed.get_salt(), "L6Bc/AlTQHyd9liGgGEZyO".to_string());
414 }
415
416 #[test]
417 fn can_get_raw_salt_from_parsed_hash() {
418 let hash = "$2y$12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u";
419 let parsed = HashParts::from_str(hash).unwrap();
420 assert_eq!(
422 super::BASE_64.encode(parsed.get_salt_raw()),
423 "L6Bc/AlTQHyd9liGgGEZyO"
424 );
425 }
426
427 #[test]
428 fn can_write_hash_for_version_without_allocating() {
429 let hash = "$2y$12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u";
430 let parsed = HashParts::from_str(hash).unwrap();
431 let mut buf = String::new();
432 parsed.write_for_version(Version::TwoY, &mut buf).unwrap();
433 assert_eq!(buf, hash);
434 }
435
436 #[test]
437 fn write_for_version_matches_format_for_version() {
438 let salt = [0u8; 16];
439 let result = _hash_password("hunter2".as_bytes(), DEFAULT_COST, salt, false).unwrap();
440 let formatted = result.format_for_version(Version::TwoA);
441 let mut written = String::new();
442 result
443 .write_for_version(Version::TwoA, &mut written)
444 .unwrap();
445 assert_eq!(formatted, written);
446 }
447
448 #[test]
449 fn returns_an_error_if_a_parsed_hash_is_baddly_formated() {
450 let hash1 = "$2y$12$L6Bc/AlTQHyd9lGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u";
451 assert!(HashParts::from_str(hash1).is_err());
452
453 let hash2 = "!2y$12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u";
454 assert!(HashParts::from_str(hash2).is_err());
455
456 let hash3 = "$2y$-12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u";
457 assert!(HashParts::from_str(hash3).is_err());
458 }
459
460 #[test]
461 fn can_verify_hash_generated_from_some_online_tool() {
462 let hash = "$2a$04$UuTkLRZZ6QofpDOlMz32MuuxEHA43WOemOYHPz6.SjsVsyO1tDU96";
463 assert!(verify("password", hash).unwrap());
464 }
465
466 #[test]
467 fn can_verify_hash_generated_from_python() {
468 let hash = "$2b$04$EGdrhbKUv8Oc9vGiXX0HQOxSg445d458Muh7DAHskb6QbtCvdxcie";
469 assert!(verify("correctbatteryhorsestapler", hash).unwrap());
470 }
471
472 #[test]
473 fn can_verify_hash_generated_from_node() {
474 let hash = "$2a$04$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk5bektyVVa5xnIi";
475 assert!(verify("correctbatteryhorsestapler", hash).unwrap());
476 }
477
478 #[test]
479 fn can_verify_hash_generated_from_go() {
480 let binary_input = vec![
501 29, 225, 195, 167, 223, 236, 85, 195, 114, 227, 7, 0, 209, 239, 189, 24, 51, 105, 124,
502 168, 151, 75, 144, 64, 198, 197, 196, 4, 241, 97, 110, 135,
503 ];
504 let hash = "$2a$04$tjARW6ZON3PhrAIRW2LG/u9aDw5eFdstYLR8nFCNaOQmsH9XD23w.";
505 assert!(verify(binary_input, hash).unwrap());
506 }
507
508 #[test]
509 fn invalid_hash_does_not_panic() {
510 let binary_input = vec![
511 29, 225, 195, 167, 223, 236, 85, 195, 114, 227, 7, 0, 209, 239, 189, 24, 51, 105, 124,
512 168, 151, 75, 144, 64, 198, 197, 196, 4, 241, 97, 110, 135,
513 ];
514 let hash = "$2a$04$tjARW6ZON3PhrAIRW2LG/u9a.";
515 assert!(verify(binary_input, hash).is_err());
516 }
517
518 #[test]
519 fn a_wrong_password_is_false() {
520 let hash = "$2b$04$EGdrhbKUv8Oc9vGiXX0HQOxSg445d458Muh7DAHskb6QbtCvdxcie";
521 assert!(!verify("wrong", hash).unwrap());
522 }
523
524 #[test]
525 fn errors_with_invalid_hash() {
526 let hash = "$2a$04$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk$5bektyVVa5xnIi";
528 assert!(verify("correctbatteryhorsestapler", hash).is_err());
529 }
530
531 #[test]
532 fn errors_with_non_number_cost() {
533 let hash = "$2a$ab$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk$5bektyVVa5xnIi";
535 assert!(verify("correctbatteryhorsestapler", hash).is_err());
536 }
537
538 #[test]
539 fn errors_with_a_hash_too_long() {
540 let hash = "$2a$04$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk$5bektyVVa5xnIerererereri";
542 assert!(verify("correctbatteryhorsestapler", hash).is_err());
543 }
544
545 #[test]
546 fn can_verify_own_generated() {
547 let hashed = hash("hunter2", 4).unwrap();
548 assert_eq!(true, verify("hunter2", &hashed).unwrap());
549 }
550
551 #[test]
552 fn long_passwords_truncate_correctly() {
553 let hash = "$2a$05$......................YgIDy4hFBdVlc/6LHnD9mX488r9cLd2";
555 assert!(verify(iter::repeat("x").take(100).collect::<String>(), hash).unwrap());
556 }
557
558 #[test]
559 fn non_truncating_operations() {
560 assert!(matches!(
561 non_truncating_hash(iter::repeat("x").take(72).collect::<String>(), DEFAULT_COST),
562 BcryptResult::Err(BcryptError::Truncation(73))
563 ));
564 assert!(matches!(
565 non_truncating_hash(iter::repeat("x").take(71).collect::<String>(), DEFAULT_COST),
566 BcryptResult::Ok(_)
567 ));
568
569 let hash = "$2a$05$......................YgIDy4hFBdVlc/6LHnD9mX488r9cLd2";
570 assert!(matches!(
571 non_truncating_verify(iter::repeat("x").take(100).collect::<String>(), hash),
572 Err(BcryptError::Truncation(101))
573 ));
574 }
575
576 #[test]
577 fn generate_versions() {
578 let password = "hunter2".as_bytes();
579 let salt = vec![0; 16];
580 let result =
581 _hash_password(password, DEFAULT_COST, salt.try_into().unwrap(), false).unwrap();
582 assert_eq!(
583 "$2a$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm",
584 result.format_for_version(Version::TwoA)
585 );
586 assert_eq!(
587 "$2b$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm",
588 result.format_for_version(Version::TwoB)
589 );
590 assert_eq!(
591 "$2x$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm",
592 result.format_for_version(Version::TwoX)
593 );
594 assert_eq!(
595 "$2y$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm",
596 result.format_for_version(Version::TwoY)
597 );
598 let hash = result.to_string();
599 assert_eq!(true, verify("hunter2", &hash).unwrap());
600 }
601
602 #[test]
603 fn allow_null_bytes() {
604 fn hash_and_check(p1: &[u8], p2: &[u8]) -> Result<bool, BcryptError> {
606 let fast_cost = 4;
607 match hash(p1, fast_cost) {
608 Ok(s) => verify(p2, &s),
609 Err(e) => Err(e),
610 }
611 }
612 fn assert_valid_password(p1: &[u8], p2: &[u8], expected: bool) {
613 match hash_and_check(p1, p2) {
614 Ok(checked) => {
615 if checked != expected {
616 panic!(
617 "checked {:?} against {:?}, incorrect result {}",
618 p1, p2, checked
619 )
620 }
621 }
622 Err(e) => panic!("error evaluating password: {} for {:?}.", e, p1),
623 }
624 }
625
626 let test_passwords = vec![
628 "\0",
629 "passw0rd\0",
630 "password\0with tail",
631 "\0passw0rd",
632 "a",
633 "a\0",
634 "a\0b\0",
635 ];
636
637 for (i, p1) in test_passwords.iter().enumerate() {
638 for (j, p2) in test_passwords.iter().enumerate() {
639 assert_valid_password(p1.as_bytes(), p2.as_bytes(), i == j);
640 }
641 }
642
643 assert_valid_password("\0\0\0\0\0\0\0\0".as_bytes(), "\0".as_bytes(), true);
646 }
647
648 #[test]
649 fn hash_with_fixed_salt() {
650 let salt = [
651 38, 113, 212, 141, 108, 213, 195, 166, 201, 38, 20, 13, 47, 40, 104, 18,
652 ];
653 let hashed = hash_with_salt("My S3cre7 P@55w0rd!", 5, salt)
654 .unwrap()
655 .to_string();
656 assert_eq!(
657 "$2b$05$HlFShUxTu4ZHHfOLJwfmCeDj/kuKFKboanXtDJXxCC7aIPTUgxNDe",
658 &hashed
659 );
660 }
661
662 #[test]
663 fn hash_bytes_returns_valid_utf8_bcrypt_string() {
664 let result = hash_bytes("hunter2", 4).unwrap();
665 let s = core::str::from_utf8(&result).unwrap();
666 assert!(s.starts_with("$2b$04$"));
667 assert_eq!(s.len(), 60);
668 assert!(verify("hunter2", s).unwrap());
669 }
670
671 #[test]
672 fn non_truncating_hash_bytes_returns_valid_utf8_bcrypt_string() {
673 let result = non_truncating_hash_bytes("hunter2", 4).unwrap();
674 let s = core::str::from_utf8(&result).unwrap();
675 assert!(s.starts_with("$2b$04$"));
676 assert_eq!(s.len(), 60);
677 assert!(verify("hunter2", s).unwrap());
678 }
679
680 #[test]
681 fn non_truncating_hash_bytes_errors_on_long_password() {
682 use core::iter;
683 let result = non_truncating_hash_bytes(iter::repeat("x").take(72).collect::<String>(), 4);
684 assert!(matches!(result, Err(BcryptError::Truncation(73))));
685 }
686
687 #[test]
688 fn hash_with_salt_bytes_matches_hash_with_salt() {
689 let salt = [
690 38, 113, 212, 141, 108, 213, 195, 166, 201, 38, 20, 13, 47, 40, 104, 18,
691 ];
692 let expected = hash_with_salt("My S3cre7 P@55w0rd!", 5, salt)
693 .unwrap()
694 .to_string();
695 let result = hash_with_salt_bytes("My S3cre7 P@55w0rd!", 5, salt).unwrap();
696 let s = core::str::from_utf8(&result).unwrap();
697 assert_eq!(expected, s);
698 }
699
700 #[test]
701 fn non_truncating_hash_with_salt_bytes_errors_on_long_password() {
702 use core::iter;
703 let salt = [0u8; 16];
704 let result = non_truncating_hash_with_salt_bytes(
705 iter::repeat("x").take(72).collect::<String>(),
706 4,
707 salt,
708 );
709 assert!(matches!(result, Err(BcryptError::Truncation(73))));
710 }
711
712 #[test]
713 fn hash_bytes_matches_hash_string() {
714 let salt = [0u8; 16];
715 let result_parts = _hash_password("hunter2".as_bytes(), 4, salt, false).unwrap();
716 let from_parts = result_parts.format_for_version(Version::TwoB);
717 let bytes_result = hash_with_salt_bytes("hunter2", 4, salt).unwrap();
718 let from_bytes = core::str::from_utf8(&bytes_result).unwrap();
719 assert_eq!(from_parts, from_bytes);
720 }
721
722 quickcheck! {
723 fn can_verify_arbitrary_own_generated(pass: Vec<u8>) -> BcryptResult<bool> {
724 let mut pass = pass;
725 pass.retain(|&b| b != 0);
726 let hashed = hash(&pass, 4)?;
727 verify(pass, &hashed)
728 }
729
730 fn doesnt_verify_different_passwords(a: Vec<u8>, b: Vec<u8>) -> BcryptResult<TestResult> {
731 let mut a = a;
732 a.retain(|&b| b != 0);
733 let mut b = b;
734 b.retain(|&b| b != 0);
735 if a == b {
736 return Ok(TestResult::discard());
737 }
738 let hashed = hash(a, 4)?;
739 Ok(TestResult::from_bool(!verify(b, &hashed)?))
740 }
741 }
742
743 #[test]
744 fn does_no_error_on_char_boundary_splitting() {
745 let _ = verify(
747 &[],
748 "2a$$$0$OOOOOOOOOOOOOOOOOOOOO£OOOOOOOOOOOOOOOOOOOOOOOOOOOOOO",
749 );
750 }
751}