Skip to main content

bcrypt/
lib.rs

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))]
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
25// Cost constants
26const 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)]
33/// A bcrypt hash result before concatenating
34pub struct HashParts {
35    cost: u32,
36    salt: [u8; 16],
37    hash: [u8; 23],
38}
39
40#[derive(Clone, Debug)]
41/// BCrypt hash version
42/// https://en.wikipedia.org/wiki/Bcrypt#Versioning_history
43pub enum Version {
44    TwoA,
45    TwoX,
46    TwoY,
47    TwoB,
48}
49
50#[cfg(any(feature = "alloc", feature = "std"))]
51impl HashParts {
52    /// Creates the bcrypt hash string (version 2b) into a fixed-size stack buffer.
53    /// The full bcrypt hash string is always exactly 60 bytes.
54    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    /// Get the bcrypt hash cost
77    pub fn get_cost(&self) -> u32 {
78        self.cost
79    }
80
81    /// Get the bcrypt hash salt as a base64-encoded string
82    pub fn get_salt(&self) -> String {
83        BASE_64.encode(self.salt)
84    }
85
86    /// Get the raw salt bytes
87    pub fn get_salt_raw(&self) -> [u8; 16] {
88        self.salt
89    }
90
91    /// Creates the bcrypt hash string from all its parts, allowing to customize the version.
92    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    /// Writes the bcrypt hash string into any `fmt::Write` sink without allocating.
100    /// Useful for writing into stack buffers (e.g. `arrayvec`, `heapless::String`).
101    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/// The main meat: actually does the hashing and does some verification with
150/// the cost to ensure it's a correct one. If err_on_truncation, this method will return
151/// `BcryptError::Truncation`; otherwise it will truncate the password.
152#[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    // The bcrypt spec specifies that passwords should be null terminated
169    // strings, but if longer than 72 bytes, are truncated at 72 bytes (thereby
170    // losing the null byte at the end).
171    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(), // infallible: output is [u8; 24]
186    })
187}
188
189/// Takes a full hash and split it into 3 parts:
190/// cost, salt and hash
191#[cfg(any(feature = "alloc", feature = "std"))]
192fn split_hash(hash: &str) -> BcryptResult<HashParts> {
193    // A valid bcrypt hash is always exactly 60 bytes:
194    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/// Generates a password hash using the cost given.
234/// The salt is generated randomly using the OS randomness
235#[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/// Generates a password hash using the cost given.
245/// The salt is generated randomly using the OS randomness
246/// Will return BcryptError::Truncation if password is longer than 72 bytes
247#[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/// Generates a password hash using the cost given, returning a fixed-size stack buffer.
257/// The salt is generated randomly using the OS randomness.
258/// The returned buffer is always exactly 60 bytes of valid UTF-8 (version 2b format).
259#[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/// Generates a password hash using the cost given, returning a fixed-size stack buffer.
265/// The salt is generated randomly using the OS randomness.
266/// The returned buffer is always exactly 60 bytes of valid UTF-8 (version 2b format).
267/// Will return BcryptError::Truncation if password is longer than 72 bytes
268#[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/// Generates a password hash using the cost given.
274/// The salt is generated randomly using the OS randomness.
275/// The function returns a result structure and allows to format the hash in different versions.
276#[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/// Generates a password hash using the cost given.
287/// The salt is generated randomly using the OS randomness.
288/// The function returns a result structure and allows to format the hash in different versions.
289/// Will return BcryptError::Truncation if password is longer than 72 bytes
290#[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/// Generates a password given a hash and a cost.
304/// The function returns a result structure and allows to format the hash in different versions.
305#[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/// Generates a password given a hash and a cost, returning a fixed-size stack buffer.
315/// The returned buffer is always exactly 60 bytes of valid UTF-8 (version 2b format).
316#[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/// Generates a password given a hash and a cost.
326/// The function returns a result structure and allows to format the hash in different versions.
327/// Will return BcryptError::Truncation if password is longer than 72 bytes
328#[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/// Generates a password given a hash and a cost, returning a fixed-size stack buffer.
338/// The returned buffer is always exactly 60 bytes of valid UTF-8 (version 2b format).
339/// Will return BcryptError::Truncation if password is longer than 72 bytes
340#[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/// Verify the password against the hash by extracting the salt from the hash and recomputing the
350/// hash from the password. If `err_on_truncation` is set to true, then this method will return
351/// `BcryptError::Truncation`.
352#[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/// Verify that a password is equivalent to the hash provided
363#[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/// Verify that a password is equivalent to the hash provided.
369/// Only use this if you are only using `non_truncating_hash` to generate the hash.
370/// It will return an error for inputs that will work if generated from other sources.
371#[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        // Raw salt must round-trip back to the same base64 string
421        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        /*
481            package main
482            import (
483                "io"
484                "os"
485                "golang.org/x/crypto/bcrypt"
486            )
487            func main() {
488                buf, err := io.ReadAll(os.Stdin)
489                if err != nil {
490                    panic(err)
491                }
492                out, err := bcrypt.GenerateFromPassword(buf, bcrypt.MinCost)
493                if err != nil {
494                    panic(err)
495                }
496                os.Stdout.Write(out)
497                os.Stdout.Write([]byte("\n"))
498            }
499        */
500        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        // there is another $ in the hash part
527        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        // the cost is not a number
534        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        // the cost is not a number
541        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        // produced with python -c 'import bcrypt; bcrypt.hashpw(b"x"*100, b"$2a$05$...............................")'
554        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        // hash p1, check the hash against p2:
605        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        // bcrypt should consider all of these distinct:
627        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        // this is a quirk of the bcrypt algorithm: passwords that are entirely null
644        // bytes hash to the same value, even if they are different lengths:
645        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        // Just checks that it does not panic
746        let _ = verify(
747            &[],
748            "2a$$$0$OOOOOOOOOOOOOOOOOOOOO£OOOOOOOOOOOOOOOOOOOOOOOOOOOOOO",
749        );
750    }
751}