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>
8.9 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 |
| § 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.