Skip to content

Commit 7b2ca8d

Browse files
committed
Add alloc-free variants for all public hash funcs
Adds allocation free variants of all public hashing functions, to provide a mechanism for users to use bcrypt fully alloc free if they want.
1 parent 950ad11 commit 7b2ca8d

1 file changed

Lines changed: 102 additions & 1 deletion

File tree

src/lib.rs

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,23 @@ pub fn non_truncating_hash<P: AsRef<[u8]>>(password: P, cost: u32) -> BcryptResu
252252
})
253253
}
254254

255+
/// Generates a password hash using the cost given, returning a fixed-size stack buffer.
256+
/// The salt is generated randomly using the OS randomness.
257+
/// The returned buffer is always exactly 60 bytes of valid UTF-8 (version 2b format).
258+
#[cfg(any(feature = "alloc", feature = "std"))]
259+
pub fn hash_bytes<P: AsRef<[u8]>>(password: P, cost: u32) -> BcryptResult<[u8; 60]> {
260+
hash_with_result(password, cost).map(|r| r.format())
261+
}
262+
263+
/// Generates a password hash using the cost given, returning a fixed-size stack buffer.
264+
/// The salt is generated randomly using the OS randomness.
265+
/// The returned buffer is always exactly 60 bytes of valid UTF-8 (version 2b format).
266+
/// Will return BcryptError::Truncation if password is longer than 72 bytes
267+
#[cfg(any(feature = "alloc", feature = "std"))]
268+
pub fn non_truncating_hash_bytes<P: AsRef<[u8]>>(password: P, cost: u32) -> BcryptResult<[u8; 60]> {
269+
non_truncating_hash_with_result(password, cost).map(|r| r.format())
270+
}
271+
255272
/// Generates a password hash using the cost given.
256273
/// The salt is generated randomly using the OS randomness.
257274
/// The function returns a result structure and allows to format the hash in different versions.
@@ -293,6 +310,17 @@ pub fn hash_with_salt<P: AsRef<[u8]>>(
293310
_hash_password(password.as_ref(), cost, salt, false)
294311
}
295312

313+
/// Generates a password given a hash and a cost, returning a fixed-size stack buffer.
314+
/// The returned buffer is always exactly 60 bytes of valid UTF-8 (version 2b format).
315+
#[cfg(any(feature = "alloc", feature = "std"))]
316+
pub fn hash_with_salt_bytes<P: AsRef<[u8]>>(
317+
password: P,
318+
cost: u32,
319+
salt: [u8; 16],
320+
) -> BcryptResult<[u8; 60]> {
321+
_hash_password(password.as_ref(), cost, salt, false).map(|r| r.format())
322+
}
323+
296324
/// Generates a password given a hash and a cost.
297325
/// The function returns a result structure and allows to format the hash in different versions.
298326
/// Will return BcryptError::Truncation if password is longer than 72 bytes
@@ -305,6 +333,18 @@ pub fn non_truncating_hash_with_salt<P: AsRef<[u8]>>(
305333
_hash_password(password.as_ref(), cost, salt, true)
306334
}
307335

336+
/// Generates a password given a hash and a cost, returning a fixed-size stack buffer.
337+
/// The returned buffer is always exactly 60 bytes of valid UTF-8 (version 2b format).
338+
/// Will return BcryptError::Truncation if password is longer than 72 bytes
339+
#[cfg(any(feature = "alloc", feature = "std"))]
340+
pub fn non_truncating_hash_with_salt_bytes<P: AsRef<[u8]>>(
341+
password: P,
342+
cost: u32,
343+
salt: [u8; 16],
344+
) -> BcryptResult<[u8; 60]> {
345+
_hash_password(password.as_ref(), cost, salt, true).map(|r| r.format())
346+
}
347+
308348
/// Verify the password against the hash by extracting the salt from the hash and recomputing the
309349
/// hash from the password. If `err_on_truncation` is set to true, then this method will return
310350
/// `BcryptError::Truncation`.
@@ -343,7 +383,8 @@ mod tests {
343383
vec,
344384
vec::Vec,
345385
},
346-
hash, hash_with_salt, non_truncating_verify, split_hash, verify,
386+
hash, hash_bytes, hash_with_salt, hash_with_salt_bytes, non_truncating_hash_bytes,
387+
non_truncating_hash_with_salt_bytes, non_truncating_verify, split_hash, verify,
347388
};
348389
use base64::Engine as _;
349390
use core::convert::TryInto;
@@ -617,6 +658,66 @@ mod tests {
617658
);
618659
}
619660

661+
#[test]
662+
fn hash_bytes_returns_valid_utf8_bcrypt_string() {
663+
let result = hash_bytes("hunter2", 4).unwrap();
664+
let s = core::str::from_utf8(&result).unwrap();
665+
assert!(s.starts_with("$2b$04$"));
666+
assert_eq!(s.len(), 60);
667+
assert!(verify("hunter2", s).unwrap());
668+
}
669+
670+
#[test]
671+
fn non_truncating_hash_bytes_returns_valid_utf8_bcrypt_string() {
672+
let result = non_truncating_hash_bytes("hunter2", 4).unwrap();
673+
let s = core::str::from_utf8(&result).unwrap();
674+
assert!(s.starts_with("$2b$04$"));
675+
assert_eq!(s.len(), 60);
676+
assert!(verify("hunter2", s).unwrap());
677+
}
678+
679+
#[test]
680+
fn non_truncating_hash_bytes_errors_on_long_password() {
681+
use core::iter;
682+
let result = non_truncating_hash_bytes(iter::repeat("x").take(72).collect::<String>(), 4);
683+
assert!(matches!(result, Err(BcryptError::Truncation(73))));
684+
}
685+
686+
#[test]
687+
fn hash_with_salt_bytes_matches_hash_with_salt() {
688+
let salt = [
689+
38, 113, 212, 141, 108, 213, 195, 166, 201, 38, 20, 13, 47, 40, 104, 18,
690+
];
691+
let expected = hash_with_salt("My S3cre7 P@55w0rd!", 5, salt)
692+
.unwrap()
693+
.to_string();
694+
let result = hash_with_salt_bytes("My S3cre7 P@55w0rd!", 5, salt).unwrap();
695+
let s = core::str::from_utf8(&result).unwrap();
696+
assert_eq!(expected, s);
697+
}
698+
699+
#[test]
700+
fn non_truncating_hash_with_salt_bytes_errors_on_long_password() {
701+
use core::iter;
702+
let salt = [0u8; 16];
703+
let result = non_truncating_hash_with_salt_bytes(
704+
iter::repeat("x").take(72).collect::<String>(),
705+
4,
706+
salt,
707+
);
708+
assert!(matches!(result, Err(BcryptError::Truncation(73))));
709+
}
710+
711+
#[test]
712+
fn hash_bytes_matches_hash_string() {
713+
let salt = [0u8; 16];
714+
let result_parts = _hash_password("hunter2".as_bytes(), 4, salt, false).unwrap();
715+
let from_parts = result_parts.format_for_version(Version::TwoB);
716+
let bytes_result = hash_with_salt_bytes("hunter2", 4, salt).unwrap();
717+
let from_bytes = core::str::from_utf8(&bytes_result).unwrap();
718+
assert_eq!(from_parts, from_bytes);
719+
}
720+
620721
quickcheck! {
621722
fn can_verify_arbitrary_own_generated(pass: Vec<u8>) -> BcryptResult<bool> {
622723
let mut pass = pass;

0 commit comments

Comments
 (0)