Skip to content
Open
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
40 changes: 40 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,45 @@ jobs:
- name: Test bindings
run: yarn workspaces foreach -A -j 1 run test

test-bcrypt-supported-node:
name: Test bcrypt on supported Node ${{ matrix.node }}
needs:
- build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: ['10', '12']
steps:
- uses: actions/checkout@v7

- name: Setup node for test dependencies
uses: actions/setup-node@v7
with:
node-version: 24
cache: yarn

- name: Install test dependencies
run: yarn install --immutable --mode=skip-build

- name: Setup node
uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node }}

- name: Download native bindings
uses: actions/download-artifact@v8
with:
name: bindings-x86_64-unknown-linux-gnu
path: packages

# Avoid the modern development toolchain when testing the published API.
- name: Test bcrypt public API and stored hashes
run: node packages/bcrypt/__tests__/supported-node.cjs

- name: Test locally imported cancellation polyfill
run: node packages/bcrypt/__tests__/polyfill-cancellation.cjs

test-linux-x64-gnu-binding:
name: Test bindings on Linux-x64-gnu - node@${{ matrix.node }}
needs:
Expand Down Expand Up @@ -558,6 +597,7 @@ jobs:
- test-linux-aarch64-musl-binding
- test-linux-arm-gnueabihf-binding
- test-macOS-windows-binding
- test-bcrypt-supported-node
- test-wasi-nodejs
steps:
- uses: actions/checkout@v7
Expand Down
3 changes: 3 additions & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ npmRegistryServer: 'https://registry.npmjs.org/'
yarnPath: .yarn/releases/yarn-4.18.0.cjs

