@strimz/shared-crypto
v0.2.8
Published
Web Crypto primitives for Strimz. HMAC webhook signing, API key generation, hashing, AES-256-GCM envelope encryption, and constant-time comparison. Runs in Node 22 and Edge runtimes.
Readme
@strimz/shared-crypto
Crypto primitives for Strimz. HMAC webhook signing, API key generation, AES-256-GCM envelope encryption, constant-time comparison, and the checkout payment nonce. Runs anywhere Web Crypto runs.
The hashing, HMAC, random and AES primitives are built on the Web
Crypto API (globalThis.crypto.subtle), so the same code runs
unchanged in Node 22, Vercel Edge, Cloudflare Workers, Deno, Bun, and
any modern browser. There is no node:crypto, no Buffer, no
platform-specific shim. checkoutPaymentNonce uses viem's
keccak256 instead.
Runtime dependencies: viem and @strimz/shared-config.
Install
pnpm add @strimz/shared-crypto
# npm install @strimz/shared-crypto
# yarn add @strimz/shared-cryptoWhat's inside
Every group is also available as a subpath import.
| Subpath | Contents |
| -------------- | ------------------------------------------------------------------------------------------------------- |
| /encoding | toHex, fromHex, toBase64Url, utf8ToBytes, bytesToUtf8 |
| /hash | sha256, sha256Hex |
| /hmac | hmacSha256, hmacSha256Hex |
| /random | randomBytes, randomHex, randomBase64Url, uuid |
| /timing-safe | timingSafeEqualBytes, timingSafeEqualString |
| /webhook | signWebhookPayload, verifyWebhookSignature, WEBHOOK_SIGNATURE_VERSION (timestamp + HMAC scheme) |
| /api-key | generateApiKey, hashApiKey, redactApiKey |
| /aes-gcm | encryptAesGcm, decryptAesGcm, generateAesGcmKey (envelope encryption for webhook secrets at rest) |
| /checkout | checkoutPaymentNonce, CHECKOUT_PAYMENT_NONCE_DOMAIN (the EIP-3009 nonce for a payment session) |
The package root re-exports every subpath.
Webhook signing
import { signWebhookPayload, verifyWebhookSignature } from '@strimz/shared-crypto'
// Sender
const header = await signWebhookPayload(JSON.stringify(body), secret)
// → "t=1735123456,v1=abc123...def"
// Receiver
const result = await verifyWebhookSignature(rawBodyString, header, secret)
if (!result.valid) {
throw new Error(`invalid signature: ${result.reason}`)
}Signatures are HMAC-SHA256 over "${t}.${payload}". The timestamp is
bound to the signature so replays are detectable. Verification rejects
any timestamp outside the tolerance window (default 5 minutes,
configured in @strimz/shared-config/webhooks).
API key generation
import { generateApiKey, hashApiKey } from '@strimz/shared-crypto'
const { secret, hash, prefix, lastFour } = await generateApiKey('secret', 'test')
// Persist { hash, prefix, lastFour, kind: 'secret', mode: 'test' }.
// Return `secret` to the caller exactly once, then drop it from memory.
// On every authenticated request:
const incomingHash = await hashApiKey(req.headers.authorization.slice('Bearer '.length))
const row = await db.apiKey.findUnique({ where: { hash: incomingHash } })AES-256-GCM envelope encryption
import { encryptAesGcm, decryptAesGcm, generateAesGcmKey } from '@strimz/shared-crypto'
const key = await generateAesGcmKey() // 32 bytes, hex-encoded
const ciphertext = await encryptAesGcm('hello world', key)
const plaintext = await decryptAesGcm(ciphertext, key)The ciphertext encoding is v1:<nonce-hex>:<ciphertext-and-tag-hex>,
safe to store in a text column. Suitable for envelope-encrypting
small secrets where the key is held outside the data store.
Runtime support
| Runtime | Supported | | ------------------ | --------- | | Node 22+ | Yes | | Browsers | Yes | | Vercel Edge | Yes | | Cloudflare Workers | Yes | | Deno / Bun | Yes |
Design notes
- No secrets are logged.
redactApiKeyexists specifically so any diagnostic that must include a key logs only the safe prefix and the last four characters. - No Node
Bufferornode:crypto. The primitives useglobalThis.crypto.subtleandUint8Array; the checkout nonce usesviem. - One nonce per session.
checkoutPaymentNonce(sessionId)iskeccak256(abi.encode(CHECKOUT_PAYMENT_NONCE_DOMAIN, sessionId)), so every authorization for a session shares it and the token accepts at most one. - Constant-time compare on signatures. Signature verification never short-circuits on the first mismatched byte.
- Policy lives elsewhere. Tolerance windows, key prefixes, and tier thresholds come from
@strimz/shared-config; this package consumes them.
Links
License
MIT © Strimz