Answers Vyvern FR shade-ws-streaming-ratchet.md with a first-class
streaming-session API rather than the documented-contract fallback.
The Double-Ratchet crypto was already safe for high-frequency
one-directional use; the send/receive wrapper was not (per-frame
saveSession keystore write; shared per-peer mutex + single stored
session row coupling reuse to the HTTP path).
- @shade/core: stream.ts — identity-bound 3-DH seeding (X3DH-minus-
prekeys, no prekey-server round trip, mutually authenticated against
the parent session's pinned identities), bootstrapStreamSession
reusing init{Sender,Receiver}Session verbatim, in-memory-only
StreamRatchet (own op-mutex, never persisted, zeroized on close).
beginStream/acceptStream on ShadeSessionManager; Stream{Closed,
Handshake}Error; stream.opened/closed events.
- @shade/proto: STREAM_OPEN/OPEN_ACK/FRAME wire (0x31/0x32/0x33),
additive; inspectEnvelopeType extended.
- @shade/sdk: Shade.openStream/acceptStream → ShadeStream
(handshakeFrame/handleHandshake/seal/open/close), transport-
agnostic, independent of encrypt/decrypt queues + parent session,
identical server (sqlite:) and browser (IndexedDB) — touches no
storage.
- Tests: 5000-frame one-directional burst (bounded skipped keys + FS
zeroize), parent-session independence, replay/rewind rejection,
mutual-auth, proto wire round-trips. Full suite green (1159 pass).
- docs/streaming-sessions.md (R1–R7 contract); SECURITY.md matrix rows.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9.8 KiB
Security Policy
Review status
| Area | Status | Notes |
|---|---|---|
| Internal review | Done | Every mitigation in THREAT-MODEL.md is cross-linked to at least one automated test (see Threat-/test-matrix below). The matrix is enforced by tests/security/* + the cross-platform vector suite. |
| Independent code review | Pending | Targeted for V4.0. No external review has been completed. |
| Independent crypto review | Pending | Targeted for V4.0 alongside the audit. |
| Pen test | Pending | Targeted for V4.0. |
Read this: Shade implements the Signal Protocol primitives (X3DH + Double Ratchet) on top of
@noble/curvesand SubtleCrypto. The protocol is well-studied; the implementation has not yet been audited externally. Treat the wire format as stable but the implementation as "production-ready in trusted contexts" until V4.0 closes the audit gap. TheTHREAT-MODEL.mdcells with no test linkage are documentary, not enforced.
Reporting a Vulnerability
If you discover a security vulnerability in Shade, please report it privately by emailing the maintainer rather than opening a public issue. We take all reports seriously and will respond within 48 hours.
How to report
- Email: the maintainer email listed in the package metadata. For coordinated disclosure, prefer email over GitHub/Gitea so the issue does not become public before a fix ships.
- PGP / age: if you need encrypted reporting, ask for a key over the same email — keys are not bound to the repo to avoid key-rotation drift.
- Scope: CVE-style severity (CVSS v3.1) is appreciated but not required. A working reproduction is more valuable than a CVSS score.
When reporting, please include:
- A description of the vulnerability
- Steps to reproduce (a runnable script or test case)
- Affected versions
- Potential impact
- Any suggested mitigation
We commit to:
- Acknowledging receipt within 48 hours.
- A first-pass triage within 7 days.
- A coordinated disclosure timeline once severity is agreed; for high-severity issues we aim to ship a patched release within 30 days of triage.
What's in scope
Shade aims to provide:
- Confidentiality of message contents (only sender and intended recipient can read)
- Forward secrecy (past messages stay safe if a key is compromised later)
- Post-compromise security (future messages re-secure after compromise)
- Authentication of identity keys (signed prekey verification, replay protection)
Vulnerabilities in any of these guarantees are in scope and high priority.
What's out of scope
Shade does NOT protect against:
- A compromised endpoint (if your device is rooted, the attacker can read messages directly)
- Metadata leakage (the prekey server sees who fetches whose bundle and when)
- Traffic analysis (encrypted message sizes and timing are visible)
- A malicious prekey server distributing fake bundles (mitigation: verify safety numbers out-of-band)
- Loss of user identity verification (if users don't compare fingerprints, MITM is possible at session establishment)
These are documented in THREAT-MODEL.md.
Identity verification recommendation
When using Shade, you should provide users with a way to compare safety numbers out-of-band (in person, over a video call, or through a separate trusted channel) before treating a session as fully verified. The getIdentityFingerprint() API returns a 60-digit number formatted in 12 groups, designed for human comparison.
Cryptographic primitives
Shade uses well-established primitives:
- X25519 for Diffie-Hellman key agreement (via @noble/curves)
- Ed25519 for digital signatures (via @noble/curves)
- AES-256-GCM for symmetric encryption (via Web Crypto SubtleCrypto)
- HKDF-SHA256 for key derivation (via Web Crypto SubtleCrypto)
- HMAC-SHA256 for chain key advancement (via Web Crypto SubtleCrypto)
These match the Signal Protocol specification.
Threat-/test-matrix
This is the consolidated index that backs THREAT-MODEL.md. Every
threat-model row that claims a mitigation must point to at least one
test file here. Pull requests that add a new mitigation must add a
matrix row in the same change.
| Threat-model row | Mitigation | Test file(s) |
|---|---|---|
| § 1 Network attacker — signed writes | Ed25519 signature on every write | packages/shade-server/tests/server.test.ts |
| § 1 Network attacker — replay window | ±5 min signedAt enforcement |
packages/shade-server/tests/server.test.ts ("rejects registration with stale signedAt") |
| § 1 Network attacker — header AAD | Ratchet headers bound to ciphertext | packages/shade-core/tests/ratchet.test.ts, packages/shade-streams/tests/tamper.test.ts, packages/shade-streams/tests/aead.test.ts |
| § 1 Network attacker — forward secrecy | DH ratchet step + chain-key zeroize | packages/shade-core/tests/ratchet.test.ts, packages/shade-crypto-web/tests/hardening.test.ts |
| § 1 Network attacker — streaming sub-session FS/replay (V4.11) | Per-frame Double-Ratchet seal/open; counter-rewind & replay rejected; in-memory-only (never persisted) |
packages/shade-core/tests/stream.test.ts ("R1: replayed / rewound frame is rejected", "R2/R3: long one-directional burst stays correct and memory-bounded") |
| § 1 Network attacker — streaming handshake auth (V4.11) | Identity-bound 3-DH against parent-session-pinned identities | packages/shade-core/tests/stream.test.ts ("handshake is mutually authenticated against pinned identities") |
| § 3 Endpoint compromise — streaming sub-session isolation (V4.11) | Stream ratchet derived without touching the stored parent session; zeroized on close | packages/shade-core/tests/stream.test.ts ("R5: opening/using/closing a stream never touches the parent session", "close() zeroizes and blocks further use; idempotent") |
| § 2 Compromised prekey server — public-only storage | Prekey store never accepts a private key | packages/shade-server/tests/server.test.ts, packages/shade-storage-sqlite/tests/sqlite-prekey-store.test.ts |
| § 2 Compromised prekey server — signed replenish/delete | Per-identity Ed25519 signature | packages/shade-server/tests/server.test.ts |
| § 2 Compromised prekey server — fake-bundle detection | Out-of-band fingerprint comparison | packages/shade-core/tests/fingerprint-session.test.ts |
| § 3 Endpoint compromise — forward secrecy | Old keys not recoverable from leak | packages/shade-core/tests/ratchet.test.ts, packages/shade-crypto-web/tests/hardening.test.ts |
| § 3 Endpoint compromise — post-compromise security | First DH ratchet evicts leaked state | packages/shade-core/tests/ratchet.test.ts ("alternating messages trigger DH ratchets") |
| § 3 Endpoint compromise — memory zeroization | Buffers wiped after use | packages/shade-crypto-web/tests/hardening.test.ts ("zeroize") |
| § 3 Endpoint compromise — identity-rotation invalidates resume | Device-key bound to signing key | packages/shade-core/tests/identity-rotation.test.ts, packages/shade-transfer/tests/resume.test.ts |
| § 4 Compromised device storage — at-rest stream secrets | Resume secret AES-GCM under device-key | packages/shade-transfer/tests/resume.test.ts |
| § 4 Compromised device storage — at-rest session DB | Pending V3.2 | none yet |
| § 5 Timing side-channel — constant-time compare | XOR accumulator | packages/shade-crypto-web/tests/hardening.test.ts ("timing variance stays bounded across mismatch positions") |
| § 5 Timing side-channel — primitives | SubtleCrypto + @noble/curves | packages/shade-crypto-web/tests/provider.test.ts, packages/shade-streams/tests/aead.test.ts |
| § 6 DoS — per-IP register/bundle rate limit | Token bucket per IP | packages/shade-server/tests/rate-limit.test.ts |
| § 6 DoS — per-identity replenish/delete rate limit | Token bucket per identity | packages/shade-server/tests/rate-limit.test.ts |
| § 6 DoS — body size cap (64 KiB) | Hono middleware | packages/shade-server/tests/server.test.ts |
| § 6 DoS — address validation | Regex + NFKC + length | packages/shade-server/tests/server.test.ts |
§ 6 DoS — per-sender ops/byte quota (@shade/files) |
RateLimiter token bucket | packages/shade-files/tests/security/quota.test.ts |
§ 6 DoS — replay protection (@shade/files) |
Idempotency cache | packages/shade-files/tests/security/replay.test.ts |
§ 6 DoS — fingerprint gate (@shade/files) |
Per-sender trust check | packages/shade-files/tests/security/fingerprint-gate.test.ts |
§ 6 DoS — tampered envelope reject (@shade/files) |
AEAD reject | packages/shade-files/tests/security/tampered-envelope.test.ts |
| § 8a Recovery — k-1 collusion impossible | Shamir Secret Sharing over GF(2^8) | packages/shade-recovery/tests/shamir.test.ts, packages/shade-recovery/tests/adversarial.test.ts |
| § 8b Recovery — forged share rejected | AES-GCM tag on backup blob + subset-search | packages/shade-recovery/tests/adversarial.test.ts ("a corrupted share never authenticates against the backup AEAD tag") |
| § 8c Recovery — guardian OOB-fingerprint gate | Two-checkbox <RecoveryApprove /> + decline propagation |
packages/shade-recovery/tests/adversarial.test.ts ("approve handler that REJECTS a wrong fingerprint never sends a grant", "throwing approve handler counts as decline with descriptive reason") |
| § 9 Cross-sender X3DH state corruption | initReceiverSession copies keypair |
packages/shade-core/tests/ratchet.test.ts ("does not mutate the caller-provided keypair after a DH ratchet step"), packages/shade-recovery/tests/integration.test.ts |
If you add a new mitigation, add a row here in the same PR — the threat model is the contract; this matrix is the proof.