npmPreapprovedPackages:
# Pinned previous-release compatibility tests, including this repo's platform binaries.
- '@node-rs/bcrypt@1.10.9'
- '@node-rs/bcrypt-*@1.10.9'
- '@napi-rs/*'
- oxlint
- '@oxlint/*'
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@
"ts"
],
"files": [
"packages/*/__test__/**/*.spec.ts"
"packages/*/__test__/**/*.spec.ts",
"packages/bcrypt/__tests__/**/*.spec.ts"
],
"nodeArguments": [
"--import",
Expand Down
44 changes: 44 additions & 0 deletions packages/bcrypt/MIGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Migrating to bcrypt 2

Stored password hashes remain usable after migrating API calls. Verification continues to use the salt and cost embedded in each stored hash, including hashes created through older custom-salt bugs. Do not rewrite hashes, replace their prefixes, or reset passwords for this upgrade.

## Calls use options objects

| 1.x | 2.x |
| -------------------------------------- | ------------------------------------------------- |
| `hash(password, 12)` | `hash(password, { cost: 12 })` |
| `hashSync(password, 12, rawSalt)` | `hashSync(password, { cost: 12, salt: rawSalt })` |
| `genSalt(12, '2b', signal)` | `genSalt({ cost: 12, version: '2b', signal })` |
| `verify(password, storedHash, signal)` | `verify(password, storedHash, { signal })` |
| `verify(password, storedHash)` | Unchanged |
| `compareSync(password, storedHash)` | Unchanged |

Omit unused options instead of passing `null`. Unsupported positional arguments, unknown options, and a bare signal where options are expected fail explicitly. All async validation errors now reject the returned Promise, so use `await` inside `try/catch` or attach `.catch()`.

## Salt creation is corrected

Raw salts must be exactly 16 bytes. String salts must be canonical encoded salts containing their cost and version, for example the result of `genSalt({ cost: 12 })`. Do not also specify cost/version when supplying an encoded salt. Generated salts now contain 29 characters without `==` padding.

Old versions treated string salts as raw text and clipped or zero-padded them. Correct interpretation intentionally changes newly computed output. Applications that authenticate by recomputing a hash from a separately saved original salt should switch to `verify(password, storedHash)`. The stored hash contains the actual salt used previously; it needs no conversion. Automatic random salts are the default for new hashes.

Costs must be finite integers in 4–31. Fractional and overflowing values are rejected instead of truncated or wrapped. Existing hashes still use their embedded effective costs; there is no new default verification cost ceiling.

Creation no longer accepts `2x`, including inside encoded salt strings. Verification retains its previous prefix handling. That existing behavior does not implement the historical sign-extension algorithm of genuine `2x` hashes; this release neither reinterprets those hashes nor tries multiple algorithms.

## Password bytes and login compatibility

Default 72-byte truncation remains in both hash creation and verification. This preserves existing logins and ordinary rehash-on-login flows. No compatibility flag is necessary. UTF-8 string encoding, raw byte inputs, embedded NULs, and empty passwords keep their previous meaning.

`rejectLongPasswords: true` is an optional creation policy. It rejects more than 72 bytes and accepts exactly 72. Enabling it can affect enrollment or rehashing of long passwords; verification never adopts it automatically. Stored bcrypt strings do not record whether the original password was truncated.

Invalid UTF-8 bytes supplied as the stored hash now return `false`, consistently with other malformed hash data. Successfully accepted noncanonical encodings, including the `+4` cost spelling, remain accepted by verification but are rejected for new salt creation.

## Cancellation and byte ownership

Async calls copy mutable byte inputs before returning. Changing a password, raw salt, or stored-hash array afterward no longer changes the queued operation.

Put `signal` in async options. A pre-aborted signal rejects before native work is queued. Later abort rejects the pending public Promise with `AbortError`; queued work is cancelled where possible, while running native computation may finish with its result discarded. Existing signal handlers are preserved, shared/reused signals work independently, and abort after observed completion has no effect.

Native signals and compatible signals from locally imported polyfills are accepted. On Node 10 and 12, import an `AbortController` polyfill and pass `controller.signal`; neither constructor needs to be installed globally.

Install the matching 2.x platform packages together with the root package. A backend contract check rejects stale binaries rather than silently interpreting new calls with old native arguments.
54 changes: 30 additions & 24 deletions packages/bcrypt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,38 @@
## Usage

```typescript
export const DEFAULT_COST: 12

export function hashSync(password: string | Buffer, round?: number): string
export function hash(password: string | Buffer, round?: number): Promise<string>
export function verifySync(password: string | Buffer, hash: string | Buffer): boolean
export function verify(password: string | Buffer, hash: string | Buffer): Promise<boolean>
/**
* The same with `verifySync`
*/
export function compareSync(password: string | Buffer, hash: string | Buffer): boolean
/**
* The same with `verify`
*/
export function compare(password: string | Buffer, hash: string | Buffer): Promise<boolean>

export type Version = '2a' | '2x' | '2y' | '2b'
/**
* @param version default '2b'
*/
export function genSaltSync(round: number, version?: Version): string
/**
* @param version default '2b'
*/
export function genSalt(round: number, version?: Version): Promise<string>
import { hash, hashSync, verify, verifySync, genSalt, compare } from '@node-rs/bcrypt'

const storedHash = await hash('password', { cost: 12 })
await verify('password', storedHash) // true

const salt = await genSalt({ cost: 12 })
const withExplicitSalt = hashSync('password', { salt })
verifySync('password', withExplicitSalt) // true
await compare('password', storedHash) // alias of verify
```

`hash` and `hashSync` accept a string or `Uint8Array` password and an options object. `cost` defaults to 12 and must be an integer from 4 through 31. Omitted salts use 16 random bytes. `salt` can be exactly 16 raw bytes or a canonical 29-character encoded bcrypt salt; an encoded salt supplies its own cost and version, so overrides are rejected. Creation supports `2a`, `2b` (default), and `2y`.

`genSalt` and `genSaltSync` accept `{ cost?, version? }`. `verify` and `verifySync` take the password first and the complete stored hash second. Both password and hash accept `Uint8Array`, including `Buffer`. `compare` and `compareSync` are exact aliases. See [the declarations](index.d.ts) for the complete API.

Bcrypt uses at most 72 password bytes. That default is unchanged for hashing and verification, including existing database hashes. To reject longer passwords when creating a hash, explicitly set `rejectLongPasswords: true`. This checks bytes, not JavaScript string length, and accepts exactly 72 bytes. Verification has no length-policy option.

Async functions accept `signal` inside their options object and report errors through Promise rejection. Synchronous functions throw. Invalid call shapes use `TypeError`; invalid creation values use `RangeError`. Wrong passwords and malformed stored hashes return `false`. Verification retains existing accepted encodings independently of the stricter creation parser.

```typescript
await hash('password', { cost: 12, signal: controller.signal })
await verify('password', storedHash, { signal: controller.signal })
```

An already-aborted signal prevents queueing. Aborting a pending operation rejects with `name: 'AbortError'`; native work that has already started may finish in the background. Shared and reused signals are supported without replacing existing handlers. The first observed completion or abort determines the result.

On Node versions without built-in cancellation, pass a signal from a locally imported `AbortController` polyfill. No global installation is required. Signals must provide a boolean `aborted` property and `addEventListener`/`removeEventListener` methods for the `abort` event; see `AbortSignalLike` in the declarations.

The browser entry uses the same public wrapper and aliases over WASI. Published packages include the matching WASI backend as an optional dependency. Platform-specific backend packages and `binding.js` are internal interfaces; import the public package entry.

Upgrading from 1.x requires call-site changes. **Existing stored hashes do not require rewriting or password resets.** See [migration instructions](MIGRATION.md).

## Bench

```
Expand Down
Loading
Loading