Skip to content

fix(Bun.password): accept optional v= segment in PHC bcrypt hashes#32316

Open
robobun wants to merge 2 commits into
mainfrom
farm/fdc38f35/bcrypt-phc-version-segment
Open

fix(Bun.password): accept optional v= segment in PHC bcrypt hashes#32316
robobun wants to merge 2 commits into
mainfrom
farm/fdc38f35/bcrypt-phc-version-segment

Conversation

@robobun

@robobun robobun commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Repro

Bun.password.verifySync("hello", "$bcrypt$v=19$r=4$MDEyMzQ1Njc4OWFiY2RlZg$AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")

Before: throws with code: "PASSWORD_INVALID_ENCODING".
After: returns false (and returns true for a matching password).

Cause

The PHC string format defines an optional $v=<version>$ segment between the algorithm id and the parameters. Zig's std.crypto.phc_format.deserialize (used by bcrypt.strVerify) parsed and discarded it, so $bcrypt$v=19$r=4$<salt>$<hash> decoded and ran the comparison. The Rust port's hand-rolled parser in src/runtime/crypto/pwhash.rs went straight from $bcrypt$ to r=, so "v=19".strip_prefix("r=") failed and surfaced as InvalidEncoding.

Fix

Skip an optional v=...$ segment before reading r=, mirroring what the argon2 path in the same file already does.

Verification

Added a test in test/js/bun/util/password.test.ts covering v=19 and v=2b, sync and async, auto-detected and explicit "bcrypt". Fails on main with PASSWORD_INVALID_ENCODING, passes with this change. Full password.test.ts is green.

The PHC string format allows an optional $v=<version>$ segment between
the algorithm id and the parameters. Zig std.crypto.phc_format parses
and discards it for bcrypt, but the hand-rolled parser in pwhash.rs
expected r= immediately after $bcrypt$, so verifySync on a hash like
$bcrypt$v=19$r=4$... threw InvalidEncoding instead of returning a bool.
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 7 minutes and 56 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ef7bea90-0367-4414-b18e-d4eb4107cc10

📥 Commits

Reviewing files that changed from the base of the PR and between e0acad3 and 62fedec.

📒 Files selected for processing (2)
  • src/runtime/crypto/pwhash.rs
  • test/js/bun/util/password.test.ts

Comment @coderabbitai help to get the list of available commands and usage tips.

@robobun

robobun commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 9:09 AM PT - Jun 15th, 2026

@robobun, your commit 62fedec has 4 failures in Build #62515 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 32316

That installs a local version of the PR into your bun-32316 executable, so you can run:

bun-32316 --bun

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Change looks correct and well-tested to me, but since this is the password-verification parse path I'd like a human to sign off.

Extended reasoning...

Overview

This PR adjusts the hand-rolled PHC string parser in src/runtime/crypto/pwhash.rs::bcrypt::verify_phc to accept (and discard) an optional v=<version>$ segment between the $bcrypt$ algorithm id and the r=N rounds parameter. It also updates the doc comment and adds a regression test in test/js/bun/util/password.test.ts that exercises v=19, v=2b, and the no-version case across sync/async and auto-detected/explicit-algorithm paths.

Security risks

This is the bcrypt verification codepath for Bun.password.verify, so it is security-sensitive by nature. The change itself only relaxes the parser to accept a spec-defined optional segment; the rounds, salt decode, digest decode, recompute, and constant-time compare are all untouched. Discarding the version value cannot cause a hash to verify against the wrong digest, and any input that previously parsed successfully still parses to the same (rounds, salt, hash) triple — the new branch only fires on inputs that used to be rejected as InvalidEncoding. I don't see a way for this to weaken verification.

Level of scrutiny

Because it sits in crypto/password-hashing code, I'm deferring rather than auto-approving even though the diff is ~10 lines and mirrors the argon2 handling already present in the same file. The logic is straightforward Rust string parsing (strip_prefix + split_once) with proper error propagation on a malformed/truncated v= segment, and it restores the Zig std.crypto.phc_format behavior described in the PR body.

Other factors

No CODEOWNERS cover these paths. The bug-hunting pass found nothing. Test coverage is good (positive verify, negative verify, both sync and async). No outstanding human review comments on the PR.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I didn't find any issues — the parser change is small and mirrors the argon2 path — but since this is in the password-verification codepath I'd like a human to confirm before merge.

Extended reasoning...

Overview

This PR adds ~10 lines to verify_phc in src/runtime/crypto/pwhash.rs so the hand-rolled PHC bcrypt parser skips an optional v=<version>$ segment between $bcrypt$ and r=N, restoring parity with both the PHC string-format spec and the Zig std.crypto.phc_format implementation this code replaced. The doc comment is updated and a regression test in test/js/bun/util/password.test.ts covers v=19 / v=2b / no-version, sync and async, auto-detected and explicit "bcrypt", with both matching and non-matching passwords.

Security risks

The change only widens what the parser accepts; the version value is read and discarded (bcrypt has no PHC-level versioning that affects the digest). Rounds, salt decode, 23-byte digest recompute, and the constant-time comparison are all untouched. I don't see a way for the discarded segment to influence verification outcome, bypass the cost check, or alter the comparison. Malformed inputs (v= with no trailing $) still surface as InvalidEncoding.

Level of scrutiny

Despite being mechanically simple and well-tested, this lives inside Bun.password.verify's bcrypt path — security-sensitive by definition. My policy is to defer crypto/auth changes to a human regardless of size.

Other factors

No CODEOWNERS cover these files. No prior human review on the PR. The bug-hunting pass found nothing. The argon2 branch in the same file already handles v= identically, so the pattern is established in-tree.

@robobun

robobun commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator Author

The diff is green. test/js/bun/util/password.test.ts passes on every lane, and there are no failures in password/crypto code on build 62515.

Remaining red is unrelated darwin flake, all of which also fail on other open PRs at the same base commit:

  • test/js/bun/io/fetch/fetch-abort-slow-connect.test.ts (darwin 14 aarch64): also failing on builds 62501, 62502, 62505, 62508, 62510, 62511
  • test/js/bun/terminal/terminal.test.ts (darwin 14 x64, 90s timeout): also failing on build 62505
  • test/js/node/http/node-http-backpressure-max.test.ts (darwin 14 x64, 60s timeout): also failing on build 62505
  • s3.test.ts / streams-leak.test.ts are in the flaky annotation and passed on retry.

The earlier build 62456 had one hard failure in test/regression/issue/30205.test.ts (napi finalizers under --isolate), which did not recur on 62515.

Ready for review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant