
Chat peer-to-peer crittografata end-to-end. Niente casella di posta. Nessun account da recuperare. Nessuno ascolta — nemmeno noi.
Chat peer-to-peer, cifrata end-to-end.
Niente inbox. Nessun account da recuperare. Nessuno ascolta — nemmeno noi.
I messaggi viaggiano direttamente tra i peer tramite libp2p e vengono cifrati con i protocolli Olm/Megolm in stile Signal (via vodozemac) prima ancora di lasciare il tuo dispositivo. L'unico server coinvolto è una piccola directory che aiuta i peer a trovare l'indirizzo attuale degli altri. Non vede mai il contenuto dei messaggi e può essere eliminata con un solo comando.
Consulta docs/THREAT_MODEL.md e docs/SECURITY.md per sapere cosa è realmente protetto e come.
Prima esecuzione — scegli un nome; non c'è altro da configurare.
Conversazioni — la barra dei gruppi, l'elenco dei contatti e un pannello di chat cifrata end-to-end.
Impostazioni — sensibilità del microfono, push-to-talk, avvio al login, raggiungibilità di rete.
crates/directory-server) associa un ID utente a un indirizzo di rete corrente e nient'altro. È strutturalmente incapace di leggere il contenuto dei messaggi: il suo Cargo.toml non dipende nemmeno dai crate che saprebbero come farlo.In questa app esistono due tipi di identità, e sono tenuti volutamente separati:
identity::Identity). Il tuo "user ID" pubblico è semplicemente l'impronta di quella chiave (wire_proto::user_id_from_ed25519). Non può essere emesso o revocato da alcun server, perché nessun server è coinvolto nella sua creazione.PeerId), usata solo per il livello di trasporto. Può cambiare tra un riavvio e l'altro senza influire affatto sulla tua identità di chat; le due sono legate solo da un record di presenza che firmi tu stesso.Trovare qualcuno e parlarci davvero sono due passaggi diversi:``` ┌────────────────────────┐ │ directory server │ │ (axum + one SQLite │ │ file: users, │ │ presence, group │ │ rosters. Never │ │ message content.) │ └─────────┬───────────────┘ 1. "where is bob │ 2. "here's my current right now?" │ address" (signed, │ expires in minutes) ┌─────────┴───────────────┐ ▼ ▼ ┌───────┐ 3. direct libp2p ┌───────┐ │ alice │◄──── connection ────►│ bob │ └───────┘ (Noise + Olm/ └───────┘ Megolm encrypted)
1. Alice cerca Bob nella directory tramite il suo ID utente. Questo restituisce
le sue chiavi pubbliche e il suo ultimo indirizzo di rete annunciato. È tutto
ciò che la directory contiene: chiavi pubbliche, nomi di visualizzazione,
liste di appartenenza ai gruppi e annunci di indirizzo di breve durata
(`crates/directory-server`).
2. Alice contatta Bob direttamente tramite libp2p (QUIC o TCP+Noise, con relay +
hole-punching per i peer dietro NAT; vedi `crates/net`). La directory è
completamente fuori gioco da qui in poi.
3. Il messaggio vero e proprio è cifrato con **Olm** per una chat 1:1, o
**Megolm** per un gruppo (`crates/crypto-session`), uno schema in stile Double-Ratchet
in cui ogni messaggio ha la propria chiave, prima di essere messo su
quella connessione libp2p. Non esiste una casella di posta lato server: se Bob è offline,
il messaggio attende in locale e viene ritentato, non viene memorizzato su infrastruttura
di nessun altro.
Tutto quanto sopra è orchestrato dall'`AppService` di `crates/core`, che è
ciò che il backend Rust dell'app Tauri (`apps/desktop/src-tauri`) richiama effettivamente;
la UI non comunica mai direttamente con la rete.
## Struttura del progetto```
crates/
wire-proto shared signed-request types for the directory API
identity vodozemac identity, OS-keychain key management
storage local encrypted store (contacts, messages, groups)
net libp2p transport + directory HTTP client
crypto-session Olm (1:1) / Megolm (group) session management
core orchestrates the above into `AppService` / `ChatNode`
directory-server the one server component (axum + SQLite)
apps/desktop the Tauri + React app
scripts/ build + backend-deployment scripts (§2, §5)
Hai bisogno di Rust e Node.js su ogni piattaforma, oltre a una toolchain specifica per piattaforma di cui Tauri ha bisogno per creare una finestra nativa. storage e directory-server compilano anche SQLite dal sorgente in bundle, il che richiede un semplice compilatore C (non è richiesta OpenSSL né alcuna altra libreria crittografica nativa in questo progetto).
Comune a tutte le piattaforme:
rustup, non tramite il gestore pacchetti del tuo sistema operativo)Installa un compilatore C, pkg-config, e i pacchetti di sviluppo WebKitGTK/AppIndicator a cui il backend Linux di Tauri si collega.
Debian/Ubuntu:```sh
sudo apt update
sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file
libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev pkg-config
Fedora:```sh
sudo dnf install webkit2gtk4.1-devel openssl-devel curl wget file \
libappindicator-gtk3-devel librsvg2-devel pkgconf-pkg-config
sudo dnf group install "C Development Tools and Libraries"
Architettura:```sh
sudo pacman -S --needed webkit2gtk-4.1 base-devel curl wget file openssl
appmenu-gtk-module libappindicator-gtk3 librsvg pkgconf
(I nomi dei pacchetti cambiano tra le release di Tauri: se una build fallisce cercando un
file `.pc` mancante, controlla i
[prerequisiti Tauri Linux attuali](https://v2.tauri.app/start/prerequisites/)
per la tua distribuzione.)
</details>
<details>
<summary><strong>Windows</strong></summary>
1. Installa il **Microsoft C++ Build Tools** (Visual Studio Installer →
carico di lavoro "Sviluppo desktop con C++"), necessario sia per la shell
nativa di Tauri sia per compilare il SQLite incluso.
2. Installa il toolchain Rust **MSVC**: `rustup default stable-msvc`.
3. **WebView2**: già presente su Windows 11 e sulla maggior parte delle installazioni
Windows 10 aggiornate; in caso contrario, la build di Tauri ti chiederà di installare
il runtime Evergreen.
</details>
---
## 2. Build
Dalla radice del repository:```sh
# Rust workspace (backend crates + the directory server)
cargo build --workspace --release
# Frontend + the actual desktop app bundle (installer/.app/.exe)
cd apps/desktop
npm install
npm run tauri build
npm run tauri build produce un installer nativo per piattaforma in
target/release/bundle/ nella root del repository (questo è un workspace Cargo, quindi tutte
le crate, inclusa l'app Tauri, condividono un'unica directory target/ di livello superiore).
La cross-compilazione (es. creare l'installer Windows da macOS) non è configurata:
compila su ciascuna piattaforma di destinazione, oppure usa il workflow GitHub Actions di Tauri
se vuoi release compilate tramite CI.
scripts/ contiene uno script di build per ogni piattaforma/output, ognuno eseguibile in modo
indipendente e verificato per produrre effettivamente un artefatto funzionante:
| Script | Produce |
|---|---|
scripts/build-mac-dmg.sh | Installer macOS .dmg |
scripts/build-mac-app.sh | Bundle macOS .app grezzo, senza installer |
scripts/build-linux.sh | Linux .AppImage + .deb |
scripts/build-windows.ps1 | Windows .msi + .exe (NSIS) |
Ognuno si limita a incapsulare npm run tauri build --bundles <...> con i flag giusti
e il controllo della piattaforma; esegui tu stesso il comando grezzo se vuoi una diversa
combinazione di bundle (npx tauri build --help da apps/desktop).
scripts/release.sh vX.Y.Z incrementa la versione ovunque sia necessario e crea il tag
del commit — vedi docs/RELEASING.md. Viene eseguito su macOS e Linux;
non esegue commit né push.
La schermata di scelta del server (§3) mostra sempre tre opzioni: Seal (la tua rete ufficiale), Server personalizzato e un piccolo collegamento Server di test locale in fondo. "Seal" è disabilitato (grigio, con "Non ancora configurato in questa build") finché non integrerai un URL al momento della build:```sh SEAL_DEFAULT_DIRECTORY_URL=https://directory.example.com npm run tauri build
Una volta che hai configurato il tuo server (§5) e hai un dominio reale puntato ad esso, imposta questo e ricompila: ogni copia che distribuisci da quel momento in poi mostra "Seal" come un'opzione reale e selezionabile usando quell'URL, senza toccare nessun altro codice. Lascialo non impostato per le build ordinarie/dev: non c'è un server ufficiale ospitato da questo repository, quindi "Seal" rimane disabilitato e gli utenti ripiegano su un server personalizzato o su quello locale, invece che l'app punti silenziosamente a un dominio segnaposto che in realtà non esegue nulla.
---
## 3. Eseguirlo in modalità dev```sh
cd apps/desktop
npm install
npm run tauri dev
Questo avvia il server di sviluppo Vite, compila il backend Rust in modalità debug e apre una finestra nativa con hot-reload sul frontend. La prima compilazione compila l'intero albero delle dipendenze e richiede alcuni minuti; le esecuzioni successive sono veloci.
Al primo avvio, Seal chiede quale server di directory usare, in questo ordine:
127.0.0.1:47100/47101, dati nella directory app-data del tuo sistema operativo), adatto per provare Seal o testare istanze su una sola macchina, non per una vera implementazione. Se una seconda istanza trova quelle porte già occupate, riutilizza semplicemente il server della prima istanza invece di avviarne un altro, ed è questo che permette a due istanze sulla stessa macchina di trovarsi. È questa la scelta automatica se "Seal" non è configurato e non scegli nient'altro.La scelta viene salvata (server.json accanto agli altri dati locali dell'app) e riutilizzata silenziosamente a ogni avvio successivo; puoi modificarla da Impostazioni → Server di directory, e la modifica ha effetto al successivo avvio dell'app invece di tentare di hot-swap di una connessione attiva. Per l'uso scriptato/di sviluppo, una variabile d'ambiente salta completamente il prompt:```sh
P2P_CHAT_DIRECTORY_URL=https://directory.example.com npm run tauri dev
### Esecuzione di due istanze localmente (per testare realmente la messaggistica)
Ogni istanza ha bisogno della propria identità. Seal supporta più account
nativamente (Impostazioni → Account su questo dispositivo), ma per due *processi
separati* sulla stessa macchina, `P2P_CHAT_PROFILE` è il percorso più rapido: crea
automaticamente (la prima volta) o riprende automaticamente (ogni volta successiva) un account
con quel nome, in modo non interattivo, saltando del tutto il selettore account:```sh
# terminal 1
P2P_CHAT_PROFILE=alice npm run tauri dev
# terminal 2
P2P_CHAT_PROFILE=bob npm run tauri dev
La scelta del server (server.json) e l'elenco degli account (accounts.json) sono entrambi condivisi tra i processi su una singola macchina, non per profilo. La prima istanza che avvii sceglie il server, e ogni profilo successivo (incluso bob qui) lo riutilizza silenziosamente. Entrambe le finestre finiscono sullo stesso server di directory integrato, quindi potete aggiungervi come contatti tramite ID e scambiarvi messaggi.
Il dev server di Vite richiede una porta reale e fissa a cui la webview di Tauri possa puntare, il che normalmente significa che può essere eseguito un solo npm run tauri dev alla volta: il secondo troverebbe la porta 1420 già occupata e fallirebbe subito. npm run tauri è in realtà un piccolo wrapper (apps/desktop/scripts/tauri.mjs) che sceglie la porta libera successiva (1421, 1422, …) per ogni istanza dopo la prima e la collega automaticamente, quindi eseguire i due comandi sopra in due terminali funziona e basta; non devi fare nulla di diverso. Cambia comportamento solo per dev — npm run tauri build e tutto il resto passano direttamente alla CLI reale.
./scripts/run-two-mac-instances.sh # profiles: alice, bob ./scripts/run-two-mac-instances.sh carol dave
Stessa idea di sopra, ma avvia l'app effettivamente compilata (l'output di `build-mac-app.sh` / `build-mac-dmg.sh`, oppure una copia installata in `/Applications`) due volte con diversi `P2P_CHAT_PROFILE` invece di `npm run tauri dev`, più vicino a quello che esegue un utente reale. Stampa i PID e come fermare entrambi.
### Debug
- **Log Rust**: imposta `RUST_LOG` prima di avviare, ad es. `RUST_LOG=debug npm run tauri dev` (oppure `RUST_LOG=p2p_core=debug,net=debug` per restringere il campo). I campi registrati sono limitati ai metadati (ID di peer/gruppo/utente, tipi di errore); vedi [`docs/SECURITY.md`](https://github.com/emn4tor/seal/blob/HEAD/docs/SECURITY.md) per capire perché è sicuro lasciarli dettagliati.
- **Frontend**: la finestra di sviluppo è una vera webview; fare clic con il tasto destro → Ispeziona elemento (oppure aprire gli strumenti di sviluppo) funziona come in un normale browser.
- **Crate del backend in isolamento**: ogni crate ha la propria suite di test che puoi eseguire e iterare senza toccare affatto l'interfaccia; vedi §4.
- **Un server directory autonomo**, invece di quello integrato: vedi §5.
---
## 4. Test```sh
# everything
cargo test --workspace
# one crate, e.g. the full backend-to-backend flow a Tauri command would trigger
cargo test -p p2p-core --test app_service
# lint + format check (what CI runs)
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
# dependency vulnerability scan
cargo install cargo-audit --locked # once
cargo audit
# frontend type-check + build
cd apps/desktop && npm run build
Riepilogo di cosa si tratta realmente, dato che è facile immaginare qualcosa di
più complesso: un processo axum, un file SQLite, tre tipi di record (chiavi
pubbliche, annunci di presenza a breve durata, roster di gruppo), tutte le
scritture firmate dalla chiave di identità del chiamante. Non è mai nel percorso
di un messaggio. Vedere docs/THREAT_MODEL.md per
capire perché questo è vero strutturalmente, e non solo per policy: il
Cargo.toml di directory-server non dipende nemmeno dalle crate che sanno leggere il contenuto dei messaggi.
sudo ./scripts/setup-backend.sh
Interattivo, solo Linux + systemd (vedi l'intestazione dello script per il motivo). Chiede su quale famiglia di distribuzioni ti trovi (Debian/Ubuntu, Fedora/RHEL/Rocky/Alma, Arch/Manjaro, o openSUSE, precompilata con un'ipotesi da `/etc/os-release`, quindi di solito basta un tasto per confermare) e installa i prerequisiti di build di quella distribuzione con una funzione dedicata per famiglia, offre di installare Rust tramite `rustup` se manca, compila il binario di release, crea un utente di sistema dedicato, genera un token di amministrazione, chiede se vuoi che configuri un dominio con HTTPS automatico tramite [Caddy](https://caddyserver.com) (installando Caddy stesso, per distribuzione, ripiegando sul binario statico ufficiale di Caddy se il pacchetto della distribuzione non è disponibile), oppure di limitarsi ad ascoltare su loopback/HTTP semplice se preferisci gestirne tu il front-end, poi scrive e abilita il servizio systemd. Sicuro da rieseguire.
Tutto ciò che segue è quello che lo script fa effettivamente, se preferisci farlo a mano o capirlo prima di eseguirlo.
### macOS: un rapido server di test LAN```sh
./scripts/run-mac-test-server.sh
Non per hosting reale: serve per testare l'app su due dispositivi nella stessa
rete (ad es. il tuo Mac + un'altra macchina, o due persone sullo stesso Wi-Fi)
senza configurare un dominio, TLS o systemd (che su macOS comunque non
esiste). Compila il binario di release, genera un token admin (riusato nelle
esecuzioni successive), vincola l'API pubblica a tutte le interfacce e stampa
l'URL da usare: l'indirizzo IP LAN reale del tuo Mac (via ipconfig getifaddr), non solo
127.0.0.1, così anche gli altri dispositivi possono raggiungerlo. La porta admin resta
in ascolto solo su loopback. Viene eseguito in primo piano; Ctrl-C lo interrompe. I dati
risiedono in ~/.seal-test-server.
DIRECTORY_DB_PATH=/var/lib/seal-directory/directory.sqlite3
DIRECTORY_PUBLIC_ADDR=0.0.0.0:8080
DIRECTORY_ADMIN_ADDR=127.0.0.1:8090
DIRECTORY_ADMIN_TOKEN=$(openssl rand -hex 32)
cargo run --release -p directory-server --bin directory-server
| Variable | Required | Meaning |
|---|---|---|
| `DIRECTORY_DB_PATH` | no (predefinito `directory.sqlite3`, cwd) | Dove risiede il singolo file SQLite. La directory padre deve esistere. |
| `DIRECTORY_PUBLIC_ADDR` | no (predefinito `0.0.0.0:8080`) | L'API rendezvous a cui le app si rivolgono. Va bene esporla pubblicamente. |
| `DIRECTORY_ADMIN_ADDR` | no (predefinito `127.0.0.1:8090`) | L'endpoint di purge. Tienilo fuori da Internet pubblico; vedi sotto. |
| `DIRECTORY_ADMIN_TOKEN` | **sì** | Token Bearer per l'API di amministrazione. Il processo rifiuta di avviarsi senza uno. Generane uno con `openssl rand -hex 32` o simile; non riutilizzarlo altrove. |
Il processo registra nei log gli indirizzi su cui è in ascolto all'avvio e avvisa chiaramente se
`DIRECTORY_ADMIN_ADDR` non è in loopback.
### Puntare l'app al server
Tre modi, nell'ordine in cui normalmente li useresti:
1. **Schermata di primo avvio**: scegli "Server personalizzato" e inserisci l'URL. Vedi §3.
2. **Impostazioni → Server di directory**: puoi cambiarlo in seguito; ha effetto al prossimo
riavvio.
3. **`P2P_CHAT_DIRECTORY_URL`**, impostata prima dell'avvio: salta interamente
la richiesta e sovrascrive qualunque valore salvato, utile per esecuzioni di sviluppo/script: ```sh
P2P_CHAT_DIRECTORY_URL=https://directory.example.com npm run tauri dev
Chiunque voglia trovarsi deve puntare alla stessa istanza di directory; è così che si cercano a vicenda.
[Service] Type=simple User=seal-directory Group=seal-directory Environment=DIRECTORY_DB_PATH=/var/lib/seal-directory/directory.sqlite3 Environment=DIRECTORY_PUBLIC_ADDR=127.0.0.1:8080 Environment=DIRECTORY_ADMIN_ADDR=127.0.0.1:8090 EnvironmentFile=/etc/seal-directory/admin-token.env ; DIRECTORY_ADMIN_TOKEN=... ExecStart=/usr/local/bin/directory-server Restart=on-failure
ProtectSystem=strict ProtectHome=true PrivateTmp=true NoNewPrivileges=true ReadWritePaths=/var/lib/seal-directory
[Install] WantedBy=multi-user.target
Note:
- `DIRECTORY_PUBLIC_ADDR` è volutamente associato a **loopback**; metti un
reverse proxy davanti per TLS (vedi sotto) invece di esporre axum direttamente
a internet.
- Crea prima l'utente/gruppo di sistema `seal-directory` e
`/var/lib/seal-directory` (`useradd --system --no-create-home
seal-directory && install -d -o seal-directory -g seal-directory
/var/lib/seal-directory`), poi copia il binario `directory-server` compilato
(da `target/release/`) in `/usr/local/bin/`.
- Metti il token admin in un `EnvironmentFile` leggibile solo da root, non direttamente
nel file unit (i file unit sono spesso leggibili da tutti).
</details>
### TLS tramite un reverse proxy
<details>
<summary>Mostra la configurazione di Caddy / nginx</summary>
[Caddy](https://caddyserver.com) ti offre HTTPS automatico con il minimo
di configurazione:```
# /etc/caddy/Caddyfile
directory.example.com {
reverse_proxy 127.0.0.1:8080
}
caddy run (o systemctl enable --now caddy) gestisce l'emissione
e il rinnovo dei certificati autonomamente. Se preferisci usare nginx, termina la TLS lì
e usa proxy_pass http://127.0.0.1:8080;, poiché l'app ha bisogno solo di HTTP semplice
dal punto di vista del proxy.
Per quanto riguarda il firewall: solo la porta pubblica deve essere raggiungibile dall'esterno
(8080 negli esempi precedenti, con la 443 come front-end tramite il proxy). La porta di amministrazione
non dovrebbe mai essere raggiungibile dall'esterno; raggiungila tramite port-forwarding SSH
(ssh -L 8090:127.0.0.1:8090 your-server) quando devi eseguire una pulizia
da remoto.
cargo run --release -p directory-server --bin directory-admin --
--admin-url http://127.0.0.1:8090 --token "$DIRECTORY_ADMIN_TOKEN" purge
Questo elimina il file SQLite e ricrea uno schema vuoto: nessuna istruzione `DELETE`,
nessuno stato parziale. È sicuro eseguirlo senza avvisare prima nessuno:
ogni record in esso è una cache di dati che ciascun client possiede già localmente
(la propria registrazione, presenza e qualsiasi elenco di gruppo di cui è membro),
quindi i client lo ripopolano entro pochi istanti dalla loro azione successiva.
Non esiste deliberatamente alcuna policy di backup per questo database; vedi
[`docs/SECURITY.md`](https://github.com/emn4tor/seal/blob/HEAD/docs/SECURITY.md) per capire perché mantenerne una minerebbe
l'intero scopo.
---
## 6. Usare l'app
1. **Primo avvio, prima domanda**: quale server di directory usare (§3).
Il predefinito è ciò che è incorporato nella build in esecuzione (un server
di test locale, a meno che chi l'ha creata non ne abbia configurato uno ufficiale); scegli
"Server personalizzato" per puntare a uno ospitato da te o da qualcuno di cui ti fidi.
2. **Scegli un nome visualizzato.** Questo genera una coppia di chiavi private sul tuo
dispositivo (niente da ricordare, e nulla di recuperabile se perso: è
intenzionale) e ti guida attraverso una breve spiegazione in-app di come la
crittografia funziona davvero. Puoi rivederla in qualsiasi momento dalle Impostazioni. Ogni successivo
avvio torna direttamente all'app senza alcun prompt; questo accade solo una volta per
account. Aggiungi altri account (identità completamente separate) da Impostazioni →
Account su questo dispositivo e passa dall'uno all'altro senza riavviare.
3. **Aggiungi qualcuno**: clicca su **+** accanto a "Direct messages" e inserisci il suo
ID (trovato nelle *sue* Impostazioni → La mia identità). Non c'è una directory da
consultare, per scelta progettuale; ti connetti allo stesso modo in cui condivideresti un numero di telefono.
4. **Scrivi loro**: scegli il loro nome dall'elenco e digita. Il primo
messaggio a qualcuno stabilisce automaticamente una sessione crittografata.
5. **Crea un gruppo**: clicca su **+** nella barra delle icone, dagli un nome, poi invita
le persone tramite ID allo stesso modo. Rimuovere qualcuno ruota la chiave del gruppo così
non possono leggere nulla inviato in seguito.
6. **Elimina tutto**: Impostazioni → Dati e privacy. Questa azione è immediata,
solo locale e irreversibile: distrugge le tue chiavi, i contatti e la
cronologia su *questo dispositivo* e non ha alcun effetto su nessuno con cui hai parlato.