Skip to content

Commit 86182f6

Browse files
authored
yescrypt: impl password-hash traits (#765)
Now that the password hash traits have been decoupled from the PHC format, this impls them for `yescrypt`, replacing the previous `simple` API with one based on the traits. In the process this also bumps `mcf` to the latest prerelease which included some useful changes to make this work.
1 parent d77b991 commit 86182f6

8 files changed

Lines changed: 171 additions & 118 deletions

File tree

Cargo.lock

Lines changed: 4 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,6 @@ opt-level = 2
2020
argon2 = { path = "./argon2" }
2121
pbkdf2 = { path = "./pbkdf2" }
2222
scrypt = { path = "./scrypt" }
23+
24+
mcf = { git = "https://github.com/RustCrypto/formats" }
25+
password-hash = { git = "https://github.com/RustCrypto/traits" }

sha-crypt/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ base64ct = { version = "1.8", default-features = false, features = ["alloc"] }
2222

2323
# optional dependencies
2424
getrandom = { version = "0.3", optional = true, default-features = false }
25-
mcf = { version = "0.2", optional = true, default-features = false, features = ["alloc", "base64"] }
25+
mcf = { version = "=0.6.0-pre", optional = true, default-features = false, features = ["alloc", "base64"] }
2626
subtle = { version = "2", optional = true, default-features = false }
2727

2828
[features]

yescrypt/Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,15 @@ sha2 = { version = "0.11.0-rc.3", default-features = false }
2121
subtle = { version = "2", default-features = false }
2222

2323
# optional dependencies
24-
mcf = { version = "0.2", optional = true, default-features = false, features = ["alloc", "base64"] }
24+
mcf = { version = "=0.6.0-pre", optional = true, default-features = false, features = ["alloc", "base64"] }
25+
password-hash = { version = "0.6.0-rc.4", optional = true, default-features = false }
2526

2627
[dev-dependencies]
2728
hex-literal = "1"
2829

2930
[features]
3031
default = ["simple"]
31-
simple = ["dep:mcf"]
32+
simple = ["dep:mcf", "dep:password-hash"]
3233

3334
[package.metadata.docs.rs]
3435
all-features = true

yescrypt/src/error.rs

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,6 @@ pub type Result<T> = core::result::Result<T, Error>;
99
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1010
#[non_exhaustive]
1111
pub enum Error {
12-
/// Invalid password hashing algorithm.
13-
#[cfg(feature = "simple")]
14-
Algorithm,
15-
1612
/// Encoding error (i.e. Base64)
1713
Encoding,
1814

@@ -21,22 +17,14 @@ pub enum Error {
2117

2218
/// Invalid params
2319
Params,
24-
25-
/// Invalid password
26-
#[cfg(feature = "simple")]
27-
Password,
2820
}
2921

3022
impl fmt::Display for Error {
3123
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3224
match self {
33-
#[cfg(feature = "simple")]
34-
Error::Algorithm => write!(f, "password hash must begin with `$y$`"),
3525
Error::Encoding => write!(f, "yescrypt encoding invalid"),
3626
Error::Internal => write!(f, "internal error"),
3727
Error::Params => write!(f, "yescrypt params invalid"),
38-
#[cfg(feature = "simple")]
39-
Error::Password => write!(f, "invalid password"),
4028
}
4129
}
4230
}
@@ -48,3 +36,14 @@ impl From<TryFromIntError> for Error {
4836
Error::Internal
4937
}
5038
}
39+
40+
#[cfg(feature = "simple")]
41+
impl From<Error> for password_hash::Error {
42+
fn from(err: Error) -> Self {
43+
match err {
44+
Error::Encoding => password_hash::Error::EncodingInvalid,
45+
Error::Internal => password_hash::Error::Internal,
46+
Error::Params => password_hash::Error::ParamsInvalid,
47+
}
48+
}
49+
}

yescrypt/src/lib.rs

Lines changed: 15 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,16 @@
2828
//! NOTE: the `simple` crate feature must be enabled (on-by-default)
2929
#![cfg_attr(feature = "simple", doc = "```")]
3030
#![cfg_attr(not(feature = "simple"), doc = "```ignore")]
31-
//! # fn main() -> yescrypt::Result<()> {
31+
//! # fn main() -> yescrypt::password_hash::Result<()> {
32+
//! use yescrypt::{Yescrypt, PasswordHasher, PasswordVerifier};
33+
//!
3234
//! let password = b"pleaseletmein"; // don't actually use this as a password!
3335
//! let salt = b"WZaPV7LSUEKMo34."; // unique per password, ideally 16-bytes and random
34-
//! let params = yescrypt::Params::default(); // use recommended settings
35-
//! let password_hash = yescrypt::yescrypt(password, salt, &params)?;
36-
//! assert!(password_hash.starts_with("$y$"));
36+
//! let password_hash = Yescrypt.hash_password(password, salt)?;
37+
//! assert!(password_hash.as_str().starts_with("$y$"));
3738
//!
3839
//! // verify password is correct for the given hash
39-
//! yescrypt::yescrypt_verify(password, &password_hash)?;
40+
//! Yescrypt.verify_password(password, &password_hash)?;
4041
//! # Ok(())
4142
//! # }
4243
//! ```
@@ -67,6 +68,8 @@ mod mode;
6768
mod params;
6869
mod pwxform;
6970
mod salsa20;
71+
#[cfg(feature = "simple")]
72+
mod simple;
7073
mod smix;
7174
mod util;
7275

