Security & Trust

Even if we wanted to, we couldn't read your backups.

Our database leaked tomorrow? Every backup is still unreadable. Zero-knowledge is not a marketing label here — it's a design constraint that makes the convenient path impossible.

Encryption architecture

We use age (X25519 asymmetric encryption), not passphrase-symmetric encryption. The distinction matters: naive zero-knowledge designs encrypt each backup with a passphrase — but cron can't type a passphrase, so the passphrase ends up stored in plaintext on disk, which defeats the point.

  1. At setup, the skill generates an age keypair on the user's machine.
  2. The public key is stored in local config and optionally mirrored to our server for dashboard display. It can only encrypt.
  3. Every backup is encrypted to the public key. Cron needs no secrets to encrypt — a compromised or unattended machine can create backups but a stolen upload can never be decrypted without the private key.
  4. The private key (AGE-SECRET-KEY-1...) is shown to the user exactly once, stored locally at ~/.hermes/hermesbackup/age-identity.txt (chmod 600).
  5. Recovery kit: setup prints private key + account email + restore one-liner. Losing the machine and the key means the backups are permanently unreadable. That is the deal.

What the server ever sees

Zero-knowledge does not mean zero metadata. Here is exactly what we can and cannot see.

What HermesBackup stores

  • Ciphertext blobs, sizes, SHA-256metadata
  • Labels and timestampsmetadata
  • Device names, public key (display only)metadata
  • Hashed device tokensauth

What we never see

  • Plaintext backupsnever uploaded
  • Private keysnever uploaded
  • Passphrasesnever transmitted
  • Ability to decryptyours alone

Transport & platform security

Every link in the chain is designed so that a single compromise is survivable.

  • All traffic TLS 1.3 via Cloudflare.
  • Device tokens: 256-bit random, stored hashed (SHA-256) in D1, scoped per device, revocable from the dashboard.
  • Presigned URLs expire in 15 minutes (PUT) / 5 minutes (GET). Backup bytes never stream through the Worker.
  • R2 bucket is private — no public access.
  • Dashboard sessions via Clerk with MFA available.
  • Webhook endpoints (Stripe, Emailit) verify signatures.
  • Rate limiting per device token via Workers KV. init requests rate-limited to 30/hour/device.
  • Object keys are u/{user_id}/{backup_id}.age — no user-supplied path components, no traversal surface.
  • Cloudflare infrastructure logs: the Worker runs on Cloudflare's edge network. Worker code is written to log only aggregate counts and error types — no request-context metadata is emitted to console.

Abuse controls

Blobs are opaque — we can't inspect content. Caps on count, size, and cadence are the enforcement surface. They're generous for real backup use while making generic file-hosting impractical.

Limit Free Pro
Max object size2 GB2 GB
Uploads per day424
Stored objects60500
Init rate limit30/hour/device30/hour/device

Threat model & mitigations

Transparency is the brand. Below is the full set of threats we model and how each is mitigated.

Server database leak Mitigated

D1 contains user metadata, device token hashes, backup metadata, and ciphertext SHA-256s — but no plaintext, no keys. A leaked D1 dump is useless for reading backups.

R2 bucket compromise Mitigated

R2 stores ciphertext blobs. An attacker with direct R2 access gets opaque .age files they cannot decrypt. Private bucket, no public access.

Worker compromise Mitigated

The Worker never sees backup bytes (presigned URLs only) and has no decryption capability. Even a fully compromised Worker cannot decrypt existing blobs.

Device token theft Medium

Tokens stored as SHA-256 hashes. User can revoke any device from the dashboard. Pairing codes expire in 10 minutes. Tokens are per-device, not global.

Private key loss By design

Recovery kit printed at setup, dashboard banner persists until confirmation. This is the cost of true zero-knowledge — we don't backdoor it.

MITM on presigned URL Low

TLS 1.3 everywhere. URLs expire in 15 min (PUT) / 5 min (GET). R2 verifies the signature.

Tampered ciphertext upload Mitigated

POST /backups/{id}/complete makes the Worker HEAD the R2 object and verify size + SHA-256. Mismatch marks the backup as failed.

Path traversal Mitigated

R2 object keys are server-generated: u/{user_id}/{backup_id}.age. No user-supplied path components are accepted.

Quota bypass Mitigated

init checks quota before issuing presigned URL. 507 returned if exceeded. Server never silently deletes.

Free tier abuse for file hosting Mitigated

Max object 2 GB, free 4 uploads/day + 60 objects. Count/size/cadence caps make generic file-hosting impractical.

Webhook forgery Mitigated

Both /webhooks/stripe and /webhooks/emailit verify signatures before acting.

Recovery-key email interception Low

We never email the private key. Recovery kit is printed at setup. Reminder emails pointedly do not contain the key.

Audit status

A full internal audit (July 2026) verified the core zero-knowledge invariants against the Worker and client implementations. 18 issues found (2 HIGH, 6 MEDIUM, 10 LOW), 5 remediation cards created. Core invariants hold: no plaintext/keys/passphrases reach the server. A third-party security audit is planned post-launch.

Warrant canary

As of July 26, 2026, HermesBackup has received zero government requests for user data, backdoors, or key escrow. This statement will be updated weekly. Its absence is evidence.

Found a vulnerability?

Email [email protected] with details and reproduction steps. We respond within 72 hours and credit responsible disclosure.