feat: Shade E2EE library — M1-M3 complete
Signal Protocol implementation with full X3DH + Double Ratchet:
- M1: Core types, CryptoProvider interface, KDF chain functions,
SubtleCrypto+noble/curves provider, MemoryStorage
- M2: X3DH key agreement (identity keys, signed prekeys, one-time
prekeys, bundle processing for both initiator and responder)
- M3: Double Ratchet (symmetric-key ratchet, DH ratchet, skipped
message key cache, out-of-order delivery, AAD-bound headers)
68 tests, 0 failures — including full integration test of
X3DH handshake → Double Ratchet conversation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:08:19 +02:00
|
|
|
import type { IdentityKeyPair, SignedPreKey, OneTimePreKey, SessionState } from './types.js';
|
|
|
|
|
|
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>
2026-04-10 17:45:34 +02:00
|
|
|
/** A retired identity kept in history during the rotation grace period */
|
|
|
|
|
export interface RetiredIdentity {
|
|
|
|
|
keyPair: IdentityKeyPair;
|
|
|
|
|
retiredAt: number;
|
|
|
|
|
}
|
|
|
|
|
|
feat: Shade E2EE library — M1-M3 complete
Signal Protocol implementation with full X3DH + Double Ratchet:
- M1: Core types, CryptoProvider interface, KDF chain functions,
SubtleCrypto+noble/curves provider, MemoryStorage
- M2: X3DH key agreement (identity keys, signed prekeys, one-time
prekeys, bundle processing for both initiator and responder)
- M3: Double Ratchet (symmetric-key ratchet, DH ratchet, skipped
message key cache, out-of-order delivery, AAD-bound headers)
68 tests, 0 failures — including full integration test of
X3DH handshake → Double Ratchet conversation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:08:19 +02:00
|
|
|
/**
|
|
|
|
|
* StorageProvider — abstract interface for persisting cryptographic state.
|
|
|
|
|
*
|
|
|
|
|
* Implementations per platform:
|
|
|
|
|
* - In-memory (testing)
|
|
|
|
|
* - IndexedDB (browser)
|
|
|
|
|
* - SQLite/PostgreSQL (server)
|
|
|
|
|
* - EncryptedSharedPreferences (Android)
|
|
|
|
|
*/
|
|
|
|
|
export interface StorageProvider {
|
|
|
|
|
// ─── Identity ──────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/** Get our local identity keypair, or null if not yet generated */
|
|
|
|
|
getIdentityKeyPair(): Promise<IdentityKeyPair | null>;
|
|
|
|
|
|
|
|
|
|
/** Persist our local identity keypair */
|
|
|
|
|
saveIdentityKeyPair(keyPair: IdentityKeyPair): Promise<void>;
|
|
|
|
|
|
|
|
|
|
/** Get our local registration ID (unique per installation) */
|
|
|
|
|
getLocalRegistrationId(): Promise<number>;
|
|
|
|
|
|
|
|
|
|
/** Save our local registration ID */
|
|
|
|
|
saveLocalRegistrationId(id: number): Promise<void>;
|
|
|
|
|
|
|
|
|
|
// ─── Signed Pre-Keys ──────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/** Get a signed prekey by ID */
|
|
|
|
|
getSignedPreKey(keyId: number): Promise<SignedPreKey | null>;
|
|
|
|
|
|
|
|
|
|
/** Persist a signed prekey */
|
|
|
|
|
saveSignedPreKey(key: SignedPreKey): Promise<void>;
|
|
|
|
|
|
|
|
|
|
/** Remove a signed prekey (after rotation grace period) */
|
|
|
|
|
removeSignedPreKey(keyId: number): Promise<void>;
|
|
|
|
|
|
|
|
|
|
// ─── One-Time Pre-Keys ────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/** Get a one-time prekey by ID */
|
|
|
|
|
getOneTimePreKey(keyId: number): Promise<OneTimePreKey | null>;
|
|
|
|
|
|
|
|
|
|
/** Persist a one-time prekey */
|
|
|
|
|
saveOneTimePreKey(key: OneTimePreKey): Promise<void>;
|
|
|
|
|
|
|
|
|
|
/** Remove a consumed one-time prekey */
|
|
|
|
|
removeOneTimePreKey(keyId: number): Promise<void>;
|
|
|
|
|
|
|
|
|
|
/** Count remaining one-time prekeys */
|
|
|
|
|
getOneTimePreKeyCount(): Promise<number>;
|
|
|
|
|
|
|
|
|
|
// ─── Sessions ─────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/** Get session state for a peer address (e.g. "device:abc123") */
|
|
|
|
|
getSession(address: string): Promise<SessionState | null>;
|
|
|
|
|
|
|
|
|
|
/** Persist session state for a peer */
|
|
|
|
|
saveSession(address: string, state: SessionState): Promise<void>;
|
|
|
|
|
|
|
|
|
|
/** Remove session for a peer */
|
|
|
|
|
removeSession(address: string): Promise<void>;
|
|
|
|
|
|
|
|
|
|
/** Check if we trust a remote identity key (for TOFU or pinned keys) */
|
|
|
|
|
isTrustedIdentity(address: string, identityKey: Uint8Array): Promise<boolean>;
|
|
|
|
|
|
|
|
|
|
/** Save a trusted remote identity key */
|
|
|
|
|
saveTrustedIdentity(address: string, identityKey: Uint8Array): Promise<void>;
|
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>
2026-04-10 17:45:34 +02:00
|
|
|
|
|
|
|
|
// ─── Identity History (rotation with grace period) ──────
|
|
|
|
|
|
|
|
|
|
/** Add an identity to the retired history */
|
|
|
|
|
addRetiredIdentity(identity: RetiredIdentity): Promise<void>;
|
|
|
|
|
|
|
|
|
|
/** Get all retired identities (for grace-period decryption) */
|
|
|
|
|
getRetiredIdentities(): Promise<RetiredIdentity[]>;
|
|
|
|
|
|
|
|
|
|
/** Remove retired identities older than the given timestamp */
|
|
|
|
|
pruneRetiredIdentities(olderThan: number): Promise<void>;
|
feat: Shade E2EE library — M1-M3 complete
Signal Protocol implementation with full X3DH + Double Ratchet:
- M1: Core types, CryptoProvider interface, KDF chain functions,
SubtleCrypto+noble/curves provider, MemoryStorage
- M2: X3DH key agreement (identity keys, signed prekeys, one-time
prekeys, bundle processing for both initiator and responder)
- M3: Double Ratchet (symmetric-key ratchet, DH ratchet, skipped
message key cache, out-of-order delivery, AAD-bound headers)
68 tests, 0 failures — including full integration test of
X3DH handshake → Double Ratchet conversation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:08:19 +02:00
|
|
|
}
|