Skip to content

yerofey/secred.link

Repository files navigation

secred.link

Secure, expiring secret sharing: you write content (Markdown or rich text), optionally attach a file and protect it with a passphrase, then share a link. The server never sees plaintext. Encryption runs in the browser; the Cloudflare Worker stores only ciphertext, verification blobs, hashed keys, and TTL metadata.


Features

  • Client-side encryption — Text-only secrets use v3.j. (PBKDF2 + AES-GCM). Secrets with a file attachment use v4.j. / v4.f.: one PBKDF2 pass, then HKDF-derived subkeys for the test string, body JSON, and file. Older links may still use v3+v3 (two PBKDF2) or legacy CryptoJS (decrypt-only).
  • Password-protected secrets — Offline guessing cost is dominated by PBKDF2 iterations; users should pick strong passphrases (the UI shows rough strength hints).
  • Optional burn-after-read — Supported for text-only flows; attachments use a separate burn-token window where configured.
  • Manage link — After creating a secret, owners get a manage URL (stored briefly in localStorage) to copy the viewer link or delete the secret server-side.
  • i18n — English and Russian (apps/frontend/src/locales).

Architecture

Browser (React/Vite)
    │  HTTPS
    ▼
Cloudflare Worker (apps/backend)
    ├── Static assets ─ apps/frontend/dist (SPA)
    └── /api/*        → HTTP handler → Durable Objects
                            ├── SECRETS → SQLite-backed SecretObject (+ R2 attachment refs)
                            └── METRICS → MetricsObject (counters)

Shared logic (packages/shared) includes:

  • Cryptographic primitives and legacy compatibility
  • Zod request validation
  • API route paths, URL builders, and stable error strings
  • Types for requests/responses and stored rows

Monorepo layout

apps/frontend     React SPA (Vite, Tailwind, shadcn-style UI)
apps/backend      Worker entrypoint + Durable Objects + api/handler.ts
packages/shared   Types, crypto, validation, routes, api-errors
tests             Bun unit tests
wrangler.jsonc    Workers config (assets, DOs, R2, env vars)

Prerequisites

  • Bun (package manager and test runner)
  • Wrangler (installed via project devDependencies; use bunx wrangler or npx wrangler if needed)

Development

Install dependencies:

bun install

Run the full stack locally (builds the frontend, then starts the Worker with the development env from wrangler.jsonc):

bun run dev

Useful scripts:

Script Purpose
bun run build Production build of the frontend + TypeScript check all packages
bun run build:frontend Vite build only (apps/frontend/dist)
bun run typecheck tsc --noEmit for shared, backend, frontend
bun run test Bun tests in tests/
bun run lint Biome check
bun run format Biome format write
bun run cf:typegen Regenerate apps/backend/src/worker-configuration.d.ts after Wrangler binding changes

Preview with local persistence behavior:

bun run preview

HTTP API (Worker)

All JSON routes live under /api. The frontend uses helpers from packages/shared (apiUrl, etc.) so paths stay consistent with apps/backend/src/api/handler.ts.

Method Path Notes
GET /api/health Liveness + version info
POST /api/secrets Create secret; body includes ciphertext, testCiphertext, hashed keys
GET /api/secrets/:accessKeyHash Fetch ciphertext bundle (64-char hex id)
DELETE /api/secrets/:accessKeyHash/:manageKeyHash Owner delete
PUT /api/secrets/:accessKeyHash/attachment Upload encrypted attachment (X-Upload-Token)
GET /api/secrets/:accessKeyHash/attachment Download (optional burnToken query)
GET /api/metrics Disabled until METRICS_TOKEN is set; then requires Authorization: Bearer …
POST /api/metrics Same auth as GET; body JSON sets absolute counter values (merge). Example: {"created":1000,"requested":5000}. Omitted keys are left unchanged.

Plaintext never appears in these payloads.

Rate limiting (/api/*)

The Worker uses Cloudflare’s Rate Limiting binding before the HTTP API handler. Limits are applied per client IP (CF-Connecting-IP, then X-Forwarded-For, else a shared unknown bucket in dev).

Default policy (see ratelimits in wrangler.jsonc): 120 requests per 60 seconds per IP per namespace. Excess traffic receives 429 with JSON { "error": "Too many requests" } and a Retry-After header (60 seconds). Static assets are not rate limited. If wrangler deploy errors on the ratelimit binding, confirm your Cloudflare account supports Workers Rate Limiting.

Production and development env each declare their own ratelimits block (Wrangler does not inherit this section). Adjust simple.limit / simple.period (allowed periods are 10 or 60 seconds) and keep API_RATE_LIMIT_PERIOD_SEC in apps/backend/src/rate-limit.ts aligned with period for Retry-After.


Cloudflare configuration

wrangler.jsonc defines:

  • Worker entry: apps/backend/src/index.ts
  • Static assets directory: apps/frontend/dist
  • R2 bucket binding for attachments
  • Durable Object namespaces SECRETS and METRICS
  • ratelimits binding API_RATE_LIMITER for /api/* (see above)
  • Non-secret vars (APP_URL, ENVIRONMENT, VERSION_PREFIX, …)

Secrets (Wrangler)

# Optional: metrics endpoint bearer token
wrangler secret put METRICS_TOKEN

# Optional: authenticated migration/import endpoint
wrangler secret put MIGRATION_TOKEN

For local development you can put the same keys in a .dev.vars file (not committed).

Seeding historical metrics (e.g. after moving from a previous stack), with METRICS_TOKEN in place:

curl -sS -X POST "https://secred.link/api/metrics" \
  -H "Authorization: Bearer $METRICS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"created":12345,"requested":67890}'

Use non-negative integers; allowed keys: created, requested, burned, deleted, expired, migration_imported. Then GET /api/metrics to verify.

Deploy

bun run build
wrangler deploy

(bun run deploy runs a frontend build then wrangler deploy with the default environment.)


Security model (short)

  • Viewer links carry the raw access key in the URL fragment; anyone with the link can request ciphertext from the API. Passphrase-protected secrets depend on password strength for confidentiality.
  • The Worker stores double-hashed access/manage identifiers; it cannot derive raw keys from stored rows.
  • Additional hardening: dashboard WAF / bot rules on your hostname, dependency updates, and CSP in apps/frontend/index.html.

AI / editor assistants

Repository-specific hints for tools and agents live in AGENTS.md (layout, commands, conventions).

License / project

Private project ("private": true in package.json). Adjust this section if you open-source the repo.

About

a privacy-first secret note service

Resources

License

Stars

Watchers

Forks

Contributors