bcrypt_wasm/
lib.rs

1//! Easily hash and verify passwords using bcrypt
2use getrandom;
3use std::convert::AsRef;
4use std::fmt;
5use std::str::FromStr;
6
7mod b64;
8mod bcrypt;
9mod errors;
10
11pub use crate::bcrypt::bcrypt;
12pub use crate::errors::{BcryptError, BcryptResult};
13
14// Cost constants
15const MIN_COST: u32 = 4;
16const MAX_COST: u32 = 31;
17pub const DEFAULT_COST: u32 = 12;
18
19#[derive(Debug, PartialEq)]
20/// A bcrypt hash result before concatenating
21pub struct HashParts {
22    cost: u32,
23    salt: String,
24    hash: String,
25}
26
27/// BCrypt hash version
28/// https://en.wikipedia.org/wiki/Bcrypt#Versioning_history
29pub enum Version {
30    TwoA,
31    TwoX,
32    TwoY,
33    TwoB,
34}
35
36impl HashParts {
37    /// Creates the bcrypt hash string from all its parts
38    fn format(self) -> String {
39        self.format_for_version(Version::TwoB)
40    }
41
42    /// Get the bcrypt hash cost
43    pub fn get_cost(&self) -> u32 {
44        self.cost
45    }
46
47    /// Get the bcrypt hash salt
48    pub fn get_salt(&self) -> String {
49        self.salt.clone()
50    }
51
52    /// Creates the bcrypt hash string from all its part, allowing to customize the version.
53    pub fn format_for_version(&self, version: Version) -> String {
54        // Cost need to have a length of 2 so padding with a 0 if cost < 10
55        format!("${}${:02}${}{}", version, self.cost, self.salt, self.hash)
56    }
57}
58
59impl FromStr for HashParts {
60    type Err = BcryptError;
61
62    fn from_str(s: &str) -> Result<Self, Self::Err> {
63        split_hash(s)
64    }
65}
66
67impl ToString for HashParts {
68    fn to_string(&self) -> String {
69        self.format_for_version(Version::TwoY)
70    }
71}
72
73impl fmt::Display for Version {
74    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
75        let str = match self {
76            Version::TwoA => "2a",
77            Version::TwoB => "2b",
78            Version::TwoX => "2x",
79            Version::TwoY => "2y",
80        };
81        write!(f, "{}", str)
82    }
83}
84
85/// The main meat: actually does the hashing and does some verification with
86/// the cost to ensure it's a correct one
87fn _hash_password(password: &[u8], cost: u32, salt: &[u8]) -> BcryptResult<HashParts> {
88    if cost > MAX_COST || cost < MIN_COST {
89        return Err(BcryptError::CostNotAllowed(cost));
90    }
91    if password.contains(&0u8) {
92        return Err(BcryptError::InvalidPassword);
93    }
94
95    // Output is 24
96    let mut output = [0u8; 24];
97    // Passwords need to be null terminated
98    let mut vec: Vec<u8> = Vec::new();
99    vec.extend_from_slice(password);
100    vec.push(0);
101    // We only consider the first 72 chars; truncate if necessary.
102    // `bcrypt` below will panic if len > 72
103    let truncated = if vec.len() > 72 { &vec[..72] } else { &vec };
104
105    bcrypt::bcrypt(cost, salt, truncated, &mut output);
106
107    Ok(HashParts {
108        cost,
109        salt: b64::encode(salt),
110        hash: b64::encode(&output[..23]), // remember to remove the last byte
111    })
112}
113
114/// Takes a full hash and split it into 3 parts:
115/// cost, salt and hash
116fn split_hash(hash: &str) -> BcryptResult<HashParts> {
117    let mut parts = HashParts {
118        cost: 0,
119        salt: "".to_string(),
120        hash: "".to_string(),
121    };
122
123    // Should be [prefix, cost, hash]
124    let raw_parts: Vec<_> = hash.split('$').filter(|s| !s.is_empty()).collect();
125
126    if raw_parts.len() != 3 {
127        return Err(BcryptError::InvalidHash(hash.to_string()));
128    }
129
130    if raw_parts[0] != "2y" && raw_parts[0] != "2b" && raw_parts[0] != "2a" {
131        return Err(BcryptError::InvalidPrefix(raw_parts[0].to_string()));
132    }
133
134    if let Ok(c) = raw_parts[1].parse::<u32>() {
135        parts.cost = c;
136    } else {
137        return Err(BcryptError::InvalidCost(raw_parts[1].to_string()));
138    }
139
140    if raw_parts[2].len() == 53 {
141        parts.salt = raw_parts[2][..22].chars().collect();
142        parts.hash = raw_parts[2][22..].chars().collect();
143    } else {
144        return Err(BcryptError::InvalidHash(hash.to_string()));
145    }
146
147    Ok(parts)
148}
149
150/// Generates a password hash using the cost given.
151/// The salt is generated randomly using the OS randomness
152pub fn hash<P: AsRef<[u8]>>(password: P, cost: u32) -> BcryptResult<String> {
153    hash_with_result(password, cost).map(|r| r.format())
154}
155
156/// Generates a password hash using the cost given.
157/// The salt is generated randomly using the OS randomness.
158/// The function returns a result structure and allows to format the hash in different versions.
159pub fn hash_with_result<P: AsRef<[u8]>>(password: P, cost: u32) -> BcryptResult<HashParts> {
160    let salt = {
161        let mut s = [0u8; 16];
162        getrandom::getrandom(&mut s).expect("An error occurred");
163        s
164    };
165
166    _hash_password(password.as_ref(), cost, salt.as_ref())
167}
168
169/// Generates a password given a hash and a cost.
170/// The function returns a result structure and allows to format the hash in different versions.
171pub fn hash_with_salt<P: AsRef<[u8]>>(password: P, cost: u32, salt: &[u8]) -> BcryptResult<HashParts> {
172    _hash_password(password.as_ref(), cost, salt)
173}
174
175/// Verify that a password is equivalent to the hash provided
176pub fn verify<P: AsRef<[u8]>>(password: P, hash: &str) -> BcryptResult<bool> {
177    let parts = split_hash(hash)?;
178    let salt = b64::decode(&parts.salt)?;
179    let generated = _hash_password(password.as_ref(), parts.cost, &salt)?;
180    let source_decoded = b64::decode(&parts.hash)?;
181    let generated_decoded = b64::decode(&generated.hash)?;
182    if source_decoded.len() != generated_decoded.len() {
183        return Ok(false);
184    }
185
186    for (a, b) in source_decoded.into_iter().zip(generated_decoded) {
187        if a != b {
188            return Ok(false);
189        }
190    }
191
192    Ok(true)
193}
194
195#[cfg(test)]
196mod tests {
197    use super::{
198        _hash_password, hash, hash_with_salt, split_hash, verify, BcryptError, BcryptResult, HashParts, Version,
199        DEFAULT_COST,
200    };
201    use quickcheck::{quickcheck, TestResult};
202    use std::iter;
203    use std::str::FromStr;
204
205    #[test]
206    fn can_split_hash() {
207        let hash = "$2y$12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u";
208        let output = split_hash(hash).unwrap();
209        let expected = HashParts {
210            cost: 12,
211            salt: "L6Bc/AlTQHyd9liGgGEZyO".to_string(),
212            hash: "FLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u".to_string(),
213        };
214        assert_eq!(output, expected);
215    }
216
217    #[test]
218    fn can_output_cost_and_salt_from_parsed_hash() {
219        let hash = "$2y$12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u";
220        let parsed = HashParts::from_str(hash).unwrap();
221        assert_eq!(parsed.get_cost(), 12);
222        assert_eq!(parsed.get_salt(), "L6Bc/AlTQHyd9liGgGEZyO".to_string());
223    }
224
225    #[test]
226    fn returns_an_error_if_a_parsed_hash_is_baddly_formated() {
227        let hash1 = "$2y$12$L6Bc/AlTQHyd9lGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u";
228        assert!(HashParts::from_str(hash1).is_err());
229
230        let hash2 = "!2y$12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u";
231        assert!(HashParts::from_str(hash2).is_err());
232
233        let hash3 = "$2y$-12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u";
234        assert!(HashParts::from_str(hash3).is_err());
235    }
236
237    #[test]
238    fn can_verify_hash_generated_from_some_online_tool() {
239        let hash = "$2a$04$UuTkLRZZ6QofpDOlMz32MuuxEHA43WOemOYHPz6.SjsVsyO1tDU96";
240        assert!(verify("password", hash).unwrap());
241    }
242
243    #[test]
244    fn can_verify_hash_generated_from_python() {
245        let hash = "$2b$04$EGdrhbKUv8Oc9vGiXX0HQOxSg445d458Muh7DAHskb6QbtCvdxcie";
246        assert!(verify("correctbatteryhorsestapler", hash).unwrap());
247    }
248
249    #[test]
250    fn can_verify_hash_generated_from_node() {
251        let hash = "$2a$04$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk5bektyVVa5xnIi";
252        assert!(verify("correctbatteryhorsestapler", hash).unwrap());
253    }
254
255    #[test]
256    fn a_wrong_password_is_false() {
257        let hash = "$2b$04$EGdrhbKUv8Oc9vGiXX0HQOxSg445d458Muh7DAHskb6QbtCvdxcie";
258        assert!(!verify("wrong", hash).unwrap());
259    }
260
261    #[test]
262    fn errors_with_invalid_hash() {
263        // there is another $ in the hash part
264        let hash = "$2a$04$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk$5bektyVVa5xnIi";
265        assert!(verify("correctbatteryhorsestapler", hash).is_err());
266    }
267
268    #[test]
269    fn errors_with_non_number_cost() {
270        // the cost is not a number
271        let hash = "$2a$ab$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk$5bektyVVa5xnIi";
272        assert!(verify("correctbatteryhorsestapler", hash).is_err());
273    }
274
275    #[test]
276    fn errors_with_a_hash_too_long() {
277        // the cost is not a number
278        let hash = "$2a$04$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk$5bektyVVa5xnIerererereri";
279        assert!(verify("correctbatteryhorsestapler", hash).is_err());
280    }
281
282    #[test]
283    fn can_verify_own_generated() {
284        let hashed = hash("hunter2", 4).unwrap();
285        assert_eq!(true, verify("hunter2", &hashed).unwrap());
286    }
287
288    #[test]
289    fn long_passwords_truncate_correctly() {
290        // produced with python -c 'import bcrypt; bcrypt.hashpw(b"x"*100, b"$2a$05$...............................")'
291        let hash = "$2a$05$......................YgIDy4hFBdVlc/6LHnD9mX488r9cLd2";
292        assert!(verify(iter::repeat("x").take(100).collect::<String>(), hash).unwrap());
293    }
294
295    #[test]
296    fn generate_versions() {
297        let password = "hunter2".as_bytes();
298        let salt = vec![0; 16];
299        let result = _hash_password(password, DEFAULT_COST, salt.as_slice()).unwrap();
300        assert_eq!(
301            "$2a$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm",
302            result.format_for_version(Version::TwoA)
303        );
304        assert_eq!(
305            "$2b$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm",
306            result.format_for_version(Version::TwoB)
307        );
308        assert_eq!(
309            "$2x$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm",
310            result.format_for_version(Version::TwoX)
311        );
312        assert_eq!(
313            "$2y$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm",
314            result.format_for_version(Version::TwoY)
315        );
316        let hash = result.to_string();
317        assert_eq!(true, verify("hunter2", &hash).unwrap());
318    }
319
320    #[test]
321    fn forbid_null_bytes() {
322        fn assert_invalid_password(password: &[u8]) {
323            match hash(password, DEFAULT_COST) {
324                Ok(_) => panic!(format!(
325                    "NULL bytes must be forbidden, but {:?} is allowed.",
326                    password
327                )),
328                Err(BcryptError::InvalidPassword) => {}
329                Err(e) => panic!(format!(
330                    "NULL bytes are forbidden but error differs: {} for {:?}.",
331                    e, password
332                )),
333            }
334        }
335        assert_invalid_password("\0".as_bytes());
336        assert_invalid_password("\0\0\0\0\0\0\0\0".as_bytes());
337        assert_invalid_password("passw0rd\0".as_bytes());
338        assert_invalid_password("passw0rd\0with tail".as_bytes());
339        assert_invalid_password("\0passw0rd".as_bytes());
340    }
341
342    #[test]
343    fn hash_with_fixed_salt() {
344        let salt = vec![38, 113, 212, 141, 108, 213, 195, 166,
345                        201, 38, 20, 13, 47, 40, 104, 18];
346        let hashed = hash_with_salt("My S3cre7 P@55w0rd!", 5, &salt).unwrap().to_string();
347        assert_eq!("$2y$05$HlFShUxTu4ZHHfOLJwfmCeDj/kuKFKboanXtDJXxCC7aIPTUgxNDe", &hashed);
348    }
349
350    quickcheck! {
351        fn can_verify_arbitrary_own_generated(pass: Vec<u8>) -> BcryptResult<bool> {
352            let mut pass = pass;
353            pass.retain(|&b| b != 0);
354            let hashed = hash(&pass, 4)?;
355            verify(pass, &hashed)
356        }
357
358        fn doesnt_verify_different_passwords(a: Vec<u8>, b: Vec<u8>) -> BcryptResult<TestResult> {
359            let mut a = a;
360            a.retain(|&b| b != 0);
361            let mut b = b;
362            b.retain(|&b| b != 0);
363            if a == b {
364                return Ok(TestResult::discard());
365            }
366            let hashed = hash(a, 4)?;
367            Ok(TestResult::from_bool(!verify(b, &hashed)?))
368        }
369    }
370}