OpenPGP.js in practice: Node 22, Web Streams, and the curves you actually get
OpenPGP implementation for JavaScript
At a glance
- What is it?
- OpenPGP.js implements RFC 9580 in JavaScript for Node.js v22+ and modern browsers, and it leans hard on what the platform provides natively: SubtleCrypto, Web Streams, and a curve table where several entries have no WebCrypto path at all. It also changed what counts as a stream in v6, and it puts AEAD behind a flag for good reason.
- Who is it for?
- Use OpenPGP.js when you need OpenPGP inside a Node.js service or a web app that can assume Node 22, a secure context, and Web Streams, and when you accept that curve choice is limited by what the platform implements natively. Do not reach for it if your code still hands it Node Readable objects, if you serve pages over plain HTTP, or if you enable aeadProtect before checking the implementations on the receiving end.
- Can I use it commercially?
- Yes, with conditions. LGPL-3.0 is a weak copyleft licence: you can use it inside commercial and closed-source software, but if you distribute changes to its own files, you must publish those changes under the same licence.
- Is it still maintained?
- Yes. The repository last received commits 12 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 13, 2026, and from our analysis. They are not legal advice.
Star history
- openpgpjs/openpgpjs5,981 ★ · +15
| Repository | 2026-09-23 | 2026-11-13 |
|---|---|---|
| openpgpjs/openpgpjs | 6k | 6k |
15 Stars in the last 51 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
The import path decides whether you get Node or browser crypto
Which build you end up with is decided by package.json, not by anything you do at runtime. The CommonJS entry is dist/node/openpgp.min.cjs, the ES module entry is dist/node/openpgp.min.mjs, and a browser field remaps those Node builds onto browser builds:
"main": "dist/node/openpgp.min.cjs",
"module": "dist/node/openpgp.min.mjs",
"browser": {
"./dist/node/openpgp.min.cjs": "./dist/openpgp.min.js",
"./dist/node/openpgp.min.mjs": "./dist/openpgp.min.mjs"
},The exports field layers conditions on top and publishes exactly two subpaths:
"exports": {
".": {
"types": "./dist/types/index.d.ts",
"browser": "./dist/openpgp.min.mjs",
"import": "./dist/node/openpgp.mjs",
"require": "./dist/node/openpgp.min.cjs"
},
"./lightweight": {
"types": "./dist/types/index.d.ts",
"browser": "./dist/lightweight/openpgp.min.mjs"
}
},A bundler that honors the browser condition sends you to dist/openpgp.min.mjs, while Node keeps dist/node/openpgp.mjs for import and dist/node/openpgp.min.cjs for require. The ./lightweight subpath has only a types entry and a browser entry, so it is not a server side replacement for the main entry. Only dist/ and lightweight/ are published, and directories maps lib to src. After an install you cannot consume the raw src/ tree, because the files field does not ship it.
Node 22, npm 12, and a secure context are the floor
The manifest states the floor, and the floor is Node 22:
"engines": {
"node": ">= 22.0.0",
"typescript": ">= 5.0.0"
},
"devEngines": {
"runtime": {
"name": "node",
"version": ">= 22.0.0"
},
"packageManager": {
"name": "npm",
"version": ">= 12.0.0",
"onFail": "warn"
}
},devEngines repeats the Node requirement and puts npm at 12.0.0 or newer, with onFail set to warn, so a package manager mismatch warns rather than aborting an install. The Node bundles are described as working in Node.js v22+, and they are what import ... from 'openpgp' or require('openpgp') resolves to by default. On the web, the dist/openpgp.min.js or .mjs bundle works with recent Chrome, Firefox, Edge and Safari 15+. Under all of it sits a hard requirement on the Web Cryptography API's SubtleCrypto, and in browsers that API exists only in secure contexts.
v6 refuses native Node Readable inputs
The largest v6 behavior change is what counts as a stream at all. OpenPGP.js v6 no longer supports native Node Readable streams in inputs, and it expects and outputs Node's Web Streams instead. Node v17+ includes utilities to convert from and to Web Streams, so a Readable coming out of the core stream module has to be converted at the call site. OpenPGP.js cannot take it as it is.
The browser side also lost its automatic polyfill. In previous versions Web Streams were polyfilled by the library itself, but from v6 that task is left to the library user, because of the more extensive browser support and the polyfilling side effects. The suggested fix is loading the web-streams-polyfills package manually, and it carries one sharp edge: when the polyfills load, the global ReadableStream property, if it exists, gets overwritten with the polyfill version. In some edge cases you need the native ReadableStream, for example when using it to create a Response object, which means storing a reference to it before the polyfills load. The web-streams-adapter library converts back and forth between the two. So adding a stream polyfill is not a drop-in repair: it can change which ReadableStream your own code sees afterwards.
curve25519 runs without a native crypto backend
Native implementations do the work wherever the platform offers them, and the curve table shows exactly where that stops. The If native curves are constant-time only when the underlying native implementation is available and constant-time, with a footnote for the when available entries:
| Curve | Encryption | Signature | NodeCrypto | WebCrypto | Constant-Time | |:---------------:|:----------:|:---------:|:----------:|:---------:|:-----------------:| | curve25519 | ECDH | N/A | No | No | Algorithmically | | ed25519 | N/A | EdDSA | No | Yes* | If native** | | nistP256 | ECDH | ECDSA | Yes* | Yes* | If native** | | nistP384 | ECDH | ECDSA | Yes* | Yes* | If native** | | nistP521 | ECDH | ECDSA | Yes* | Yes* | If native** | | brainpoolP256r1 | ECDH | ECDSA | Yes* | No | If native** | | brainpoolP384r1 | ECDH | ECDSA | Yes* | No | If native** | | brainpoolP512r1 | ECDH | ECDSA | Yes* | No | If native** | | secp256k1 | ECDH | ECDSA | Yes* | No | If native** |
curve25519 is the outlier: it does ECDH, it has no NodeCrypto entry, no WebCrypto entry, and its constant-time status reads Algorithmically rather than If native. ed25519 runs the other way, no encryption, EdDSA signatures, no NodeCrypto but WebCrypto when available. nistP256, nistP384 and nistP521 are marked yes for both engines. The brainpool curves and secp256k1 carry NodeCrypto yes and WebCrypto no, so a browser gets no native path for them. Elliptic curve support arrived in version 3.0.0, and against RSA the claim is stronger security per bits of key, which is what allows for much faster operations.
aeadProtect = true trades interoperability for speed
Authenticated encryption is implemented but deliberately gated. The library implements AEAD as per RFC 9580 using AES-GCM, OCB, or EAX, which makes symmetric encryption faster on platforms with native implementations. Since the specification is very recent and other OpenPGP implementations are still in the process of adopting it, the feature sits behind a flag that you turn on by setting `openpgp.config.aeadProtect = true`. The warning attached to it is direct: activating this setting can break compatibility with other OpenPGP implementations which have yet to implement the feature. There is also a version trap. The v5 behavior implemented support for a provisional version of AEAD from RFC 4880bis, and this v6 setting has a different effect. The same config name therefore does not mean the same wire format across the major version boundary, and enabling it for a quick test tells you nothing about how a message reads on the far side.
prepare builds the bundle and the types before tests run
The scripts section carries the build and test story, and prepare is where it starts:
"build": "rollup --config",
"build-types": "rm -rf dist/types && tsc --project tsconfig.dist.json && cpy 'src/**/*.d.ts' dist/types",
"build-test": "npm run build -- --config-build-only=test",
"prepare": "npm run build && npm run build-types",
"test": "mocha --timeout 120000 test/unittests.js",prepare runs the rollup build and the type build, so installing straight from a git checkout builds first. The type build deletes dist/types, runs tsc against tsconfig.dist.json, then copies the .d.ts files out of src with cpy. The unit tests run mocha against test/unittests.js with a 120000 millisecond timeout, and .mocharc.json sits at the root to configure the runner. Type definitions get their own check that runs tsc and ts-node against test/typescript/tsconfig.test.json. build-test passes --config-build-only=test into the rollup config, which is how a test-only bundle avoids the full build. Around those sit eslint.config.js, tsconfig.json, tsconfig.dist.json, .jsdocrc.cjs, patches/, and update_deps.sh. Bundling itself is driven by rollup.config.js.
The manifest version and the newest release tag disagree
Two version numbers exist and they are not in step. package.json carries version 6.3.1, while the tagged releases run v6.3.0 from 2025-12-09, v6.3.1 from 2026-06-04, and v6.3.2 from 2026-09-24. The last push to main landed on 2026-10-08, after that newest tag. In practical terms an install pinned to a release tag gets the published 6.3.2 tree, while an install from the main branch gets whatever the manifest still says, 6.3.1, plus everything committed since the tag. That gap is invisible unless you look for it, because nothing in the manifest declares itself a pre-release or a development build. The homepage field points at https://openpgpjs.org/ and the package description is the one line that has drifted the most, which is the subject of the next point. For reproducible installs, take the tag.
The npm description still points at RFC 4880
The README says OpenPGP.js implements RFC 9580, which supersedes RFC 4880 and RFC 4880bis. The npm package description says something older: OpenPGP.js is a Javascript implementation of the OpenPGP protocol, and this is defined in RFC 4880. Treat the registry text as a stale label rather than the specification target, because the AEAD note, the curve table, and the RFC 9580 citation all sit on the newer document. The code examples the README walks through cover ten examples: encrypt and decrypt Uint8Array data with a password, encrypt and decrypt String data with PGP keys, encrypt symmetrically with compression, streaming encrypt Uint8Array data with a password, streaming encrypt and decrypt String data with PGP keys, generate a new key pair, revoke a key, sign and verify cleartext messages, create and verify detached signatures, and streaming sign and verify Uint8Array data. Anything past that set, keyserver lookups for instance, is not among the examples, and the docs/ directory in the tree is where the rest of the surface lives. The repository also carries SECURITY.md and patches/ at the root, plus a lightweight/ directory that is published alongside dist/.
Editorial conclusion
Use OpenPGP.js when you need OpenPGP inside a Node.js service or a web app that can assume Node 22, a secure context, and Web Streams, and when you accept that curve choice is limited by what the platform implements natively. Do not reach for it if your code still hands it Node Readable objects, if you serve pages over plain HTTP, or if you enable aeadProtect before checking the implementations on the receiving end. Before shipping, pin a release tag rather than git main, since the manifest version and the newest tag do not agree.
Frequently asked questions
What is OpenPGP used for?
OpenPGP.js implements the OpenPGP protocol in JavaScript, covering encryption and decryption with a password or with PGP keys, symmetric encryption with compression, streaming encryption and decryption, key pair generation, key revocation, and signing and verifying cleartext, detached, and streamed data.
Is OpenPGP free?
It ships as the npm package openpgp under LGPL-3.0+, with the LICENSE file at the repository root. It is a library you install or bundle, and openpgpjs.org is listed as its homepage.
Is OpenPGP safe?
The library implements RFC 9580 and uses the platform's native Web Crypto API, plus the native crypto module on Node.js where it offers extra functionality. SECURITY.md sits at the repository root, the README has Security Audit and Security recommendations sections, and whether a curve is constant-time depends on whether a constant-time native implementation is available.
Is GPG still relevant?
The npm package lists gpg among its keywords, and the package description still ties the protocol to RFC 4880. The README states the library implements RFC 9580, which supersedes RFC 4880 and RFC 4880bis, so the newer document is the wire format to target.
what is openpgp js
It is a JavaScript implementation of the OpenPGP protocol published to npm as openpgp. It requires Node.js v22+ or a recent browser, and it needs SubtleCrypto from the Web Cryptography API plus Web Streams for stream inputs.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/openpgpjs-openpgpjs)Star history chart for your README
[](https://hysenlabs.com/projects/openpgpjs-openpgpjs)Was this useful?