# CloakDrop

Files are encrypted in your browser before anything else happens. The key never
leaves the tab it was made in — it lives in the `#fragment` of the link, and a
fragment is the one part of a URL a browser never sends to a server.

This repository is the client. It is the whole client: every line that touches
your files or your keys is here, because a zero-knowledge claim you cannot read
is just a claim.

**Live:** <https://cloakdrop.com>

---

## What this build does and does not do

It encrypts, it stores locally, it decrypts. It does **not** yet send anything
anywhere.

A drop is written to IndexedDB in the browser that created it, so a link opens
only in that browser. The relay that will carry drops from one browser to
another is being built and its absence is the single biggest limitation of this
build. Nothing in the interface pretends otherwise, and neither does this file.

What works today:

- AES-256-GCM in 4 MB chunks, keys generated with `crypto.getRandomValues`
- filenames, sizes and types in an encrypted manifest — the storage layer never
  sees them
- an optional password on a drop: the AES key is wrapped with PBKDF2-SHA256
  over 310,000 rounds, so the link alone is not enough to open it
- an expiry you choose, enforced on open and swept from storage on next start
- a per-page Content-Security-Policy with `default-src 'none'` and a SHA-256 for
  every inline block — no `unsafe-inline` for scripts, no external requests

## Licence

None. Copyright (C) 2026 the CloakDrop authors, all rights reserved.

That is deliberate rather than an omission. The client is meant to be read —
it is one unminified file and every claim on the site can be checked against
it — but reading needs no licence, and an open one would hand out rights that
nobody has without it. This repository is private.

## Running it

There is no build step and no dependency to install — the client is static
files, with the CSS and JS inlined per page.

```
python tasks/testserver.py          # http://127.0.0.1:8900, with the real headers
```

Any static server will do, but `testserver.py` replays `_headers`, so you get
the same CSP the live site runs under. `127.0.0.1` counts as a secure context,
so WebCrypto is fully available.

## Checking it

The claims above are meant to be checked, not believed:

```
python tasks/build_headers.py       # regenerates _headers — MUST run after any inline-script change
python tasks/verify_csp.py          # every page under its real CSP, no violation, no console error
python tasks/verify_drop.py         # 17 checks against the real app, driven by Playwright
python tasks/verify_zip.py          # "Download all" — opened and compared with Python's zipfile
python tasks/verify_runde3.py       # the defects found in the third round of customer testing
python tasks/verify_live.py         # the deployed site: headers, console, stray requests
```

`verify_drop.py` is the interesting one. It does not read the interface, it
reads IndexedDB: that a deleted drop is gone with all its chunks, that an
expired one is swept, that a flipped bit in the ciphertext produces a visible
error rather than a quiet one, that a wrong password yields nothing, and that a
truncated key is reported as truncated.

The quickest check needs no tooling at all: open dev tools, watch the Network
tab while you cloak a file, and see that nothing leaves.

## Layout

```
index.html          the entire app — markup, styles and logic in one file
*.html              the standalone pages (limits, privacy, verify, …)
functions/          Cloudflare Pages middleware for the pre-launch access gate
status-worker/      status.cloakdrop.com — a separate service, on purpose
assets/             icons and the social preview
tasks/              build scripts, verifiers and working notes
```

One file for the app is a deliberate choice, not an accident of growth: it keeps
the CSP hashes honest and means there is exactly one place to look.

## Contributing

Issues and patches are welcome. Two rules that are not negotiable:

1. **No external requests.** Not a font, not an analytics pixel, not a CDN. The
   CSP says `default-src 'none'` and `connect-src 'none'`, and it says so because
   the key is in the URL.
2. **Run `python tasks/build_headers.py` after touching any inline script**, or
   the page will not execute at all. The CSP binds every block by hash.
