bcrypt_no_getrandom/
lib.rs

1//! Easily hash and verify passwords using bcrypt
2//!
3//! Forked to remove getrandom dependency completely
4
5#![cfg_attr(not(feature = "std"), no_std)]
6
7#[cfg(any(feature = "alloc", feature = "std", test))]
8extern crate alloc;
9
10#[cfg(any(feature = "alloc", feature = "std", test))]
11use alloc::string::{String, ToString};
12
13use base64::Engine;
14use base64::{alphabet::BCRYPT, engine::general_purpose::NO_PAD, engine::GeneralPurpose};
15#[cfg(any(feature = "alloc", feature = "std"))]
16use core::convert::AsRef;
17use core::{
18    fmt,
19    str::{self, FromStr},
20};
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)]
36/// A bcrypt hash result before concatenating
37pub struct HashParts {
38    cost: u32,
39    salt: [u8; 22],
40    hash: [u8; 31],
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
53impl Version {
54    pub fn as_static_str(self) -> &'static str {
55        match self {
56            Version::TwoA => "2a",
57            Version::TwoB => "2b",
58            Version::TwoX => "2x",
59            Version::TwoY => "2y",
60        }
61    }
62}
63
64impl HashParts {
65    /// Get the bcrypt hash cost
66    pub fn get_cost(&self) -> u32 {
67        self.cost
68    }
69
70    /// Get the bcrypt hash salt
71    pub fn get_salt(&self) -> &str {
72        str::from_utf8(&self.salt).unwrap()
73    }
74
75    /// Creates the bcrypt hash string from all its part, allowing to customize the version.
76    ///
77    /// Expects an exactly 60-byte output buffer.
78    ///
79    /// *See also: [`Self::format_for_version`]*
80    pub fn format_for_version_into(&self, version: Version, output: &mut [u8]) {
81        output[0] = b'$';
82        output[1..3].copy_from_slice(version.as_static_str().as_bytes());
83        output[3] = b'$';
84
85        output[4] = b'0' + (self.cost / 10) as u8;
86        output[5] = b'0' + (self.cost % 10) as u8;
87
88        output[6] = b'$';
89        output[7..29].copy_from_slice(&self.salt);
90        output[29..].copy_from_slice(&self.hash);
91    }
92
93    #[cfg(any(feature = "alloc", feature = "std"))]
94    /// Creates the bcrypt hash string from all its part, allowing to customize the version.
95    pub fn format_for_version(&self, version: Version) -> String {
96        // Cost need to have a length of 2 so padding with a 0 if cost < 10
97        alloc::format!(
98            "${}${:02}${}{}",
99            version,
100            self.cost,
101            self.get_salt(),
102            str::from_utf8(&self.hash).unwrap(),
103        )
104    }
105}
106
107impl FromStr for HashParts {
108    type Err = BcryptError;
109
110    fn from_str(s: &str) -> Result<Self, Self::Err> {
111        split_hash(s)
112    }
113}
114
115#[cfg(any(feature = "alloc", feature = "std"))]
116impl ToString for HashParts {
117    fn to_string(&self) -> String {
118        self.format_for_version(Version::TwoY)
119    }
120}
121
122impl fmt::Display for Version {
123    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
124        let str = match self {
125            Version::TwoA => "2a",
126            Version::TwoB => "2b",
127            Version::TwoX => "2x",
128            Version::TwoY => "2y",
129        };
130        write!(f, "{}", str)
131    }
132}
133
134/// The main meat: actually does the hashing and does some verification with
135/// the cost to ensure it's a correct one
136fn _hash_password(password: &[u8], cost: u32, salt: [u8; 16]) -> BcryptResult<HashParts> {
137    if !(MIN_COST..=MAX_COST).contains(&cost) {
138        return Err(BcryptError::CostNotAllowed(cost));
139    }
140
141    let mut truncated = [0; 72];
142    let capped_len = password.len().min(truncated.len());
143
144    truncated[..capped_len].copy_from_slice(&password[..capped_len]);
145
146    let borrowed_len = (capped_len + 1).min(truncated.len());
147
148    let output = bcrypt::bcrypt(cost, salt, &truncated[..borrowed_len]);
149
150    unsafe {
151        // Zeroize the truncated buffer (not optimized away)
152        core::ptr::write_volatile(&mut truncated, [0; 72]);
153    }
154
155    let mut salt_buf = [0; 22];
156    let mut hash_buf = [0; 31];
157
158    if BASE_64.encode_slice(salt, &mut salt_buf).is_err() {
159        return Err(BcryptError::Other("Failed to encode bcrypt output"));
160    }
161
162    if BASE_64.encode_slice(&output[..23], &mut hash_buf).is_err() {
163        // remember to remove the last byte
164        return Err(BcryptError::Other("Failed to encode bcrypt output"));
165    }
166
167    Ok(HashParts {
168        cost,
169        salt: salt_buf,
170        hash: hash_buf,
171    })
172}
173
174/// Takes a full hash and split it into 3 parts:
175/// cost, salt and hash
176fn split_hash(hash: &str) -> BcryptResult<HashParts> {
177    let mut parts = HashParts {
178        cost: 0,
179        salt: [0; 22],
180        hash: [0; 31],
181    };
182
183    let hash = hash.trim_start_matches('$');
184
185    let Some((prefix, cost_and_hash)) = hash.split_once('$') else {
186        return Err(BcryptError::InvalidHash("Wrong number of parts"));
187    };
188
189    let Some((cost, hash)) = cost_and_hash.split_once('$') else {
190        return Err(BcryptError::InvalidHash("Wrong number of parts"));
191    };
192
193    if hash.contains('$') {
194        return Err(BcryptError::InvalidHash("Wrong number of parts"));
195    }
196
197    if prefix != "2y" && prefix != "2b" && prefix != "2a" && prefix != "2x" {
198        return Err(BcryptError::InvalidPrefix);
199    }
200
201    if let Ok(c) = cost.parse::<u32>() {
202        parts.cost = c;
203    } else {
204        return Err(BcryptError::InvalidCost);
205    }
206
207    if hash.len() == 53 && hash.is_char_boundary(22) {
208        parts.salt = hash.as_bytes()[..22]
209            .try_into()
210            .map_err(|_| BcryptError::InvalidSalt)?;
211        parts.hash = hash.as_bytes()[22..]
212            .try_into()
213            .map_err(|_| BcryptError::InvalidSalt)?;
214    } else {
215        return Err(BcryptError::InvalidHash("Wrong hash length"));
216    }
217
218    Ok(parts)
219}
220
221/// Generates a password given a hash and a cost.
222/// The function returns a result structure and allows to format the hash in different versions.
223pub fn hash_with_salt<P: AsRef<[u8]>>(
224    password: P,
225    cost: u32,
226    salt: [u8; 16],
227) -> BcryptResult<HashParts> {
228    _hash_password(password.as_ref(), cost, salt)
229}
230
231/// Verify that a password is equivalent to the hash provided
232pub fn verify<P: AsRef<[u8]>>(password: P, hash: &str) -> BcryptResult<bool> {
233    use subtle::ConstantTimeEq;
234
235    let parts = split_hash(hash)?;
236    let mut salt = [0; 16];
237    BASE_64.decode_slice(&parts.salt, &mut salt)?;
238
239    let generated = _hash_password(password.as_ref(), parts.cost, salt)?;
240
241    let mut source_decoded = [0; 23];
242    let mut generated_decoded = [0; 23];
243    BASE_64.decode_slice(parts.hash, &mut source_decoded)?;
244    BASE_64.decode_slice(generated.hash, &mut generated_decoded)?;
245
246    Ok(source_decoded.ct_eq(&generated_decoded).into())
247}
248
249#[cfg(all(test))]
250mod tests {
251    use super::{
252        _hash_password,
253        alloc::{
254            string::{String, ToString},
255            vec,
256        },
257        hash_with_salt, split_hash, verify, BcryptError, HashParts, Version, DEFAULT_COST,
258    };
259    use core::convert::TryInto;
260    use core::iter;
261    use core::str::FromStr;
262
263    #[test]
264    fn can_split_hash() {
265        let hash = "$2y$12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u";
266        let output = split_hash(hash).unwrap();
267        let expected = HashParts {
268            cost: 12,
269            salt: "L6Bc/AlTQHyd9liGgGEZyO".as_bytes().try_into().unwrap(),
270            hash: "FLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u"
271                .as_bytes()
272                .try_into()
273                .unwrap(),
274        };
275        assert_eq!(output, expected);
276    }
277
278    #[test]
279    fn can_output_cost_and_salt_from_parsed_hash() {
280        let hash = "$2y$12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u";
281        let parsed = HashParts::from_str(hash).unwrap();
282        assert_eq!(parsed.get_cost(), 12);
283        assert_eq!(parsed.get_salt(), "L6Bc/AlTQHyd9liGgGEZyO".to_string());
284    }
285
286    #[test]
287    fn returns_an_error_if_a_parsed_hash_is_baddly_formated() {
288        let hash1 = "$2y$12$L6Bc/AlTQHyd9lGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u";
289        assert!(HashParts::from_str(hash1).is_err());
290
291        let hash2 = "!2y$12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u";
292        assert!(HashParts::from_str(hash2).is_err());
293
294        let hash3 = "$2y$-12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u";
295        assert!(HashParts::from_str(hash3).is_err());
296    }
297
298    #[test]
299    fn can_verify_hash_generated_from_some_online_tool() {
300        let hash = "$2a$04$UuTkLRZZ6QofpDOlMz32MuuxEHA43WOemOYHPz6.SjsVsyO1tDU96";
301        assert!(verify("password", hash).unwrap());
302    }
303
304    #[test]
305    fn can_verify_hash_generated_from_python() {
306        let hash = "$2b$04$EGdrhbKUv8Oc9vGiXX0HQOxSg445d458Muh7DAHskb6QbtCvdxcie";
307        assert!(verify("correctbatteryhorsestapler", hash).unwrap());
308    }
309
310    #[test]
311    fn can_verify_hash_generated_from_node() {
312        let hash = "$2a$04$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk5bektyVVa5xnIi";
313        assert!(verify("correctbatteryhorsestapler", hash).unwrap());
314    }
315
316    #[test]
317    fn can_verify_hash_generated_from_go() {
318        /*
319            package main
320            import (
321                "io"
322                "os"
323                "golang.org/x/crypto/bcrypt"
324            )
325            func main() {
326                buf, err := io.ReadAll(os.Stdin)
327                if err != nil {
328                    panic(err)
329                }
330                out, err := bcrypt.GenerateFromPassword(buf, bcrypt.MinCost)
331                if err != nil {
332                    panic(err)
333                }
334                os.Stdout.Write(out)
335                os.Stdout.Write([]byte("\n"))
336            }
337        */
338        let binary_input = vec![
339            29, 225, 195, 167, 223, 236, 85, 195, 114, 227, 7, 0, 209, 239, 189, 24, 51, 105, 124,
340            168, 151, 75, 144, 64, 198, 197, 196, 4, 241, 97, 110, 135,
341        ];
342        let hash = "$2a$04$tjARW6ZON3PhrAIRW2LG/u9aDw5eFdstYLR8nFCNaOQmsH9XD23w.";
343        assert!(verify(binary_input, hash).unwrap());
344    }
345
346    #[test]
347    fn invalid_hash_does_not_panic() {
348        let binary_input = vec![
349            29, 225, 195, 167, 223, 236, 85, 195, 114, 227, 7, 0, 209, 239, 189, 24, 51, 105, 124,
350            168, 151, 75, 144, 64, 198, 197, 196, 4, 241, 97, 110, 135,
351        ];
352        let hash = "$2a$04$tjARW6ZON3PhrAIRW2LG/u9a.";
353        assert!(verify(binary_input, hash).is_err());
354    }
355
356    #[test]
357    fn a_wrong_password_is_false() {
358        let hash = "$2b$04$EGdrhbKUv8Oc9vGiXX0HQOxSg445d458Muh7DAHskb6QbtCvdxcie";
359        assert!(!verify("wrong", hash).unwrap());
360    }
361
362    #[test]
363    fn errors_with_invalid_hash() {
364        // there is another $ in the hash part
365        let hash = "$2a$04$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk$5bektyVVa5xnIi";
366        assert!(verify("correctbatteryhorsestapler", hash).is_err());
367    }
368
369    #[test]
370    fn errors_with_non_number_cost() {
371        // the cost is not a number
372        let hash = "$2a$ab$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk$5bektyVVa5xnIi";
373        assert!(verify("correctbatteryhorsestapler", hash).is_err());
374    }
375
376    #[test]
377    fn errors_with_a_hash_too_long() {
378        // the cost is not a number
379        let hash = "$2a$04$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk$5bektyVVa5xnIerererereri";
380        assert!(verify("correctbatteryhorsestapler", hash).is_err());
381    }
382
383    #[test]
384    fn long_passwords_truncate_correctly() {
385        // produced with python -c 'import bcrypt; bcrypt.hashpw(b"x"*100, b"$2a$05$...............................")'
386        let hash = "$2a$05$......................YgIDy4hFBdVlc/6LHnD9mX488r9cLd2";
387        assert!(verify(iter::repeat("x").take(100).collect::<String>(), hash).unwrap());
388    }
389
390    #[cfg(any(feature = "alloc", feature = "std"))]
391    #[test]
392    fn generate_versions() {
393        let password = "hunter2".as_bytes();
394        let salt = vec![0; 16];
395        let result = _hash_password(password, DEFAULT_COST, salt.try_into().unwrap()).unwrap();
396        assert_eq!(
397            "$2a$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm",
398            result.format_for_version(Version::TwoA)
399        );
400        assert_eq!(
401            "$2b$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm",
402            result.format_for_version(Version::TwoB)
403        );
404        assert_eq!(
405            "$2x$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm",
406            result.format_for_version(Version::TwoX)
407        );
408        assert_eq!(
409            "$2y$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm",
410            result.format_for_version(Version::TwoY)
411        );
412        let hash = result.to_string();
413        assert_eq!(true, verify("hunter2", &hash).unwrap());
414    }
415
416    #[cfg(any(feature = "alloc", feature = "std"))]
417    #[test]
418    fn allow_null_bytes() {
419        // hash p1, check the hash against p2:
420        fn hash_and_check(p1: &[u8], p2: &[u8]) -> Result<bool, BcryptError> {
421            let fast_cost = 4;
422            match hash_with_salt(p1, fast_cost, [0x11; 16]) {
423                Ok(s) => verify(p2, &s.format_for_version(Version::TwoB)),
424                Err(e) => Err(e),
425            }
426        }
427        fn assert_valid_password(p1: &[u8], p2: &[u8], expected: bool) {
428            match hash_and_check(p1, p2) {
429                Ok(checked) => {
430                    if checked != expected {
431                        panic!(
432                            "checked {:?} against {:?}, incorrect result {}",
433                            p1, p2, checked
434                        )
435                    }
436                }
437                Err(e) => panic!("error evaluating password: {} for {:?}.", e, p1),
438            }
439        }
440
441        // bcrypt should consider all of these distinct:
442        let test_passwords = vec![
443            "\0",
444            "passw0rd\0",
445            "password\0with tail",
446            "\0passw0rd",
447            "a",
448            "a\0",
449            "a\0b\0",
450        ];
451
452        for (i, p1) in test_passwords.iter().enumerate() {
453            for (j, p2) in test_passwords.iter().enumerate() {
454                assert_valid_password(p1.as_bytes(), p2.as_bytes(), i == j);
455            }
456        }
457
458        // this is a quirk of the bcrypt algorithm: passwords that are entirely null
459        // bytes hash to the same value, even if they are different lengths:
460        assert_valid_password("\0\0\0\0\0\0\0\0".as_bytes(), "\0".as_bytes(), true);
461    }
462
463    #[cfg(any(feature = "alloc", feature = "std"))]
464    #[test]
465    fn hash_with_fixed_salt() {
466        let salt = [
467            38, 113, 212, 141, 108, 213, 195, 166, 201, 38, 20, 13, 47, 40, 104, 18,
468        ];
469        let hashed = hash_with_salt("My S3cre7 P@55w0rd!", 5, salt)
470            .unwrap()
471            .to_string();
472        assert_eq!(
473            "$2y$05$HlFShUxTu4ZHHfOLJwfmCeDj/kuKFKboanXtDJXxCC7aIPTUgxNDe",
474            &hashed
475        );
476    }
477
478    #[test]
479    fn does_no_error_on_char_boundary_splitting() {
480        // Just checks that it does not panic
481        let _ = verify(
482            &[],
483            "2a$$$0$OOOOOOOOOOOOOOOOOOOOO£OOOOOOOOOOOOOOOOOOOOOOOOOOOOOO",
484        );
485    }
486}