npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

native-jwe

v2.0.0

Published

Zero-dependency RFC 7516/7515 compact JWE (RSA-OAEP-256 + A256GCM) and JWS (RS256) implementation for Node.js

Readme

native-jwe

Donate with PayPal

A zero-runtime-dependency implementation of:

  • JWE (RFC 7516, Compact Serialization), fixed to RSA-OAEP-256 (key management) + A256GCM (content encryption).
  • JWS (RFC 7515, Compact Serialization), fixed to RS256.

for Node.js >= 18, built only on node:crypto.

This library deliberately supports exactly one algorithm pair for JWE and exactly one algorithm for JWS. It is not a general-purpose JOSE library, does not negotiate algorithms, and does not read alg off an incoming token to decide what crypto primitive to call — every primitive name is a hardcoded string literal in src/jwe.ts / src/jws.ts. See Limitations for what is explicitly out of scope.

If you are upgrading from v1.x, read MIGRATION-v1-to-v2.md first — v1 tokens cannot be read by v2.

Install

npm install native-jwe

Quickstart

const { generateKeyPairSync } = require('node:crypto');
const { JWE, JWS } = require('native-jwe');

const { publicKey, privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });

// --- JWE: encrypt / decrypt ---
const claims = JSON.stringify({ sub: 'alice', scope: ['read'] });

const jwe = JWE.encrypt(claims, publicKey);
const decrypted = JWE.decrypt(jwe, privateKey);
// decrypted === claims

// --- JWS: sign / verify ---
const jws = JWS.encode(claims, privateKey);

const isValid = JWS.verify(jws, publicKey); // boolean
const payload = JWS.decodeUnverified(jws);  // string — see warning below

Every snippet in this README was executed against the compiled dist/ output before being committed; none of them are hypothetical.

API reference

The public surface is exactly what src/index.ts exports: JWE, JWS, MEDIA_TYPES, the error classes, and their TypeScript types. There is no other entry point.

JWE.encrypt(value, publicKey, options?) => string

Encrypts value (a string) into a JWE Compact Serialization for publicKey (a PEM string, DER Buffer, or KeyObject — anything node:crypto accepts as KeyLike).

options:

| Option | Type | Default | Notes | | --- | --- | --- | --- | | cty | MEDIA_TYPES.JSON | MEDIA_TYPES.JSON | Optional. Only "json" is accepted if supplied. |

Throws JweError (code: 'ERR_JWE_UNSUPPORTED_OPTION') if cty is set to anything other than the one value it currently supports.

JWE.decrypt(payload, privateKey, passphrase?) => string

Decrypts a JWE Compact Serialization produced by JWE.encrypt — or by any other spec-compliant RSA-OAEP-256/A256GCM producer (verified by interop test, see below).

  • passphrase — optional; only needed if privateKey is an encrypted PEM.
  • A protected header carrying a zip parameter, of any value, is rejected outright (RFC 7516 §4.1.13 principle applied to zip; see Limitations).

Every failure mode throws the same JweError. See Error handling below — this is intentional, not an oversight.

JWS.encode(value, privateKey, passphrase?) => string

Signs value (a string) with RS256, returning a JWS Compact Serialization. passphrase is optional, for an encrypted PEM private key.

JWS.verify(jwsPayload, publicKey) => boolean

Verifies a JWS Compact Serialization against publicKey. Returns true or falseit never throws. Every failure mode (malformed input, wrong segment count, non-canonical base64url, an alg other than RS256, a crit header, a bad signature) returns false.

JWS.decodeUnverified(jwsPayload) => string

Warning — this function does NOT verify the signature. It extracts and base64url-decodes the payload segment only. Do not trust the result unless JWS.verify has already returned true for the same jwsPayload, or you have some other reason to deliberately skip verification. This was named decode in v1.0.0, which gave no indication that the result was unverified; it is now decodeUnverified for exactly that reason.

Throws JwsError (code: 'ERR_JWS_INVALID_INPUT') if the input is not a string, is empty/oversized, or does not have exactly 3 non-empty segments.

MEDIA_TYPES

