1#![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
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#[derive(Debug, PartialEq)]
36pub struct HashParts {
38 cost: u32,
39 salt: [u8; 22],
40 hash: [u8; 31],
41}
42
43#[derive(Clone, Debug)]
44pub 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 pub fn get_cost(&self) -> u32 {
67 self.cost
68 }
69
70 pub fn get_salt(&self) -> &str {
72 str::from_utf8(&self.salt).unwrap()
73 }
74
75 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 pub fn format_for_version(&self, version: Version) -> String {
96 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
134fn _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 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 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
174fn 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
221pub 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
231pub 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 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 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 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 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 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 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 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 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 let _ = verify(
482 &[],
483 "2a$$$0$OOOOOOOOOOOOOOOOOOOOO£OOOOOOOOOOOOOOOOOOOOOOOOOOOOOO",
484 );
485 }
486}