Š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
+163
View File
@@ -0,0 +1,163 @@
//! Šepot relay — hloupý store-and-forward server.
//!
//! Nevidí obsah (E2E), nevidí odesílatele (sealed sender), neloguje IP.
//! Zná jen: rotovatelný routing token příjemce, msg_uuid (dedup) a opaque
//! payload. Jediná krypto role: certifikační autorita sealed senderu
//! (vydává SenderCertificate) + ověření XEdDSA podpisu při AUTH.
mod apns;
mod attachments;
mod certs;
mod fcm;
mod push;
mod rest;
mod state;
mod ws;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use axum::extract::DefaultBodyLimit;
use axum::http::StatusCode;
use axum::routing::{delete, get, post};
use axum::Router;
use sqlx::postgres::PgPoolOptions;
use tower_governor::governor::GovernorConfigBuilder;
use tower_governor::key_extractor::SmartIpKeyExtractor;
use tower_governor::GovernorLayer;
use tower_http::timeout::TimeoutLayer;
use tracing_subscriber::EnvFilter;
/// Max velikost request body (registrace se 100 OTP klíči má ~15 KB).
const MAX_BODY_BYTES: usize = 64 * 1024;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Log formát bez IP adres a bez identifikátorů klientů.
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
)
.init();
let database_url = std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "postgres://sepot:sepot_dev@127.0.0.1/sepot_relay".into());
let bind = std::env::var("SEPOT_BIND").unwrap_or_else(|_| "127.0.0.1:8470".into());
let pool = PgPoolOptions::new()
.max_connections(8)
.connect(&database_url)
.await?;
sqlx::migrate!("./migrations").run(&pool).await?;
let certs = certs::CertAuthority::init(&pool).await?;
let attachments_cfg = attachments::Config::from_env();
tokio::fs::create_dir_all(attachments_cfg.dir.join("tmp")).await?;
let state = Arc::new(state::AppState::new(pool, certs, attachments_cfg));
attachments::spawn_cleanup(state.clone());
// ── Rate limiting (základní minimum; plný PoW/Privacy Pass později) ─
//
// Klíč = klientská IP: za traefikem X-Forwarded-For/X-Real-Ip (na
// relay dosáhne jen traefik z docker sítě), jinak peer adresa.
// In-memory token bucket — nikdy se nezapisuje na disk (privacy).
//
// Známý limit: Tor onion klienti přicházejí z tor kontejneru bez XFF
// → sdílí jeden bucket. Pro v1 (nízký provoz) přijatelné, řeší až
// Privacy Pass — limity proto raději volnější.
// registrace: zápis do DB bez auth → nejpřísnější (burst 10, +1/5 min)
let gov_register = Arc::new(
GovernorConfigBuilder::default()
.key_extractor(SmartIpKeyExtractor)
.burst_size(10)
.period(Duration::from_secs(300))
.finish()
.expect("governor config"),
);
// čtecí API (bundle žere OTP klíče → drain ochrana): burst 60, +2/s
let gov_general = Arc::new(
GovernorConfigBuilder::default()
.key_extractor(SmartIpKeyExtractor)
.burst_size(60)
.per_second(2)
.finish()
.expect("governor config"),
);
// přílohy: upload je autentizovaný, ale drahý (disk) → burst 30, +1/s
let gov_attachments = Arc::new(
GovernorConfigBuilder::default()
.key_extractor(SmartIpKeyExtractor)
.burst_size(30)
.per_second(1)
.finish()
.expect("governor config"),
);
// Governor drží mapu IP→bucket v paměti — periodicky vyhazovat staré
for limiter in [
gov_register.limiter().clone(),
gov_general.limiter().clone(),
gov_attachments.limiter().clone(),
] {
tokio::spawn(async move {
let mut tick = tokio::time::interval(Duration::from_secs(60));
loop {
tick.tick().await;
limiter.retain_recent();
}
});
}
let register_routes = Router::new()
.route("/v1/register", post(rest::register))
.layer(GovernorLayer::new(gov_register));
let general_routes = Router::new()
.route("/v1/server-info", get(rest::server_info))
.route("/v1/keys/{delivery_address}", get(rest::fetch_bundle))
.route("/v1/certificate/delivery", get(rest::certificate_delivery))
.route("/v1/identity", delete(rest::delete_identity))
.layer(GovernorLayer::new(gov_general));
let rest_routes = register_routes
.merge(general_routes)
.layer(TimeoutLayer::with_status_code(
StatusCode::REQUEST_TIMEOUT,
Duration::from_secs(30),
));
// Přílohy: velká těla tečou streamem na disk (strop hlídá handler,
// DefaultBodyLimit se na raw Body nevztahuje) + delší timeout na
// pomalé linky (25 MiB přes mobilní síť).
let attachment_routes = Router::new()
.route("/v1/attachments", post(attachments::upload))
.route("/v1/attachments/{blob_id}", get(attachments::download))
.layer(GovernorLayer::new(gov_attachments))
.layer(TimeoutLayer::with_status_code(
StatusCode::REQUEST_TIMEOUT,
Duration::from_secs(120),
));
let app = Router::new()
// health bez limitů — interní docker/uptime checky
.route("/v1/health", get(rest::health))
// WS má vlastní limity (conn cap per IP, auth deadline, frame rate)
.route("/v1/ws", get(ws::ws_handler))
.merge(rest_routes)
.merge(attachment_routes)
.layer(DefaultBodyLimit::max(MAX_BODY_BYTES))
.with_state(state);
tracing::info!(%bind, "sepot-relay startuje");
let listener = tokio::net::TcpListener::bind(&bind).await?;
// ConnectInfo: peer adresa pro rate-limit fallback a WS conn cap
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await?;
Ok(())
}