Skip to content

Security

The security model

This page is the whole story: what is encrypted, with which primitives, what the server can see and where the design stops protecting you. Nothing here is a claim we expect you to take on faith, there is a short section on how to check it.

What is encrypted, and how

The unit of encryption is the whole file. hush push encrypts the bytes of your .env into a single authenticated blob with a key derived from your passphrase on your machine, and only then does a request exist.

libsodium primitives used by hush and what each one does
JobPrimitiveDetails
Passphrase → master keyArgon2id (interactive preset)Random 16-byte salt, 32-byte output. The parameters travel with the ciphertext, so an old blob can always be opened again.
Whole-file encryptionXChaCha20-Poly130524-byte nonce per version, 32-byte project key. Authenticated: a modified blob fails to decrypt instead of decrypting to garbage.
Bound context (AAD)projectId|environment|version|keyVersionCiphertext is tied to one exact slot. Moving a blob to another environment, or replaying an old version over a new one, fails the authentication check.
Device identityX25519 keypairGenerated on your machine. The private key exists in the clear only while unlocked; at rest it is sealed with your passphrase-derived key.
Sharing a projectcrypto_box_sealThe project key is sealed to each member's public key. There is no shared secret and no key-agreement handshake for the server to observe or mediate.
RecoveryRandom 32-byte keyShown once at setup. It seals the same device key, so it opens your data without your passphrase, and it is the only other way in.

The largest accepted version is 1024 KB of ciphertext. Everything uses libsodium, there is no crypto written for this project, and no fallback to anything weaker.

Key hierarchy

A passphrase is not your data key. It unlocks a device keypair, and that keypair unwraps per-project keys. Five steps, each with one job:

passphrase    ──Argon2id──▶  master key
master key    ──seals─────▶  device key  (X25519 keypair)
recovery key  ──seals─────▶  device key
device key    ──unseals───▶  project key (32 bytes)
project key   ──encrypts──▶  version ciphertext (XChaCha20-Poly1305)
  • Your passphrase never leaves the machine. Argon2id turns it into a master key that seals your X25519 private key at rest.
  • The recovery key is a second, random sealing of the same private key. Two independent ways in, no way to reset either.
  • The project key does the actual file encryption, and is sealed with crypto_box_seal to every member, so sharing a project is per person, not per password.
  • Rotation replaces the project key and re-seals it to the remaining members. New versions are unreadable to the removed device; old versions are not retroactively protected.

Where the bytes go

Encrypt on one machine, store ciphertext, decrypt on another. The server is a relay that happens to keep a copy, it never holds the material that would open one.

Candour

What the server can see.

We would rather hand you the whole list than let you guess at it. Everything in the right column is already unreadable before the request leaves your machine.

Sees

  • Project and environment names
  • Size of every stored version
  • Timestamps, created, pushed, pulled
  • Which account pushed which version
  • Your GitHub handle and avatar URL

Never sees

  • Variable names
  • Variable values
  • The project key
  • Your private key
  • Your passphrase
  • The recovery key

The left column is what audits, rate limits and billing need. It is not enough to reconstruct a single secret, there is no code path on the server that holds one.

Read the security model

Threat model, and where we stop

A security page that only lists strengths is marketing. These are the limits we design around, in plain language:

  • Metadata is not secret. Project names, environment names, sizes, timestamps and who pushed are visible to us and to anyone who reads the database. An observer of the traffic sees when a device syncs and roughly how much. Name your projects accordingly.
  • A compromised server can serve malicious JavaScript. Anything a browser downloads can be swapped if this host is compromised, that is true of every web app, including this one. It is why the CLI is the recommended way to decrypt: installed code does not change under you, and you can read it in the repository.
  • Browser decryption is opt-in and weaker. Long-lived keys in a tab, plus every browser extension on your machine, is a larger surface than a keyring on your own disk. It is a convenience, not an upgrade.
  • A revoked member keeps what they already pulled. Rotation changes the key for future versions; it cannot reach into somebody else's laptop. Treat the moment of revocation as the last day you trust that person.
  • Lose your passphrase and your recovery key, and the data is unrecoverable. Those two secrets are the only ways into your device key. Lose both and there is no reset link, no escrow and no support override, any of those would be a backdoor into everyone's files.
  • Your endpoint is the perimeter. Clipboard history, backups, shell history and malware on your own machine sit outside our reach. hush reduces how far a secret travels, it cannot stop a laptop from leaking one.

Verify us

Everything below runs from your own terminal against the live API. If one of these checks contradicts this page, treat it as a bug and tell us via the repository.

  1. 1. Read the code.

    The server is small and public: one route writes ciphertext rows, and no route derives or holds a key.

  2. 2. Check the token list.

    Only a SHA-256 hash of a CLI token is stored, so a database dump cannot be replayed as a login. The API hands back names, prefixes and timestamps, never a usable secret.

    curl -s https://api.hush.hanx.pro/api/me/tokens \
      -H "Authorization: Bearer hush_…"
  3. 3. Push a marker and grep for it.

    Put a unique string in your .env, push, then read the version back through the API. You get base64 ciphertext and a content hash, the marker is nowhere. The same test works against the version list and the audit log.

    hush push                       # .env contains MARKER_7f3a1c
    curl -s https://api.hush.hanx.pro/api/projects/api/environments/dev/versions/4 \
      -H "Authorization: Bearer hush_…" | grep -c MARKER_7f3a1c
    # → 0
  4. 4. Watch what the device flow stores.

    The device record is a code, a device name, an IP, a status and an expiry, exactly what the approval page shows you, and nothing else. Codes expire after 10 minutes.

  5. 5. Watch the network while decrypting.

    Your passphrase is used by the local Argon2id derivation. Open the network panel and pull a version: no request carries it, and there is no key-derivation endpoint to call.

Short version: we can lose your metadata, and a compromised host can lie to your browser. We cannot read your values, and nobody, including us, can reset a lost passphrase and recovery key.