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-channel-sdk

v3.0.0

Published

Secure End-to-End Encryption SDK using AES-GCM and HKDF

Readme

secure-channel-sdk

A production-ready SDK for client-side end-to-end authenticated encryption built entirely on the Web Crypto API. Works in browsers and Node.js without native dependencies.

License: ISC Version


Features

  • AES-GCM 256-bit — authenticated encryption with tamper detection
  • PBKDF2-SHA256 (600 000 iterations) — password hardening per OWASP guidance
  • HKDF per-message key derivation — unique encryption key for every message
  • Context binding (AAD) — ciphertext is cryptographically bound to method, path, userId; swapping context fails decryption
  • Replay protection — sequence number + timestamp sliding window blocks replayed packets
  • Deterministic mode (AES-GCM-DET) — reproducible ciphertext for auditable channels
  • Rate-limiting & brute-force lockout — exponential backoff + 30 s lockout after 5 failed decryption attempts
  • Non-extractable keys — raw key material never leaves the WebCrypto heap
  • Universal — browsers (Web Crypto API) and Node.js 18+

Installation

npm install secure-channel-sdk

Quick Start

Password-based encryption (PasswordChannel)

Best for user-facing scenarios: encrypting data with a passphrase before sending it to a server.

import { PasswordChannel } from 'secure-channel-sdk';

const context = { method: 'POST', path: '/api/notes', userId: 'user_42' };

// Encrypt
const pkg = await PasswordChannel.encrypt('correct-horse-battery-staple', 'top secret payload', context);
// pkg is a plain object — store it or POST it as JSON

// Decrypt (same password + same context required)
const plaintext = await PasswordChannel.decrypt('correct-horse-battery-staple', pkg, context);
console.log(plaintext); // 'top secret payload'

If the password is wrong, the context differs, or any byte of pkg is tampered with, decrypt throws.

Session-based encryption (SecureChannelSession)

Best for high-throughput channels: both sides share a raw key, messages carry sequence numbers and timestamps.

import { SecureChannelSession } from 'secure-channel-sdk';

const sharedKey = crypto.getRandomValues(new Uint8Array(32)); // 256-bit master key
const ctx = { method: 'POST', path: '/api/data', userId: 'user_42' };

// Sender
const sender = await SecureChannelSession.init(sharedKey);
const pkg = await sender.encrypt('hello world', ctx);

// Receiver — reconstruct from the same key and the session salt carried in pkg
const receiver = await SecureChannelSession.fromExisting(sharedKey, base64ToBytes(pkg.saltB64));
const bytes = await receiver.decrypt(pkg, ctx);
console.log(new TextDecoder().decode(bytes)); // 'hello world'

Replaying the same pkg a second time throws Replay detected.


API Reference

PasswordChannel

All methods are static.

encrypt(password, data, context?, options?): Promise<PasswordEncryptResult>

| Parameter | Type | Description | | ---------- | ---------------------- | ------------------------------------- | | password | string | Non-empty passphrase | | data | string | Plaintext to encrypt | | context | Partial<HttpContext> | Optional — method, path, userId | | options | SecureChannelOptions | Optional — algorithm, key length |

Returns a PasswordEncryptResult object (JSON-serialisable). All fields are base64 strings.

decrypt(password, pkg, context?, options?): Promise<string>

Validates input, enforces rate limiting, derives the key, and decrypts. Throws on any failure.


SecureChannelSession

SecureChannelSession.init(masterKey, options?): Promise<SecureChannelSession>

Creates a new session with a fresh random salt. masterKey may be Uint8Array (raw bytes) or a non-extractable CryptoKey with deriveKey usage.

SecureChannelSession.fromExisting(masterKey, salt, options?): Promise<SecureChannelSession>

Reconstructs a session from an existing salt (e.g. received from the sender).

session.encrypt(plaintext, httpCtx): Promise<EncryptResult>

| Parameter | Type | Notes | | ----------- | ---------------------- | -------------------------------------------------------- | | plaintext | string \| Uint8Array | Data to encrypt | | httpCtx | HttpContext | method, path, userId (optional ts, apiVersion) |

