Skip to content

fix: keep RSA-OAEP-256 digest correct in runtimes that ignore oaepHash - #106

Open
mmorales-post wants to merge 1 commit into
Mastercard:mainfrom
mmorales-post:fix/oaep-digest-outside-node
Open

fix: keep RSA-OAEP-256 digest correct in runtimes that ignore oaepHash#106
mmorales-post wants to merge 1 commit into
Mastercard:mainfrom
mmorales-post:fix/oaep-digest-outside-node

Conversation

@mmorales-post

Copy link
Copy Markdown

PR checklist

  • An issue/feature request has been created for this PR
  • Pull Request title clearly describes the work in the pull request and the Pull Request description provides details about how to validate the work
  • File the PR against the master branch
  • The code in this PR is covered by unit tests

Link to issue/feature request: #105

Description

The JWE key wrap asked node's crypto.publicEncrypt for oaepHash: "sha256". node
honours that; the crypto-browserify polyfill silently ignores it and wraps with SHA-1,
producing a token whose header claims RSA-OAEP-256. It fails only at the recipient.

node callers are unaffected. The native path is retained and selected by a one-time
probe. Measured 0.055 ms against 0.050 ms on main for encryptData, within noise.

The probe. node validates the digest name and throws on an unrecognised one; a
polyfill that ignores oaepHash accepts anything. Attempting a wrap under a deliberately
bogus digest therefore distinguishes them, costs one call, and is memoised. It also treats
an absent or stubbed crypto module as unsupported, which is what
resolve.fallback.crypto = false in this repo's webpack.config.js produces.

The fallback goes through node-forge with SHA-256 set for both the OAEP label digest
and MGF1. MGF1 has to be set explicitly or forge defaults it to SHA-1, which would
reintroduce the same class of bug one layer down. The helper mirrors createOAEPOptions
in field-level-crypto.js. Forge keys are parsed lazily, so node callers never parse them.

decryptData gets the same treatment, so a browser build can read responses as well as
send requests.

Scope. This fixes the digest, which is the failure that is silent. It does not make
the library work under resolve.fallback.crypto = false, because randomBytes,
createCipheriv and createDecipheriv still require the module. Under a bundler that
polyfills crypto the full JWE path now works. Happy to raise the remaining piece
separately if that is useful.

Validation

Three tests added to test/jwe-crypto.test.js:

  • still wraps with SHA-256 when the runtime ignores oaepHash stubs publicEncrypt to
    behave like the polyfill, then asserts the emitted key unwraps with SHA-256 and not
    SHA-1. Fails on main.
  • detects whether the runtime honours oaepHash covers the probe against real node, a
    polyfill, and an empty module. Fails on main.
  • wraps the content encryption key with RSA-OAEP-256, not SHA-1 locks the digest for the
    native path. This one passes on main as well, by design: under node the old code was
    already correct, which is why the bug survived. It is there to stop a regression.

Cross-checked that forge and native interoperate in both directions on the same key, and
that a SHA-1 unwrap of the forge output is rejected.

Full suite: 192 passing. eslint: 0 errors, 5 warnings, unchanged from main.
Validated on Node 24.

@mmorales-post
mmorales-post marked this pull request as ready for review August 24, 2026 21:03
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