Skip to content
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ uses of the following methods. Specify the numbers iterations when calling the m
if your code depends on the iterations to stay the same : `derive_key_pbkdf2`, `deriveKeyPbkdf2`, `DeriveKeyPbkdf2`,
`EncryptWithPassword`, `EncryptWithPasswordAsBase64String`, `DecryptWithPassword`, `DecryptWithPasswordAsUtf8String`, `DeriveKey`

- `hash_password` no longer accepts an `iterations` parameter. The function signature is now `hash_password(password, version)`. Callers that previously passed a custom iteration count must switch to the new `hash_password_with_parameters` / `HashPasswordWithParams` / `hash_password_with_params` APIs and supply explicit `DerivationParameters`.

### Added

- **`PasswordHashVersion::V2`**: new Argon2id-based password hashing using OWASP-recommended defaults (memory = 64 MiB, iterations = 3). This is now the default (`PasswordHashVersion::Latest`).

### Changed

- Multiple functions, such as `generate_key` and `hash_password`, now return a `Result` due to the `rand` library upgrade.
Expand Down
3 changes: 1 addition & 2 deletions README_RUST.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,7 @@ You can use this module to hash a password and validate it afterward. This is th
use devolutions_crypto::password_hash::{hash_password, PasswordHashVersion};

let password = b"somesuperstrongpa$$w0rd!";

let hashed_password = hash_password(password, 600000, PasswordHashVersion::Latest);
let hashed_password = hash_password(password, PasswordHashVersion::Latest).unwrap();

assert!(hashed_password.verify_password(b"somesuperstrongpa$$w0rd!"));
assert!(!hashed_password.verify_password(b"someweakpa$$w0rd!"));
Expand Down
39 changes: 25 additions & 14 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,10 @@ enum Commands {
/// The password to hash
password: String,

/// The number of iteration for the derivation algorithm
/// Serialized DerivationParameters in base64. When omitted, uses the latest default
/// algorithm (Argon2id with OWASP-recommended parameters).
#[arg(short, long)]
iterations: Option<u32>,
params: Option<String>,
},

/// Verify a password from its hash
Expand Down Expand Up @@ -193,10 +194,7 @@ fn main() {
}
Commands::Decrypt { data, key } => decrypt(data, key),
Commands::DecryptAsymmetric { data, key } => decrypt_asymmetric(data, key),
Commands::HashPassword {
password,
iterations,
} => hash_password(password, iterations),
Commands::HashPassword { password, params } => hash_password(password, params),
Commands::VerifyPassword { password, hash } => verify_password(hash, password),
Commands::MixKeyExchange { private, public } => mix_key_exchange(private, public),
Commands::JoinShares { shares } => join_shares(shares),
Expand Down Expand Up @@ -401,14 +399,27 @@ fn decrypt_asymmetric(data: String, key: String) {
println!("{}", String::from_utf8_lossy(&result));
}

