ASSURANCE.md
Rama main · réplica de lectura del repositorio original.
# Security assurance case
This is the argument for why CLIPLINK meets its security requirements. It is
also a map of where each claim is enforced, so a reviewer can check it.
[SECURITY.md](SECURITY.md) is the user-facing half: what to expect, and how to
report a problem. When the two disagree, fix whichever is wrong.
## Security requirements
1. **Confidentiality of private rooms.** Nobody without the room key can read clip text, device names, file names, file sizes or WebRTC session descriptions, and that includes the server operator.
2. **Integrity.** A tampered clip or signaling message is rejected, not shown. A ciphertext cannot be moved from one room into another, and the sender named on a clip cannot be moved onto a different clip.
3. **No file bytes on the server.** Files travel peer to peer and are never relayed, buffered or stored server-side.
4. **Bounded resources.** One client cannot make the server hold unbounded data or keep a room alive forever.
5. **No execution of untrusted input.** Clip text and file metadata are rendered as data, never as markup or code.
6. **Deletion is for key holders.** Nobody without the room key can delete a clip from a private room; the room code alone is not enough. A one-time clip is deleted by the device that reads it, and the server is trusted to carry that deletion out, not to decide it — a server that withholds it keeps ciphertext it still cannot read.
Open rooms deliberately give up requirement 1 with respect to the operator, since their key is derived from the room code. SECURITY.md says so to users.
## Threat model and trust boundaries
```
browser / CLI ──TLS──▶ Next.js routes + WebSocket ──TLS──▶ Redis (Upstash)
(trusted: (untrusted for content: (untrusted:
holds the key) sees ciphertext, codes, stores ciphertext
peer IDs, sizes, timing) and metadata)
browser ◀──── WebRTC data channel (DTLS) ────▶ browser
```
- **Boundary 1 — the device.** The key is generated with `crypto.getRandomValues`, kept in memory, and carried only in the URL fragment, which browsers never send to the server. Everything past this boundary sees only ciphertext.
- **Boundary 2 — the server.** Every request is untrusted input. The server authenticates nobody (rooms are capability-by-code) and can read nothing in a private room.
- **Boundary 3 — the peer.** The other device holds the key, so it is trusted with the content but not with the protocol. Every signal it sends is parsed and bounded before use.
Attackers considered: another client in the same room, a client guessing codes, a network attacker, and a compromised or curious server or storage operator. Out of scope: a compromised device, and a malicious browser or extension.
## How each requirement is met
| Requirement | Mechanism | Where |
| --- | --- | --- |
| 1 | AES-GCM-256 through Web Crypto, with subkeys for clips and for signaling derived by HKDF-SHA-256. The key never enters a path, query string or request body. The server holds only a one-way key fingerprint. | `packages/cliplink/src/crypto.ts` |
| 2 | GCM authentication with the room code as additional data, and a fresh random 96-bit IV per message. Ciphertexts from earlier builds are pinned by tests, so the format cannot drift unnoticed. | `packages/cliplink/src/crypto.ts`, `packages/cliplink/test/crypto.test.ts` |
| 3 | The socket route relays signaling envelopes only, capped by `maxPayload` at `MAX_SIGNAL_BYTES` (24 KiB). File bytes move over a WebRTC data channel between peers. | `app/rooms/[code]/socket/route.ts`, `packages/rtc-file-transfer` |
| 4 | Server-side validation of every field (room code, IDs, ciphertext shape and size, TTL bounds). Token-bucket rate limits per IP. Room TTL between 1 and 24 hours, refreshed only on write. | `packages/cliplink/src/validation.ts`, `packages/cliplink/src/protocol.ts`, `lib/cliplink/rate-limit.ts`, `lib/cliplink/storage.ts` |
| 5 | React renders clip text as text. No `dangerouslySetInnerHTML`, `innerHTML` or `eval` anywhere in the app or packages. Incoming signals are rebuilt from known fields only by `parseFileSignal`. | `components/`, `packages/rtc-file-transfer/src/parse.ts` |
## Secure design principles applied
- **Economy of mechanism.** There is one implementation of the crypto, the wire types and the transports, shared by the app and the CLI. See [`@thebkht/cliplink`](packages/cliplink).
- **Fail-safe defaults.** A room joined without its key opens locked: content is unreadable and sending is disabled. Malformed input is rejected, not repaired.
- **Complete mediation.** Every clip and every socket message passes the validators on every request. Nothing is trusted because of an earlier check.
- **Least privilege.** CI tokens are read-only by default. The job that publishes to npm cannot write to the repository, and the job that creates releases cannot publish.
- **Separation of privilege.** Publishing needs a pushed tag and a human approval on the `npm-publish` environment.
- **Minimal data.** No accounts, no plaintext, no file bytes, and data expires by TTL.
- **Least common mechanism.** Rate-limit buckets and pub/sub channels are keyed per room and per IP.
- **Open design.** The protocol and the threat model are public, and security does not depend on keeping the design secret.
## Common weaknesses countered
| Weakness (CWE / OWASP) | Countermeasure |
| --- | --- |
| Injection, XSS (CWE-79) | React escaping; no raw HTML; strict ciphertext pattern `^v1\.[A-Za-z0-9_-]+$` on the server; a Content-Security-Policy that allows scripts only by a per-request nonce (`proxy.ts`) |
| SSRF (CWE-918) | The server's one outbound fetch goes to a fixed URL; no user-controlled URL is ever fetched |
| Weak randomness (CWE-330) | Keys, IVs and room codes come from the CSPRNG; `Math.random` is used only for non-secret transfer IDs when `randomUUID` is unavailable |
| Broken crypto (CWE-327) | Only published primitives through Web Crypto; no custom crypto, MD5, SHA-1 or ECB |
| Key exposure in logs (CWE-532) | The key lives only in the URL fragment and is never in a request line |
| Resource exhaustion (CWE-400) | Payload caps, rate limits and TTL bounds, as above |
| Cross-room replay | The room code is GCM additional data |
| Supply chain | Lockfile installs, actions pinned by SHA, Dependabot with a 3-day cooldown, dependency review on PRs, npm provenance, signed release tarballs |
## Known limits
These are accepted and documented rather than countered:
- The rate limiter fails open when Redis is unreachable, choosing availability over strict limiting.
- A malicious server can replay or reorder signaling messages.
- WebRTC shows each peer the other's IP address.
- Open rooms are not end-to-end encrypted.
- The Content-Security-Policy allows inline styles: a dependency injects a stylesheet that cannot carry the nonce, and several components set `style` attributes, which a nonce does not cover.
## Verification
- **Tests:** the package test suites pin the crypto format, validation and protocol compatibility (`pnpm -F @thebkht/cliplink test`, `pnpm -F @thebkht/rtc-file-transfer test`). `packages/cliplink/test/validation.fuzz.test.ts` fuzzes the validators.
- **Static analysis:** CodeQL runs on every push and pull request.
- **Supply chain:** OpenSSF Scorecard runs weekly.