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
10 changes: 10 additions & 0 deletions .changes/bounded-password-records.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"rscrypto" = "minor"
---

Argon2 and scrypt password verification now reject noncanonical or over-budget
PHC records before decoding, allocating, or running a KDF. The pre-1.0 API is
split into verifier-owned `Argon2idPassword` and `ScryptPassword` records and
raw `derive` operations with valid-by-construction parameters; legacy public
PHC parsing, unbounded verification, builder, version-selection, and
caller-supplied-salt password helpers have been removed.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ required-features = ["rsa"]
[[bench]]
name = "password_hashing"
harness = false
required-features = ["argon2", "scrypt", "phc-strings"]
required-features = ["argon2", "scrypt", "phc-strings", "getrandom"]

[[bench]]
name = "xxh3"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Use [`docs/types.md`](docs/types.md) when you need the full type map, and
|---|---|---|
| Cryptographic Hashes | SHA-2, SHA-3, SHAKE, cSHAKE128/256, BLAKE2, BLAKE3, Ascon-Hash/XOF/CXOF | `hashes` or leaf features |
| MACs & KDFs | HMAC-SHA-2/SHA-3, KMAC128/256, standalone Poly1305, HKDF-SHA-2, PBKDF2-HMAC-SHA-2 | `auth` or leaf features |
| Password Hashing | Argon2d/i/id, scrypt, PHC string encode/verify | `auth`, `argon2`, `scrypt`, `phc-strings` |
| Password Hashing | Raw Argon2d/i/id and scrypt KDFs; generated, bounded PHC password records | `auth`, `argon2`, `scrypt`, `phc-strings` |
| Public-Key Primitives | ECDSA P-256/P-384 signing/verification, Ed25519 signatures, RSA signing/verification/OAEP/RSAES-PKCS1-v1_5/key generation, X25519 key exchange, ML-KEM-512/768/1024 KEMs | `auth`, `signatures`, `key-exchange`, `ecdsa`, `ecdsa-p256`, `ecdsa-p384`, `ed25519`, `rsa`, `x25519`, `ml-kem` |
| AEAD Encryption | AES-128/256-GCM, AES-128/256-GCM-SIV, ChaCha20-Poly1305, XChaCha20-Poly1305, AEGIS-256, Ascon-AEAD128 | `aead` or leaf features |
| Checksums | CRC-16, CRC-24, CRC-32, CRC-32C, CRC-64/XZ, CRC-64/NVMe | `checksums` or leaf features |
Expand Down
73 changes: 36 additions & 37 deletions benches/password_hashing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,19 @@ use core::{hint::black_box, time::Duration};

use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
use dryoc::classic::crypto_pwhash::{PasswordHashAlgorithm, crypto_pwhash};
use rscrypto::{Argon2Error, Argon2Params, Argon2Version, Argon2d, Argon2i, Argon2id, Scrypt, ScryptParams};
use rscrypto::{
Argon2Error, Argon2Params, Argon2d, Argon2i, Argon2id, Argon2idPassword, Scrypt, ScryptParams, ScryptPassword,
};

/// Function pointer shared by all three `Argon2{d,i,id}::hash` variants.
/// Function pointer shared by all three raw Argon2 variants.
type ArgonHashFn = fn(&Argon2Params, &[u8], &[u8], &mut [u8]) -> Result<(), Argon2Error>;

const PASSWORD: &[u8] = b"correct horse battery staple";
const SALT: &[u8] = b"rscrypto-bench-salt-16bytes!";

