Skip to content

Commit ca6cfd0

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 0e79aa0 commit ca6cfd0

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
@@ -265,6 +265,23 @@ pub fn non_truncating_hash<P: AsRef<[u8]>>(password: P, cost: u32) -> BcryptResu
265265
})
266266
}
267267

268+
/// Generates a password hash using the cost given, returning a fixed-size stack buffer.
269+
/// The salt is generated randomly using the OS randomness.
270+
/// The returned buffer is always exactly 60 bytes of valid UTF-8 (version 2b format).
271+
#[cfg(any(feature = "alloc", feature = "std"))]
272+
pub fn hash_bytes<P: AsRef<[u8]>>(password: P, cost: u32) -> BcryptResult<[u8; 60]> {
273+
hash_with_result(password, cost).map(|r| r.format())
274+
}
275+
276+
/// Generates a password hash using the cost given, returning a fixed-size stack buffer.
277+
/// The salt is generated randomly using the OS randomness.
278+
/// The returned buffer is always exactly 60 bytes of valid UTF-8 (version 2b format).
279+
/// Will return BcryptError::Truncation if password is longer than 72 bytes
280+
#[cfg(any(feature = "alloc", feature = "std"))]
281+
pub fn non_truncating_hash_bytes<P: AsRef<[u8]>>(password: P, cost: u32) -> BcryptResult<[u8; 60]> {
282+
non_truncating_hash_with_result(password, cost).map(|r| r.format())
283+
}
284+
268285
/// Generates a password hash using the cost given.
269286
/// The salt is generated randomly using the OS randomness.
270287
/// The function returns a result structure and allows to format the hash in different versions.
@@ -306,6 +323,17 @@ pub fn hash_with_salt<P: AsRef<[u8]>>(
306323
_hash_password(password.as_ref(), cost, salt, false)
307324
}
308325

326+
/// Generates a password given a hash and a cost, returning a fixed-size stack buffer.
327+
/// The returned buffer is always exactly 60 bytes of valid UTF-8 (version 2b format).
328+
#[cfg(any(feature = "alloc", feature = "std"))]
329+
pub fn hash_with_salt_bytes<P: AsRef<[u8]>>(
330+
password: P,
331+
cost: u32,
332+
salt: [u8; 16],
333+
) -> BcryptResult<[u8; 60]> {
334+
_hash_password(password.as_ref(), cost, salt, false).map(|r| r.format())
335+
}
336+
309337
/// Generates a password given a hash and a cost.
310338
/// The function returns a result structure and allows to format the hash in different versions.
311339
/// Will return BcryptError::Truncation if password is longer than 72 bytes
@@ -318,6 +346,18 @@ pub fn non_truncating_hash_with_salt<P: AsRef<[u8]>>(
318346
_hash_password(password.as_ref(), cost, salt, true)
319347
}
320348

349+
/// Generates a password given a hash and a cost, returning a fixed-size stack buffer.
350+
/// The returned buffer is always exactly 60 bytes of valid UTF-8 (version 2b format).
351+
/// Will return BcryptError::Truncation if password is longer than 72 bytes
352+
#[cfg(any(feature = "alloc", feature = "std"))]
353+
pub fn non_truncating_hash_with_salt_bytes<P: AsRef<[u8]>>(
354+
password: P,
355+
cost: u32,
356+
salt: [u8; 16],
357+
) -> BcryptResult<[u8; 60]> {
358+
_hash_password(password.as_ref(), cost, salt, true).map(|r| r.format())
359+
}
360+
321361
/// Verify the password against the hash by extracting the salt from the hash and recomputing the
322362
/// hash from the password. If `err_on_truncation` is set to true, then this method will return
323363
/// `BcryptError::Truncation`.
@@ -356,7 +396,8 @@ mod tests {
356396
vec,
357397
vec::Vec,
358398
},
359-
hash, hash_with_salt, non_truncating_verify, split_hash, verify,
399+
hash, hash_bytes, hash_with_salt, hash_with_salt_bytes, non_truncating_hash_bytes,
400+
non_truncating_hash_with_salt_bytes, non_truncating_verify, split_hash, verify,
360401
};
361402
use base64::Engine as _;
362403
use core::convert::TryInto;
@@ -630,6 +671,66 @@ mod tests {
630671
);
631672
}
632673

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

0 commit comments

Comments
 (0)