@@ -76,96 +79,15 @@ pub use crate::{
7679
params::Params,
7780
};
7881

79-
use alloc::vec;
80-
use sha2::{Digest, Sha256};
81-
82-
#[cfg(feature = "simple")]
83-
use {alloc::string::String, mcf::Base64};
84-
85-
/// Identifier for yescrypt when encoding to the Modular Crypt Format, i.e. `$y$`
86-
#[cfg(feature = "simple")]
87-
const YESCRYPT_MCF_ID: &str = "y";
88-
89-
/// Base64 variant used by yescrypt.
90-
#[cfg(feature = "simple")]
91-
const YESCRYPT_BASE64: Base64 = Base64::ShaCrypt;
92-
93-
/// yescrypt password hashing function.
94-
///
95-
/// This function produces an (s)crypt-style password hash string starting with the prefix `$y$`.
96-
///
97-
/// If using yescrypt as a key derivation, consider [`yescrypt_kdf`] instead.
98-
#[cfg(feature = "simple")]
99-
pub fn yescrypt(passwd: &[u8], salt: &[u8], params: &Params) -> Result<String> {
100-
// TODO(tarcieri): tunable hash output size?
101-
const HASH_SIZE: usize = 32;
102-
103-
let mut out = [0u8; HASH_SIZE];
104-
yescrypt_kdf(passwd, salt, params, &mut out)?;
105-
106-
// Begin building the Modular Crypt Format hash.
107-
let mut mcf_hash = mcf::PasswordHash::from_id(YESCRYPT_MCF_ID).expect("should be valid");
108-
109-
// Add params string to the hash
110-
let mut params_buf = [0u8; Params::MAX_ENCODED_LEN];
111-
let params_str = params.encode(&mut params_buf)?;
112-
mcf_hash.push_str(params_str).map_err(|_| Error::Encoding)?;
113-
114-
// Add salt
115-
mcf_hash.push_base64(salt, YESCRYPT_BASE64);
116-
117-
// Add yescrypt output
118-
mcf_hash.push_base64(&out, YESCRYPT_BASE64);
119-
120-
// Convert to a normal `String` to keep `mcf` out of the public API (for now)
121-
Ok(mcf_hash.into())
122-
}
123-
124-
/// Verify a password matches the given yescrypt password hash.
125-
///
126-
/// Password hash should begin with `$y$` in Modular Crypt Format (MCF).
12782
#[cfg(feature = "simple")]
128-
pub fn yescrypt_verify(passwd: &[u8], hash: &str) -> Result<()> {
129-
let hash = mcf::PasswordHashRef::new(hash).map_err(|_| Error::Encoding)?;
130-
131-
// verify id matches `$y`
132-
if hash.id() != YESCRYPT_MCF_ID {
133-
return Err(Error::Algorithm);
134-
}
135-
136-
let mut fields = hash.fields();
137-
138-
// decode params
139-
let params: Params = fields.next().ok_or(Error::Encoding)?.as_str().parse()?;
140-
141-
// decode salt
142-
let salt = fields
143-
.next()
144-
.ok_or(Error::Encoding)?
145-
.decode_base64(YESCRYPT_BASE64)
146-
.map_err(|_| Error::Encoding)?;
147-
148-
// decode expected password hash
149-
let expected = fields
150-
.next()
151-
.ok_or(Error::Encoding)?
152-
.decode_base64(YESCRYPT_BASE64)
153-
.map_err(|_| Error::Encoding)?;
154-
155-
// should be the last field
156-
if fields.next().is_some() {
157-
return Err(Error::Encoding);
158-
}
159-
160-
let mut actual = vec![0u8; expected.len()];
161-
yescrypt_kdf(passwd, &salt, &params, &mut actual)?;
162-
163-
if subtle::ConstantTimeEq::ct_ne(actual.as_slice(), &expected).into() {
164-
return Err(Error::Password);
165-
}
83+
pub use {
84+
mcf::{PasswordHash, PasswordHashRef},
85+
password_hash::{self, CustomizedPasswordHasher, PasswordHasher, PasswordVerifier},
86+
simple::Yescrypt,
87+
};
16688

167-
Ok(())
168-
}
89+
use alloc::vec;
90+
use sha2::{Digest, Sha256};
16991