fn hash_password(password: String, iterations: Option<u32>) {
let iterations = iterations.unwrap_or(DEFAULT_PBKDF2_ITERATIONS);

let hash: Vec<u8> = devolutions_crypto::password_hash::hash_password(
&password.as_bytes(),
iterations,
Default::default(),
)
fn hash_password(password: String, params: Option<String>) {
let hash: Vec<u8> = match params {
Some(p) => {
let params_bytes = decode_base64_arg("--params", &p);
let params = devolutions_crypto::key_derivation::DerivationParameters::try_from(
params_bytes.as_slice(),
)
.unwrap_or_else(|e| {
eprintln!("Error: '--params' - invalid DerivationParameters: {}.", e);
std::process::exit(1);
});
devolutions_crypto::password_hash::hash_password_with_parameters(
password.as_bytes(),
params,
)
}
None => devolutions_crypto::password_hash::hash_password(
password.as_bytes(),
Default::default(),
),
}
.unwrap()
.into();
println!("{}", base64::encode(&hash));
Expand Down
215 changes: 191 additions & 24 deletions ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ use devolutions_crypto::ciphertext::{
use devolutions_crypto::key::{
generate_keypair, generate_secret_key, mix_key_exchange, KeyVersion, PrivateKey, PublicKey,
};
use devolutions_crypto::key_derivation::{Argon2, Pbkdf2};
use devolutions_crypto::password_hash::{hash_password, PasswordHash, PasswordHashVersion};
use devolutions_crypto::key_derivation::{Argon2, DerivationParameters, Pbkdf2};
use devolutions_crypto::password_hash::{
hash_password, hash_password_with_parameters, PasswordHash, PasswordHashVersion,
};
use devolutions_crypto::secret_sharing::{
generate_shared_key, join_shares, SecretSharingVersion, Share,
};
Expand All @@ -42,9 +44,6 @@ use devolutions_crypto::{

use devolutions_crypto::Result;

#[cfg(test)]
use devolutions_crypto::key_derivation::DerivationParameters;

use std::borrow::Borrow;
use std::ffi::c_void;
use std::slice;
Expand Down Expand Up @@ -459,8 +458,6 @@ pub extern "C" fn SignSize(_version: u16) -> i64 {
/// # Arguments
/// * `password` - Pointer to the password to hash.
/// * `password_length` - Length of the password to hash.
/// * `iterations` - Number of iterations of the password hash.
/// A higher number is slower but harder to brute-force. The recommended value is 600000.
/// * `result` - Pointer to the buffer to write the hash to.
/// * `result_length` - Length of the buffer to write the hash to. You can get the value by
/// calling HashPasswordLength() beforehand.
Expand All @@ -473,7 +470,6 @@ pub extern "C" fn SignSize(_version: u16) -> i64 {
pub unsafe extern "C" fn HashPassword(
password: *const u8,
password_length: usize,
iterations: u32,
result: *mut u8,
result_length: usize,
) -> i64 {
Expand All @@ -488,23 +484,105 @@ pub unsafe extern "C" fn HashPassword(
let password = slice::from_raw_parts(password, password_length);
let result = slice::from_raw_parts_mut(result, result_length);

let res: Zeroizing<Vec<u8>> =
match hash_password(password, iterations, PasswordHashVersion::Latest) {
Ok(x) => Zeroizing::new(x.into()),
Err(e) => return e.error_code(),
};
let res: Zeroizing<Vec<u8>> = match hash_password(password, PasswordHashVersion::Latest) {
Ok(x) => Zeroizing::new(x.into()),
Err(e) => return e.error_code(),
};

let length = res.len();
result[0..length].copy_from_slice(&res);
length as i64
}

/// Get the size of the resulting hash.
/// # Returns
/// Returns the length of the hash to input as `result_length` in `HashPassword()`.
/// The size reflects the default Argon2id parameters.
/// # Returns
/// Returns the length of the hash.
#[no_mangle]
pub extern "C" fn HashPasswordLength() -> i64 {
8 + 4 + 32 + 32 // Header + iterations + salt + hash
// 8 (PasswordHash header)
// + 4 (u32 params_len)
// + 8 (DerivationParameters header)
// + GetDefaultArgon2ParametersSize() (Argon2Parameters default)
// + 32 (Argon2 default output length)
8 + 4 + 8 + GetDefaultArgon2ParametersSize() + 32
}

/// Hash a password using caller-supplied serialized [`DerivationParameters`].
///
/// This allows full control over the hashing algorithm (Argon2id or PBKDF2) and
/// its parameters. Use `HashPasswordWithParamsLength()` to obtain the required output buffer size.
/// # Arguments
/// * `password` - Pointer to the password to hash.
/// * `password_length` - Length of the password.
/// * `params` - Pointer to the serialized `DerivationParameters` bytes.
/// * `params_length` - Length of the serialized `DerivationParameters`.
/// * `result` - Pointer to the output buffer.
/// * `result_length` - Length of the output buffer (use `HashPasswordWithParamsLength()`).
/// # Returns
/// Returns the number of bytes written, or a negative error code.
/// # Safety
/// This method is made to be called by C, so it is therefore unsafe.
#[no_mangle]
pub unsafe extern "C" fn HashPasswordWithParams(
password: *const u8,
password_length: usize,
params: *const u8,
params_length: usize,
result: *mut u8,
result_length: usize,
) -> i64 {
if password.is_null() || params.is_null() || result.is_null() {
return Error::NullPointer.error_code();
};

let password = slice::from_raw_parts(password, password_length);
let params_slice = slice::from_raw_parts(params, params_length);
let result = slice::from_raw_parts_mut(result, result_length);

let dp = match DerivationParameters::try_from(params_slice) {
Ok(p) => p,
Err(e) => return e.error_code(),
};

let expected_len = HashPasswordWithParamsLength(params, params_length) as usize;
if result_length != expected_len {
return Error::InvalidOutputLength.error_code();
};

let res: Zeroizing<Vec<u8>> = match hash_password_with_parameters(password, dp) {
Ok(x) => Zeroizing::new(x.into()),
Err(e) => return e.error_code(),
};

let length = res.len();
result[0..length].copy_from_slice(&res);
length as i64
}

/// Returns the output buffer size required for `HashPasswordWithParams()`.
/// # Arguments
/// * `params` - Pointer to the serialized `DerivationParameters` bytes.
/// * `params_length` - Length of the serialized `DerivationParameters`.
/// # Returns
/// Returns the required output length, or a negative error code.
/// # Safety
/// This method is made to be called by C, so it is therefore unsafe.
#[no_mangle]
pub unsafe extern "C" fn HashPasswordWithParamsLength(
params: *const u8,
params_length: usize,
) -> i64 {
if params.is_null() {
return Error::NullPointer.error_code();
};
let params_slice = slice::from_raw_parts(params, params_length);
let dp = match DerivationParameters::try_from(params_slice) {
Ok(p) => p,
Err(e) => return e.error_code(),
};
// 8 (PasswordHash header) + 4 (u32 params_len) + params_length + hash_length
(8 + 4 + params_length + dp.output_length()) as i64
}

/// Verify a password against a hash with constant-time equality.
Expand Down Expand Up @@ -1619,6 +1697,97 @@ pub extern "C" fn DeriveSecretKeyArgon2ParametersSize(argon2_parameters_length:
(8 + argon2_parameters_length) as i64
}

/// Returns the required output buffer size for `GetArgon2DerivationParameters()`.
/// The size is: 8 (header) + argon2_parameters_length (serialized Argon2Parameters bytes).
/// # Arguments
/// * argon2_parameters_length - The length of the Argon2Parameters bytes.
#[no_mangle]
pub extern "C" fn GetArgon2DerivationParametersSize(argon2_parameters_length: usize) -> i64 {
(8 + argon2_parameters_length) as i64
}

/// Build a serialized `DerivationParameters` from the given `Argon2Parameters` without
/// performing any key derivation. This is the low-cost counterpart to `DeriveSecretKeyArgon2()`.
/// # Arguments
/// * `argon2_parameters` - Pointer to the serialized `Argon2Parameters`.
/// * `argon2_parameters_length` - Length of the `Argon2Parameters` buffer.
/// * `result` - Pointer to the output buffer.
/// Must be `GetArgon2DerivationParametersSize(argon2_parameters_length)` bytes.
/// * `result_length` - Length of the output buffer.
/// # Returns
/// Returns the number of bytes written, or a negative error code.
/// # Safety
/// This method is made to be called by C, so it is therefore unsafe.
#[no_mangle]
pub unsafe extern "C" fn GetArgon2DerivationParameters(
argon2_parameters: *const u8,
argon2_parameters_length: usize,
result: *mut u8,
result_length: usize,
) -> i64 {
if argon2_parameters.is_null() || result.is_null() {
return Error::NullPointer.error_code();
}

if result_length != GetArgon2DerivationParametersSize(argon2_parameters_length) as usize {
return Error::InvalidOutputLength.error_code();
}

let argon2_parameters_raw = slice::from_raw_parts(argon2_parameters, argon2_parameters_length);
let argon2_params = match Argon2Parameters::try_from(argon2_parameters_raw) {
Ok(x) => x,
Err(e) => return e.error_code(),
};

let dp_bytes: Vec<u8> = Argon2::with_params(argon2_params).parameters().into();
let result = slice::from_raw_parts_mut(result, result_length);
result.copy_from_slice(&dp_bytes);
result_length as i64
}

/// Returns the required output buffer size for `GetPbkdf2DerivationParameters()`.
/// The size is always 32 bytes: 8 (header) + 4 (iterations) + 4 (salt length) + 16 (salt).
#[no_mangle]
pub extern "C" fn GetPbkdf2DerivationParametersSize() -> i64 {
32 // 8 header + 4 iterations + 4 salt_len + 16 salt
}

/// Build a serialized `DerivationParameters` for PBKDF2 with the given iteration count,
/// without performing any key derivation.
/// # Arguments
/// * `iterations` - Number of PBKDF2 iterations.
/// * `result` - Pointer to the output buffer.
/// Must be `GetPbkdf2DerivationParametersSize()` bytes.
/// * `result_length` - Length of the output buffer.
/// # Returns
/// Returns the number of bytes written, or a negative error code.
/// # Safety
/// This method is made to be called by C, so it is therefore unsafe.
#[no_mangle]
pub unsafe extern "C" fn GetPbkdf2DerivationParameters(
iterations: u32,
result: *mut u8,
result_length: usize,
) -> i64 {
if result.is_null() {
return Error::NullPointer.error_code();
}

if result_length != GetPbkdf2DerivationParametersSize() as usize {
return Error::InvalidOutputLength.error_code();
}

let dp = match Pbkdf2::with_params(iterations).parameters() {
Ok(x) => x,
Err(e) => return e.error_code(),
};

let dp_bytes: Vec<u8> = dp.into();
let result = slice::from_raw_parts_mut(result, result_length);
result.copy_from_slice(&dp_bytes);
result_length as i64
}

/// # Arguments
/// * `data` - Pointer to the input buffer.
/// * `data_length` - Length of the input buffer.
Expand Down Expand Up @@ -1960,14 +2129,12 @@ fn test_hash_password_length() {
let long_password = b"this is a very long and complicated password that is, I hope,\
longer than the length of the actual hash. It also contains we1rd pa$$w0rd///s.\\";

let small_password_hash: Vec<u8> =
hash_password(small_password, 100, PasswordHashVersion::Latest)
.unwrap()
.into();
let long_password_hash: Vec<u8> =
hash_password(long_password, 2642, PasswordHashVersion::Latest)
.unwrap()
.into();
let small_password_hash: Vec<u8> = hash_password(small_password, PasswordHashVersion::Latest)
.unwrap()
.into();
let long_password_hash: Vec<u8> = hash_password(long_password, PasswordHashVersion::Latest)
.unwrap()
.into();

assert_eq!(HashPasswordLength() as usize, small_password_hash.len());
assert_eq!(HashPasswordLength() as usize, long_password_hash.len());
Expand Down
8 changes: 0 additions & 8 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,6 @@ path = "fuzz_targets/ciphertext/decrypt_asymmetric.rs"
name = "password_hash_deserialization"
path = "fuzz_targets/password_hash/password_hash_deserialization.rs"

[[bin]]
name = "hash_password"
path = "fuzz_targets/password_hash/hash_password.rs"

[[bin]]
name = "verify_password"
path = "fuzz_targets/password_hash/verify_password.rs"

[[bin]]
name = "public_key_deserialization"
path = "fuzz_targets/key/public_key_deserialization.rs"
Expand Down
16 changes: 0 additions & 16 deletions fuzz/fuzz_targets/password_hash/hash_password.rs

This file was deleted.

Loading
Loading