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::{
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
28const 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)]
36pub struct HashParts {
38 cost: u32,
39 salt: String,
40 hash: String,
41}
42
43#[derive(Clone, Debug)]
44pub enum Version {
47 TwoA,
48 TwoX,
49 TwoY,
50 TwoB,
51}
52
53#[cfg(any(feature = "alloc", feature = "std"))]
54impl HashParts {
55 fn format(&self) -> String {
57 self.format_for_version(Version::TwoB)
58 }
59
60 pub fn get_cost(&self) -> u32 {
62 self.cost
63 }
64
65 pub fn get_salt(&self) -> String {
67 self.salt.clone()
68 }
69
70 pub fn format_for_version(&self, version: Version) -> String {
72 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#[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 let mut vec = Vec::with_capacity(password.len() + 1);
121 vec.extend_from_slice(password);
122 vec.push(0);
123 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]), })
144}
145
146#[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 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#[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#[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#[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#[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#[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#[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#[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#[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#[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 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 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 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 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 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 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 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 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 let _ = verify(
573 &[],
574 "2a$$$0$OOOOOOOOOOOOOOOOOOOOO£OOOOOOOOOOOOOOOOOOOOOOOOOOOOOO",
575 );
576 }
577}