Skip to content

Repository files navigation

binthere wordmark

Say it once. Sealed.

Zero-knowledge, end-to-end encrypted notes that disappear after one read.

Try it live · Install the CLI · Documentation · Report a bug

binthere is a zero-knowledge, end-to-end encrypted pastebin. Write a note, get a link, share it — and the note self-destructs the moment it's read. Your browser encrypts everything with AES-256-GCM before it leaves your device, so the server only ever holds ciphertext it can't read. Think of it as a self-destructing envelope for text: secrets, credentials, a private message, a snippet of code.

Node.js JavaScript HTML CSS ESLint

Why binthere?

binthere is a clean-room rebuild inspired by PrivateBin's zero-knowledge model — modern Web Crypto, a strict CSP, atomic burn-after-read, and a real test suite, with the ~700 KB of jQuery/Bootstrap/zlib-WASM stripped out. It runs as a single Cloudflare Worker (Static Assets + KV + a Durable Object), so hosting is cheap and there is no server to maintain.

  • No accounts, no tracking. Paste, share, done. There is nothing to sign up for and no analytics watching you do it.
  • Nobody can recover a lost link. Not even the operator — there is no key to look up and no index of pastes. The link is the only copy of the key, by design.
  • Honest limits. The server still sees IPs, timings, and ciphertext sizes (it's private, not anonymous), and like all in-browser crypto it trusts the code the site serves — the full threat model is in SECURITY.md and summarized under Limitations.

Pick binthere if you want a paste service you can deploy in one command with nothing to patch, back up, or keep online yourself.

How it works

The whole design rests on one trick: where the decryption key lives. It travels in the URL fragment — the part after # — which browsers never send to any server.

you type ──▶ browser encrypts (AES-256-GCM) ──▶ Worker stores ciphertext only
                     │                                      │
              key stays in the URL #fragment          KV or Durable Object
                     │                                      │
recipient opens link ─▶ browser fetches ciphertext ─▶ browser decrypts ─▶ plaintext
  1. You write a note. Your browser generates a random 256-bit key and encrypts the note locally with AES-256-GCM — before any network request is made.
  2. Only ciphertext is uploaded. The key is never sent; it's appended to your link after #. The server stores an opaque blob it has no way to read.
  3. You share the link. It carries both the note's id and the key (…/p/<id>#<key>) — the link is the capability to read the note. Optionally, add a password: it's mixed into the key derivation, so neither the link nor the password alone can decrypt.
  4. The recipient opens it. Their browser fetches the ciphertext, reads the key from the fragment, and decrypts locally. The server never sees plaintext at any point.

Every note is one-time view: the first reader atomically consumes it (exactly one winner, even under simultaneous clicks — a Durable Object guarantees it), and everyone after gets 410 Gone. Unread notes self-delete after 24 hours regardless.

Features

Feature Details
Zero-knowledge Encryption and decryption happen only on your device (browser or CLI); the server stores opaque ciphertext and non-secret metadata.
Optional password Layered on top of the URL key — neither alone can decrypt.
Burn-after-read Every note is a strict, atomic single-consumer read (Durable Object). The first reader gets it; everyone else gets 410 Gone.
Auto-expiry Notes delete themselves after 24 hours.
Safe rendering Auto-detected syntax highlighting and a safe Markdown subset (no raw HTML, sanitized links). All rendering is DOM-construction only — never innerHTML.
Sharing tools Copy link, QR code, delete link.
Minimal surface Strict CSP, self-hosted fonts, no third-party scripts, no analytics, no accounts.

Note

The wire format supports the full expiry range (5 minutes–1 year or never) and non-burn pastes; the current UI simply fixes 24 h + one-time view, so older links keep working.

How it compares

All of these are solid zero-knowledge paste/secret tools — the difference is mostly in how they are hosted and what they optimize for:

Project Server Storage Distinguishing traits
binthere Cloudflare Worker (serverless, no origin server) Workers KV + Durable Object Frozen spec with test vectors, atomic burn-after-read, no client framework or build step
PrivateBin PHP Filesystem / DB / S3 Mature, many formats, discussions, i18n
Yopass Go Memcached / Redis Secret-sharing focus, CLI client
cryptgeon Rust Redis File sharing, view limits

Getting started

Requires Node.js ≥ 20 (.nvmrc pins 22).

npm install
npm run dev      # wrangler dev → http://127.0.0.1:8787

KV, the Durable Object, and rate limiting are all emulated locally — no Cloudflare account needed for development.

Command Description
npm run dev Local dev server at http://127.0.0.1:8787
npm test Full Vitest suite: Worker/frontend in the real workerd runtime, then the CLI suite in Node
npm run test:cli Just the CLI suite (cli/, plain Node environment)
npm run test:watch Tests in watch mode
npm run test:coverage Tests with coverage report
npm run lint ESLint 9 (flat config)
npm run kv:create Create the PASTES KV namespace (+ preview)
npm run deploy Deploy to Cloudflare

CI runs lint, a byte-for-byte test-vector diff, and the full suite.

CLI

An official command-line client lives in cli/ and is published to npm as binthere. It implements the same frozen protocol as the web client — encryption happens locally, only ciphertext is uploaded, and notes have the same one read / 24 hours lifecycle as the website. Zero runtime dependencies (Node ≥ 20 built-ins only).

npm install -g binthere    # or try it without installing anything: npx binthere

A bare binthere opens an interactive full-screen menu; it also composes in pipelines (git diff | npx binthere prints a share URL on stdout). See cli/README.md for the full command reference, interactive-mode tour, and security notes.

Self-hosting

You can run your own binthere — nothing about the design ties it to the public instance, and there is no managed service in the loop. Because it's a single Cloudflare Worker (Static Assets + KV + a Durable Object), it's optimized for Cloudflare Workers and one of its biggest advantages is cost: a complete instance fits inside Cloudflare's free tier, so you can host binthere completely free.

Deploy to Cloudflare

The button above clones the repo and provisions everything declared in wrangler.toml — the static assets, the PASTES KV binding, the BurnPaste Durable Object + migration, and the CREATE_RL rate limiter — on your own Cloudflare account. The importer creates fresh resources and rewrites the resource ids in your copy of the config; the checked-in ids belong to the origin deployment and are identifiers, not secrets.

If the one-click path ever misbehaves, the manual route below is the guaranteed fallback:

npm run kv:create        # create your own PASTES KV namespace (+ preview)
# paste the printed id / preview_id into wrangler.toml
npm run deploy           # creates the Worker, Durable Object, and rate limiter
Self-hosting checklist
  • Replace the KV id / preview_id in wrangler.toml with your own (a pristine template is in wrangler.toml.example).
  • Update the hardcoded canonical URLs: og:url / og:image in public/index.html and Canonical in public/.well-known/security.txt point at binthere.gaury.dev; the footer and security.txt Policy point at github.com/nxfu/binthere.
  • npm run dev works with placeholder KV ids — KV is emulated locally.
  • Cost note on never expiry: the official clients always create 24-hour one-time notes, but the wire format (and the API) accepts expire: "never". Such a paste is stored with no KV TTL and no Durable Object alarm — an unread burn note kept forever carries a small perpetual cost under SQLite-backed DO storage billing. If you expose never to third-party clients, decide whether to cap it or accept the standing cost.

Architecture

Piece Role
Static Assets (public/) SPA frontend, served directly by the Worker
Worker (src/index.js) /api/* paste API — stores ciphertext, enforces size/rate/burn
KV (PASTES) Normal pastes, with native TTL expiry
Durable Object (BurnPaste) Burn-after-read pastes, atomic single-consumer
Rate Limiting binding Abuse mitigation on paste creation (fail-open)

See ARCHITECTURE.md for the request path and SPEC.md for the exact cryptographic protocol and paste format v1, including frozen test vectors.

HTTP API

The API only ever handles ciphertext — encryption happens in the client before POST, and the key fragment never appears in any request. Full details in SPEC.md §10.

Method & path Purpose Success Errors
POST /api/paste Create a paste (format v1 JSON) 201 400 invalid · 413 too large · 429 rate-limited
GET /api/paste/:id Fetch a paste (consumes a burn) 200 404 missing/expired · 410 burned
GET /api/paste/:id?meta=1 Peek a burn head without consuming 200 404 missing · 410 burned/expired
DELETE /api/paste/:id Delete, with X-Delete-Token header 200 400 missing token · 403 wrong token · 404 missing

The delete token travels in a header — never in the URL — so it cannot land in request logs; the server stores and compares only its SHA-256.

Project layout
public/            static frontend (CSP-clean; served by Workers Static Assets)
  index.html  css/styles.css  js/*.js  fonts/*.woff2  img/ (favicon.svg + png fallbacks + wordmark[-dark].svg)
  _headers  robots.txt  favicon.ico  opengraph.png  .well-known/security.txt
src/
  index.js         Worker: /api/paste routing + asset fallback
  burn-do.js       BurnPaste Durable Object (atomic burn-after-read)
  lib/             ids, storage routing, rate-limit wrapper
test/              vitest suites (run in workerd) + genvectors.mjs (vector regenerator)
                   + vectors.expected.txt (pinned vector output, diffed in CI)
tools/             verify-vectors.py — independent Python cross-check of the frozen vectors
cli/               the npm-published CLI client (own package.json + Node-environment tests;
                   vendor/ mirrors public/js/{bytes,format,crypto,qrcode}.js, drift-tested)
SPEC.md SECURITY.md ARCHITECTURE.md
CHANGELOG.md CONTRIBUTING.md CODE_OF_CONDUCT.md LICENSE

public/js/{bytes,crypto,format,markdown}.js are shared: the browser imports them as static assets and the Worker bundles the same files, so the paste format has a single source of truth.

Limitations

Most of these are deliberate scope choices, not bugs. Know them before relying on binthere:

  • Not anonymous or metadata-free. The server sees IP, timing, ciphertext size, and the non-secret adata (IVs, KDF params, format flags). It only cannot read your plaintext (SECURITY.md §3).
  • No protection from a compromised deployment. Decryption runs in JavaScript the server delivers, so a malicious or hacked host could serve code that leaks your key. In-browser E2E encryption trusts the origin (SECURITY.md §4).
  • Lose the link, lose the note. No accounts, no server-side index — the id + key exist only in the URL you share. Nobody, including you, can recover or list pastes.
More limitations
  • Burn passwords can be brute-forced offline. The non-consuming peek returns the wrapped key so a password can be checked before the single read — someone who already has the URL secret can guess a weak password without burning the note. Use a strong password (SPEC.md §8 documents the trade-off).
  • Password KDF is PBKDF2-SHA256 (310k iterations), not a memory-hard KDF. Argon2id is on the roadmap.
  • The UI fixes expiry at 24 h and one-time view. The wire format supports more; the controls are just hidden.
  • English only.
  • The rate limiter fails open — it is abuse mitigation, not access control.
  • Canonical URLs are hardcoded to the origin deployment; update them when self-hosting (see Self-hosting).

FAQ

Can the operator read my notes?

No. Content is encrypted with AES-256-GCM in your browser before upload; the server stores only ciphertext and non-secret metadata. The decryption key lives in the URL fragment, which browsers never send to the server. What the server does see (IP, timing, sizes) is spelled out in SECURITY.md §3.

I lost the link — can the note be recovered?

No. There are no accounts and no server-side index; the paste id and decryption key exist only in the URL. Without it, the ciphertext is unrecoverable — by design.

Why does my link say "expired or was already opened"?

Every note is one-time view: the first reader atomically consumes it, and everyone after (including you, if you open your own link first) gets 410 Gone. Notes also self-delete after 24 hours even if never opened.

Does adding a password make the link safe to send in the clear?

It helps — the password is mixed into the key derivation, so the link alone cannot decrypt. But someone holding the link can test passwords offline without burning the note, so a weak password only slows them down. Use a strong password and send it over a different channel (SPEC.md §8).

What does the recipient need?

Just the link and any modern browser — Web Crypto (SubtleCrypto) is the only requirement. No account, extension, or app.

Can I create pastes from a script or CLI?

Yes — the official CLI is published to npm: npm install -g binthere (or npx binthere). It speaks the same frozen protocol as the web client and is tested against the same vectors; see the CLI section. Third-party clients are possible too: the HTTP API accepts only ciphertext in paste format v1, and the frozen test vectors in SPEC.md make an independent implementation verifiable.

Roadmap

Roughly in priority order:

  • Official CLI client — shipped; on npm as binthere (see CLI)
  • Argon2id as a versioned password-KDF option alongside PBKDF2 (spec-first: vectors before code)
  • File attachments — encrypted binary blobs with size limits (likely R2 for large files)
  • Headless-browser CSP + render test (Playwright) in CI, asserting zero CSP violations across the create/view/burn flows

Security

binthere is a security-sensitive cryptographic application. The threat model, explicit non-goals, and vulnerability-reporting process are in SECURITY.md; the frozen protocol and paste format live in SPEC.md.

Important

Report suspected vulnerabilities privately to nxfu@proton.me (see SECURITY.md §8). Do not open a public issue with exploit details.

Contributing

Issues and PRs are welcome — see CONTRIBUTING.md. Two hard rules:

  1. Crypto is spec-first. Any change to the protocol, paste format, or canonical AAD must update SPEC.md first — never silently — then regenerate the frozen vectors with node test/genvectors.mjs (and refresh test/vectors.expected.txt, which CI diffs byte-for-byte). Never hand-edit the pinned hexes in test/crypto.test.js.
  2. Keep the CSP strict and rendering XSS-safe. No inline styles/scripts, no CDNs, no innerHTML on user content. New rendering paths need a case in test/markdown.test.js.

Run npm run lint and npm test before opening a PR; all suites run in the real workerd runtime.

Tech stack

  • Cloudflare Workers — Static Assets, KV, Durable Objects, native rate limiting
  • Vanilla JavaScript (native ES modules) — no framework, no bundler for the frontend
  • Web Crypto API — AES-256-GCM, PBKDF2-SHA256
  • Vitest — Worker suites in the real workerd runtime, CLI suites in Node
  • ESLint 9 — flat config

Acknowledgements

License

MIT © 2026 nxfu


Built to be shared once and forgotten.
If binthere is useful to you, consider giving it a ⭐ — it helps others find it.

About

A zero-knowledge, end-to-end encrypted pastebin. Notes are encrypted client-side with AES-256-GCM, self-destruct after one view or 24h, and the server never sees the decryption key.

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

Contributors

Languages