session.decrypt(pkg, httpCtx): Promise<Uint8Array>

Validates fields, checks timestamps, checks replay window, then decrypts. Returns raw bytes.


SecureChannelOptions

interface SecureChannelOptions {
	algo?: 'AES-GCM' | 'AES-GCM-DET'; // default: 'AES-GCM'
	keyLengthBits?: 128 | 192 | 256; // default: 256
	initialSeq?: number; // default: 1
	replayWindowSize?: number; // default: 1024
	maxAgeSeconds?: number; // default: 60
	useAdaptiveConfig?: boolean; // run benchmark and pick optimal config
}

EncryptResult / PasswordEncryptResult

interface EncryptResult {
	version: number; // protocol version (currently 1)
	ctB64: string; // ciphertext (base64)
	tagB64: string; // AES-GCM auth tag, 16 bytes (base64)
	ivB64: string; // initialisation vector, 12 bytes (base64)
	saltB64: string; // HKDF session salt (base64)
	seq: number; // sequence number
	algo: 'AES-GCM' | 'AES-GCM-DET';
	ts: number; // Unix timestamp ms at encrypt time
	keyLengthBits: 128 | 192 | 256;
}

interface PasswordEncryptResult extends EncryptResult {
	passwordSaltB64: string; // PBKDF2 salt (base64)
}

Security Architecture

Key derivation chain

Password
   │
   ▼ PBKDF2-SHA256 (600 000 iterations, 16-byte random salt)
Master Key (HKDF, non-extractable)
   │
   ├─▶ HKDF(salt=sessionSalt, info="enc|{seq}") ──▶ AES-GCM message key
   └─▶ HKDF(salt=sessionSalt, info="iv_gen")   ──▶ HMAC-SHA256 IV key (DET mode only)

Each message gets a unique AES-GCM key derived from the master key, the session salt, and the sequence number. Compromising one message key exposes only that message.

Context binding (AAD)

Every ciphertext is bound to:

["<apiVersion>", "<METHOD>", "<path>", "<userId>", "<ts>", "<seq>"]

Serialised as JSON and passed to AES-GCM as Additional Authenticated Data. Changing any field makes AES-GCM authentication fail — the ciphertext cannot be transplanted to a different endpoint or user.

Replay protection

SecureChannelSession maintains a sliding window of replayWindowSize sequence numbers (default 1 024). Any seq seen before, or outside the window, is rejected. Messages older than maxAgeSeconds (default 60 s) or more than 60 s in the future are also rejected.

Client-side rate limiting (PasswordChannel)

This is a usability defence, not a cryptographic guarantee. A motivated attacker with direct WebCrypto access can bypass it. Pair it with server-side rate limiting in production.

  • Exponential backoff: 500 ms → 1 s → 2 s → … after each failed attempt
  • Hard lockout for 30 s after 5 failures
  • Lockout timestamp persisted in sessionStorage and synced to in-memory state
  • All decrypt calls serialised through an async mutex — parallel brute-force within one JS context is blocked

Adaptive Configuration

import { CryptoBenchmark, SecureChannelSession } from 'secure-channel-sdk';

// Measure AES-GCM throughput and pick the optimal key length
const session = await SecureChannelSession.init(key, { useAdaptiveConfig: true });

CryptoBenchmark.run() encrypts 64 KB × 5 with AES-GCM-256 and falls back to 128-bit only if throughput is below 50 MB/s and 128-bit is measurably faster.


Browser & Node.js Compatibility

| Environment | Minimum version | | ------------- | ---------------------------------- | | Chrome / Edge | 37+ | | Firefox | 34+ | | Safari | 11+ | | Node.js | 18+ (globalThis.crypto built-in) |


Development

npm install
npm test          # vitest run
npm run build     # tsc → dist/

See CONTRIBUTING.md for contribution guidelines and SECURITY.md for the vulnerability disclosure policy.


License

ISC © 2024 Mykola Dzoban