A multi-algorithm password hashing library for Rust with PHC string
storage, auto-rehash on policy drift, an in-process HMAC-SHA-256
pepper with versioned key rotation (KMS providers stubbed for 0.1.x),
and a fail-closed FIPS 140-3 contract (validated runtime via
aws-lc-rs lands in 0.1.x) — written from scratch with
#![forbid(unsafe_code)] across the workspace.
Getting started
- Install — Cargo, Homebrew, Arch, Scoop, Docker, source
- Quick Start — hash, verify, and auto-rehash in ten lines
- The hsh ecosystem —
hsh,hsh-cli,hsh-kms,hsh-digestat a glance
Library reference
- One-minute migration from
argonautica,rust-argon2,bcrypt,password-auth,djangohashers— name-for-name mapping - Why this approach? — design rationale
- Capabilities in v0.0.9 — release inventory
- Algorithms — Argon2id, bcrypt, scrypt, PBKDF2
- Policy / PolicyBuilder — preset, builder-from-preset, builder-from-scratch
- Cargo features — opt-in matrix
- Benchmarks — headline numbers; full table at
doc/BENCHMARKS.md - Ecosystem comparison — short matrix; full table at
doc/COMPARISON.md - Examples — runnable example index
Operational
- When not to use hsh — limitations
- Development — make targets, fuzzing, Miri, CI
- Security — guarantees and compliance
- Documentation — all reference docs
- License
[dependencies]
hsh = "0.0.9"The hsh binary ships from the
hsh-cli companion crate
(the hsh library crate itself contains no binaries — the split
keeps clap + rpassword + anyhow out of the library's
dependency graph for downstream embedders).
| Channel | Install |
|---|---|
| Cargo (crates.io) | cargo install hsh-cli --locked |
| Cargo (from source) | cargo install --locked --path crates/hsh-cli |
| Homebrew (personal tap) | brew tap sebastienrousseau/tap && brew install hsh |
| Arch Linux (AUR) | yay -S hsh-bin (binary) or yay -S hsh (source) |
| Scoop (Windows) | scoop bucket add sebastienrousseau https://github.com/sebastienrousseau/scoop-bucket && scoop install hsh |
| Debian / Ubuntu (.deb) | curl -fsSL https://github.com/sebastienrousseau/hsh/releases/latest/download/hsh_0.0.9_amd64.deb -o hsh.deb && sudo dpkg -i hsh.deb |
| Container (GHCR) | docker run --rm ghcr.io/sebastienrousseau/hsh:0.0.9 --help |
| Shell installer | curl -fsSL https://github.com/sebastienrousseau/hsh/releases/latest/download/hsh-installer.sh | sh |
GitHub Releases additionally publish pre-built tarballs for Linux (gnu + musl), macOS (Intel + Apple Silicon + universal), and Windows (x86_64, aarch64). Each archive ships with the binary, man page, shell completions, license bundle, and a cosign keyless signature + SLSA L3 attestation.
See pkg/README.md for the per-channel maintainer
runbook.
[dependencies]
hsh = { version = "0.0.9", features = ["pepper"] }Brings in hsh-kms and exposes Policy::with_pepper(...). The
pepper feature is off by default so applications without a KMS
don't pay the hmac / sha2 cost on the dep graph.
Important
Readiness status (v0.0.9). The hsh-kms crate ships
LocalPepper (in-process HMAC-SHA-256 with versioned key rotation)
as a fully-functional provider. The four cloud providers — AWS KMS,
GCP Cloud KMS, Azure Key Vault, HashiCorp Vault Transit — are stub
interfaces in this release; their fetch_pepper always returns
PepperError::Backend. Real network-backed implementations land in
0.1.x. Similarly, Backend::Fips140Required enforces the mint-time
contract (fail-closed when Argon2 is requested) but does not
route PBKDF2 through a FIPS-validated module today — that requires
the forthcoming hsh-backend-awslc crate (also 0.1.x). See
doc/COMPARISON.md and doc/FIPS.md
for the precise scope.
git clone https://github.com/sebastienrousseau/hsh.git
cd hsh
make ci # fmt + clippy + test + docMSRV by crate. Each workspace crate carries its own
rust-version; the CI matrix gates each independently so a satellite
never silently breaks downstream users pinned to the core's floor.
| Crate | MSRV | Why |
|---|---|---|
hsh (core lib) |
1.75.0 | The committed floor for default-features = false + the standard pepper feature. Library; broad consumability is the goal. |
hsh-kms |
1.75.0 | Same floor; KMS providers slot in behind feature flags. |
hsh-digest |
1.75.0 | Same floor; re-exports RustCrypto primitives. |
hsh-cli (binary) |
1.85.0 | Edition 2024; clap 4.5 + derive macros require a recent stable. |
rust-toolchain.toml selects stable for local development; the
1.75.0 floor on the core surface is enforced by the dedicated
MSRV CI job on every PR.
use hsh::{api, Outcome, Policy};
fn main() -> Result<(), hsh::Error> {
let policy = Policy::owasp_minimum_2025();
let stored = api::hash(&policy, "correct horse battery staple")?;
let outcome = api::verify_and_upgrade(
&policy,
"correct horse battery staple",
&stored,
)?;
match outcome {
Outcome::Valid { rehashed: Some(new_phc) } => {
// Policy drifted; persist `new_phc` to keep stored
// material at the current bar.
let _ = new_phc;
}
Outcome::Valid { rehashed: None } => { /* OK */ }
Outcome::Invalid => { /* deny */ }
}
Ok(())
}The CLI surface mirrors the library — six verbs:
echo -n "correct horse battery staple" | hsh hash --algorithm argon2id
echo -n "correct horse battery staple" | hsh verify -H '$argon2id$v=19$m=19456,t=2,p=1$…'
echo -n "correct horse battery staple" | hsh rehash -H '$argon2id$v=19$m=19456,t=2,p=1$…'
hsh inspect '$argon2id$v=19$m=19456,t=2,p=1$…'
hsh calibrate --algorithm argon2id --target-ms 500
hsh completions zsh > ~/.zsh/functions/_hshFour crates ship from this workspace. The library is the core; the three satellites wrap it for specific delivery surfaces.
| Crate | What it is | Use case |
|---|---|---|
hsh |
Library — multi-algorithm password hashing, PHC parser, verify + auto-rehash, FIPS contract | Embed password hashing in any Rust binary or library. |
hsh-cli |
One binary: hsh (hash / verify / rehash / inspect / calibrate / completions) |
CI gates, container images, ad-hoc command-line use. |
hsh-kms |
Pepper trait + LocalPepper + four KMS provider stubs (AWS / GCP / Azure / Vault) |
HMAC-SHA-256 server-side peppering with versioned key rotation. |
hsh-digest |
General-purpose cryptographic digests (SHA-2 / SHA-3 / BLAKE3) — not for passwords | Content addressing, MAC building blocks, non-password digest needs. |
Per-crate READMEs:
hsh · hsh-cli · hsh-kms · hsh-digest
| If you need… | Drop-in config |
|---|---|
A drop-in for argonautica / rust-argon2 / bcrypt / password-auth / djangohashers |
migration guides in doc/ — name-for-name mapping tables, behavioural notes, checklists |
| Passkey-primary architecture with password fallback | doc/PASSKEY-ERA.md — positioning, three recipes (passkey + password sign-in, recovery credential hardening, staged migration off passwords) |
| FIPS 140-3 deployment | doc/FIPS.md — mint-time fail-closed contract, verify-side rehash from legacy Argon2/bcrypt/scrypt to PBKDF2, aws-lc-rs validated-runtime roadmap |
| AWS / GCP / Azure / HashiCorp Vault peppering | doc/KMS-INTEGRATION.md — provider configs, key rotation, LocalPepper snapshot pattern |
| Per-host benchmark calibration | hsh calibrate --algorithm argon2id --target-ms 500 + doc/BENCHMARKS.md |
| Day-2 operations runbook | doc/OPERATIONS.md — pre-deployment inspect-backend check, fleet sizing, rotation TL;DR, hash-format inspection |
| Pre-commit / CI gating | crates/hsh-cli/examples/* + the hsh verify exit-code contract |
| IP / standards governance | doc/IP-GOVERNANCE.md — patent watchlist, annual OWASP / NIST / FIDO review cadence, pre-commercialisation legal checklist |
The rest of this README covers the library surface (hsh
itself). For the satellite crates, jump straight to their READMEs
above.
Most call sites are mechanical to update. The headline mapping for
the five most common legacy crates is below; per-crate guides with
verified function tables and behavioural notes live in
doc/MIGRATION-from-*.md.
-[dependencies]
-argonautica = "0.2"
+[dependencies]
+hsh = "0.0.9"-use argonautica::{Hasher, Verifier};
-let mut h = Hasher::default();
-let stored = h.with_password("hunter2").with_secret_key("server-key").hash()?;
-let ok = Verifier::default().with_hash(&stored).with_password("hunter2").with_secret_key("server-key").verify()?;
+use hsh::{api, Policy};
+let policy = Policy::owasp_minimum_2025();
+let stored = api::hash(&policy, "hunter2")?;
+let outcome = api::verify_and_upgrade(&policy, "hunter2", &stored)?;
+let ok = outcome.is_valid();-let cfg = argon2::Config::owasp5();
-let salt = b"saltsaltsalt";
-let stored = argon2::hash_encoded(b"hunter2", salt, &cfg)?;
-let ok = argon2::verify_encoded(&stored, b"hunter2")?;
+let policy = hsh::Policy::owasp_minimum_2025();
+let stored = hsh::api::hash(&policy, "hunter2")?;
+let outcome = hsh::api::verify_and_upgrade(&policy, "hunter2", &stored)?;
+let ok = outcome.is_valid();-use bcrypt::{hash, verify, DEFAULT_COST};
-let stored = hash("hunter2", DEFAULT_COST)?;
-let ok = verify("hunter2", &stored)?;
+use hsh::{api, Policy, PrimaryAlgorithm};
+use hsh::policy::PolicyBuilder;
+let policy = PolicyBuilder::from_preset(&Policy::owasp_minimum_2025())
+ .primary(PrimaryAlgorithm::Bcrypt)
+ .build()?;
+let stored = api::hash(&policy, "hunter2")?;
+let outcome = api::verify_and_upgrade(&policy, "hunter2", &stored)?;
+let ok = outcome.is_valid();Each crate has a standalone migration guide with TL;DR diff, function-mapping table, behavioural notes, and a checklist:
| Crate | Version | Drop-in for hsh? |
Migration guide |
|---|---|---|---|
argonautica |
0.2.0 (archived 2019) |
no (FFI wrapper, no PHC strings, no rehash) | MIGRATION-from-argonautica.md |
rust-argon2 |
2.1.0 |
partial — Argon2 only | MIGRATION-from-rust-argon2.md |
bcrypt |
0.16.0 |
verify-only — bcrypt only | MIGRATION-from-bcrypt.md |
password-auth |
0.3.0 |
partial — RustCrypto facade | MIGRATION-from-password-hash.md |
djangohashers |
1.8.0 |
no (Django format, not PHC) | MIGRATION-from-djangohashers.md |
If your call sites can't change at all, enable the
compat-v0_0_x feature to keep the pre-0.0.9 stringly-typed
shape during cut-over.
hsh targets the niche argonautica / rust-argon2 / bcrypt /
password-auth occupy — take a password, return a verifiable hash
string, and verify a candidate against it — and is written from
scratch against the OWASP Password Storage Cheat Sheet (2025),
RFC 9106 (Argon2), RFC 7914 (scrypt), and RFC 8018 (PBKDF2). It is
not a fork of any existing crate; the API layer, PHC encoding,
backend dispatch, and pepper integration are independent code on
top of the audited RustCrypto primitives (argon2, pbkdf2,
scrypt, bcrypt, sha2).
Five architectural choices motivate the rewrite:
-
Auto-rehash on policy drift.
api::verify_and_upgradereturns a(Outcome, Option<String>)pair — when the stored hash uses a weaker algorithm, lower cost parameters, an older PBKDF2 PRF, or a previous pepper key version than the livePolicymandates, the verifier mints a fresh PHC string and the caller persists it on next login. No background jobs, no "force users to reset on rotation" workflows, no dead-in-DB weak hashes that survive the next breach. -
#![forbid(unsafe_code)]at the workspace root. No FFI to a C library, no raw-pointer dereferences, nounsafeblocks in any crate. CI enforces the attribute on every push. The historical class oflibargon2/libcryptoFFI memory-safety CVEs is structurally absent (ADR-0006). -
Peppered HMAC with versioned key rotation. Optional
Peppertrait (inhsh-kms) applies HMAC-SHA-256 over the password before the KDF — an attacker who exfiltrates the password database alone cannot brute-force credentials offline.KeyVersionis embedded in a customhsh-pepper:<version>:<inner>wrapper so rotation is non-destructive; the auto-rehash path transparently re-encodes under the new version on next verify. -
FIPS 140-3 fail-closed contract.
Backend::Fips140Requiredcausesapi::hashto refuse to mint a hash unless the policy's primary algorithm isPrimaryAlgorithm::Pbkdf2and the build can satisfy FIPS 140-3 — never silently degrade to a non-FIPS primitive, never re-route Argon2/bcrypt/scrypt to PBKDF2 silently. Callers asking for a non-PBKDF2 primary under a FIPS backend get a clearError::InvalidParameterinstead. The contract is enforced today in v0.0.9; the validated runtime (PBKDF2 routed throughaws-lc-rs) is delivered by the forthcominghsh-backend-awslccrate in 0.1.x, at which pointBackend::fips_available_in_build()will returntruein FIPS-capable builds. The contract is documented indoc/FIPS.md. -
Constant-time everywhere it matters.
subtle::ConstantTimeEqgates every hash comparison;zeroize::ZeroizeOnDropwipes password / hash / salt / pepper-key buffers on scope exit;getrandom::OsRngis the only salt source (nevervrdor a user-supplied seed). The bcrypt path enforces the 72-byte input safety rail (CVE-2025-22228 class) unless the caller explicitly opts intowith_prehash.
A few features built on top of those choices:
- PHC string storage for Argon2id / scrypt / PBKDF2 + MCF
(
$2b$…) for bcrypt + the bespokehsh-pepper:wrapper for peppered hashes. Verify accepts all three formats interchangeably. - Streaming verify —
api::verify_and_upgradeparses the PHC envelope, dispatches to the recorded algorithm, and only routes through the livePolicyparameters on the rehash path. Old hashes verify at their original cost. - CLI symmetry — every library entry point has a CLI verb of
the same name (
hsh hash/verify/rehash/inspect/calibrate), andhsh calibratemeasures the host's actual Argon2id throughput to suggest cost parameters for a given target time budget.
The default profile compiles eight crates in the runtime graph:
argon2, bcrypt, scrypt, pbkdf2, password-hash, subtle,
zeroize, getrandom. No archived or unmaintained crate appears
in the graph — argonautica (archived 2019), argon2rs
(archived 2017), and openssl (FFI) are all banned via
deny.toml. cargo audit, cargo deny, and Miri
are CI gates on every push.
The 0.0.9 release covers a complete password-hashing stack. See
CHANGELOG.md for the detailed inventory; the
table below groups the inventory by capability theme.
| Theme | Headline deliverables |
|---|---|
| Foundation | Cargo workspace; per-crate MSRV (1.75 lib / 1.85 CLI); #![forbid(unsafe_code)] workspace-wide (ADR-0006) |
| Algorithms | Argon2id (RFC 9106), Argon2i / Argon2d (verify-only legacy), bcrypt (with 72-byte safety rail), scrypt (RFC 7914), PBKDF2-HMAC-SHA-256 / SHA-512 (RFC 8018) |
| General hashing | hsh-digest ships SHA-256 / 384 / 512, SHA3-256 / 384 / 512, BLAKE3-256; KangarooTwelve / TurboSHAKE (per RFC 9861, published Oct 2025) and Ascon-Hash256 / Ascon-XOF128 (per NIST SP 800-232, finalised Aug 2025) are published standards whose Rust implementations are currently stubbed — implementation tracked as a Phase 6 follow-up |
| Storage formats | PHC strings for Argon2id / scrypt / PBKDF2; MCF ($2b$…) for bcrypt; bespoke hsh-pepper:<keyver>:<inner> wrapper for peppered hashes |
| Verify + auto-rehash | Algorithm drift, Argon2 m/t/p/output-len drift, bcrypt cost drift, bcrypt prehash-mode drift, scrypt log_n/r/p/dk_len drift, PBKDF2 iter/dk_len/PRF drift, and pepper-version drift all trigger rehash on next successful verify |
| Pepper integration | hsh-kms ships LocalPepper (in-process HMAC-SHA-256, versioned key rotation) as a real provider. Four cloud providers (AWS KMS, GCP Cloud KMS, Azure Key Vault, HashiCorp Vault Transit) are stub interfaces in v0.0.9 — stable shape, PepperError::Backend at fetch — with network-backed implementations landing in 0.1.x |
| FIPS contract vs runtime | Backend::Fips140Required enforces the mint-time contract (refuses any non-PBKDF2 primary, fails closed if the build can't satisfy FIPS) in v0.0.9. The validated runtime (PBKDF2 routed through aws-lc-rs) lands as hsh-backend-awslc in 0.1.x (doc/FIPS.md) |
| Operational hardening | 5 libfuzzer targets (nightly), 7 proptest invariants, Miri focused (per-PR, 60 min) + full sweep (weekly, 90 min), SLSA L3 build provenance, sigstore keyless signing, OpenSSF Scorecard |
| CLI | hsh-cli with 7 subcommands (hash / verify / rehash / inspect / inspect-backend / calibrate / completions), shell completions for bash / zsh / fish / PowerShell / elvish, multi-platform packaging templates (Docker / Homebrew / Debian / Arch / Scoop) |
| Documentation | 5 ADRs (pepper key versioning, FIPS strategy, general-hashing scope, zero-unsafe policy, v1.0 stability contract), 5 migration guides (argonautica / rust-argon2 / bcrypt / djangohashers / password-hash), passkey-era positioning + day-2 operations + IP-governance runbooks, API stability + release runbook + support doc |
| Test coverage | 13 KAT vectors for hsh-digest (SHA-2 / SHA-3 / BLAKE3 from NIST CAVP + BLAKE3 project), 7 property invariants in hsh (round-trip, drift detection, pepper version) plus 11 property invariants in hsh-digest (one-shot vs streaming, chunking, output length, determinism, cross-algorithm) |
Phase-by-phase breakdown: CHANGELOG.md.
Milestone: https://github.com/sebastienrousseau/hsh/milestone/1.
| Algorithm | Status | OWASP-2025 default | Notes |
|---|---|---|---|
| Argon2id | ✅ Recommended | m = 19 456 KiB, t = 2, p = 1 |
RFC 9106 §4; verify-only support for Argon2i / Argon2d |
| Bcrypt | ✅ Hardened | cost = 10 |
72-byte safety rail (CVE-2025-22228); opt-in with_prehash |
| Scrypt | ✅ Configurable | N = 2^17, r = 8, p = 1 |
Bumped from N = 2^14 in v0.0.8; reproduce via ScryptParams |
| PBKDF2 | ✅ FIPS-eligible | iters = 600 000 (SHA-256) / 210 000 (SHA-512), dk_len = 32 |
Routed under Backend::Fips140Required |
| Argon2i | Verify-only (legacy) | (same params) | #[deprecated]; gated behind cfg(feature = "compat-v0_0_x") |
| Argon2d | Available | (same params) | Exposed for completeness; not OWASP-recommended for passwords |
The verifier accepts any of the four production algorithms
above interchangeably; the live Policy only governs new hashes
and rehash targets.
Three ways to construct a Policy:
use hsh::{Backend, Policy, PrimaryAlgorithm};
use hsh::policy::PolicyBuilder;
// 1. Preset (most common):
let p1 = Policy::owasp_minimum_2025();
// 2. Builder seeded from a preset:
let p2 = PolicyBuilder::from_preset(&Policy::owasp_minimum_2025())
.primary(PrimaryAlgorithm::Bcrypt)
.build()
.unwrap();
// 3. Builder from scratch (must set primary):
let p3 = PolicyBuilder::new()
.primary(PrimaryAlgorithm::Pbkdf2)
.backend(Backend::Native)
.build()
.unwrap();Read fields via accessors: policy.primary(), policy.backend(),
policy.argon2_params(), policy.bcrypt_params(),
policy.scrypt_params(), policy.pbkdf2_params(),
policy.has_pepper().
The full per-symbol stability tier list (Tier 1 — stable / Tier 2 —
evolving / Tier 3 — experimental) lives at
doc/API-STABILITY.md.
All optional integrations are off by default. Enable only what the application needs.
| Feature | Crate | Pulls in | Adds | Documented in |
|---|---|---|---|---|
pepper |
hsh |
hsh-kms |
Policy::with_pepper(...), HMAC-SHA-256 peppering, KeyVersion rotation |
doc/KMS-INTEGRATION.md |
fips |
hsh |
— | Forward-compat marker for aws-lc-rs routing (Phase 4) |
doc/FIPS.md |
compat-v0_0_x |
hsh |
— | Re-exposes the pre-0.0.9 stringly-typed API for migration | Migration |
aws-kms |
hsh-kms |
(future) aws-sdk-kms |
AWS KMS pepper backend (stub today) | doc/KMS-INTEGRATION.md |
gcp-kms |
hsh-kms |
(future) gcloud-kms |
GCP Cloud KMS pepper backend (stub today) | doc/KMS-INTEGRATION.md |
azure-key-vault |
hsh-kms |
(future) azure_security_keyvault |
Azure Key Vault pepper backend (stub today) | doc/KMS-INTEGRATION.md |
hashicorp-vault |
hsh-kms |
(future) vaultrs |
HashiCorp Vault Transit backend (stub today) | doc/KMS-INTEGRATION.md |
sha2 (default) |
hsh-digest |
sha2 |
SHA-256 / 384 / 512 | crates/hsh-digest/README.md |
sha3 (default) |
hsh-digest |
sha3 |
SHA3-256 / 384 / 512 | crates/hsh-digest/README.md |
blake3 (default) |
hsh-digest |
blake3 |
BLAKE3-256 | crates/hsh-digest/README.md |
k12 |
hsh-digest |
(future) k12 |
KangarooTwelve / TurboSHAKE128/256 — standard published as RFC 9861 (Oct 2025); Rust impl stubbed | Capabilities |
ascon |
hsh-digest |
(future) ascon-hash |
Ascon-Hash256 / Ascon-XOF128 — standard finalised as NIST SP 800-232 (Aug 2025); Rust impl stubbed | Capabilities |
# Example: peppered password hashing with AWS KMS backend
[dependencies]
hsh = { version = "0.0.9", features = ["pepper"] }
hsh-kms = { version = "0.0.9", features = ["aws-kms"] }Criterion benchmarks live in
crates/hsh/benches/criterion.rs
and are organised into three groups:
| Group | What it measures |
|---|---|
hash_owasp_2025 |
api::hash cost at OWASP-2025 minimum parameters per algorithm |
verify_owasp_2025 |
api::verify_and_upgrade cost at the same parameters |
fast_params |
Same shape with non-production parameters used by tests / fuzz / proptest |
Headline numbers are host-specific and evaluated locally via
cargo bench --bench benchmark. The README intentionally does not
ship pinned numbers — they would mislead readers running on a
different CPU / memory tier. See doc/BENCHMARKS.md
for the maintainer's reference-host measurement methodology and
historical numbers; see hsh calibrate below for a one-command
host-specific parameter suggestion.
Reproduce:
cargo bench --bench benchmark # full criterion run
cargo bench --bench benchmark -- --quick # smoke run (~30 s total)
hsh calibrate --algorithm argon2id --target-ms 500 # per-host parameter suggestionPer-host calibration guide and the full methodology live in
doc/BENCHMARKS.md.
hsh is the only Rust password-hashing library that ships
multi-algorithm verify-with-auto-rehash, KMS-backed peppering
with versioned rotation, a FIPS 140-3 fail-closed contract,
SLSA L3 build provenance, and a dedicated CLI in one
workspace.
The full feature matrix — every row, every column, with the
reading-the-table notes — lives at
doc/COMPARISON.md so the README stays
fast to scan.
Quick orientation:
| Crate | Drop-in for hsh? |
Key gap vs hsh |
|---|---|---|
argonautica |
no (archived 2019) | FFI wrapper; no PHC strings; no rehash-on-verify; unmaintained |
rust-argon2 |
partial — Argon2 only | No multi-algorithm fallback; no pepper; no FIPS contract; no CLI |
bcrypt |
verify-only — bcrypt only | No 72-byte safety rail; no auto-rehash; no Argon2 / scrypt / PBKDF2 path |
password-auth |
partial — RustCrypto facade | No pepper; no FIPS contract; no CLI; no calibration |
djangohashers |
no (Django format only) | Custom string format; no auto-rehash to modern KDFs; no PHC |
Per-crate migration guides at
doc/MIGRATION-from-*.md.
Run individual examples per crate:
cargo run -p hsh-cli --example library_shape
cargo run -p hsh --example quickstart
cargo run -p hsh --example fips_policy
cargo run -p hsh --example migration_from_bcrypt
cargo run -p hsh-kms --example local_pepper
cargo run -p hsh-kms --example rotation
cargo run -p hsh-kms --example refuse_without_pepper
cargo run -p hsh-digest --example streaming
cargo run -p hsh-digest --example content_addressing| Category | Example | Purpose |
|---|---|---|
| Core | hsh/examples/quickstart |
Hash + verify + auto-rehash round-trip |
hsh-cli/examples/library_shape |
Library-shape demonstration of what hsh-cli does under the hood |
|
hsh/examples/builder_pattern |
PolicyBuilder::new() / from_preset() / setters |
|
| FIPS | hsh/examples/fips_policy |
Backend::Fips140Required fail-closed contract |
| Migration | hsh/examples/migration_from_bcrypt |
Bcrypt → Argon2id transparent upgrade on next verify |
| Pepper / KMS | hsh-kms/examples/local_pepper |
LocalPepper::builder() keyset construction |
hsh-kms/examples/rotation |
Two-version keyset; verify under old version triggers rehash under new | |
hsh-kms/examples/refuse_without_pepper |
Fail-closed when verifier doesn't carry the pepper | |
| General hashing | hsh-digest/examples/streaming |
Hasher::new + update + finalize over a Read source |
hsh-digest/examples/content_addressing |
Git-style blob hash with BLAKE3 |
A few cases where another tool fits better, listed because the short answer is "we don't do that yet" rather than because of a disagreement on priorities.
- You need quantum-resistant signatures / KEMs. Use
aws-lc-rs(ML-KEM, ML-DSA, SLH-DSA).hshcovers password hashing only; the post-quantum signature landscape is moving fast and a dedicated library tracks it better. - You need a general-purpose digest only. Use
hsh-digestdirectly — the password APIs inhshare deliberately slow. Or reach for the underlying RustCrypto crates (sha2,sha3,blake3) if you don't want theAlgorithmdispatch layer. - You need streaming HMAC / HKDF. Use the RustCrypto
hmac/hkdfcrates directly.hsh-kmsexposes HMAC-SHA-256 only in the context of peppering. - You're targeting embedded /
no_std.hshrequiresstd(forgetrandom::OsRngand the PHC parser);hsh-digestisno_std-friendly withalloc. For constrained environments with no allocator at all, use the RustCrypto crates' streaming APIs directly. - You need a self-validating FIPS 140-3 module.
hshitself isn't FIPS-validated. TheBackend::Fips140Requiredcontract delegates the primitive toaws-lc-rs(Phase 4 follow-up); for v0.0.9 the backend selection refuses Argon2 and routes to the audited PBKDF2 path.
If you hit a case that should be on this list, please open an issue — that's how it gets fixed or moved into the supported set.
make ci # what CI runs on every PR (fmt + clippy + test + doc)
make test # full workspace test suite
make miri-focused # per-PR Miri (60 min budget)
make miri-full # full Miri sweep (90 min budget)
make fuzz-smoke # 30 s per fuzz target (nightly cargo-fuzz)
make bench # full criterion bench suite
make bench-quick # criterion --quick smoke
make deny # cargo-deny check all sections
make audit-strict # cargo-audit --deny warnings
make sbom # cargo-about NOTICE.md
make coverage # cargo llvm-cov → lcov.info + HTML report
make calibrate # measure Argon2id params for ~500 ms targetFive cargo-fuzz targets ship under
fuzz/fuzz_targets/:
cargo +nightly fuzz run fuzz_api_round_trip # api::hash → api::verify_and_upgrade
cargo +nightly fuzz run fuzz_phc_parse # PHC envelope parser robustness
cargo +nightly fuzz run fuzz_argon2id_verify # Argon2id verify with crafted PHC strings
cargo +nightly fuzz run fuzz_bcrypt_verify # bcrypt verify with crafted MCF strings
cargo +nightly fuzz run fuzz_legacy_from_string # compat-v0_0_x deserialisation surfaceSeed corpus included in fuzz/corpus/<target>/. Nightly cron
runs each target for 10 minutes via
.github/workflows/fuzz.yml; any
crash uploads to artefacts for triage.
hsh is #![forbid(unsafe_code)] so Miri does not police hsh's
own code — every byte is checked at compile time. The reason a
Miri job exists is to verify the interaction with the runtime
dependencies (argon2, bcrypt, scrypt, pbkdf2, subtle,
zeroize, getrandom, hmac, sha2 — RustCrypto uses unsafe
internally for SIMD and constant-time primitives) is sound.
make miri-focused # focused suite — api + backend_policy (per-PR, 60 min)
make miri-full # full sweep (weekly, 90 min)
# Or invoke the script directly:
./scripts/miri.sh focused
./scripts/miri.sh fullThe CI matrix runs the focused suite on every PR (miri.yml) and
the full sweep on Sunday 03:00 UTC.
| Workflow | Trigger | Purpose |
|---|---|---|
ci.yml |
PR + push to main |
fmt + clippy + test + doc; cargo-hack feature powerset; cargo-public-api drift; dependency-review |
codeql.yml |
PR + push + weekly | CodeQL on rust and actions languages; config-pinned to exclude test/example fixtures |
miri.yml |
PR + Sunday 03:00 UTC | Focused per-PR + full weekly sweep |
scorecard.yml |
Weekly + push to main | OpenSSF Scorecard; SARIF uploaded to code-scanning |
fuzz.yml |
Daily 04:00 UTC cron | 5-target matrix; 10 min budget per target |
supply-chain.yml |
Dep change + weekly | cargo-deny + cargo-audit |
release.yml |
Tag v*.*.* |
Quality gate; SBOM via cargo-about; SLSA L3; sigstore; cargo publish |
See CONTRIBUTING.md for signed commits and PR
guidelines.
Password hashing is the line of defence between a database breach
and credential reuse across the user's other accounts. The historical
record is brutal — libargon2 FFI memory bugs, bcrypt's 72-byte
truncation (CVE-2025-22228), pepper-without-rotation deployments
that turned a single key compromise into a permanent loss. hsh's
posture is built around closing each of those vectors at the
architectural level, not via opt-in flags.
hsh does not link to any C library. No libargon2, no
libcrypto, no libssl. Every primitive in the dependency graph
is pure Rust from the RustCrypto stack, and the workspace declares
#![forbid(unsafe_code)] at the crate roots. The historical class
of "FFI to a hash library has a heap-overflow under crafted input"
CVEs is structurally absent — there is no FFI surface to overflow.
Crates banned via deny.toml:
| Crate | Reason |
|---|---|
argonautica |
Abandoned (last release 2019); use argon2 RustCrypto |
argon2rs |
Abandoned (last release 2017); use argon2 RustCrypto |
openssl |
Prefer rustls + RustCrypto / aws-lc-rs |
OWASP 2025 minimums are the floor, not the ceiling. Every cost
parameter is configurable via the builder — the defaults below
are the values Policy::owasp_minimum_2025() ships with.
| Surface | Default | Protects against |
|---|---|---|
| Argon2id memory cost | m = 19 456 KiB (~19 MiB) |
GPU / ASIC offline cracking |
| Argon2id time cost | t = 2 |
Same |
| Argon2id parallelism | p = 1 |
Server-side parallelism budget |
| Bcrypt cost | 10 |
GPU / ASIC offline cracking |
| Bcrypt input length | hard 72-byte cap (rejects) | Silent truncation (CVE-2025-22228 class) |
| Scrypt N | 2^17 |
GPU / ASIC offline cracking (bumped from 2^14 in v0.0.8) |
| PBKDF2 iterations | 600 000 (SHA-256), 210 000 (SHA-512) |
GPU / ASIC offline cracking |
| Salt source | getrandom::OsRng only |
Salt prediction (no vrd, no user-supplied seed) |
| Stack-overflow guard | overflow-checks = true in release |
Arithmetic wrap on crafted cost parameters |
- Constant-time verify —
subtle::ConstantTimeEqeverywhere a hash is compared. Timing side-channels on the verify path do not leak information about the stored hash. - Zeroized on drop — password / hash / salt / pepper-key buffers
wiped via
zeroize::ZeroizeOnDrop. Heap residue after a hash operation does not contain the password. - Bcrypt 72-byte safety rail —
api::hashrejects oversized inputs unlesswith_prehashis set. CVE-2025-22228 was the class bug where bcrypt silently truncated long passwords. - FIPS fail-closed —
Backend::Fips140Requiredcausesapi::hashto refuse to mint Argon2 hashes when the build can't satisfy FIPS 140-3, never silently degrade (doc/FIPS.md). - Pepper refuse-without-key — a peppered hash verified against
a pepperless policy returns
Outcome::Invalid, never silently fails open. #![forbid(unsafe_code)]— workspace-wide, CI-enforced (ADR-0006).
cargo auditclean — zero advisories.cargo denyclean — license / advisory / ban / source checks.cargo-hackfeature powerset gated on every PR — every feature combination compiles.- SLSA L3 build provenance via
actions/attest-build-provenanceon every tagged release. - Sigstore keyless signing via
cosign sign-blobon every release artefact. - SBOM via
cargo-about(NOTICE.mdattached to the release). - OpenSSF Scorecard weekly; SARIF uploaded to code-scanning.
- 5 libfuzzer harnesses running nightly.
- Miri per-PR (focused, 60 min) + weekly full sweep (90 min).
- Pinned GitHub Actions by SHA — every third-party action reference in our workflows resolves to a 40-character commit hash, with the semver tag in a trailing comment for readability.
- Signed commits enforced via CI.
Vulnerability reporting policy:
SECURITY.md.
- The
hsh-clibinary reads passwords from stdin (withrpasswordfor no-echo TTY input) and never logs them. Operators are still responsible for not piping passwords through shell history or process-table-visible argv. - The
compat-v0_0_xfeature exposes the pre-0.0.9 stringly-typed API for migration only. It is#[deprecated]and will be removed in 0.1.0 (doc/API-STABILITY.md).
| Document | Covers |
|---|---|
doc/API-STABILITY.md |
Per-crate, per-symbol stability tier (1 — stable / 2 — evolving / 3 — experimental) + semver bump policy |
doc/FIPS.md |
FIPS 140-3 deployment, mint-time fail-closed contract, verify-side rehash to PBKDF2, aws-lc-rs integration roadmap |
doc/KMS-INTEGRATION.md |
Pepper / KMS deployment for AWS / GCP / Azure / HashiCorp Vault |
doc/BENCHMARKS.md |
Criterion methodology, reproduction commands, per-host calibration |
doc/COMPARISON.md |
Feature matrix vs argonautica, rust-argon2, bcrypt, password-auth, djangohashers |
doc/RELEASE.md |
Maintainer release runbook |
doc/SUPPORT.md |
Where to ask, response windows |
doc/pre-commit.md |
Local pre-commit hook setup |
doc/MIGRATION-from-*.md |
5 migration guides (argonautica, rust-argon2, bcrypt, djangohashers, password-hash) |
doc/adr/ |
5 ADRs covering pepper-key versioning, FIPS strategy, general-hashing scope, zero-unsafe policy, and the v1.0 stability contract |
SECURITY.md |
Vulnerability reporting, supported versions, threat model |
CONTRIBUTING.md |
Setup, signed commits, PR guidelines |
CHANGELOG.md |
Per-release notes following Keep a Changelog 1.1.0 |
The per-crate READMEs at
crates/hsh,
crates/hsh-cli,
crates/hsh-kms, and
crates/hsh-digest document the
surface specific to each artifact (library API, CLI subcommands,
Pepper trait + KMS providers, digest primitives).
Dual-licensed under Apache 2.0 or MIT, at your option.
See CHANGELOG.md for release history.