Skip to content

Commit

Permalink
scrypt: add PHC hash support using password-hash crate (RustCrypto#111
Browse files Browse the repository at this point in the history
)

Implements the PHC string format for hashes, gated under the `simple`
cargo feature.

Specifics of how the format is encoded/decoded are modeled off of how
passlib implements PHC scrypt hashes:

https://passlib.readthedocs.io/en/stable/lib/passlib.hash.scrypt.html
  • Loading branch information
tarcieri authored Jan 29, 2021
1 parent 1848af8 commit 1160c5e
Show file tree
Hide file tree
Showing 9 changed files with 252 additions and 234 deletions.
40 changes: 21 additions & 19 deletions .github/workflows/scrypt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,27 @@ env:
RUSTFLAGS: "-Dwarnings"

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
rust:
- 1.47.0 # MSRV
- stable
target:
- thumbv7em-none-eabi
- wasm32-unknown-unknown
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
override: true
- run: cargo build --no-default-features --release --target ${{ matrix.target }}
# TODO(tarcieri): test when new cargo resolver is available (RFC 2957)
# build:
# runs-on: ubuntu-latest
# strategy:
# matrix:
# rust:
# - 1.47.0 # MSRV
# - stable
# target:
# - thumbv7em-none-eabi
# - wasm32-unknown-unknown
# steps:
# - uses: actions/checkout@v1
# - uses: actions-rs/toolchain@v1
# with:
# profile: minimal
# toolchain: ${{ matrix.rust }}
# target: ${{ matrix.target }}
# override: true
# - run: cargo build --target ${{ matrix.target }} --release --no-default-features
# - run: cargo build --target ${{ matrix.target }} --release --no-default-features --features simple

test:
runs-on: ubuntu-latest
Expand Down
11 changes: 2 additions & 9 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pbkdf2/src/simple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ impl McfHasher for Pbkdf2 {
}
_ => {
// TODO(tarcieri): better errors here?
return Err(HasherError::Parse(ParseError::InvalidChar('?')));
return Err(ParseError::InvalidChar('?').into());
}
};

Expand Down
15 changes: 8 additions & 7 deletions scrypt/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,18 @@ edition = "2018"
readme = "README.md"

[dependencies]
base64 = { version = "0.13", default-features = false, features = ["alloc"], optional = true }
base64ct = { version = "0.1", default-features = false, features = ["alloc"], optional = true }
hmac = "0.10"
password-hash = { version = "0.1", default-features = false, optional = true }
pbkdf2 = { version = "0.7", default-features = false, path = "../pbkdf2" }
rand_core = { version = "0.6", default-features = false, features = ["getrandom"], optional = true }
rand = { version = "0.8", default-features = false, optional = true }
salsa20 = { version = "0.7.2", default-features = false, features = ["expose-core"] }
sha2 = { version = "0.9", default-features = false }
subtle = { version = "2", default-features = false, optional = true }

[dev-dependencies]
password-hash = { version = "0.1", features = ["rand_core"] }
rand_core = { version = "0.6", features = ["std"] }

[features]
default = ["simple", "thread_rng", "std"]
simple = ["rand_core", "base64", "subtle"]
thread_rng = ["rand"]
default = ["simple", "std"]
simple = ["password-hash", "base64ct"]
std = []
30 changes: 0 additions & 30 deletions scrypt/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,33 +25,3 @@ impl fmt::Display for InvalidParams {

#[cfg(feature = "std")]
impl std::error::Error for InvalidParams {}

/// `scrypt_check` error
#[cfg(feature = "simple")]
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum CheckError {
/// Password hash mismatch, e.g. due to the incorrect password.
HashMismatch,
/// Invalid format of the hash string.
InvalidFormat,
}

#[cfg(feature = "simple")]
impl fmt::Display for CheckError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match *self {
CheckError::HashMismatch => "password hash mismatch",
CheckError::InvalidFormat => "invalid `hashed_value` format",
})
}
}

#[cfg(feature = "simple")]
impl From<base64::DecodeError> for CheckError {
fn from(_e: ::base64::DecodeError) -> Self {
CheckError::InvalidFormat
}
}

