Š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:
@@ -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(())
|
||||
}
|
||||
Reference in New Issue
Block a user