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

secure-crypto-kit

v1.0.1

Published

End-to-end hybrid encryption (RSA-OAEP + AES-256-GCM) with replay protection and HMAC integrity for browser ↔ Node.js communication.

Readme

secure-crypto-kit

End-to-end hybrid encryption for browser ↔ Node.js — RSA-OAEP + AES-256-GCM with replay protection, HMAC integrity, and automatic key rotation.


Features

  • 🔐 Hybrid encryption — AES-256-GCM for payload, RSA-OAEP-4096 to wrap the key
  • 🛡️ Replay protection — per-request nonce + timestamp checked server-side
  • Tamper detection — HMAC-SHA256 over all ciphertext fields (encrypt-then-MAC)
  • 🔑 Non-exportable private keys — FE private key cannot be extracted from WebCrypto
  • ♻️ Automatic key rotation — FE key pair rotates every 30 minutes (configurable)
  • 📦 Tree-shakeable — separate /fe and /be entry points; no Node built-ins in browser bundle
  • 🟦 Full TypeScript — 100% typed, ships declaration files

Installation

npm install secure-crypto-kit

Peer dependency: express >= 4 is required only if you use the built-in Express middleware.


Quick Start

1. Generate your backend RSA key pair (one-time setup)

# Generate 4096-bit private key
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out be-private.pem

# Extract public key
openssl rsa -pubout -in be-private.pem -out be-public.pem

Set environment variables:

# .env (server)
BE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"

# .env (frontend / Vite)
VITE_BE_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"

2. Frontend usage

import { SecureCryptoSession } from 'secure-crypto-kit/fe';

const session = new SecureCryptoSession({
  bePublicKeyPem: import.meta.env.VITE_BE_PUBLIC_KEY,
  sessionTtlMs: 30 * 60 * 1000, // optional, default 30 min
});

// --- Encrypt a request ---
const { encryptedPackageB64, hmacKeyB64 } = await session.encrypt({
  userId: 123,
  action: 'transfer',
  amount: 500,
});

const res = await fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-HMAC-Key': hmacKeyB64,          // ← required header
  },
  body: JSON.stringify({ encryptedPackageB64 }),
});

// --- Decrypt the response ---
const decrypted = await session.decrypt(await res.json());
console.log(decrypted); // { success: true, txId: '...' }

3. Backend usage (Express)

import express from 'express';
import { loadBePrivateKey, decryptWebRequestPayload, encryptResponsePayload } from 'secure-crypto-kit/be';

// Load key ONCE at startup — never store the raw string after this
loadBePrivateKey(process.env.BE_PRIVATE_KEY!);

const app = express();
app.use(express.json());

// Apply decryption middleware to all /api routes
app.use('/api', decryptWebRequestPayload({
  maxBodyBytes:    10 * 1024 * 1024, // 10 MB (default)
  requestExpiryMs: 5 * 60 * 1000,   // 5 min (default)
}));

app.post('/api/transfer', async (req, res) => {
  // req.body.payload  → your decrypted data
  // req.body.fePublicKey → FE public key for encrypting the response
  const { userId, amount } = req.body.payload as { userId: number; amount: number };

  const result = { success: true, txId: 'abc-123' };

  // Encrypt the response back to the frontend
  const encryptedPackageB64 = await encryptResponsePayload(
    result,
    req.body.fePublicKey,
    req.headers['x-hmac-key'] as string,
  );

  res.json({ encryptedPackageB64 });
});

4. Backend usage (without Express)

import { loadBePrivateKey, decryptRequest, encryptResponsePayload } from 'secure-crypto-kit/be';

loadBePrivateKey(process.env.BE_PRIVATE_KEY!);

// In any HTTP handler (Fastify, Hono, plain Node http, etc.)
async function handler(body: { encryptedPackageB64: string }, hmacKey: string) {
  const decrypted = decryptRequest(body.encryptedPackageB64, hmacKey);
  const payload   = decrypted.data;                  // your data
  const feKey     = decrypted.fePublicKey as string; // for encrypting response

  const encrypted = await encryptResponsePayload({ ok: true }, feKey, hmacKey);
  return { encryptedPackageB64: encrypted };
}