#[cfg(all(feature = "simple", feature = "std"))]
impl std::error::Error for CheckError {}
35 changes: 20 additions & 15 deletions scrypt/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,27 @@
//! scrypt = { version = "0.2", default-features = false }
//! ```
//!
//! # Usage
//! # Usage (simple with default params)
//!
//! ```
//! extern crate scrypt;
//! # #[cfg(feature = "password-hash")]
//! # {
//! use scrypt::{
//! password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
//! Scrypt
//! };
//! use rand_core::OsRng;
//!
//! # #[cfg(feature="simple")]
//! # fn main() {
//! use scrypt::{ScryptParams, scrypt_simple, scrypt_check};
//! let password = b"hunter42"; // Bad password; don't actually use!
//! let salt = SaltString::generate(&mut OsRng);
//!
//! // First setup the ScryptParams arguments with the recommended defaults
//! let params = ScryptParams::recommended();
//! // Hash the password for storage
//! let hashed_password = scrypt_simple("Not so secure password", &params)
//! .expect("OS RNG should not fail");
//! // Verifying a stored password
//! assert!(scrypt_check("Not so secure password", &hashed_password).is_ok());
//! // Hash password to PHC string ($scrypt$...)
//! let password_hash = Scrypt.hash_password_simple(password, salt.as_ref()).unwrap().to_string();
//!
//! // Verify password against PHC string
//! let parsed_hash = PasswordHash::new(&password_hash).unwrap();
//! assert!(Scrypt.verify_password(password, &parsed_hash).is_ok());
//! # }
//! # #[cfg(not(feature="simple"))]
//! # fn main() {}
//! ```
//!
//! # References
Expand All @@ -53,9 +55,12 @@ mod romix;
#[cfg(feature = "simple")]
mod simple;

#[cfg(feature = "simple")]
pub use password_hash;

pub use crate::params::ScryptParams;
#[cfg(feature = "simple")]
pub use crate::simple::{scrypt_check, scrypt_simple};
pub use crate::simple::{Scrypt, ALG_ID};

/// The scrypt key derivation function.
///
Expand Down
70 changes: 66 additions & 4 deletions scrypt/src/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,24 @@ use core::{mem::size_of, usize};

use crate::errors::InvalidParams;

#[cfg(feature = "simple")]
use {
core::convert::{TryFrom, TryInto},
password_hash::{HasherError, ParamsError, ParamsString},
};

const RECOMMENDED_LOG_N: u8 = 15;
const RECOMMENDED_R: u32 = 8;
const RECOMMENDED_P: u32 = 1;
const RECOMMENDED_LEN: usize = 32;

/// The Scrypt parameter values.
#[derive(Clone, Copy)]
#[derive(Clone, Copy, Debug)]
pub struct ScryptParams {
pub(crate) log_n: u8,
pub(crate) r: u32,
pub(crate) p: u32,
pub(crate) len: usize,
}

impl ScryptParams {
Expand Down Expand Up @@ -62,6 +74,7 @@ impl ScryptParams {
log_n,
r: r as u32,
p: p as u32,
len: RECOMMENDED_LEN,
})
}

Expand All @@ -71,9 +84,58 @@ impl ScryptParams {
/// - `p = 1`
pub fn recommended() -> ScryptParams {
ScryptParams {
log_n: 15,
r: 8,
p: 1,
log_n: RECOMMENDED_LOG_N,
r: RECOMMENDED_R,
p: RECOMMENDED_P,
len: RECOMMENDED_LEN,
}
}
}

impl Default for ScryptParams {
fn default() -> ScryptParams {
ScryptParams::recommended()
}
}

#[cfg(feature = "simple")]
#[cfg_attr(docsrs, doc(cfg(feature = "simple")))]
impl TryFrom<&ParamsString> for ScryptParams {
type Error = HasherError;

fn try_from(input: &ParamsString) -> Result<Self, HasherError> {
let mut log_n = RECOMMENDED_LOG_N;
let mut r = RECOMMENDED_R;
let mut p = RECOMMENDED_P;

for (ident, value) in input.iter() {
match ident.as_str() {
"ln" => {
log_n = value
.decimal()?
.try_into()
.map_err(|_| ParamsError::InvalidValue)?
}
"r" => r = value.decimal()?,
"p" => p = value.decimal()?,
_ => return Err(ParamsError::InvalidName.into()),
}
}

ScryptParams::new(log_n, r, p).map_err(|_| ParamsError::InvalidValue.into())
}
}

#[cfg(feature = "simple")]
#[cfg_attr(docsrs, doc(cfg(feature = "simple")))]
impl<'a> TryFrom<ScryptParams> for ParamsString {
type Error = HasherError;

fn try_from(input: ScryptParams) -> Result<ParamsString, HasherError> {
let mut output = ParamsString::new();
output.add_decimal("ln", input.log_n as u32)?;
output.add_decimal("r", input.r)?;
output.add_decimal("p", input.p)?;
Ok(output)
}
}
Loading

0 comments on commit 1160c5e

Please sign in to comment.