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::{
10    string::{String, ToString},
11    vec::Vec,
12};
13
14#[cfg(feature = "zeroize")]
15use zeroize::Zeroize;
16
17use base64::{alphabet::BCRYPT, engine::GeneralPurpose, engine::general_purpose::NO_PAD};
18use core::fmt;
19#[cfg(any(feature = "alloc", feature = "std"))]
20use {base64::Engine, core::convert::AsRef, core::str::FromStr};
21
22mod bcrypt;
23mod errors;
24
25pub use crate::bcrypt::bcrypt;
26pub use crate::errors::{BcryptError, BcryptResult};
27
28// Cost constants
29const MIN_COST: u32 = 4;
30const MAX_COST: u32 = 31;
31pub const DEFAULT_COST: u32 = 12;
32pub 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
37pub 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
46pub enum Version {
47    TwoA,
48    TwoX,
49    TwoY,
50    TwoB,
51}
52
53#[cfg(any(feature = "alloc", feature = "std"))]
54impl HashParts {
55    /// Creates the bcrypt hash string from all its parts
56    fn format(&self) -> String {
57        self.format_for_version(Version::TwoB)
58    }
59
60    /// Get the bcrypt hash cost
61    pub fn get_cost(&self) -> u32 {
62        self.cost
63    }
64
65    /// Get the bcrypt hash salt
66    pub fn get_salt(&self) -> String {
67        self.salt.clone()
68    }
69
70    /// Creates the bcrypt hash string from all its part, allowing to customize the version.
71    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        alloc::format!("${}${:02}${}{}", version, self.cost, self.salt, self.hash)
74    }
75}
76
77#[cfg(any(feature = "alloc", feature = "std"))]
78impl FromStr for HashParts {
79    type Err = BcryptError;
80
81    fn from_str(s: &str) -> Result<Self, Self::Err> {
82        split_hash(s)
83    }
84}
85
86#[cfg(any(feature = "alloc", feature = "std"))]
87impl fmt::Display for HashParts {
88    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
89        write!(f, "{}", self.format())
90    }
91}
92
93impl fmt::Display for Version {
94    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
95        let str = match self {
96            Version::TwoA => "2a",
97            Version::TwoB => "2b",
98            Version::TwoX => "2x",
99            Version::TwoY => "2y",
100        };
101        write!(f, "{}", str)
102    }
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"))]
109fn _hash_password(
110    password: &[u8],
111    cost: u32,
112    salt: [u8; 16],
113    err_on_truncation: bool,
114) -> BcryptResult<HashParts> {
115    if !(MIN_COST..=MAX_COST).contains(&cost) {
116        return Err(BcryptError::CostNotAllowed(cost));
117    }
118
119    // Passwords need to be null terminated
120    let mut vec = Vec::with_capacity(password.len() + 1);
121    vec.extend_from_slice(password);
122    vec.push(0);
123    // We only consider the first 72 chars; truncate if necessary.
124    // `bcrypt` below will panic if len > 72
125    let truncated = if vec.len() > 72 {
126        if err_on_truncation {
127            return Err(BcryptError::Truncation(vec.len()));
128        }
129        &vec[..72]
130    } else {
131        &vec
132    };
133
134    let output = bcrypt::bcrypt(cost, salt, truncated);
135
136    #[cfg(feature = "zeroize")]
137    vec.zeroize();
138
139    Ok(HashParts {
140        cost,
141        salt: BASE_64.encode(salt),
142        hash: BASE_64.encode(&output[..23]), // remember to remove the last byte
143    })
144}
145
146/// Takes a full hash and split it into 3 parts:
147/// cost, salt and hash
148#[cfg(any(feature = "alloc", feature = "std"))]
149fn split_hash(hash: &str) -> BcryptResult<HashParts> {
150    let mut parts = HashParts {
151        cost: 0,
152        salt: "".to_string(),
153        hash: "".to_string(),
154    };
155
156    // Should be [prefix, cost, hash]
157    let raw_parts: Vec<_> = hash.split('$').filter(|s| !s.is_empty()).collect();
158
159    if raw_parts.len() != 3 {
160        return Err(BcryptError::InvalidHash("the hash format is malformed"));
161    }
162
163    if raw_parts[0] != "2y" && raw_parts[0] != "2b" && raw_parts[0] != "2a" && raw_parts[0] != "2x"
164    {
165        return Err(BcryptError::InvalidHash(
166            "the hash prefix is not a bcrypt prefix",
167        ));
168    }
169
170    if let Ok(c) = raw_parts[1].parse::<u32>() {
171        parts.cost = c;
172    } else {
173        return Err(BcryptError::InvalidHash("the cost value is not a number"));
174    }
175
176    if raw_parts[2].len() == 53 && raw_parts[2].is_char_boundary(22) {
177        parts.salt = raw_parts[2][..22].chars().collect();
178        parts.hash = raw_parts[2][22..].chars().collect();
179    } else {
180        return Err(BcryptError::InvalidHash("the hash format is malformed"));
181    }
182
183    Ok(parts)
184}
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"))]
189pub fn hash<P: AsRef<[u8]>>(password: P, cost: u32) -> BcryptResult<String> {
190    hash_with_result(password, cost).map(|r| r.format())
191}
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"))]
197pub fn non_truncating_hash<P: AsRef<[u8]>>(password: P, cost: u32) -> BcryptResult<String> {
198    non_truncating_hash_with_result(password, cost).map(|r| r.format())
199}
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"))]
205pub fn hash_with_result<P: AsRef<[u8]>>(password: P, cost: u32) -> BcryptResult<HashParts> {
206    let salt = {
207        let mut s = [0u8; 16];
208        getrandom::fill(&mut s).map(|_| s)
209    }?;
210
211    _hash_password(password.as_ref(), cost, salt, false)
212}
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"))]
219pub fn non_truncating_hash_with_result<P: AsRef<[u8]>>(
220    password: P,
221    cost: u32,
222) -> BcryptResult<HashParts> {
223    let salt = {
224        let mut s = [0u8; 16];
225        getrandom::fill(&mut s).map(|_| s)
226    }?;
227
228    _hash_password(password.as_ref(), cost, salt, true)
229}
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"))]
234pub fn hash_with_salt<P: AsRef<[u8]>>(
235    password: P,
236    cost: u32,
237    salt: [u8; 16],
238) -> BcryptResult<HashParts> {
239    _hash_password(password.as_ref(), cost, salt, false)
240}
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"))]
246pub fn non_truncating_hash_with_salt<P: AsRef<[u8]>>(
247    password: P,
248    cost: u32,
249    salt: [u8; 16],
250) -> BcryptResult<HashParts> {
251    _hash_password(password.as_ref(), cost, salt, true)
252}
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"))]
258fn _verify<P: AsRef<[u8]>>(password: P, hash: &str, err_on_truncation: bool) -> BcryptResult<bool> {
259    use subtle::ConstantTimeEq;
260
261    let parts = split_hash(hash)?;
262    let salt = BASE_64
263        .decode(&parts.salt)
264        .map_err(|_| BcryptError::InvalidHash("the salt part is not valid base64"))?;
265    let generated = _hash_password(
266        password.as_ref(),
267        parts.cost,
268        salt.try_into()
269            .map_err(|_| BcryptError::InvalidHash("the salt length is not 16 bytes"))?,
270        err_on_truncation,
271    )?;
272    let source_decoded = BASE_64
273        .decode(parts.hash)
274        .map_err(|_| BcryptError::InvalidHash("the hash to verify against is not valid base64"))?;
275    let generated_decoded = BASE_64.decode(generated.hash).map_err(|_| {
276        BcryptError::InvalidHash("the generated hash for the password is not valid base64")
277    })?;
278
279    Ok(source_decoded.ct_eq(&generated_decoded).into())
280}
281
282/// Verify that a password is equivalent to the hash provided
283#[cfg(any(feature = "alloc", feature = "std"))]
284pub fn verify<P: AsRef<[u8]>>(password: P, hash: &str) -> BcryptResult<bool> {
285    _verify(password, hash, false)
286}
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"))]
292pub fn non_truncating_verify<P: AsRef<[u8]>>(password: P, hash: &str) -> BcryptResult<bool> {
293    _verify(password, hash, true)
294}
295
296#[cfg(all(test, any(feature = "alloc", feature = "std")))]
297mod 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}