webcrypto-creds
v0.1.0
Published
AES-256-GCM + HKDF-SHA256 credentials encryption for Web Crypto API environments (Cloudflare Workers, browsers, Deno, Node ≥ 20)
Maintainers
Readme
webcrypto-creds
AES-256-GCM + HKDF-SHA256 credentials encryption using the native Web Crypto API.
Zero dependencies. Works on Cloudflare Workers, Deno Deploy, modern browsers, and Node.js ≥ 20.
Why?
Storing third-party API keys in a database is risky if the database is compromised.
This library encrypts credentials using AES-256-GCM with a per-context derived key (HKDF) so that:
- A single master key manages all users.
- Each
contextId(e.g. user ID) produces a different derived key → one user's encrypted data can't decrypt another user's. - No external KMS or key server required.
Key Architecture
MASTER_KEY (32-byte hex, stored as an environment secret)
│
▼ HKDF-SHA256 (salt = contextId, info = "webcrypto-creds")
DERIVED_KEY (256-bit AES-GCM key — unique per contextId)
│
▼ AES-256-GCM (random 12-byte IV per call)
EncryptedBundle { ciphertext, iv, tag } — all base64url stringsInstallation
npm install webcrypto-creds
# or
pnpm add webcrypto-credsQuick Start
import { encrypt, decrypt, generateMasterKey } from 'webcrypto-creds';
// 1. Generate a master key once and store it as a secret:
const masterKey = generateMasterKey(); // 64 hex chars (32 bytes)
// → store as CREDS_MASTER_KEY env var / wrangler secret
// 2. Encrypt user credentials before storing in the DB:
const bundle = await encrypt(masterKey, userId, {
api_key: 'pk_live_…',
api_secret: 'sk_live_…',
});
// Store bundle.ciphertext, bundle.iv, bundle.tag in your DB
// 3. Decrypt when needed (e.g. before calling an exchange API):
const creds = await decrypt<{ api_key: string; api_secret: string }>(masterKey, userId, bundle);
console.log(creds.api_key);API
encrypt(masterKeyHex, contextId, payload, options?)
Encrypts any JSON-serialisable payload. Returns an EncryptedBundle.
| Param | Type | Description |
| -------------- | --------- | ------------------------------------------------- |
| masterKeyHex | string | 64 hex char (32-byte) master key |
| contextId | string | Unique context — used as HKDF salt (e.g. user ID) |
| payload | unknown | Any JSON-serialisable value |
| options.info | string | HKDF info string (default: "webcrypto-creds") |
decrypt<T>(masterKeyHex, contextId, bundle, options?)
Decrypts an EncryptedBundle. Throws if the auth tag is invalid (tampered data) or the key/context don't match.
deriveKey(masterKeyHex, contextId, options?)
Returns a CryptoKey you can reuse for multiple encrypt/decrypt calls within the same request lifecycle.
generateMasterKey()
Returns a cryptographically secure 64-char hex string (32 bytes). Use once at setup time.
EncryptedBundle
interface EncryptedBundle {
ciphertext: string; // base64url-encoded AES-GCM ciphertext
iv: string; // base64url-encoded 12-byte IV
tag: string; // base64url-encoded 16-byte auth tag
}Cloudflare Workers Example
// wrangler.toml:
// [vars] CREDS_MASTER_KEY = "..." — use `wrangler secret put CREDS_MASTER_KEY`
import { encrypt, decrypt } from 'webcrypto-creds';
export default {
async fetch(request: Request, env: Env) {
const userId = 'user_clerk_123';
// Save credentials
const bundle = await encrypt(env.CREDS_MASTER_KEY, userId, {
api_token: 'Trading212-token',
});
await db.save({ userId, ...bundle });
// Load and decrypt credentials
const row = await db.load(userId);
const creds = await decrypt<{ api_token: string }>(env.CREDS_MASTER_KEY, userId, row);
return Response.json(creds);
},
};Security Notes
| Concern | Mitigation |
| -------------------- | ----------------------------------------------------------------------- |
| DB breach | Credentials are AES-256-GCM encrypted; plaintext never stored |
| Master key breach | Compromise only affects recovery if attacker also has ciphertexts + IVs |
| Cross-user isolation | HKDF derives unique keys per contextId |
| IV reuse | Each encrypt() call generates a fresh random 12-byte IV |
| Tamper detection | AES-GCM auth tag detects any modification to the ciphertext |
License
MIT
