Šepot — end-to-end encrypted messenger

Initial public release. Anonymous on-device identity (no phone number,
e-mail, or password), Signal-protocol cryptography (X3DH + PQXDH, Double
Ratchet, sealed sender) via libsignal, a metadata-minimizing relay,
group chats, disappearing messages, encrypted local storage, optional
Tor transport, and a UI localized into Czech, English, German, Russian,
and Italian.

Monorepo: Flutter client (app/), Rust relay + wire types (rust/),
protobuf definitions (proto/), self-host deploy (deploy/).

Licensed under AGPL-3.0 with an app-store distribution exception (§7).
This commit is contained in:
Julius Joska
2026-07-11 17:06:39 +02:00
commit 8081d98c67
309 changed files with 44102 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
# Zkopíruj jako `.env` vedle docker-compose.yml a vyplň. NIKDY necommitovat.
# --- Povinné ---
# Heslo k PostgreSQL (zvol silné, náhodné).
POSTGRES_PASSWORD=
# --- Volitelné: nativní APNs push (iOS). Nech prázdné, pokud nepoužíváš —
# relay pak jede na UnifiedPush. Klíč .p8 se mountuje v docker-compose.yml.
SEPOT_APNS_KEY_ID=
SEPOT_APNS_TEAM_ID=
SEPOT_APNS_TOPIC=
+37
View File
@@ -0,0 +1,37 @@
# syntax=docker/dockerfile:1
# sepot-relay — multi-stage build (kontext = kořen monorepa, viz compose)
#
# Build stage potřebuje: protoc (prost-build generuje z proto/sepot.proto),
# git (libsignal-protocol je git dependency s [patch.crates-io] zrcadlem).
FROM rust:1.96-slim AS build
RUN apt-get update && apt-get install -y --no-install-recommends \
protobuf-compiler git pkg-config build-essential cmake \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /src
COPY proto/ proto/
COPY rust/ rust/
WORKDIR /src/rust
# Cache mounty: rebuild po změně zdrojáků nekompiluje závislosti znovu.
# Binárka se musí zkopírovat VEN z cache mountu (po RUN kroku zmizí).
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/src/rust/target \
cargo build --release -p sepot-relay \
&& cp target/release/sepot-relay /sepot-relay
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl \
&& rm -rf /var/lib/apt/lists/* \
&& useradd -r -s /usr/sbin/nologin sepot \
# předvytvořit s právy sepot — named volume zdědí ownership z image
&& mkdir -p /var/lib/sepot/attachments \
&& chown -R sepot /var/lib/sepot
COPY --from=build /sepot-relay /usr/local/bin/sepot-relay
USER sepot
EXPOSE 8470
# Migrace (sqlx::migrate!) jsou zakompilované v binárce, běží při startu
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s \
CMD curl -fsS http://127.0.0.1:8470/v1/health || exit 1
CMD ["sepot-relay"]
+74
View File
@@ -0,0 +1,74 @@
# sepot-relay — self-hosting
Relay je „hloupý" doručovací server: přijímá a doručuje opaque sealed-sender
obálky, drží offline frontu (TTL) a šifrované bloby příloh. **Nevidí obsah ani
odesílatele, neváže identitu na telefon/e-mail a neloguje IP.**
Stack: `sepot-relay` (Rust/axum) + PostgreSQL + volitelný Tor onion.
## Rychlý start
```bash
cd deploy/relay
cp .env.example .env
# vyplň POSTGRES_PASSWORD, např.: openssl rand -base64 24
docker compose build
docker compose up -d
```
TLS/WSS neterminuje relay sám — postav před něj reverzní proxy (Caddy, Traefik,
nginx…) s platným certifikátem, která forwarduje na `sepot-relay:8470`. Uvnitř
sítě jede plain WS (proxy dělá TLS termination). Klient se pak připojuje na
`wss://<tvoje-doména>`.
### Ověření
```bash
curl -s http://127.0.0.1:8470/v1/health # {"status":"ok"}
curl -s http://127.0.0.1:8470/v1/server-info # capabilities + trust_root
```
### Tor onion (volitelné)
Compose obsahuje kontejner `tor`, který relay vystaví jako onion hidden service.
Adresu zjistíš po startu:
```bash
docker exec sepot-relay-tor cat /var/lib/tor/sepot/hostname
```
Tuhle `.onion` adresu pak nastav v klientu (`app/lib/core/constants/app_constants.dart`,
`AppConstants.relayOnion`) pro svůj vlastní build.
## Konfigurace (`.env`)
| Proměnná | Popis |
|---|---|
| `POSTGRES_PASSWORD` | heslo DB — **povinné**, nikdy necommituj |
| `SEPOT_APNS_*` | volitelné, jen pro iOS APNs push (Key ID, Team ID, cesta ke klíči) |
| FCM service account | volitelné, jen pro Android FCM „fast mode" (mimo repo) |
Push je volitelný. Bez něj appka funguje přes UnifiedPush (degooglované) nebo
si zprávy stáhne po otevření.
## Persistentní data a zálohy
Zálohuj **dvě** volumes, obě obsahují kritické tajné klíče (`chmod 600`):
- **`db-data`** — kromě dat drží `relay_keys` = **trust root CA** sealed
senderu. Klienti si ho pinují. Jeho ztráta / regenerace = všichni klienti
tvrdě odmítnou komunikovat („Trust root se změnil").
- **`tor-keys`** — onion identita (`hs_ed25519_secret_key`). Ztráta = nová
onion adresa pro všechny.
```bash
# DB dump: docker exec sepot-relay-db pg_dump -U sepot sepot_relay | gzip > db.sql.gz
# Tor klíče: docker run --rm -v sepot-relay_tor-keys:/k -v "$PWD":/o alpine tar czf /o/tor-keys.tgz -C /k .
```
## Privacy — Definition of Done
- Relay neloguje obsah, odesílatele ani IP (vynuceno v kódu, ověřeno testem).
- Access log reverzní proxy pro relay endpoint drž **vypnutý**.
- Rate limity jsou in-memory token buckety — nikdy se nezapisují na disk.
- DB nemá IP/PII sloupce; offline fronta má TTL, ACK = hard delete.
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# Denní záloha sepot-relay — KRITICKÉ.
#
# db-data → drží relay_keys = trust root CA sealed senderu. Klienti ho
# pinují; ztráta/regenerace = všichni klienti tvrdě odmítnou
# relay ("trust root se změnil").
# tor-keys → onion identita (hs_ed25519_secret_key). Ztráta = nová onion
# adresa pro všechny.
#
# Obojí obsahuje TAJNÉ klíče → zálohy jen chmod 700, umask 077.
# Nasazení cronu: viz README.md „Zálohy".
set -euo pipefail
umask 077
BACKUP_DIR=/opt/backups/sepot-relay
KEEP_DAYS=14
STAMP=$(date +%F_%H%M)
mkdir -p "$BACKUP_DIR"
chmod 700 "$BACKUP_DIR"
db_file="$BACKUP_DIR/db_${STAMP}.sql.gz"
tor_file="$BACKUP_DIR/tor-keys_${STAMP}.tgz"
# 1) Postgres dump (relay_keys = trust root CA)
docker exec sepot-relay-db pg_dump -U sepot sepot_relay | gzip > "$db_file"
# 2) Tor onion identita — jen podadresář hidden service (hostname +
# hs_ed25519_*). Zbytek DataDirectory (cache konsenzu) je regenerovatelný
# a jen by churnoval. Read-only mount, ať se běžícímu toru nic nerozbije.
docker run --rm -v sepot-relay_tor-keys:/k:ro -v "$BACKUP_DIR":/b alpine \
tar czf "/b/tor-keys_${STAMP}.tgz" -C /k sepot
# docker run vytvoří soubor s umaskou kontejneru (ne skriptu) → obsahuje
# TAJNÝ onion klíč, natvrdo zamknout
chmod 600 "$tor_file"
# 3) Integrita — prázdný/rozbitý dump je horší než žádný (maskuje selhání)
if [ ! -s "$db_file" ] || ! gzip -t "$db_file" 2>/dev/null; then
echo "$(date -Is) CHYBA: DB dump prázdný nebo poškozený ($db_file)" >&2
rm -f "$db_file"
exit 1
fi
if [ ! -s "$tor_file" ]; then
echo "$(date -Is) CHYBA: tor-keys archiv prázdný ($tor_file)" >&2
exit 1
fi
# 4) Rotace — držet posledních KEEP_DAYS dní
find "$BACKUP_DIR" -maxdepth 1 -name 'db_*.sql.gz' -mtime +"$KEEP_DAYS" -delete
find "$BACKUP_DIR" -maxdepth 1 -name 'tor-keys_*.tgz' -mtime +"$KEEP_DAYS" -delete
echo "$(date -Is) OK db=$(du -h "$db_file" | cut -f1) tor=$(du -h "$tor_file" | cut -f1)"
+104
View File
@@ -0,0 +1,104 @@
# sepot-relay produkční stack: relay + PostgreSQL 16 + Tor onion
#
# Nasazení: viz README.md. Kontext buildu = kořen monorepa (../..).
# Secrets: .env vedle tohoto souboru (POSTGRES_PASSWORD) — NIKDY do gitu.
#
# Sítě:
# backend (internal) — jen relay ↔ db, bez internetu
# tornet — tor ↔ relay; tor potřebuje egress do Tor sítě
# Relay publikuje 8470 na loopback hostitele — postav před něj vlastní
# reverzní proxy (Caddy/Traefik/nginx) s TLS a forwardni na 127.0.0.1:8470.
name: sepot-relay
services:
sepot-relay:
build:
context: ../..
dockerfile: deploy/relay/Dockerfile
container_name: sepot-relay
restart: unless-stopped
environment:
DATABASE_URL: postgres://sepot:${POSTGRES_PASSWORD}@db:5432/sepot_relay
SEPOT_BIND: 0.0.0.0:8470
SEPOT_ATTACHMENTS_DIR: /var/lib/sepot/attachments
# FCM fast mode (opt-in budíčky přes Google) — SA key ŽIJE JEN NA
# serveru (fcm-sa.json vedle .env, chmod 600, NIKDY do gitu)
SEPOT_FCM_CREDENTIALS: /etc/sepot/fcm-sa.json
# APNs (nativní iOS push, bez Googlu) — auth key .p8 + IDs. Klíč
# žije JEN na serveru (apns-key.p8 vedle .env). Bez těchto proměnných
# zůstane APNs vypnutý.
SEPOT_APNS_CREDENTIALS: /etc/sepot/apns-key.p8
# Key ID a Team ID nejsou tajné (tajný je jen .p8) → přímo v compose
SEPOT_APNS_KEY_ID: ${SEPOT_APNS_KEY_ID}
SEPOT_APNS_TEAM_ID: ${SEPOT_APNS_TEAM_ID}
SEPOT_APNS_TOPIC: ${SEPOT_APNS_TOPIC}
# log bez obsahu/IP je vynucený v kódu; info stačí
RUST_LOG: info
volumes:
# šifrované bloby příloh (TTL 30 d) — NEzálohují se: efemérní data,
# klíče k nim mají jen klienti; ztráta = nedoručené přílohy vyprší
- attachments-data:/var/lib/sepot/attachments
# VOLITELNÉ push mounty — odkomentuj JEN při zapnutém FCM/APNs push,
# zároveň s příslušnými SEPOT_FCM_/SEPOT_APNS_ proměnnými výše.
# Bez nich compose běží normálně (rychlý start bez push konfigurace);
# necháš-li je odkomentované bez existujících souborů, start selže.
# - ./fcm-sa.json:/etc/sepot/fcm-sa.json:ro
# - ./apns-key.p8:/etc/sepot/apns-key.p8:ro
depends_on:
db:
condition: service_healthy
# Publikováno jen na loopback — reverzní proxy s TLS forwardni sem.
ports:
- "127.0.0.1:8470:8470"
networks:
- backend
- tornet
logging: &log
driver: json-file
options:
max-size: "10m"
max-file: "3"
db:
image: postgres:16-alpine
container_name: sepot-relay-db
restart: unless-stopped
environment:
POSTGRES_DB: sepot_relay
POSTGRES_USER: sepot
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
# Drží i relay_keys (trust root CA!) — záloha = pg_dump, viz README
- db-data:/var/lib/postgresql/data
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U sepot -d sepot_relay"]
interval: 10s
timeout: 5s
retries: 5
logging: *log
tor:
build: ./tor
container_name: sepot-relay-tor
restart: unless-stopped
volumes:
# onion klíče — ZÁLOHOVAT (ztráta = nová onion adresa)
- tor-keys:/var/lib/tor
depends_on:
- sepot-relay
networks:
- tornet
logging: *log
networks:
backend:
internal: true
tornet: {}
volumes:
db-data:
tor-keys:
attachments-data:
+10
View File
@@ -0,0 +1,10 @@
# Tor hidden service pro sepot-relay — vlastní minimální image
# (žádný third-party tor image; privacy produkt nemá kam outsourcovat důvěru)
FROM alpine:3.22
RUN apk add --no-cache tor \
&& mkdir -p /var/lib/tor \
&& chown -R tor:tor /var/lib/tor \
&& chmod 700 /var/lib/tor
COPY torrc /etc/tor/torrc
USER tor
CMD ["tor", "-f", "/etc/tor/torrc"]
+15
View File
@@ -0,0 +1,15 @@
# Tor onion vstup pro sepot-relay (onion služba je výchozí přístupový bod)
#
# Klíče hidden service žijí ve volume `tor-keys` — ZÁLOHOVAT, ztráta
# klíčů = nová onion adresa pro všechny klienty.
DataDirectory /var/lib/tor
HiddenServiceDir /var/lib/tor/sepot
HiddenServiceVersion 3
# port 80 uvnitř onion → relay v docker síti; onion vrstva šifruje,
# TLS uvnitř není potřeba (standardní praxe)
HiddenServicePort 80 sepot-relay:8470
# Jen hidden service, žádný SOCKS proxy ven
SocksPort 0
# Log bez klientských dat; notice stačí
Log notice stderr