enum MEDIA_TYPES { JSON = 'json' }

The only value this library currently understands for the cty header parameter.

Error handling

All errors are typed classes (src/errors.ts), never a bare Error:

| Class | Thrown by | code | Notes | | --- | --- | --- | --- | | JweError | JWE.encrypt | ERR_JWE_UNSUPPORTED_OPTION | Distinguishable — this is a caller programming error (bad options), not a token being decrypted. | | JweError | JWE.decrypt | ERR_JWE_DECRYPT_FAILED | The only error JWE.decrypt ever throws. See below. | | JwsError | JWS.decodeUnverified | ERR_JWS_INVALID_INPUT | Malformed/empty/oversized input. |

JWS.verify never throws; it returns boolean (see API reference above).

JWE.decrypt failures are deliberately indistinguishable

A bad alg, a malformed base64url segment, a truncated auth tag, a wrong-length IV, a CEK that RSA-decrypts to the wrong length, a crit header, a failed GCM authentication tag check, and a wrong private key all produce the exact same error: the same message ("JWE decryption failed"), the same code (ERR_JWE_DECRYPT_FAILED), and no .cause.

This is intentional, not a debugging shortcoming. RFC 7516's security considerations (via the RSA padding-oracle countermeasures in RFC 3218 that they reference) require it: if an attacker can distinguish why a decryption failed, that difference itself becomes a decryption oracle they can use to recover key material or plaintext one bit at a time. JWE.decrypt never logs, and never attaches the real underlying error as .cause, for the same reason — see test/tamper.test.js, test "JWE: every distinct decrypt failure mode produces an identical message and code (decryption-oracle guard)".

The library also never writes to stdout/stderr on any failure path (test/tamper.test.js, test "library writes nothing to stdout or stderr on a failed decrypt or a failed verify"). If you need to detect and monitor decryption failures, catch the typed error at your application layer and log it there — with whatever context (request ID, caller IP, etc.) makes sense for your system, not the library's:

const { generateKeyPairSync } = require('node:crypto');
const { JWE, JweError } = require('native-jwe');

