feat(hardening): M-Hard 1-5 — crypto, auth, rate limit, fingerprints, rotation

M-Hard 1: Cryptographic Hardening
- constantTimeEqual, zeroize, randomUint32 on CryptoProvider
- Fix Math.random() → crypto.randomUint32() for registrationId
- Zero message keys and chain keys after use in ratchet.ts
- Constant-time trust comparison in MemoryStorage + SQLiteStorage
- Timing variance test catches early-exit regressions

M-Hard 2: Self-Authenticated Prekey Server
- Ed25519 signature verification on all write routes
- signPayload/verifyPayload with canonical JSON, ±5 min replay window
- Address validation (NFKC, alphanumeric + :_-.)
- Global ShadeError → HTTP status mapping
- ShadeFetchTransport signs requests when signingPrivateKey provided
- Anonymous bundle fetches still allowed (read-only)

M-Hard 3: Rate Limiting + DoS Protection
- Token bucket rate limiter with pluggable store
- Per-route limits: register 5/h/IP, fetch 60/min/IP, replenish 10/min/id
- 64KB body size limit on POST
- Retry-After header on 429 responses

M-Hard 4: Auto-replenish + Fingerprints + Session Reset
- Safety numbers (12 groups × 5 digits, Signal-style)
- ensurePreKeyStock, resetSession, acceptIdentityChange
- verifyRemoteIdentity for out-of-band comparison

M-Hard 5: Identity Rotation with Grace Period
- rotateIdentity archives old identity, generates fresh signed prekey
- RetiredIdentity storage with addRetired/getRetired/pruneRetired
- 7-day default grace period for decrypting old sessions
- pruneExpiredIdentities for cleanup

M-Hard 8: Error Hierarchy
- New error types: Network, Storage, Validation, Timeout, RateLimit,
  Configuration, Unauthorized, Replay, IdentityRotation
- All errors have stable SHADE_* codes
- errorToHttpStatus for consistent HTTP mapping
- toJSON() for network serialization

188 tests passing, zero failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 17:45:34 +02:00
parent 7d214dc614
commit 96a8c210b2
25 changed files with 1835 additions and 257 deletions

View File

@@ -1,4 +1,5 @@
import { Hono } from 'hono';
import type { Hono } from 'hono';
import type { CryptoProvider } from '@shade/core';
import { createPrekeyRoutes } from './routes.js';
import { MemoryPrekeyStore } from './memory-store.js';
import type { PrekeyStore } from './store.js';
@@ -6,23 +7,30 @@ import type { PrekeyStore } from './store.js';
export { createPrekeyRoutes } from './routes.js';
export { MemoryPrekeyStore } from './memory-store.js';
export type { PrekeyStore } from './store.js';
export { verifyPayload, signPayload, canonicalizePayload, validateAddress } from './auth.js';
/**
* Create a standalone Shade Prekey Server.
*
* Can be used standalone (Docker) or embedded in another Hono app.
* Requires a CryptoProvider for signature verification on write routes.
*
* Standalone:
* const server = createPrekeyServer();
* const crypto = new SubtleCryptoProvider();
* const server = createPrekeyServer({ crypto });
* export default { port: 3900, fetch: server.fetch };
*
* Embedded:
* Embedded in another Hono app:
* const app = new Hono();
* app.route('/shade', createPrekeyServer());
* app.route('/shade', createPrekeyServer({ crypto }));
*/
export function createPrekeyServer(options?: {
export function createPrekeyServer(options: {
crypto: CryptoProvider;
store?: PrekeyStore;
disableRateLimit?: boolean;
}): Hono {
const store = options?.store ?? new MemoryPrekeyStore();
return createPrekeyRoutes(store);
const store = options.store ?? new MemoryPrekeyStore();
return createPrekeyRoutes(store, options.crypto, { disableRateLimit: options.disableRateLimit });
}
export { RateLimiter, MemoryRateLimitStore } from './rate-limit.js';
export type { RateLimitStore, RateLimitConfig } from './rate-limit.js';