API Reference

secure-crypto-kit/fe

new SecureCryptoSession(options)

| Option | Type | Default | Description | |---|---|---|---| | bePublicKeyPem | string | required | PEM-encoded RSA public key of the backend | | sessionTtlMs | number | 1800000 | Key pair rotation interval in ms |

session.encrypt(payload)Promise<EncryptOutput>

Encrypts any JSON-serialisable value. Returns:

| Field | Description | |---|---| | encryptedPackageB64 | Send as request body | | hmacKeyB64 | Send as X-HMAC-Key header | | fePublicKeyPem | Current FE public key (automatically embedded) |

session.decrypt({ encryptedPackageB64 })Promise<Record<string, unknown>>

Decrypts a backend response.

session.rotateKeys()void

Manually rotates the FE key pair before the TTL expires.


secure-crypto-kit/be

loadBePrivateKey(rawKeyEnv: string)void

Call once at startup. Accepts PEM or JSON-stringified PEM. Idempotent.

decryptWebRequestPayload(options?) → Express middleware

After this middleware runs:

  • req.body.payload — your decrypted data
  • req.body.fePublicKey — FE public key for encrypting the response

Returns 400 for malformed requests, 401 for MAC failure / replay / decryption errors.

decryptRequest(encryptedPackageB64, hmacKeyB64, options?)DecryptedPayload

Framework-agnostic version. Throws on any failure.

encryptResponsePayload(payload, fePublicKey, hmacKeyB64, options?)Promise<string>

Encrypts a response. Returns base64-encoded package string.


Security Model

  Browser                                          Server
  ──────────────────────────────────────────────────────────────────
  1. Generate RSA-4096 key pair (non-exportable private key)
  2. Generate ephemeral AES-256-GCM key
  3. Generate 96-bit IV
  4. Embed _nonce + _issuedAt in payload
  5. AES-GCM encrypt(payload + nonce + timestamp)
  6. RSA-OAEP wrap(AES key) using BE public key
  7. HMAC-SHA256 over (iv + encryptedKey + ciphertext + authTag)
  8. Send: body={ encryptedPackageB64 }, header: X-HMAC-Key
                                         ──────────────────────────▶
                                         9.  Verify HMAC (before RSA — fail-fast)
                                         10. RSA-OAEP unwrap AES key
                                         11. AES-GCM decrypt + verify authTag
                                         12. Check _nonce not seen before
                                         13. Check _issuedAt within 5 min window
                                         14. Validate fePublicKey PEM
                                         15. Process req.body.payload
                                         16. AES-GCM encrypt response with FE public key
                                         17. HMAC-sign response package
                          ◀──────────────────────────────────────────
  18. RSA-OAEP unwrap response AES key
  19. AES-GCM decrypt response

What each layer prevents

| Mechanism | Threat | |---|---| | RSA-OAEP-4096 | Passive eavesdropping | | AES-256-GCM authTag | In-transit payload tampering | | HMAC over package fields | Cross-component splice attacks | | Nonce | Replay attacks | | Timestamp window | Delayed replay after nonce cache expires | | Non-exportable private key | JS-context key exfiltration | | Key rotation (TTL) | Long-term session compromise | | MAC verified before RSA | DoS via expensive RSA on garbage payloads |


Dev / Test Bypass

Set ENCRYPTION_BYPASS_SECRET on the server and send the same value as the X-Bypass-Secret header to skip encryption in dev/Postman. Never set this in production.

ENCRYPTION_BYPASS_SECRET=my-local-dev-secret
curl -X POST http://localhost:3000/api/transfer \
  -H "X-Bypass-Secret: my-local-dev-secret" \
  -H "Content-Type: application/json" \
  -d '{ "userId": 1, "amount": 500 }'

License

MIT