encrypt-rsa
v6.1.0
Published
This is a little module use to encrypt and decrypt strings with RSA keys (public and private keys)
Maintainers
Readme
encrypt-rsa
RSA encryption, authenticated hybrid encryption, and RSA-PSS signatures for Node.js and browsers. Both builds expose the same asynchronous NodeRSA API and use platform cryptography. There are no runtime dependencies.
Install and import
npm install encrypt-rsaNode ESM and browser bundlers:
import NodeRSA, { isValidRSAPublicKey } from 'encrypt-rsa';Node CommonJS:
const { default: NodeRSA, isValidRSAPublicKey } = require('encrypt-rsa');Conditional exports choose the Node implementation in Node and the Web Crypto implementation for browser bundles. The browser ESM build is also available at build/web/index.mjs in the installed package; the standalone CDN/global bundle is build/web/encrypt-rsa.global.js and exposes encryptRSA.NodeRSA. Use the package root for application imports. Browser cryptography requires HTTPS or localhost.
Development and CI use Node 22 and 24. The browser build uses Web Crypto, TextEncoder, TextDecoder, atob, and btoa.
Quick start
import NodeRSA from 'encrypt-rsa';
const rsa = new NodeRSA();
const { publicKey, privateKey } = await rsa.createPrivateAndPublicKeys(2048);
const encrypted = await rsa.encryptLarge({
text: JSON.stringify({ message: 'Hello, العربية 😀' }),
publicKey,
oaepHash: 'sha256', // selects the self-describing v1 hybrid payload
});
const decrypted = await rsa.decryptLarge({ text: encrypted, privateKey });Use encryptLarge for text that may exceed the RSA plaintext limit. It encrypts UTF-8 data with a fresh AES-256-GCM key and wraps that key with RSA-OAEP. The complete payload is a single string that can be stored in one field. The API processes data in memory; it is not a streaming file API.
Choose the operation
| Goal | Methods | Node | Browser |
|---|---|---|---|
| Encrypt short UTF-8 text | encryptStringWithRsaPublicKey / decryptStringWithRsaPrivateKey | Yes | Yes |
| Encrypt arbitrary-length UTF-8 text | encryptLarge / decryptLarge | Yes | Yes |
| Encrypt a small binary value | encryptBufferWithRsaPublicKey / decryptBufferWithRsaPrivateKey | Yes | Yes |
| Encrypt validated JSON | encryptJSON / decryptJSON | Yes | Yes |
| Sign scoped messages with replay protection | signMessage / verifyMessage | Yes | Yes |
| Sign and verify text | sign / verify | Yes | Yes |
| Generate keys | createPrivateAndPublicKeys | Yes, nonblocking | Yes |
| Legacy private-key operation | encrypt / decrypt | Yes | Rejects with a Promise |
Compatibility guide · Payload specification · Migration guide
Constructor and keys
const rsa = new NodeRSA(publicKey?, privateKey?, modulusLength?);The default modulus length is 2048 bits. Keys supplied to the constructor are used when a method's key argument is omitted. Per-call keys override constructor keys. createPrivateAndPublicKeys returns a key pair; it does not store the pair on the instance.
Generated public keys are PEM/SPKI (BEGIN PUBLIC KEY); private keys are PEM/PKCS#8 (BEGIN PRIVATE KEY). Keys generated by either implementation work in both. The Node implementation also accepts legacy PKCS#1 keys for cryptographic operations; the browser and strict validation helpers require SPKI/PKCS#8.
const { publicKey, privateKey } = await new NodeRSA().createPrivateAndPublicKeys(2048);
const rsa = new NodeRSA(publicKey, privateKey);Direct RSA encryption
SHA-1 remains the default OAEP hash for compatibility with existing ciphertext. SHA-256 is an explicit option; the decryptor must use the same hash because direct ciphertext has no algorithm header.
const encrypted = await rsa.encryptStringWithRsaPublicKey({
text: 'Short message',
oaepHash: 'sha256',
});
const decrypted = await rsa.decryptStringWithRsaPrivateKey({
text: encrypted,
oaepHash: 'sha256',
});Limits apply to UTF-8 bytes, not JavaScript string length:
| RSA modulus | OAEP/SHA-1 | OAEP/SHA-256 | |---|---:|---:| | 2048 bits | 214 bytes | 190 bytes | | 4096 bits | 470 bytes | 446 bytes |
The formula is modulusBytes - 2 * hashBytes - 2. Use new TextEncoder().encode(text).length to measure text. Use hybrid encryption when data may exceed these limits.
Hybrid encryption and compatibility
// Existing behavior: SHA-1, legacy four-field payload.
const legacy = await rsa.encryptLarge({ text: 'Long text'.repeat(100) });
// Explicit versioned payload, retaining SHA-1.
const versioned = await rsa.encryptLarge({ text: 'Long text', payloadVersion: 'v1' });
// SHA-256 selects v1 automatically.
const modern = await rsa.encryptLarge({ text: 'Long text', oaepHash: 'sha256' });
// Algorithm is read from the payload; legacy payloads imply SHA-1.
const plaintext = await rsa.decryptLarge({ text: modern });The default still emits encryptedKey:iv:tag:ciphertext. The v1 form adds a version and algorithm header, which is authenticated as AES-GCM additional data. Decryptors accept legacy and v1 payloads, enforce a 32-byte AES key, a 12-byte IV, and a 16-byte tag, and reject malformed or tampered values. Empty plaintext is supported.
An explicitly supplied decryption oaepHash must match the payload's algorithm. { oaepHash: 'sha256', payloadVersion: 'legacy' } is rejected: legacy payloads cannot describe that algorithm. Older releases cannot read v1 payloads; upgrade readers before enabling v1 writers. See the payload specification for encoding and rollout details.
JSON and AI integrations
const text = await rsa.encryptJSON({ value: { note: 'Hello' }, publicKey });
const value = await rsa.decryptJSON({ text, privateKey }); // JsonValue, not an assumed schemaJSON helpers use v1 hybrid SHA-256, reject lossy/non-JSON values, and bound UTF-8 bytes and nesting. Supply a synchronous or asynchronous parse callback to validate an application schema and infer its result type. Defaults are 1 MiB JSON, 2 MiB encoded input, and depth 128. See the JSON and AI guide for supported values, limits, tenant-bound encrypted memory, key rotation, and full AI SDK conversation persistence.
AI SDK is installed only in the private example app. The core library stays dependency-free. The app includes offline fixture tests and a local docs assistant that selects validated templates without accepting secrets or executing generated code. Real provider calls are an explicit CLI opt-in. At-rest encryption does not hide plaintext sent to an AI provider.
Signed messages
signMessage signs a canonical envelope containing purpose, issuer, audience, keyId, issuedAt, expiresAt, nonce, and a JSON payload. verifyMessage requires expected identities, a trusted key resolver, and an atomic nonce store; it checks signature, expiry, maximum lifetime, and replay before returning the payload. An optional schema parser validates the payload. Defaults allow a five-minute lifetime with zero clock skew. See the complete contract and replay-store requirements and runnable example.
Use separate signing/encryption keys. Signatures authenticate the signer and data; they do not establish truth, authorize tools, or prevent prompt injection.
Binary values
const bytes = new Uint8Array([0, 127, 128, 255]);
const encrypted = await rsa.encryptBufferWithRsaPublicKey(bytes);
const decrypted = await rsa.decryptBufferWithRsaPrivateKey(encrypted);Buffer methods encode bytes as base64 before direct RSA encryption. With a 2048-bit key and SHA-1, they accept at most 159 raw bytes; base64 expansion consumes RSA capacity. They use the default SHA-1 hash and do not expose a hash option. Node returns a Buffer (also a Uint8Array); browsers return a Uint8Array. For larger binary values, encode them as base64 and use hybrid encryption:
// Node example
const payload = await rsa.encryptLarge({ text: buffer.toString('base64'), oaepHash: 'sha256' });
const restored = Buffer.from(await rsa.decryptLarge({ text: payload }), 'base64');Signatures and authentication
sign and verify use RSA-PSS with SHA-256 and a fixed 32-byte salt in both implementations. Signatures are base64 strings. A valid signature proves that the holder of the private key signed the exact UTF-8 text; it does not encrypt or hide that text.
const text = JSON.stringify({ requestId: '123', action: 'confirm' });
const signature = await rsa.sign({ text, privateKey });
const valid = await rsa.verify({ text, signature, publicKey });Modified text, a mismatched key, or a malformed signature produces false. A missing or invalid verification key rejects the Promise. Trust the signer's public key through your application's key distribution mechanism; design timestamps/nonces or request identifiers into the signed message if replay prevention is needed.
The older Node-only encrypt({ text, privateKey }) / decrypt({ text, publicKey }) methods remain available for compatibility. Their output is readable by anyone with the public key and provides no confidentiality. Use sign / verify for new authentication flows.
Validate keys
import { isValidRSAPublicKey, isValidRSAPrivateKey } from 'encrypt-rsa';
const publicKeyIsValid = await isValidRSAPublicKey(publicKey);
const privateKeyIsValid = await isValidRSAPrivateKey(privateKey);These asynchronous helpers parse RSA key material through the platform crypto API. They return false for malformed, non-RSA, or wrong-role keys. They check SPKI public and PKCS#8 private keys; validation does not prove that two keys are a matching pair or meet your application's minimum-strength policy.
The existing synchronous isValidPEMPublicKey, isValidPEMPrivateKey, and isValidPEMKey helpers only inspect headers/footers. They remain available for formatting checks and do not validate key material.
splitIntoChunks(text, chunkSize = 214) splits on Unicode code-point boundaries using UTF-8 byte counts; joinChunks(chunks) concatenates the result. Prefer authenticated hybrid encryption for larger data rather than independently encrypting chunks. A chunk size smaller than a single encoded character cannot provide that byte bound.
Complete public API
Every class method returns a Promise, including failure paths.
| Method | Result |
|---|---|
| createPrivateAndPublicKeys(modulusLength?) | { publicKey: string, privateKey: string } |
| encryptStringWithRsaPublicKey({ text, publicKey?, oaepHash? }) | Base64 ciphertext |
| decryptStringWithRsaPrivateKey({ text, privateKey?, oaepHash? }) | UTF-8 plaintext |
| encryptLarge({ text, publicKey?, oaepHash?, payloadVersion? }) | Legacy or v1 payload |
| decryptLarge({ text, privateKey?, oaepHash? }) | UTF-8 plaintext |
| encryptBufferWithRsaPublicKey(bytes, publicKey?) | Base64 ciphertext |
| decryptBufferWithRsaPrivateKey(ciphertext, privateKey?) | Uint8Array |
| encryptJSON({ value, publicKey?, limits? }) | v1 SHA-256 hybrid payload |
| decryptJSON({ text, privateKey?, limits?, parse? }) | JsonValue or validated parser result |
| signMessage({ message, privateKey?, limits? }) | Canonical signed JSON envelope |
| verifyMessage({ text, expected, resolvePublicKey, consumeNonce, limits?, parse?, now?, clockSkewMs?, maxLifetimeMs? }) | Verified message claims with validated payload |
| sign({ text, privateKey? }) | Base64 RSA-PSS signature |
| verify({ text, signature, publicKey? }) | boolean |
| encrypt({ text, privateKey? }) | Legacy private-key operation; Node only |
| decrypt({ text, publicKey? }) | Legacy public-key operation; Node only |
Named exports: isValidRSAPublicKey, isValidRSAPrivateKey, isValidPEMPublicKey, isValidPEMPrivateKey, isValidPEMKey, splitIntoChunks, joinChunks. Exported types include INodeRSA, OaepHash, parametersOfEncrypt, parametersOfEncryptLarge, parametersOfDecrypt, parametersOfEncryptPrivate, parametersOfDecryptPublic, parametersOfSign, parametersOfVerify, returnCreateKeys, JsonValue, JsonLimits, JsonParser, MessageClaims, NonceClaim, and the JSON/message parameter types.
The standalone browser global exports NodeRSA (also default), createPrivateAndPublicKeys, direct string encryption/decryption, encryptLarge, decryptLarge, encryptJSON, decryptJSON, signMessage, verifyMessage, sign, verify, and the two strict RSA validation helpers. Use its class for buffer methods and constructor-key defaults.
Errors and operational boundaries
await rsa.encryptLarge({ text: 'message' }).catch((error) => {
console.error(error.message);
});| Failure | Response |
|---|---|
| Missing key | Rejected Promise with Public key is required or Private key is required |
| Invalid RSA key material | Rejected Promise identifying the key role |
| Oversized direct RSA plaintext | Rejected Promise from platform crypto; switch to encryptLarge |
| Invalid hybrid version, algorithm, base64, IV, or tag | Rejected Promise with a payload/configuration error |
| Wrong hybrid key or authentication failure | Rejected Promise with Decryption failed |
| Invalid signature or modified message | verify resolves false |
| Unsupported browser private/public legacy operation | Rejected Promise |
Protect private keys, authenticate public keys, and use HTTPS for network transport. Encryption alone does not authenticate the sender. This package does not manage keys, rotate them, provide forward secrecy, or supply replay storage. verifyMessage enforces replay protection through the atomic store you provide. Avoid logging private keys or decrypted secrets.
Examples and development
npm ci
npm run build
npm run lint
npm test
npm run test:release
npm run smoke
npm run smoke:install
npx playwright install chromium
npm run smoke:browser
npm run docs
npm run smoke:docs
npm --prefix examples/ai-integrations ci
npm run test:ainpm test covers Node and Web Crypto implementations, both OAEP hashes, legacy/v1 interoperability, signatures, JSON/schema limits, signed-message identity/time/replay boundaries, Unicode, binary values, errors, and authentication boundaries. test:ai type-checks and exercises the separate SDK recipes without external model calls. test:release checks registry/version failures and repeat-release behavior. smoke:install checks an installed tarball using CommonJS, native ESM, and TypeScript Node16 resolution. smoke:browser checks the installed browser build in Chromium through bundler resolution, native ESM, and the standalone global bundle. CI runs these checks on Node 22 and 24; the publish job runs them on Node 24 before publishing.
Run the Node example with node examples/node-basic.js. Serve the repository using python3 -m http.server 8080 and open the browser example at http://localhost:8080/examples/browser-basic.html. See examples.
Generated API documentation lives in docs/. npm run docs regenerates the class/interface documentation and the guides in documentation/; npm run docs:serve serves it at http://localhost:3000.
Release and contribution
Use conventional commits and run the validation commands above before opening a PR. npm run changelog updates the changelog; npm run release -- --release-as minor prepares a version bump and tag. The publish workflow on master uses the committed lockfile with npm ci and authenticates through npm trusted publishing, without NPM_TOKEN. It skips an unchanged version and rejects versions older than npm's latest stable version. New opt-in APIs can be released as a minor version; changing cryptographic defaults or requiring v1 payloads would require a compatibility-impacting release. See the release setup and recovery guide.
Report reproducible issues through the issue tracker, including your Node/browser version, import style, operation, and a non-sensitive sample. See the code of conduct. Licensed under MIT.
