Files
Shade/SECURITY.md

132 lines
9.8 KiB
Markdown
Raw Normal View History

# 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](#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/curves` and 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. The `THREAT-MODEL.md` cells 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
1. **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.
2. **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.
3. **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](./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` |
release(v4.11.0): streaming Double-Ratchet sub-sessions (ShadeStream) 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>
2026-05-15 11:29:09 +02:00
| § 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.