/// Build rscrypto params.
fn rs_params(m_kib: u32, t: u32, p: u32, out_len: u32) -> Argon2Params {
Argon2Params::new()
.memory_cost_kib(m_kib)
.time_cost(t)
.parallelism(p)
.output_len(out_len)
.version(Argon2Version::V0x13)
.build()
.unwrap()
fn rs_params(m_kib: u32, t: u32, p: u32, _out_len: u32) -> Argon2Params {
Argon2Params::new(m_kib, t, p).unwrap()
}

/// Build RustCrypto oracle context.
Expand Down Expand Up @@ -110,14 +105,14 @@ fn bench_small_variant(

fn argon2d_small(c: &mut Criterion) {
// dryoc has no Argon2d (libsodium only ships Argon2i and Argon2id).
bench_small_variant(c, "argon2d-small", Argon2d::hash, argon2::Algorithm::Argon2d, None);
bench_small_variant(c, "argon2d-small", Argon2d::derive, argon2::Algorithm::Argon2d, None);
}

fn argon2i_small(c: &mut Criterion) {
bench_small_variant(
c,
"argon2i-small",
Argon2i::hash,
Argon2i::derive,
argon2::Algorithm::Argon2i,
Some(PasswordHashAlgorithm::Argon2i13),
);
Expand All @@ -127,7 +122,7 @@ fn argon2id_small(c: &mut Criterion) {
bench_small_variant(
c,
"argon2id-small",
Argon2id::hash,
Argon2id::derive,
argon2::Algorithm::Argon2id,
Some(PasswordHashAlgorithm::Argon2id13),
);
Expand All @@ -150,7 +145,7 @@ fn argon2id_owasp(c: &mut Criterion) {
g.bench_function(BenchmarkId::new("rscrypto", "m=19MiB_t=2_p=1"), |b| {
let mut out = [0u8; 32];
b.iter(|| {
Argon2id::hash(
Argon2id::derive(
black_box(&rs_params),
black_box(PASSWORD),
black_box(SALT),
Expand Down Expand Up @@ -190,14 +185,8 @@ fn argon2id_owasp(c: &mut Criterion) {
}

/// Build rscrypto scrypt params.
fn rs_scrypt_params(log_n: u8, r: u32, p: u32, out_len: u32) -> ScryptParams {
ScryptParams::new()
.log_n(log_n)
.r(r)
.p(p)
.output_len(out_len)
.build()
.unwrap()
fn rs_scrypt_params(log_n: u8, r: u32, p: u32, _out_len: u32) -> ScryptParams {
ScryptParams::new(log_n, r, p).unwrap()
}

/// Build RustCrypto scrypt oracle params.
Expand All @@ -221,7 +210,7 @@ fn scrypt_small(c: &mut Criterion) {
g.bench_with_input(BenchmarkId::new("rscrypto", &id), &rs, |b, params| {
let mut out = [0u8; 32];
b.iter(|| {
Scrypt::hash(
Scrypt::derive(
black_box(params),
black_box(PASSWORD),
black_box(SALT),
Expand Down Expand Up @@ -255,7 +244,7 @@ fn scrypt_owasp(c: &mut Criterion) {
g.bench_function(BenchmarkId::new("rscrypto", "log_n=17_r=8_p=1"), |b| {
let mut out = [0u8; 32];
b.iter(|| {
Scrypt::hash(
Scrypt::derive(
black_box(&rs),
black_box(PASSWORD),
black_box(SALT),
Expand All @@ -281,13 +270,18 @@ fn scrypt_phc_roundtrip(c: &mut Criterion) {
g.sample_size(20);

let params = rs_scrypt_params(10, 8, 1, 32);
g.bench_function("hash_string_with_salt", |b| {
b.iter(|| Scrypt::hash_string_with_salt(black_box(&params), black_box(PASSWORD), black_box(SALT)).unwrap());
let password = ScryptPassword::new(params).unwrap();
g.bench_function("hash_password", |b| {
b.iter(|| password.hash_password(black_box(PASSWORD)).unwrap());
});

let encoded = Scrypt::hash_string_with_salt(&params, PASSWORD, SALT).unwrap();
g.bench_function("verify_string", |b| {
b.iter(|| Scrypt::verify_string(black_box(PASSWORD), black_box(&encoded)).unwrap());
let encoded = password.hash_password(PASSWORD).unwrap();
g.bench_function("verify_password", |b| {
b.iter(|| {
password
.verify_password(black_box(PASSWORD), black_box(&encoded))
.unwrap()
});
});

g.finish();
Expand All @@ -304,13 +298,18 @@ fn argon2id_phc_roundtrip(c: &mut Criterion) {
g.sample_size(30);

let params = rs_params(32, 2, 1, 32);
g.bench_function("hash_string_with_salt", |b| {
b.iter(|| Argon2id::hash_string_with_salt(black_box(&params), black_box(PASSWORD), black_box(SALT)).unwrap());
let password = Argon2idPassword::new(params).unwrap();
g.bench_function("hash_password", |b| {
b.iter(|| password.hash_password(black_box(PASSWORD)).unwrap());
});

let encoded = Argon2id::hash_string_with_salt(&params, PASSWORD, SALT).unwrap();
g.bench_function("verify_string", |b| {
b.iter(|| Argon2id::verify_string(black_box(PASSWORD), black_box(&encoded)).unwrap());
let encoded = password.hash_password(PASSWORD).unwrap();
g.bench_function("verify_password", |b| {
b.iter(|| {
password
.verify_password(black_box(PASSWORD), black_box(&encoded))
.unwrap()
});
});

g.finish();
Expand Down Expand Up @@ -349,7 +348,7 @@ fn argon2id_parallel_scaling(c: &mut Criterion) {
g.bench_with_input(BenchmarkId::new("rscrypto", &id), &params, |b, params| {
let mut out = [0u8; 32];
b.iter(|| {
Argon2id::hash(
Argon2id::derive(
black_box(params),
black_box(PASSWORD),
black_box(SALT),
Expand Down Expand Up @@ -385,7 +384,7 @@ fn argon2id_parallel_owasp(c: &mut Criterion) {
g.bench_with_input(BenchmarkId::new("rscrypto", &id), &params, |b, params| {
let mut out = [0u8; 32];
b.iter(|| {
Argon2id::hash(
Argon2id::derive(
black_box(params),
black_box(PASSWORD),
black_box(SALT),
Expand Down
14 changes: 8 additions & 6 deletions ct.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3009,10 +3009,8 @@ tier = "A"
claim = "ct-intended"
features = ["argon2"]
entrypoints = [
"Argon2i::hash",
"Argon2i::derive",
"Argon2i::verify",
"Argon2i::verify_string",
"Argon2i::verify_string_with_policy",
]
secrets = ["password", "derived_key", "expected_key"]
public = ["salt", "params", "phc_string"]
Expand All @@ -3029,7 +3027,12 @@ id = "password.argon2d_argon2id"
tier = "A"
claim = "best-effort"
features = ["argon2"]
entrypoints = ["Argon2d::*", "Argon2id::*"]
entrypoints = [
"Argon2d::*",
"Argon2id::*",
"Argon2idPassword::verify_password",
"Argon2idPassword::verify_password_with_context",
]
secrets = ["password", "derived_key", "expected_key"]
public = ["salt", "params", "phc_string"]
may_leak = ["input_length", "public_parameter_error", "opaque_success_or_failure"]
Expand All @@ -3055,8 +3058,7 @@ features = ["scrypt"]
entrypoints = [
"Scrypt::derive",
"Scrypt::verify",
"Scrypt::verify_string",
"Scrypt::verify_string_with_policy",
"ScryptPassword::verify_password",
]
secrets = ["password", "derived_key", "expected_key"]
public = ["salt", "params", "phc_string"]
Expand Down
4 changes: 2 additions & 2 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ rscrypto = { version = "0.6.4", features = ["full", "portable-only"] }
| `hkdf` | `hmac` | HKDF-SHA256, HKDF-SHA384, and HKDF-SHA512 |
| `poly1305` | -- | Standalone Poly1305 one-time MAC |
| `pbkdf2` | `hmac` | PBKDF2-HMAC-SHA256 and PBKDF2-HMAC-SHA512 |
| `phc-strings` | `alloc` | PHC string encode/decode support |
| `phc-strings` | `alloc` | Canonical password-record generation and bounded PHC verification |
| `argon2` | `blake2b`, `alloc` | Argon2i, Argon2d, Argon2id |
| `scrypt` | `pbkdf2`, `alloc` | scrypt |
| `ecdsa-p256` | `hmac` | ECDSA P-256/SHA-256 signing and verification |
Expand All @@ -104,7 +104,7 @@ rscrypto = { version = "0.6.4", features = ["full", "portable-only"] }

| Feature | Effect |
|---|---|
| `getrandom` | Enables OS-RNG constructors such as `random()` / `try_random()`, `try_generate()` for Ed25519/X25519/ECDSA, `Poly1305OneTimeKey::try_generate()`, ML-KEM `try_generate_keypair()` / `try_encapsulate()`, AEAD random sealing helpers, RSA key generation, signing salt/blinding, encryption randomness, and private-operation blinding. Caller-supplied byte-filling closures remain available for deterministic tests and constrained integrations; deterministic ECDSA signing does not use OS randomness. RSA key generation uses OS entropy to seed its key-generation HMAC_DRBG; no separate DRBG feature is required. |
| `getrandom` | Enables OS-RNG constructors such as `random()` / `try_random()`, canonical Argon2id/scrypt password-record generation, `try_generate()` for Ed25519/X25519/ECDSA, `Poly1305OneTimeKey::try_generate()`, ML-KEM `try_generate_keypair()` / `try_encapsulate()`, AEAD random sealing helpers, RSA key generation, signing salt/blinding, encryption randomness, and private-operation blinding. Password-record salts are intentionally OS-owned; other APIs retain caller-supplied byte-filling closures where deterministic tests or constrained integrations need them. Deterministic ECDSA signing does not use OS randomness. RSA key generation uses OS entropy to seed its key-generation HMAC_DRBG; no separate DRBG feature is required. |
| `serde` | Serde for non-secret byte wrappers (nonces, tags, public keys, signatures). |
| `serde-secrets` | Serde for secret-key and shared-secret bytes. Implies `serde`. Use only for controlled key-material storage, not logs or DTOs. |
| `parallel` | Rayon-backed BLAKE3 and Argon2 lane parallelism. Requires `std`, `blake3`, `argon2`. |
Expand Down
4 changes: 2 additions & 2 deletions docs/migration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ API shape change, or not a good fit.

| From | To | Status |
|---|---|---|
| [`argon2`](RustCrypto/argon2.md) (RustCrypto) | `Argon2id`, `Argon2i`, `Argon2d`, `Argon2Params`, `Argon2VerifyPolicy` | Verified against `argon2 0.6.0-rc.8` |
| [`scrypt`](RustCrypto/scrypt.md) (RustCrypto) | `Scrypt`, `ScryptParams`, `ScryptVerifyPolicy` | Verified against `scrypt 0.12.0` |
| [`argon2`](RustCrypto/argon2.md) (RustCrypto) | Raw `Argon2{d,i,id}` KDFs; bounded `Argon2idPassword` records | Verified against `argon2 0.6.0-rc.8` |
| [`scrypt`](RustCrypto/scrypt.md) (RustCrypto) | Raw `Scrypt` KDF; bounded `ScryptPassword` records | Verified against `scrypt 0.12.0` |

## Stack Migrations

Expand Down
Loading