pw-punch
v1.1.3
Published
π Ultra-lightweight password hashing & token signing with WebCrypto. Zero dependencies. Edge-native. Built for Cloudflare, Deno, Bun, and Vercel.
Maintainers
Readme
π₯ pw-punch
π Ultra-lightweight password hashing & JWT-style token signing with pure WebCrypto.
Built for Edge, Serverless, and modern runtimes like Cloudflare, Deno, Vercel, Bun β no Node.js required.
Zero dependencies. Zero overhead. Just crypto.
β‘ Why pw-punch?
- β 0 dependencies β no extra weight
- β 0 Node.js required β pure WebCrypto API
- β 0 config β import and go
- β ~4KB gzipped β tiny footprint
- β Crypto only β no extra fluff
π Features
- π Password hashing with PBKDF2 + random salt
- βοΈ RSA-SHA256 (RS256) token signing (JWT standard)
- π΅οΈ Token verification with standard claim checks (
exp,nbf,iat,iss,aud,sub) - π Supports key rotation (
kidsupport) - π§ͺ Constant-time comparison utilities
- π§© WebCrypto only β works on:
- β Cloudflare Workers
- β Deno Deploy
- β Bun
- β Modern Browsers
- β Node 18+ (WebCrypto)
- π‘ Fully tree-shakable
π¦ Install
npm install pw-punchπ§ API Usage
π Hash a password
import { hashPassword } from 'pw-punch'
const hashed = await hashPassword('hunter2')
// "base64salt:base64hash"β Verify a password
import { verifyPassword } from 'pw-punch'
const isValid = await verifyPassword('hunter2', hashed)
// true or falseβοΈ Sign a token
import { signToken } from 'pw-punch'
// Generate RSA key pair first
const keyPair = await crypto.subtle.generateKey(
{
name: 'RSASSA-PKCS1-v1_5',
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: 'SHA-256'
},
true,
['sign', 'verify']
)
const token = await signToken(keyPair.privateKey, { sub: 'user' }, { kid: 'key-1' })π΅οΈ Verify a token
import { verifyToken } from 'pw-punch'
const payload = await verifyToken(token, keyPair.publicKey)
// returns payload or nullπ Decode token (without verifying)
import { decodeToken } from 'pw-punch'
const { header, payload, signature } = decodeToken(token)π Full Example
// Generate RSA key pair
const keyPair = await crypto.subtle.generateKey(
{
name: 'RSASSA-PKCS1-v1_5',
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: 'SHA-256'
},
true,
['sign', 'verify']
)
// Sign token with minimal header (recommended)
const token = await signToken(keyPair.privateKey, { sub: 'user' })
// Sign token with key rotation
const tokenWithKid = await signToken(keyPair.privateKey, { sub: 'user' }, { kid: 'key-v1' })
// Sign token without typ field (shorter)
const shortToken = await signToken(keyPair.privateKey, { sub: 'user' }, { includeTyp: false })
// Verify token
const payload = await verifyToken(token, keyPair.publicKey)π Security Guidelines
π Key Management Best Practices
- Key Size: Use minimum 2048-bit RSA keys (4096-bit recommended for high security)
- Key Rotation: Rotate keys regularly and use
kidfield for versioning - Key Storage: Never store private keys in client-side code
- Key Generation: Use crypto.subtle.generateKey() for secure random generation
// β
Good: Strong key generation
const keyPair = await crypto.subtle.generateKey(
{
name: 'RSASSA-PKCS1-v1_5',
modulusLength: 4096, // Strong key size
publicExponent: new Uint8Array([1, 0, 1]),
hash: 'SHA-256'
},
false, // Non-extractable for security
['sign', 'verify']
)β° Token Expiration Guidelines
- Short-lived tokens: 15 minutes to 1 hour for high-security APIs
- Regular tokens: 1-24 hours for standard applications
- Refresh strategy: Use refresh tokens for longer sessions
// β
Good: Appropriate expiration times
const now = Math.floor(Date.now() / 1000)
const payload = {
sub: 'user123',
iat: now,
exp: now + 3600, // 1 hour expiration
nbf: now // Valid from now
}π‘οΈ Validation Best Practices
// β
Good: Custom validation with security checks
const payload = await verifyToken(token, publicKey, (claims) => {
// Check issuer
if (claims.iss !== 'trusted-issuer') return false
// Check audience
if (!claims.aud?.includes('my-api')) return false
// Check custom claims
if (claims.role !== 'admin' && claims.action === 'delete') return false
return true
})π API Reference
hashPassword(password, type?, iterations?)
Hashes a password using PBKDF2 with SHA-256 or SHA-512.
Parameters:
password(string): Plain-text password to hashtype(256 | 512): Hash algorithm. Default: 256iterations(number): PBKDF2 iterations. Default: 150,000
Returns: Promise<string> - Base64-encoded "salt:hash"
verifyPassword(password, hashed, type?, iterations?)
Verifies a password against a PBKDF2 hash.
Parameters:
password(string): Plain-text password to verifyhashed(string): Stored hash from hashPassword()type(256 | 512): Hash algorithm. Default: 256iterations(number): PBKDF2 iterations. Default: 150,000
Returns: Promise<boolean> - True if password matches
signToken(privateKey, payload, options?)
Signs a JWT token using RS256.
Parameters:
privateKey(CryptoKey): RSA private key for signingpayload(TokenPayload): JWT payload with claimsoptions(object, optional):kid(string): Key ID for key rotationincludeTyp(boolean): Include "typ: JWT" header. Default: true
Returns: Promise<string> - Signed JWT token
verifyToken(token, publicKey, customValidate?)
Verifies and decodes a JWT token.
Parameters:
token(string): JWT token to verifypublicKey(CryptoKey): RSA public key for verificationcustomValidate(function, optional): Custom validation function
Returns: Promise<TokenPayload | null> - Decoded payload or null if invalid
decodeToken(token)
Decodes a JWT token without verification (for inspection only).
Parameters:
token(string): JWT token to decode
Returns: {header, payload, signature} - Decoded parts or null if invalid
π§ͺ Tests & Demo
- β
All core features tested using
bun test - β Additional interactive demo available:
npm run demoSelect and run hashing/token functions in CLI with colored output. Great for dev previewing & inspection.
π¦ Built With
- π 100% WebCrypto (FIPS-compliant)
- β‘ Bun for test/dev (optional)
- π TypeScript +
tscbuild - π¬ No dependencies at all
β οΈ Disclaimer
This is not a full JWT spec implementation.
- Only
RS256is supported (no HMAC/EC) - You must check claims like
aud,issyourself, or provide acustomValidate()hook - No support for JWE/JWS standards
- RSA key pair management is up to the user
This is the way.
π License
MIT
