Julius Joska 90a1390716 fix: reliable first-message send, QR routing, auto-lock recovery
Fixes reported by testers and found in an app review:

- First message right after install no longer fails. Before the app
  finished registering with the relay, the outgoing message could crash on
  a null delivery address. The profile field is now optional and the send
  path throws a clear "still connecting, try again shortly" error while
  registration completes in the background.
- The QR code and share link now appear only once the delivery address is
  ready. Previously a link generated before the first connection lacked
  routing, so the other side could not send the first message. The screen
  now shows a connecting state and reactively swaps in the QR when ready.
- Incomplete contact links are rejected on import instead of failing at
  first send, with a clear prompt to ask for a fresh link.
- Auto-lock no longer silences the app: after locking and unlocking,
  messaging is restarted (it previously stayed dead until app restart).
- Group messages are marked failed on a non-per-member send error instead
  of hanging in the "sending" state forever.
- Hardened the relay client against a rare duplicate connection when
  toggling Tor or locking mid-connect, and fromShareString now rejects a
  delivery address of the wrong length.
- Pin unifiedpush below 6.2.0 to drop the webcrypto dependency (its native
  framework lacks LC_ENCRYPTION_INFO and blocked the iOS upload).
- Skip Android release lint (network dependency check hangs on the build
  host and adds no value for a Flutter app).
2026-07-18 19:53:41 +02:00

Š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 .onion to 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.

S
Description
End-to-end encrypted messenger — anonymous on-device identity, Signal-protocol crypto, metadata-minimizing relay. AGPL-3.0.
Readme AGPL-3.0 3.4 MiB
Šepot 1.1.1 Latest
2026-07-20 06:24:11 +00:00
Languages
Dart 86.7%
Rust 7.8%
C++ 2.5%
CMake 1.8%
HTML 0.3%
Other 0.9%