const { publicKey, privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
// A mismatched key pair, purely so this example actually exercises the
// catch branch below — in real code `token` would come over the wire.
const { privateKey: wrongPrivateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
const token = JWE.encrypt(JSON.stringify({ sub: 'alice' }), publicKey);

try {
  const claims = JWE.decrypt(token, wrongPrivateKey);
  console.log(claims);
} catch (err) {
  if (err instanceof JweError) {
    // Log at the application layer, with whatever context (request ID,
    // caller IP, etc.) makes sense for *your* system — the error itself
    // intentionally carries no information about which check failed.
    console.warn({ code: err.code, requestId: 'req-123' }, 'JWE decrypt failed');
  }
}

Security properties

Only properties that are enforced in code and covered by a test are listed here, each cited to its test file:

| Property | Enforced in | Proven by | | --- | --- | --- | | OAEP key-wrap uses SHA-256 (not Node's SHA-1 default) for RSA-OAEP-256 | src/jwe.ts (oaepHash: 'sha256') | test/tamper.test.js ("the encrypted CEK is bound to SHA-256 OAEP") | | AAD is the exact base64url protected-header bytes as received on the wire, never a re-serialization (RFC 7516 §5.1) | src/jwe.ts | test/tamper.test.js ("re-serializing the protected header breaks AAD binding") | | IV is exactly 12 bytes (RFC 7518 §5.3); any other length rejected | src/jwe.ts | test/jwe.test.js, test/tamper.test.js ("emitted IV is exactly 12 bytes...") | | Auth tag is exactly 16 bytes; truncated tags rejected before setAuthTag | src/jwe.ts | test/tamper.test.js ("a truncated auth tag is rejected") | | Decrypted CEK must be exactly 32 bytes, checked before createDecipheriv is ever called | src/jwe.ts | test/tamper.test.js ("rejected before createDecipheriv is ever called") | | JWE requires exactly 5 non-empty segments, JWS exactly 3; extra/missing segments rejected | src/jwe.ts, src/jws.ts | test/tamper.test.js ("rejects 4-segment and 6-segment tokens", "rejects 2-segment and 4-segment tokens") | | base64url segments must be in canonical form (no stray padding, no standard-base64 +//) | src/base64url.ts | test/base64url.test.js, test/tamper.test.js ("every segment rejects added padding...") | | A crit header of any content is rejected outright (RFC 7516 §4.1.13 / RFC 7515 §4.1.11) | src/jwe.ts, src/jws.ts | test/tamper.test.js ("header containing crit is rejected") | | A zip header parameter of any value is rejected outright — zip is not implemented (see Limitations) | src/jwe.ts | test/tamper.test.js ("header containing zip (any value) is rejected"), test/interop.test.js ("a jose-produced JWE carrying zip: DEF is rejected") | | Size limits are measured in UTF-8 bytes, not UTF-16 code units | src/jwe.ts, src/jws.ts | test/tamper.test.js ("oversized-input check is byte-based") | | All JWE.decrypt failure modes are observationally identical (no decryption oracle); nothing is written to stdout/stderr on failure | src/errors.ts, src/jwe.ts | test/tamper.test.js ("every distinct decrypt failure mode produces an identical message and code", "library writes nothing to stdout or stderr...") | | RSA-OAEP-256 / A256GCM JWEs and RS256 JWSs interoperate with a spec-correct third-party implementation in both directions | src/jwe.ts, src/jws.ts | test/interop.test.js, cross-checked against jose (devDependency, test-only — never a runtime dependency) |

Two claims from the v1.0.0 README were removed rather than softened, because the code does not support them:

  • No timing-attack claim. There is no crypto.timingSafeEqual and no constant-time comparison anywhere in src/. Header/option string comparisons (===) are ordinary, not constant-time. If you need constant-time comparison semantics for your own header handling on top of this library, you must add it yourself.
  • No performance claim. v1.0.0 claimed speed "comparable to compiled languages like Go/Rust." That claim shipped with zero benchmarks and is not repeated here. "Zero runtime dependencies" and "uses Node's native OpenSSL bindings via node:crypto" are the only performance-adjacent statements this README makes, and both are verifiable from package.json (no runtime dependencies) and src/jwe.ts/src/jws.ts (calls to createCipheriv, publicEncrypt, createSign, etc.).

Limitations

This library deliberately does not implement:

  • kid / keystore / key rotation. There is no concept of a key identifier or a key store. Callers are responsible for knowing which key pair to use.
  • Claims validation. exp, nbf, iss, aud, or any other registered claim are never inspected. JWE.decrypt and JWS.decodeUnverified return the raw payload string you gave to encrypt/encode; validating its contents (including expiry) is entirely the calling application's job.
  • JWK support. Keys are KeyLike (PEM string, DER Buffer, or KeyObject) only; there is no JWK parsing or serialization.
  • Algorithm agility. Exactly one algorithm pair for JWE (RSA-OAEP-256 + A256GCM) and one for JWS (RS256). No alg/enc negotiation, no A128GCM, no ECDH-ES.
  • JSON Serialization. Compact Serialization only — no JWE/JWS JSON Serialization, no multi-recipient tokens.
  • Constant-time header comparison. Header and option field comparisons use ordinary ===, not crypto.timingSafeEqual.
  • npm provenance attestation. Not yet configured for this package.
  • Compression (zip) is deliberately not supported. Compressing the plaintext before AEAD encryption creates a compression-oracle side channel (the CRIME/BREACH class of attack): because AES-GCM preserves plaintext length, the length of the compressed-then-encrypted output leaks information about the plaintext whenever compressible attacker-controlled data is mixed with a secret in the same payload. An earlier revision of this library implemented zip and an internal adversarial review recovered a full secret value from token lengths alone under exactly that condition. JWE.decrypt rejects any protected header carrying a zip parameter, of any value, rather than attempting to process it.

If your application needs any of the above, it must be built on top of this library, not inside it.

Credits & acknowledgments

This project is inspired by and based on portions of the work from psenger/jwe_example (MIT License, Copyright (c) 2019 Philip A Senger).

License

MIT — see LICENSE.