17092
/// yescrypt Key Derivation Function (KDF)
17193
pub fn yescrypt_kdf(passwd: &[u8], salt: &[u8], params: &Params, out: &mut [u8]) -> Result<()> {

yescrypt/src/simple.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
//! Implementation of the `password-hash` crate API.
2+
3+
use crate::{Params, yescrypt_kdf};
4+
use alloc::vec;
5+
use mcf::{Base64, PasswordHash, PasswordHashRef};
6+
use password_hash::{
7+
CustomizedPasswordHasher, Error, PasswordHasher, PasswordVerifier, Result, Version,
8+
};
9+
10+
/// Identifier for yescrypt when encoding to the Modular Crypt Format, i.e. `$y$`
11+
#[cfg(feature = "simple")]
12+
const YESCRYPT_MCF_ID: &str = "y";
13+
14+
/// Base64 variant used by yescrypt.
15+
const YESCRYPT_BASE64: Base64 = Base64::ShaCrypt;
16+
17+
/// yescrypt type for use with [`PasswordHasher`].
18+
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
19+
pub struct Yescrypt;
20+
21+
impl CustomizedPasswordHasher<PasswordHash> for Yescrypt {
22+
type Params = Params;
23+
24+
fn hash_password_customized(
25+
&self,
26+
password: &[u8],
27+
salt: &[u8],
28+
alg_id: Option<&str>,
29+
version: Option<Version>,
30+
params: Params,
31+
) -> Result<PasswordHash> {
32+
// TODO(tarcieri): tunable hash output size?
33+
const HASH_SIZE: usize = 32;
34+
35+
match alg_id {
36+
Some(YESCRYPT_MCF_ID) | None => (),
37+
_ => return Err(Error::Algorithm),
38+
}
39+
40+
if version.is_some() {
41+
return Err(Error::Version);
42+
}
43+
44+
let mut out = [0u8; HASH_SIZE];
45+
yescrypt_kdf(password, salt, &params, &mut out)?;
46+
47+
// Begin building the Modular Crypt Format hash.
48+
let mut mcf_hash = PasswordHash::from_id(YESCRYPT_MCF_ID).expect("should be valid");
49+
50+
// Add params string to the hash
51+
mcf_hash
52+
.push_displayable(params)
53+
.map_err(|_| Error::EncodingInvalid)?;
54+
55+
// Add salt
56+
mcf_hash.push_base64(salt, YESCRYPT_BASE64);
57+
58+
// Add yescrypt password hashing function output
59+
mcf_hash.push_base64(&out, YESCRYPT_BASE64);
60+
61+
Ok(mcf_hash)
62+
}
63+
}
64+
65+
impl PasswordHasher<PasswordHash> for Yescrypt {
66+
fn hash_password(&self, password: &[u8], salt: &[u8]) -> Result<PasswordHash> {
67+
self.hash_password_customized(password, salt, None, None, Params::default())
68+
}
69+
}
70+
71+
impl PasswordVerifier<PasswordHash> for Yescrypt {
72+
fn verify_password(&self, password: &[u8], hash: &PasswordHash) -> Result<()> {
73+
self.verify_password(password, hash.as_password_hash_ref())
74+
}
75+
}
76+
77+
impl PasswordVerifier<PasswordHashRef> for Yescrypt {
78+
fn verify_password(&self, password: &[u8], hash: &PasswordHashRef) -> Result<()> {
79+
// verify id matches `$y`
80+
if hash.id() != YESCRYPT_MCF_ID {
81+
return Err(Error::Algorithm);
82+
}
83+
84+
let mut fields = hash.fields();
85+
86+
// decode params
87+
let params: Params = fields
88+
.next()
89+
.ok_or(Error::EncodingInvalid)?
90+
.as_str()
91+
.parse()?;
92+
93+
// decode salt
94+
let salt = fields
95+
.next()
96+
.ok_or(Error::EncodingInvalid)?
97+
.decode_base64(YESCRYPT_BASE64)
98+
.map_err(|_| Error::EncodingInvalid)?;
99+
100+
// decode expected password hash
101+
let expected = fields
102+
.next()
103+
.ok_or(Error::EncodingInvalid)?
104+
.decode_base64(YESCRYPT_BASE64)
105+
.map_err(|_| Error::EncodingInvalid)?;
106+
107+
// should be the last field
108+
if fields.next().is_some() {
109+
return Err(Error::EncodingInvalid);
110+
}
111+
112+
let mut actual = vec![0u8; expected.len()];
113+
yescrypt_kdf(password, &salt, &params, &mut actual)?;
114+
115+
if subtle::ConstantTimeEq::ct_ne(actual.as_slice(), &expected).into() {
116+
return Err(Error::PasswordInvalid);
117+
}
118+
119+
Ok(())
120+
}
121+
}

0 commit comments

Comments
 (0)