-
Notifications
You must be signed in to change notification settings - Fork 55
feat: Add password-hash feature
#97
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,8 @@ | ||
| use blowfish::Blowfish; | ||
|
|
||
| #[cfg(feature = "password-hash")] | ||
| use crate::BcryptError; | ||
|
|
||
| fn setup(cost: u32, salt: &[u8], key: &[u8]) -> Blowfish { | ||
| assert!(cost < 32); | ||
| let mut state = Blowfish::bc_init_state(); | ||
|
|
@@ -41,6 +44,33 @@ pub fn bcrypt(cost: u32, salt: [u8; 16], password: &[u8]) -> [u8; 24] { | |
| output | ||
| } | ||
|
|
||
| #[cfg(feature = "password-hash")] | ||
| #[derive(Clone, Copy, Debug, Eq, PartialEq)] | ||
| /// bcrypt context. | ||
| pub struct Bcrypt { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can that be used internally? a bit weird to have that just for the password-hash feature and this mean we duplicate the cost check |
||
| pub(crate) cost: u32, | ||
| } | ||
|
|
||
| #[cfg(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(feature = "password-hash")] | ||
| impl Default for Bcrypt { | ||
| fn default() -> Self { | ||
| Self { | ||
| cost: crate::DEFAULT_COST, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::bcrypt; | ||
|
|
||
| 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, | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is the params the cost? |
||
| ) -> 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) | ||
| ); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it probably shouldn't be part of the default feature