Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,21 @@ edition = "2024"
include = ["src/**/*", "LICENSE", "README.md"]

[features]
default = ["std", "zeroize"]
default = ["std", "zeroize", "kdf", "password-hash"]
std = ["getrandom/std", "base64/std"]
alloc = ["base64/alloc", "getrandom"]
kdf = ["dep:kdf"]
password-hash = ["dep:mcf", "dep:password-hash", "alloc"]

[dependencies]
blowfish = { version = "0.9", features = ["bcrypt"] }
getrandom = { version = "0.4", default-features = false, optional = true }
base64 = { version = "0.22", default-features = false }
zeroize = { version = "1.5.4", optional = true }
subtle = { version = "2.4.1", default-features = false }
kdf = { version = "0.1.0", optional = true }
mcf = { version = "0.6.0", optional = true }
password-hash = { version = "0.6.0", default-features = false, optional = true }

[dev-dependencies]
# no default features avoid pulling in log
Expand Down
102 changes: 102 additions & 0 deletions src/bcrypt.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
use blowfish::Blowfish;
#[cfg(feature = "kdf")]
use kdf::{Kdf, Pbkdf};

#[cfg(any(feature = "kdf", feature = "password-hash"))]
use crate::BcryptError;

fn setup(cost: u32, salt: &[u8], key: &[u8]) -> Blowfish {
assert!(cost < 32);
Expand Down Expand Up @@ -41,9 +46,51 @@ pub fn bcrypt(cost: u32, salt: [u8; 16], password: &[u8]) -> [u8; 24] {
output
}

#[cfg(any(feature = "kdf", feature = "password-hash"))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
/// bcrypt context.
pub struct Bcrypt {
pub(crate) cost: u32,
}

#[cfg(any(feature = "kdf", feature = "password-hash"))]
impl Bcrypt {
/// Creates a new [`Bcrypt`] with the given `cost`.
pub const fn new(cost: u32) -> Result<Self, BcryptError> {
match cost {
cost @ 4..=31 => Ok(Self { cost }),
cost => Err(BcryptError::CostNotAllowed(cost)),
}
}
}

#[cfg(any(feature = "kdf", feature = "password-hash"))]
impl Default for Bcrypt {
fn default() -> Self {
Self {
cost: crate::DEFAULT_COST,
}
}
}

#[cfg(feature = "kdf")]
impl Kdf for Bcrypt {
fn derive_key(&self, password: &[u8], salt: &[u8], out: &mut [u8]) -> kdf::Result<()> {
let salt = salt.try_into().map_err(|_| kdf::Error)?;
let hash = bcrypt(self.cost, salt, password);
out.copy_from_slice(&hash);
Ok(())
}
}

#[cfg(feature = "kdf")]
impl Pbkdf for Bcrypt {}

#[cfg(test)]
mod tests {
use super::bcrypt;
#[cfg(feature = "kdf")]
use super::{Bcrypt, Kdf};

#[test]
fn raw_bcrypt() {
Expand Down Expand Up @@ -89,4 +136,59 @@ mod tests {
];
assert_eq!(bcrypt(cost, salt, pw)[..23], result);
}

#[cfg(feature = "kdf")]
#[test]
fn derive_key() {
// test vectors unbase64ed from
// https://github.com/djmdjm/jBCrypt/blob/master/test/org/mindrot/jbcrypt/TestBCrypt.java

// $2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.
let pw = b"\0";
let cost = 6;
let salt = [
0x14, 0x4b, 0x3d, 0x69, 0x1a, 0x7b, 0x4e, 0xcf, 0x39, 0xcf, 0x73, 0x5c, 0x7f, 0xa7,
0xa7, 0x9c,
];
let result = [
0x55, 0x7e, 0x94, 0xf3, 0x4b, 0xf2, 0x86, 0xe8, 0x71, 0x9a, 0x26, 0xbe, 0x94, 0xac,
0x1e, 0x16, 0xd9, 0x5e, 0xf9, 0xf8, 0x19, 0xde, 0xe0,
];
let bcrypt = Bcrypt::new(cost).unwrap();
let mut output = [0_u8; 24];
bcrypt.derive_key(pw, &salt, &mut output).unwrap();
assert_eq!(output[..23], result);

// $2a$06$m0CrhHm10qJ3lXRY.5zDGO3rS2KdeeWLuGmsfGlMfOxih58VYVfxe
let pw = b"a\0";
let cost = 6;
let salt = [
0xa3, 0x61, 0x2d, 0x8c, 0x9a, 0x37, 0xda, 0xc2, 0xf9, 0x9d, 0x94, 0xda, 0x3, 0xbd,
0x45, 0x21,
];
let result = [
0xe6, 0xd5, 0x38, 0x31, 0xf8, 0x20, 0x60, 0xdc, 0x8, 0xa2, 0xe8, 0x48, 0x9c, 0xe8,
0x50, 0xce, 0x48, 0xfb, 0xf9, 0x76, 0x97, 0x87, 0x38,
];
let bcrypt = Bcrypt::new(cost).unwrap();
let mut output = [0_u8; 24];
bcrypt.derive_key(pw, &salt, &mut output).unwrap();
assert_eq!(output[..23], result);

// // $2a$08$aTsUwsyowQuzRrDqFflhgekJ8d9/7Z3GV3UcgvzQW3J5zMyrTvlz.
let pw = b"abcdefghijklmnopqrstuvwxyz\0";
let cost = 8;
let salt = [
0x71, 0x5b, 0x96, 0xca, 0xed, 0x2a, 0xc9, 0x2c, 0x35, 0x4e, 0xd1, 0x6c, 0x1e, 0x19,
0xe3, 0x8a,
];
let result = [
0x98, 0xbf, 0x9f, 0xfc, 0x1f, 0x5b, 0xe4, 0x85, 0xf9, 0x59, 0xe8, 0xb1, 0xd5, 0x26,
0x39, 0x2f, 0xbd, 0x4e, 0xd2, 0xd5, 0x71, 0x9f, 0x50,
];
let bcrypt = Bcrypt::new(cost).unwrap();
let mut output = [0_u8; 24];
bcrypt.derive_key(pw, &salt, &mut output).unwrap();
assert_eq!(output[..23], result);
}
}
9 changes: 9 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,18 @@ use core::fmt;
#[cfg(any(feature = "alloc", feature = "std"))]
use {base64::Engine, core::convert::AsRef, core::str::FromStr};

#[cfg(feature = "kdf")]
pub use kdf::{self, Kdf, Pbkdf};
#[cfg(feature = "password-hash")]
pub use password_hash;

mod bcrypt;
mod errors;
#[cfg(feature = "password-hash")]
pub mod mcf;

#[cfg(any(feature = "kdf", feature = "password-hash"))]
pub use crate::bcrypt::Bcrypt;
pub use crate::bcrypt::bcrypt;
pub use crate::errors::{BcryptError, BcryptResult};

Expand Down
190 changes: 190 additions & 0 deletions src/mcf.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
//! Implementation of the [`password_hash`] traits for Modular Crypt Format
//! (MCF) password hash strings which begin with `$2b$` or any other alternative
//! prefix:
//!
//! <https://man.archlinux.org/man/crypt.5#bcrypt>

pub use mcf::{PasswordHash, PasswordHashRef};
use password_hash::{CustomizedPasswordHasher, Error, PasswordHasher, PasswordVerifier, Result};

use crate::{Bcrypt, Version};

impl CustomizedPasswordHasher<PasswordHash> for Bcrypt {
type Params = u32;

fn hash_password_customized(
&self,
password: &[u8],
salt: &[u8],
alg_id: Option<&str>,
version: Option<password_hash::Version>,
params: Self::Params,
) -> Result<PasswordHash> {
let hash_version = match alg_id {
Some("2a") => Version::TwoA,
Some("2b") | None => Version::TwoB,
Some("2x") => Version::TwoX,
Some("2y") => Version::TwoY,
_ => return Err(Error::Algorithm),
};

if version.is_some() {
return Err(Error::Version);
}

let salt = salt.try_into().map_err(|_| Error::Internal)?;
let hash = crate::hash_with_salt(password, params, salt).map_err(|_| Error::Internal)?;

let mcf_hash = hash.format_for_version(hash_version);
let mcf_hash = PasswordHash::new(mcf_hash).unwrap();
Ok(mcf_hash)
}
}

impl PasswordHasher<PasswordHash> for Bcrypt {
fn hash_password_with_salt(&self, password: &[u8], salt: &[u8]) -> Result<PasswordHash> {
self.hash_password_customized(password, salt, None, None, self.cost)
}
}

impl PasswordVerifier<PasswordHash> for Bcrypt {
fn verify_password(&self, password: &[u8], hash: &PasswordHash) -> Result<()> {
self.verify_password(password, hash.as_password_hash_ref())
}
}

impl PasswordVerifier<PasswordHashRef> for Bcrypt {
fn verify_password(&self, password: &[u8], hash: &PasswordHashRef) -> Result<()> {
let is_valid = crate::verify(password, hash.as_str()).map_err(|_| Error::Internal)?;
if is_valid {
Ok(())
} else {
Err(Error::PasswordInvalid)
}
}
}

#[cfg(test)]
mod tests {
use super::{
Bcrypt, CustomizedPasswordHasher, Error, PasswordHash, PasswordHashRef, PasswordHasher,
PasswordVerifier,
};

#[test]
fn hash_password() {
// 2a
let actual_hash: PasswordHash = Bcrypt::default()
.hash_password_customized(b"hunter2", &[0; 16], Some("2a"), None, crate::DEFAULT_COST)
.unwrap();
let expected_hash =
PasswordHash::new("$2a$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm")
.unwrap();
assert_eq!(expected_hash, actual_hash);
// 2b
let actual_hash: PasswordHash = Bcrypt::default()
.hash_password_with_salt(b"hunter2", &[0; 16])
.unwrap();
let expected_hash =
PasswordHash::new("$2b$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm")
.unwrap();
assert_eq!(expected_hash, actual_hash);
// 2x
let actual_hash: PasswordHash = Bcrypt::default()
.hash_password_customized(b"hunter2", &[0; 16], Some("2x"), None, crate::DEFAULT_COST)
.unwrap();
let expected_hash =
PasswordHash::new("$2x$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm")
.unwrap();
assert_eq!(expected_hash, actual_hash);
// 2y
let actual_hash: PasswordHash = Bcrypt::default()
.hash_password_customized(b"hunter2", &[0; 16], Some("2y"), None, crate::DEFAULT_COST)
.unwrap();
let expected_hash =
PasswordHash::new("$2y$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm")
.unwrap();
assert_eq!(expected_hash, actual_hash);
}

#[test]
fn verify_password() {
// `can_verify_hash_generated_from_some_online_tool`
let hash =
PasswordHashRef::new("$2a$04$UuTkLRZZ6QofpDOlMz32MuuxEHA43WOemOYHPz6.SjsVsyO1tDU96")
.unwrap();
assert_eq!(Bcrypt::default().verify_password(b"password", hash), Ok(()));
// `can_verify_hash_generated_from_python`
let hash =
PasswordHashRef::new("$2b$04$EGdrhbKUv8Oc9vGiXX0HQOxSg445d458Muh7DAHskb6QbtCvdxcie")
.unwrap();
assert_eq!(
Bcrypt::default().verify_password(b"correctbatteryhorsestapler", hash),
Ok(())
);
// `can_verify_hash_generated_from_node`
let hash =
PasswordHashRef::new("$2a$04$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk5bektyVVa5xnIi")
.unwrap();
assert_eq!(
Bcrypt::default().verify_password(b"correctbatteryhorsestapler", hash),
Ok(())
);
// `can_verify_hash_generated_from_go`
let binary_input = [
29, 225, 195, 167, 223, 236, 85, 195, 114, 227, 7, 0, 209, 239, 189, 24, 51, 105, 124,
168, 151, 75, 144, 64, 198, 197, 196, 4, 241, 97, 110, 135,
];
let hash =
PasswordHashRef::new("$2a$04$tjARW6ZON3PhrAIRW2LG/u9aDw5eFdstYLR8nFCNaOQmsH9XD23w.")
.unwrap();
assert_eq!(
Bcrypt::default().verify_password(&binary_input, hash),
Ok(())
);

// `invalid_hash_does_not_panic`
let binary_input = [
29, 225, 195, 167, 223, 236, 85, 195, 114, 227, 7, 0, 209, 239, 189, 24, 51, 105, 124,
168, 151, 75, 144, 64, 198, 197, 196, 4, 241, 97, 110, 135,
];
let hash = PasswordHashRef::new("$2a$04$tjARW6ZON3PhrAIRW2LG/u9a.").unwrap();
assert_eq!(
Bcrypt::default().verify_password(&binary_input, hash),
Err(Error::Internal)
);
// `a_wrong_password_is_false`
let hash =
PasswordHashRef::new("$2b$04$EGdrhbKUv8Oc9vGiXX0HQOxSg445d458Muh7DAHskb6QbtCvdxcie")
.unwrap();
assert_eq!(
Bcrypt::default().verify_password(b"wrong", hash),
Err(Error::PasswordInvalid)
);
// `errors_with_invalid_hash`
let hash =
PasswordHashRef::new("$2a$04$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk$5bektyVVa5xnIi")
.unwrap();
assert_eq!(
Bcrypt::default().verify_password(b"correctbatteryhorsestapler", hash),
Err(Error::Internal)
);
// `errors_with_non_number_cost`
let hash =
PasswordHashRef::new("$2a$ab$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk$5bektyVVa5xnIi")
.unwrap();
assert_eq!(
Bcrypt::default().verify_password(b"correctbatteryhorsestapler", hash),
Err(Error::Internal)
);
// `errors_with_a_hash_too_long`
let hash = PasswordHashRef::new(
"$2a$04$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk$5bektyVVa5xnIerererereri",
)
.unwrap();
assert_eq!(
Bcrypt::default().verify_password(b"correctbatteryhorsestapler", hash),
Err(Error::Internal)
);
}
}