# 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` | | § 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 `` + 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.