- stop counting ACK and Pong frames toward the per-connection frame window; draining a large queue used to trip the 429 limit, disconnect the client and, after eight rounds, drop the remaining envelopes - deliver to online recipients with try_send: a recipient that stops reading its socket no longer pins the sender's task and memory - throttle call wake-ups per recipient (1 per 10 s, 6 per 10 min) and ordinary wake-ups (1 per 5 s); collapse ids for APNs and FCM - POST /v1/register accepts X-Sepot-Identity-Proof and lets a new device take over an existing identity (restore from the recovery phrase) - POST /v1/keys uploads one-time prekeys and rotates signed/Kyber prekeys; PrekeysLow is sent after AUTH when fewer than 20 remain - GET /v1/keys hands out at most 30 one-time prekeys per address per hour - reqwest errors are logged without the URL; UnifiedPush POSTs pin the addresses the SSRF guard resolved; NAT64/6to4 embedded addresses are checked - absolute AUTH deadline, at most 3 AUTH attempts, length limits on push endpoints and VoIP tokens - TURN credentials live 12 h and are reissued per connection every 10 min - global attachment storage cap (SEPOT_ATTACHMENTS_TOTAL_BYTES), nosniff and attachment disposition on downloads - SEPOT_MAX_CONNS_PER_IP, /v1/ready backed by the database, graceful shutdown, RLIMIT_NOFILE raised to the hard limit, server-info reports source and revision - Dockerfile: rust 1.99, GIT_SHA build arg for the image label and SEPOT_REVISION; Tor image on alpine 3.24
Šepot
Šepot is an end-to-end encrypted messenger with anonymous, on-device identity. There is no phone number, e-mail, or password: an account is a keypair generated on the device, your ID is the fingerprint of your public key, and the only backup is a mnemonic phrase.
The design goal is metadata minimization with a public threat model — never "zero metadata". The relay is deliberately dumb: it cannot read message content, does not learn who sent a message, and does not log IP addresses. What it does see is documented honestly below.
Status: pre-1.0. The Android client is the primary target; iOS builds run through TestFlight. See
CHANGELOG.md.
Features
- Signal-grade cryptography via the official Rust
libsignal: X3DH + post-quantum PQXDH (Kyber-1024) key agreement, Double Ratchet, and sealed sender. - Anonymous identity — no phone/e-mail/password. Recovery through a 24-word mnemonic that encodes the identity key itself (not a KDF seed).
- Encrypted at rest — all local data lives in SQLCipher, unlocked by a PIN (Argon2id-derived key) or biometrics. Optional duress PIN opens a separate empty database.
- Group chats — up to 128 members, pairwise fan-out so every member gets full forward secrecy; the relay never learns a group exists.
- Attachments — images and files are encrypted client-side (AES-256-GCM) and uploaded as opaque blobs with a TTL; image metadata (EXIF/GPS) is stripped on send.
- Disappearing messages, read receipts (off by default), voice messages, replies, reactions, edits, contact verification (safety numbers).
- Push without Google by default — UnifiedPush/ntfy; FCM (Android) and APNs (iOS) are opt-in. A push wake-up never carries content, sender, or count.
- Tor transport (Android, via Orbot) — routes relay traffic over a
.onionto hide the client IP. Off by default. - Five languages — Czech, English, German, Russian, Italian. The UI follows the system language and can be overridden in Settings.
How it works
┌────────────┐ sealed-sender ciphertext ┌──────────────┐
│ Šepot app │ ───────(WSS or Tor)─────────▶ │ relay │ ──▶ PostgreSQL
│ (Flutter) │ ◀────── wake push ─────────── │ (Rust/axum) │ (no content,
└────────────┘ └──────────────┘ no IP, no PII)
libsignal (FFI) offline queue, TTL
SQLCipher at rest opaque attachment blobs
Every message is encrypted end-to-end with libsignal and wrapped in a sealed-sender envelope, so the relay routes by recipient delivery address without learning the sender. Undelivered messages sit in a per-recipient queue with a fixed TTL and are hard-deleted on acknowledgement.
What the relay can see: the recipient's delivery address, message size, and timing. What it cannot see: message content, the sender's identity, group membership, or (with logging disabled as shipped) client IP addresses. The full model — including where the trust boundary sits — is in THIRD-PARTY-NOTICES.md and the crypto facade app/lib/core/crypto/.
Repository layout
| Path | Contents |
|---|---|
app/ |
Flutter client (Android, iOS; desktop scaffolding present) |
rust/ |
Cargo workspace — sepot-relay (relay server), sepot-proto (prost wire types) |
proto/ |
Protobuf wire definitions — single source for both Rust and Dart |
deploy/ |
Docker Compose for self-hosting the relay (with an optional Tor onion) |
tool/ |
Helper scripts (protobuf codegen for Dart) |
Building
Client (Android)
cd app
flutter pub get
flutter build apk --release
The native libsignal binary is fetched by the Flutter native-assets hook at build time. To point a dev build at your own relay:
flutter build apk --dart-define=SEPOT_RELAY=http://<dev-relay-host>:8470
Release signing reads app/android/key.properties (kept out of the repo); without it the build falls back to the debug signing config so flutter run still works. FCM is optional — the google-services plugin is only applied if you supply your own app/android/app/google-services.json (copy google-services.json.example). Without it the app runs on UnifiedPush.
Relay (self-host)
See deploy/relay/README.md. The relay is a single Rust binary plus PostgreSQL; put a TLS-terminating reverse proxy in front of it and, optionally, expose it as a Tor onion service.
Languages
UI strings live in app/lib/l10n/app_*.arb (cs, en, de, ru, it) and are compiled by Flutter's gen-l10n. Contributions of new translations are welcome — add an app_<locale>.arb, translate the keys from app_cs.arb, and run flutter gen-l10n.
Security
Cryptography is implemented by Signal's libsignal, not by hand-rolled primitives. If you find a vulnerability, please report it privately to bezpecnost@sepot.cz rather than opening a public issue.
License
Šepot is licensed under the GNU Affero General Public License v3.0 — see LICENSE.
The license includes an AGPL §7 additional permission for app-store distribution: Šepot may be distributed through App Store / Google Play provided the complete corresponding source stays available under the AGPL through a channel without those store restrictions (this repository, F-Droid, direct APK). That permission covers only Šepot's own code. The bundled Signal libsignal is AGPL-3.0 held by Signal Messenger, LLC — its app-store status is documented in THIRD-PARTY-NOTICES.md.