Zeph Docs
CLI & SDK

Encryption

How push bodies are encrypted, and what the threat model does and does not cover.

End-to-end encryption is off by default, and turning it on requires Zeph Pro. The switch is in the Zeph app under Settings → Encryption; until you flip it, every push and attachment reaches the backend in plaintext. If the account later loses Pro, the server refuses the encrypted send with PRO_REQUIRED and the CLI resends the same push in the clear.

With it on, push bodies and long-body attachments are encrypted with AES-256-GCM. This host holds its own ECDH P-256 keypair in ~/.zeph/device-keys.json, generated on first use, and the private half never leaves the machine — the backend stores public keys only and rejects a private-key upload outright.

Each push is encrypted once, and its AES key is wrapped separately for every device on your account using ECDH against that device's public key. The title and URL move inside the ciphertext with the body, so the stored push carries no readable fields.

Threat model

Against a passive backend — a leaked snapshot, an operator reading the table — the stored ciphertext and wrapped keys are useless, so push contents stay private.

Three limits are worth knowing:

  • No protection from an active malicious operator. Recipient public keys come from GET /devices on that same server, unsigned and unpinned. A backend that injects a device record carrying its own key gets the message key wrapped for it, and reads everything. The Zeph app carries the counter-measure — every device shows a SHA-256 fingerprint of its public key, you mark a device verified once you have compared them side by side, and strict mode then wraps only for verified devices. It is off by default, its verified list is stored per browser profile rather than synced, and neither the CLI nor the MCP server consults it: those wrap for every device the server lists. So the app can close this for pushes it sends itself, and a CLI push cannot.
  • No forward secrecy. The ECDH secret for a given sender and device pair is static, so compromising either private key opens every past push wrapped for that pair.
  • senderPublicKey is unsigned, so a swapped one makes a push undecryptable. That direction fails closed rather than leaking.

Devices without a key

A device that has not registered a per-device public key cannot be sent to; it is skipped. If no device qualifies, the push goes out in the clear rather than arriving as something nothing can open.

The listener does not decrypt

zeph listener ignores isEncrypted pushes for now — it does not try to decrypt them. Stop-hook auto-pushes and zeph_ask responses are not part of the injection path, so this does not affect normal use.

On this page