Skip to content

feat!: redesign bcrypt API while preserving stored hash verification - #1207

Merged
Brooooooklyn merged 19 commits into
mainfrom
codex/password-api-major
Oct 5, 2026
Merged

Brooooooklyn merged 19 commits into
mainfrom
codex/password-api-major

Conversation

@Brooooooklyn

@Brooooooklyn Brooooooklyn commented Sep 10, 2026 •

Copy link
Copy Markdown
Member

Bcrypt's positional salt API clips or pads text instead of parsing encoded salts, and its salt generator emits padding. This prepares bcrypt 2.0.0 with explicit creation and verification contracts while preserving existing stored-hash verification behavior.

API changes

  • Replace positional arguments with options objects. Validate integer costs before conversion; accept exactly 16 raw salt bytes or a canonical 29-character encoded salt. Generate canonical salts and allow only 2a, 2b, and 2y for creation.
  • Preserve default 72-byte truncation for creation and verification, including rehash-on-login. Add an opt-in rejectLongPasswords creation policy that accepts exactly 72 bytes.
  • Make async validation reject its Promise. Errors carry Node-style codes: wrong option types throw TypeError with code: 'ERR_INVALID_ARG_TYPE'; values of the right type that are out of range or malformed throw RangeError with code: 'ERR_OUT_OF_RANGE' (native InvalidArg errors are translated the same way).
  • Add parseOptions(hash) returning { version, cost } through the verifier's parser, mirroring argon2's parseOptions in this repo. Every hash verify can accept is parseable, including the +4 cost spelling and imported 2x labels; hashes verify always rejects throw RangeError. The strict creation parser is not involved. This gives rehash-on-login flows a supported way to read the stored cost.
  • Snapshot mutable bytes before returning (async work keeps only the 72 bytes bcrypt reads, wiped on drop), preserve caller signal handlers, and give shared/reused signals independent cancellation state. Accept native signals and locally imported polyfills through AbortSignalLike, without requiring global cancellation constructors. Pending public calls reject on abort even if native work is already running, with an AbortError (code: 'ABORT_ERR', like Node's) whose cause is signal.reason when defined.
  • Use the same public adapter for native, Node WASI, and browser entries, retaining comparison aliases. Keep the browser adapter separate from generated files and reject incompatible native backends at import time with code: 'ERR_BCRYPT_INCOMPATIBLE_BINARY'. Generated bindings are refreshed with @napi-rs/cli 3.10.5 from native and wasm32-wasip1-threads builds. The WASI package is not an automatic dependency; browser builds install @node-rs/bcrypt-wasm32-wasi explicitly.
  • Declare exports, so only the package root and package.json can be imported.
  • Drop the unused blowfish and quickcheck dependencies from the bcrypt crate and the workspace.

Stored credentials

No database rewrite, prefix replacement, compatibility flag, or password reset is required. The hashing backend is now bcrypt-rust (v1), whose strict HashParts parser rejects some spellings the old bcrypt crate accepted; the verifier therefore parses stored hashes with a lenient parser that mirrors the old backend byte-for-byte — same acceptance set including +4 costs and 2x labels — then canonicalizes and verifies through bcrypt::verify. Raw password bytes, long-password suffix equivalence, and the false-on-invalid contract are preserved. The stricter salt parser applies only to creation. Invalid UTF-8 hash bytes now return false under the documented error contract.

Applications that authenticate by recomputing a hash with a separately saved original salt should migrate to verify(password, storedHash). The migration guide covers this and the new call shapes.

Release

  • The package version stays at 1.10.9 here; the release step bumps it to 2.0.0. After the bump, regenerate binding.js (yarn build in packages/bcrypt), because napi version does not update its expected platform-package version.
  • CHANGELOG.md has a 2.0.0 (Unreleased) entry; set the date at release.
  • .yarnrc.yml preapproves this repo's own @node-rs/* packages and disables transparent workspaces, so bcrypt-previous (npm:@node-rs/bcrypt@1.10.9) always comes from npm instead of linking to the workspace.

Performance

Measured end-to-end on real x86_64 hardware (Cloudflare Sandbox, 4-vCPU EPYC) with a plain-process bench mirroring benchmark/bcrypt.ts; on this PR's final build we are the fastest implementation in all three suites.

  • hashSync / verifySync (cost 10, fixed salt): bcrypt-rust's scalar kernel was rewritten in crypt_blowfish's fused-round shape and then given an xor_late asm boundary so LLVM stops reassociating the fused round into two serial xors after the S-box loads. Raw kernel: 54.6ms vs C crypt_blowfish 54.9ms; JS level: hashSync 54.8-55.1ms vs node bcrypt (C++) 55.8-56.6ms, verifySync 54.8ms (next best: wasm Openwall 60.5ms). CodSpeed on bcrypt-rust#8 reports -4% on all single-hash benches. wasm32/arm64 keep the scalar fallback. Ships via Cargo.lock pinning bcrypt-rust 1.0.2.
  • genSaltSync: thread-local OS-entropy pool (one getrandom per 16 salts, raw SysRng — no userspace PRNG), format-free salt string, LazyLock base64 engine, and a tightened api.cjs fast path. ~630-650ns vs wasm OpenBSD ~810-840ns, node bcrypt ~2.7-2.8µs.
  • packages/bcrypt/benchmark/bcrypt.ts was rewritten for 1:1 comparisons (uniform adapters, fixed salt, cost 10 across all suites, sorted tinybench report) and argon2's harness was migrated to tinybench to match.

Validation

  • Bcrypt AVA suite: 25 tests passed on native and 25 on WASI on macOS arm64, Node 24.21.0, after rebasing on main. supported-node.cjs and polyfill-cancellation.cjs pass there too (now also covering parseOptions and the error codes); the Node 10/12 CI jobs run them on the advertised runtimes. api.cjs still parses as ES2019.
  • Frozen fixtures: 112 bcrypt hashes from 1.7.3, 1.9.2, 1.10.5, and 1.10.9, plus frozen acceptance/rejection cases. These run in the normal CI suite. New output is checked with bcryptjs and the pinned previous release. Every accepted fixture is also parsed by parseOptions and compared against the text of its own prefix and cost.
  • TypeScript project build, immutable dependency install, oxlint, oxfmt, Rust formatting, and Clippy (--locked, warnings denied) pass.
  • Earlier revisions of this PR were also checked on Node 10.24.1 and 12.22.12 with the macOS x64 native artifact, with packed native/WASI entries and backend version enforcement, and in headless Chrome 152 (436 assertions passed). These manual checks were not repeated for the latest commits.

Cross-platform results will come from this PR's CI. No packages have been published.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-01T16:33:21.059727Z 2daac89 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1ebacd2e69

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/bcrypt/api.cjs Outdated
@Brooooooklyn Brooooooklyn changed the title feat!: redesign password APIs while preserving stored hash verification feat!: redesign bcrypt API while preserving stored hash verification Sep 10, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 06c3b9576b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/bcrypt/api.cjs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: abcf89c7aa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/bcrypt/api.cjs Outdated
@Brooooooklyn
Brooooooklyn force-pushed the codex/password-api-major branch from 4ebb5c5 to 7ebc07f Compare October 1, 2026 07:09

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7ebc07f50c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/bcrypt/api.cjs Outdated
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

3 similar comments
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

Brooooooklyn and others added 17 commits October 5, 2026 17:28
- Settle the public Promise before removing the abort listener, and remove
  it with the same options, so throwing or capture-flag EventTargets cannot
  hang a call or leak the listener.
- Keep signal.reason as a non-enumerable AbortError cause, so callers can
  tell a TimeoutError or custom reason apart.
- Throw TypeError for option values of the wrong type and keep RangeError
  for out-of-range or malformed values.
- Copy only the 72 password bytes bcrypt reads for async work, and wipe
  those copies on drop; replace the unreachable!() salt branch.
- Declare package exports so internal files cannot be deep-imported.
- Stop declaring the WASI package as an optional dependency, so installs do
  not pull it and a missing native binary fails loudly.
- Leave the version bump to the release step, add the 2.0.0 changelog, and
  preapprove this repo's own @node-rs/* packages.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
….timeout

AbortSignal.timeout() uses a timer that does not keep the event loop
alive, and the controlled backend has no native work that would. The
test passed only when other tests kept the worker busy, and failed with
"Promise returned by test never resolved" on the Linux and WASI jobs.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
With the bcrypt workspace back at 1.10.9, yarn's transparent workspaces
link bcrypt-previous (npm:@node-rs/bcrypt@1.10.9) to the workspace
itself whenever the lockfile is regenerated, so the previous-release
tests would compare the new code with itself. No workspace depends on
another, so only the workspace: protocol needs to link workspaces.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
main moved @napi-rs/cli from 3.9.0 to 3.10.5. Regenerate the bcrypt
loaders and type declarations from native and wasm32-wasip1-threads
builds so the committed files match what the release build produces.
binding.js still parses as ES2019 for Node 10 and 12.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ed calls

A signal whose removeEventListener throws made a successful call raise an
unhandled rejection, which exits modern Node, and on abort it skipped the
native cancellation. A signal whose reason getter throws stopped the abort
listener before it settled, so the call stayed pending or resolved after
the abort. Ignore cleanup failures once the call has settled, and read the
optional reason defensively.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Bundle two more contract changes into the 2.0 break so the error and
rehash-on-login story is complete at release:

- Thrown errors carry Node-style codes: TypeError ERR_INVALID_ARG_TYPE,
  RangeError ERR_OUT_OF_RANGE (including native InvalidArg translations),
  and AbortError ABORT_ERR, matching Node's own AbortError.
- parseOptions(hash) returns { version, cost } through the verifier's
  parser (bcrypt::HashParts), so every hash verify accepts is parseable,
  including +4 costs and imported 2x labels; hashes verify always rejects
  throw RangeError. Mirrors argon2's parseOptions in this repo.
- AbortSignalLike.removeEventListener declares the options argument the
  adapter actually passes.
- Drop unused blowfish and quickcheck dependencies from the bcrypt crate
  and the workspace.

Native and wasm32-wasip1-threads bindings are regenerated. api.cjs still
parses as ES2019 for Node 10 and 12.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- Use one options type for add/removeEventListener on AbortSignalLike,
  matching the comment that the same object is passed to both.
- Give parseOptions its own arity message; it never had positional
  options to drop.
- Cover the 60-byte non-ASCII parser branch in the test, which the
  previous 61-byte input missed.
- Mention in the README that parseOptions throws for hashes verify
  always rejects.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
A stale platform package now fails at import time with
code ERR_BCRYPT_INCOMPATIBLE_BINARY, so every error the package throws
sits inside the documented ErrorCode contract.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Replace the bcrypt 0.19 dependency with bcrypt-rust 1.0, keeping the crate
name so imports stay valid. The new crate is a drop-in for every API used
here except one deliberate difference: its HashParts parser is strict and
rejects the noncanonical '+4' cost spelling the old backend accepted.

Verify and parseOptions now use a lenient parser that mirrors the old
backend byte-for-byte (60 ASCII bytes, 2a/2b/2x/2y markers, u32::parse for
cost), then hand the canonicalized hash to bcrypt::verify so the cost gate
and constant-time compare stay in the library.

~20% faster than the C++ backend on hash and verify.
bcrypt: uniform call signatures via per-impl adapters, a fixed salt so the
hash suite measures hashing only, a real sync compare for verify (it
previously timed an unawaited async call with a hash inside the loop), one
cost across suites, all five implementations in every suite, and a compact
sorted table with latency, rme, ops/s, and a ratio to the baseline.

argon2: replace the hand-rolled performance.now() harness with tinybench
fixed-iteration runs, keep the per-iteration tag assertion and the
native-sync / native-async / js-wasm grouping, and adopt the same report
format.
TaskResult is a discriminated union; latency/throughput exist only on
completed and aborted-with-statistics. CI type-checks benchmark files
through packages/argon2/tsconfig.json, unlike oxnode which strips types.
…in bench comment

genSaltSync previously ran every call through options() + creation(),
allocating a fresh Object.create(null) copy even for {} and {cost}. Now
undefined uses a frozen empty options object and reusable options shapes
validate in place — identical error paths preserved for non-plain or
extra-key objects. Saves ~200ns of JS wrapper overhead per call.
Picks up the crypt_blowfish fused-round kernel shape
(Brooooooklyn/bcrypt-rust@f65cd90): bcrypt() at cost 10 on x86_64
drops ~19% (138.2ms -> 112.6ms vs vendored C at 108.7ms); arm64
unchanged.
@Brooooooklyn
Brooooooklyn force-pushed the codex/password-api-major branch from e98cb03 to 142af8f Compare October 5, 2026 09:28
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

genSaltSync now serves 16-byte salts from a 256-byte thread-local pool
refilled by SysRng (direct getrandom), amortizing one syscall over 16
salts without any userspace PRNG in between. The salt string is built
without format! machinery and the bcrypt base64 engine is constructed
once in a LazyLock instead of per call.

api.cjs gains a specialized fast path for the {cost,version} shapes:
getOwnPropertyNames/getOwnPropertySymbols cover the same key space as
Reflect.ownKeys at half the cost, the sync() closure is inlined, and
anything the fast path cannot prove valid funnels into a shared full-
validation path so error types, codes, and messages stay identical.

x86_64 sandbox (30k-iteration amortized loop, best of reps):
  @node-rs/bcrypt ~380-560ns vs wasm OpenBSD ~550-730ns
  per-call min: 210-250ns vs 320-370ns; bench.js min 290ns vs 860ns.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

Picks up the xor_late asm boundary (Brooooooklyn/bcrypt-rust#8): LLVM no
longer reassociates the fused round into two serial xors after the S-box
loads. On x86_64 EPYC the raw kernel now beats crypt_blowfish C
(~54.6ms vs ~54.9ms at cost 10) and JS hashSync/verifySync beat node
bcrypt (C++); arm64/wasm32 untouched via cfg fallback. CodSpeed: -4% on
all single-hash benches.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@Brooooooklyn
Brooooooklyn merged commit fe73c75 into main Oct 5, 2026
38 checks passed
@Brooooooklyn
Brooooooklyn deleted the codex/password-api-major branch October 5, 2026 15:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant