Hash (HSH)
Contents
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
Install
As a Rust library (crates.io)
[]
= "0.0.9"
As a CLI tool
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.
pepper feature
[]
= { = "0.0.9", = ["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-kmscrate shipsLocalPepper(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; theirfetch_pepperalways returnsPepperError::Backend. Real network-backed implementations land in 0.1.x. Similarly,Backend::Fips140Requiredenforces the mint-time contract (fail-closed when Argon2 is requested) but does not route PBKDF2 through a FIPS-validated module today — that requires the forthcominghsh-backend-awslccrate (also 0.1.x). Seedoc/COMPARISON.mdanddoc/FIPS.mdfor the precise scope.
Build from source
MSRV 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.
Quick Start
use ;
The CLI surface mirrors the library — six verbs:
|
|
|
The hsh ecosystem
Four 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
Per-context quick links
| 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.
One-minute migration
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.
From argonautica 0.2 (archived 2019)
-[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();
From rust-argon2 2.x
-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();
From bcrypt 0.16
-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();
Coming from a different password-hashing crate?
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.
Why this approach?
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.
Capabilities in v0.0.9
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.
Algorithms
| 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.
Policy / PolicyBuilder
Three ways to construct a Policy:
use ;
use PolicyBuilder;
// 1. Preset (most common):
let p1 = owasp_minimum_2025;
// 2. Builder seeded from a preset:
let p2 = from_preset
.primary
.build
.unwrap;
// 3. Builder from scratch (must set primary):
let p3 = new
.primary
.backend
.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.
Cargo features
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
[]
= { = "0.0.9", = ["pepper"] }
= { = "0.0.9", = ["aws-kms"] }
Benchmarks
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:
Per-host calibration guide and the full methodology live in
doc/BENCHMARKS.md.
Ecosystem comparison
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.
Examples
Run individual examples per crate:
| 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 |
When not to use hsh
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.
Development
Fuzzing
Five cargo-fuzz targets ship under
fuzz/fuzz_targets/:
Seed 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.
Miri (UB / aliasing / leak verification)
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.
# Or invoke the script directly:
The CI matrix runs the focused suite on every PR (miri.yml) and
the full sweep on Sunday 03:00 UTC.
CI workflows
| 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.
Security
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.
RCE prevention (no unsafe, no FFI)
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 |
Configurable resource budgets
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 |
Defence in depth
- 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).
Supply chain
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.
Notes
- 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).
Documentation
| 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).
License
Dual-licensed under Apache 2.0 or MIT, at your option.
See CHANGELOG.md for release history.