Skip to content

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

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

mmorales-post wants to merge 2 commits into
Mastercard:mainfrom
mmorales-post:fix/oaep-digest-outside-node

Conversation

@mmorales-post

Copy link
Copy Markdown
Contributor

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
@mmorales-post
mmorales-post force-pushed the fix/oaep-digest-outside-node branch from f19a834 to ccb1501 Compare October 15, 2026 21:16

@talha-API talha-API 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.

Thanks for raising this PR and issue @mmorales-post and apologies for the delay in the response.

The changes are sound, just a few points to address. Please also update the PR with latest changes from main (if not done already) and add any relevant tests for the comments provided.

Comment thread lib/mcapi/crypto/jwe-crypto.js Outdated
if (
typeof nodeCrypto.publicEncrypt !== "function" ||
!nodeCrypto.constants ||
!keyPem

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.

honoursOaepHash will cache false when the key is missing. The key is not related to the runtime environment.

Also, an encrypt-only instance that calls decryptData sets the module-level nativeOaepHash to false. Every JweCrypto in the process then falls back to node-forge, even on a real Node runtime, which should not be the case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. The probe now always uses the encryption certificate, so a missing private key can't affect it.

Comment thread test/jwe-crypto.test.js
});
}
});

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.

Please add a test covering the forge path for decryptData.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added tests for forge decrypt: tokens wrapped by node and by forge, plus a SHA-1 wrapped key that gets rejected.

Comment thread lib/mcapi/crypto/jwe-crypto.js Outdated
Buffer.alloc(1)
);
nativeOaepHash = false;
} catch {

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.

This could catch unrelated exceptions, we should probably check for a digest specific one.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. It now only treats node's invalid-digest error as support. Anything else isn't cached and falls back to forge.

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.

2 participants