Pull-mode httpClient + drainer + parallel RPCs against the same peer
deteriorated after ~10s with `DecryptionError`. Two bugs combined:
- `OutboundQueue.enqueue` woke `drain` waiters with a `since=0`
snapshot, replaying already-processed events into
`Shade.acceptTransferEnvelope` → `manager.decrypt` twice. The
duplicate consumed an already-used skipped key and corrupted the
Double Ratchet receive chain.
- `ratchetDecrypt` then propagated the corruption: a same-DH
message behind the chain with no cached skipped key fell through
to `kdfChainKey` on the ahead state and rewound `chain.counter`,
permanently desyncing the chain.
Fix `OutboundQueue` to honor each waiter's `since`, and harden
`ratchetDecrypt` so any future duplicate fails cleanly without
mutating state. Adds regression coverage at all three layers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
V3.1 → V3.12 consolidated and tagged for the first GA release. Wire
format unchanged from 0.4.x — 4.0 peers interoperate with 0.4.x peers
byte-for-byte. The version bump is semantic: audit-cycle complete,
opt-in surface fully exposed, threat model refreshed for every new
surface.
Highlights:
- All 24 @shade/* packages bumped to 4.0.0 in lockstep.
- CHANGELOG 4.0.0 section is the canonical manifest of what landed.
- THREAT-MODEL extended (§10 fingerprint gates, §11 WebRTC P2P, §12
Web-Worker boundary) + residual-risks table refreshed.
- OpenAPI now covers all 27 routes: prekey, transfer, KT, inbox,
bridge, observer, /metrics, /healthz, /ready.
- MIGRATION 0.3.x → 4.0 documented + smoke-tested against
shade migrate-storage on a real SQLite DB.
- docs/audit/REVIEW-BUNDLE.md + SCOPE.md ready for external reviewer.
- scripts/soak.ts harness for the GA-stable 2-week soak window.
- All V*.md plans archived under docs/archive/ with Status: Done.
- Voice/Video carved out into V5.0; 4.0 audit focuses on the frozen
non-realtime stack.
Tests: TS 1000/1000 + Kotlin 11/11 cross-platform vectors green.
Docker: gt.zyon.no/stian/shade-prekey:4.0.0 builds and reports
version 4.0.0 on /health.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase D complete. Shade is now at parity with Signal libsignal's core
feature set.
M-Adv 1: Multi-device support (simplified Sesame)
- DeviceListManager tracks per-user device lists ("user:deviceId" addresses)
- fanOutEncrypt() sends one message to all known devices via independent
1:1 Double Ratchet sessions
- observeIncoming() auto-registers new devices from received messages
- JSON serialization for persistence
- userOfDevice/deviceIdOf address parsers
M-Adv 2: Backup and restore
- @shade/sdk exports BackupBlob format: version + salt + nonce + ciphertext
- Passphrase-derived key via HKDF (note: upgrade path to Argon2id documented)
- exportBackup()/importBackup() handle identity, prekeys, sessions, trust
- backupToString/backupFromString for single-string transport (copy/paste, QR)
- shade.exportBackup()/importBackup() convenience methods on SDK
- CLI: shade backup export <file> / shade backup restore <file>
- Rebuilds manager + transport after restore so ratchet state is consistent
M-Adv 3: Group messaging (Sender Keys)
- Per-sender chain key + Ed25519 signing key per group
- createSenderKey / buildDistribution / installDistribution for key distribution
- senderKeyEncrypt advances chain and signs ciphertext+header
- senderKeyDecrypt verifies signature then advances the sender's chain
- Out-of-order handling with bounded skip
- O(1) per message (once distributions are installed)
- Defensive ByteArray copies in distribution to prevent zeroize-across-refs
276 tests passing, 0 failures. All 13 SDK/tooling/platform/advanced
milestones complete. Shade is feature-complete for v2.0.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
M-Obs 1: Event bus in @shade/core
- ShadeEventEmitter with typed event union, ring buffer for replay
- 12 event types covering session lifecycle, ratchet operations,
prekey changes, identity rotation, trust changes
- Wired into ShadeSessionManager (zero overhead when not enabled)
- shortHash helper for safe display of public keys
- Security test: regex-checks event payloads contain no key material
M-Obs 2: Prekey server event hooks
- PrekeyServerEvents emitter mirroring core's pattern
- 5 server event types: registered, fetched, replenished, deleted, rate_limited
- Wired into all routes including the rate-limit error handler
- shortHash helper using crypto.subtle directly (no provider dep)
M-Obs 3: @shade/observer package
- StateAggregator subscribes to client + server events, builds rolling snapshot
- Hono routes: GET /api/state (snapshot), GET /api/events (SSE stream)
- Bearer token auth via SHADE_OBSERVER_TOKEN, query string for SSE
- Refuses to start with token < 16 chars (ConfigurationError)
- Static file serving for bundled dashboard at /dashboard/
- Placeholder dashboard renders when no built SPA present
220 tests passing, 0 failures.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Shade sessions and keys now survive server crashes, container restarts,
and power outages via SQLite with WAL mode.
New packages:
- @shade/storage-sqlite: SQLiteStorage (StorageProvider) + SqlitePrekeyStore
(PrekeyStore), both using bun:sqlite with auto-created tables and WAL mode
- Serialization layer in shade-core for SessionState/keys ↔ JSON/base64
Docker usage: mount volume at /data, set SHADE_DB_PATH=/data/shade-client.db
Prekey server auto-detects SHADE_PREKEY_DB_PATH for SQLite persistence
Includes crash recovery integration test: encrypt → close DB → reopen →
conversation continues seamlessly.
129 tests, 0 failures.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>