
Peer-to-peer, chat chiffré de bout en bout. Pas de boîte de réception. Aucun compte à récupérer. Personne n'écoute — pas même nous.
Messagerie pair-à-pair, chiffrée de bout en bout.
Pas de boîte de réception. Aucun compte à récupérer. Personne n'écoute — pas même nous.
Les messages voyagent directement entre pairs via libp2p et sont chiffrés avec les protocoles Olm/Megolm de type Signal (via vodozemac) avant de quitter votre appareil. Le seul serveur impliqué est un petit annuaire qui aide les pairs à trouver l'adresse actuelle des uns et des autres. Il ne voit jamais le contenu des messages, et il peut être purgé en une seule commande.
Voir docs/THREAT_MODEL.md et
docs/SECURITY.md pour savoir ce qui est réellement protégé
et comment.
Premier lancement — choisissez un nom ; rien d'autre à configurer.
Conversations — le rail de groupes, la liste de contacts et un volet de discussion chiffré de bout en bout.
Paramètres — sensibilité du micro, push-to-talk, lancement à la connexion, accessibilité réseau.
crates/directory-server) associe un identifiant d'utilisateur à une adresse réseau actuelle
et rien d'autre. Il est structurellement incapable de lire le contenu des messages :
son Cargo.toml ne dépend même pas des crates qui savent le faire.Il existe deux types d'identité dans cette application, et ils sont délibérément tenus séparés :
identity::Identity). Votre "identifiant d'utilisateur" public n'est que l'
empreinte de cette clé (wire_proto::user_id_from_ed25519). Elle ne peut être
émise ni révoquée par aucun serveur, car aucun serveur n'intervient dans sa
création.PeerId), utilisée
uniquement pour la couche de transport. Elle peut changer entre les redémarrages sans
affecter du tout votre identité de discussion ; les deux ne sont liées que par un
enregistrement de présence que vous signez vous-même.Trouver quelqu'un et lui parler réellement sont deux étapes distinctes :``` ┌────────────────────────┐ │ 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 cherche Bob dans l’annuaire à l’aide de son identifiant utilisateur. Cela renvoie ses
clés publiques et sa dernière adresse réseau annoncée. C’est tout
ce que contient l’annuaire : clés publiques, noms d’affichage, listes
d’appartenance aux groupes et annonces d’adresse à courte durée de vie
(`crates/directory-server`).
2. Alice appelle Bob directement via libp2p (QUIC ou TCP+Noise, avec relais +
hole-punching pour les pairs derrière des NAT ; voir `crates/net`). L’annuaire est
complètement hors du coup à partir de là.
3. Le message proprement dit est chiffré avec **Olm** pour une conversation 1:1, ou
**Megolm** pour un groupe (`crates/crypto-session`), un schéma à la Double Ratchet
où chaque message possède sa propre clé, avant même d’être placé sur
cette connexion libp2p. Il n’existe pas de boîte de réception côté serveur : si Bob est hors ligne,
le message attend localement et est réessayé, sans être stocké sur l’infrastructure
de quiconque.
Tout ce qui précède est orchestré par le `AppService` de `crates/core`, qui est
ce que le backend Rust de l’application Tauri (`apps/desktop/src-tauri`) appelle
réellement ; l’interface utilisateur ne parle jamais au réseau directement.
## Structure du projet```
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)
Vous avez besoin de Rust et Node.js sur chaque plateforme, plus d'une
chaîne d'outils spécifique à la plateforme dont Tauri a besoin pour créer une fenêtre native. storage et directory-server
compilent également SQLite à partir des sources lors de l'empaquetage, ce qui nécessite un compilateur C standard (aucune
bibliothèque OpenSSL ou autre bibliothèque de cryptographie native n'est requise dans ce projet).
Commun à toutes les plateformes :
rustup, pas votre gestionnaire de paquets de l'OS)Installez un compilateur C, pkg-config, et les paquets de développement WebKitGTK/AppIndicator nécessaires à l'édition de liens du backend Linux de Tauri.
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"
Arch:```sh
sudo pacman -S --needed webkit2gtk-4.1 base-devel curl wget file openssl
appmenu-gtk-module libappindicator-gtk3 librsvg pkgconf
(Les noms des paquets changent entre les versions de Tauri : si une compilation échoue en cherchant un
fichier `.pc` manquant, consultez les
[prérequis Tauri Linux actuels](https://v2.tauri.app/start/prerequisites/)
pour votre distribution.)
</details>
<details>
<summary><strong>Windows</strong></summary>
1. Installez les **Microsoft C++ Build Tools** (Visual Studio Installer →
charge de travail « Desktop development with C++ »), nécessaires à la fois pour le shell natif de Tauri
et pour compiler le SQLite intégré.
2. Installez la chaîne d'outils Rust **MSVC** : `rustup default stable-msvc`.
3. **WebView2** : déjà présent sur Windows 11 et sur la plupart des installations Windows
10 à jour ; sinon, le build de Tauri vous invitera à installer le
runtime Evergreen.
</details>
---
## 2. Compilation
Depuis la racine du dépôt :```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 produit un installateur natif pour la plateforme sous
target/release/bundle/ à la racine du dépôt (c'est un espace de travail Cargo, donc toutes
les crates, y compris l'application Tauri, partagent un répertoire target/ de premier niveau).
La compilation croisée (par ex. construire l'installateur Windows depuis macOS) n'est pas
configurée : compilez sur chaque plateforme cible, ou utilisez le workflow GitHub Actions de Tauri
si vous voulez des versions compilées par CI.
scripts/ contient un script de build par plateforme/sortie, chacun exécutable
indépendamment et chacun vérifié pour produire réellement un artefact fonctionnel :
| Script | Produit |
|---|---|
scripts/build-mac-dmg.sh | installateur macOS .dmg |
scripts/build-mac-app.sh | Bundle macOS .app brut, sans installateur |
scripts/build-linux.sh | Linux .AppImage + .deb |
scripts/build-windows.ps1 | Windows .msi + .exe (NSIS) |
Chacun ne fait qu'envelopper npm run tauri build --bundles <...> avec les bonnes options
et la vérification de plateforme ; exécutez vous-même la commande directement si vous voulez une
combinaison de bundles différente (npx tauri build --help depuis apps/desktop).
scripts/release.sh vX.Y.Z met à jour la version partout où elle doit l'être
et étiquette le commit — voir docs/RELEASING.md. S'exécute sur
macOS et Linux ; ne commit ni ne pousse.
L'écran de choix du serveur (§3) affiche toujours trois options : Seal (votre propre réseau officiel), Serveur personnalisé, et un petit lien Serveur de test local en bas. "Seal" est désactivé (grisé, avec "Pas encore configuré dans cette version") jusqu'à ce que vous intégriez une URL au moment du build :```sh SEAL_DEFAULT_DIRECTORY_URL=https://directory.example.com npm run tauri build
Une fois que vous avez mis en place votre propre serveur (§5) et pointé un vrai domaine vers
celui-ci, définissez ce paramètre et recompilez : chaque copie que vous distribuerez à partir de là affichera
« Seal » comme une véritable option sélectionnable utilisant cette URL, sans toucher à aucun
autre code. Laissez-le non défini pour les versions ordinaires/de dev : ce dépôt n'héberge
aucun serveur officiel, donc « Seal » reste désactivé et les utilisateurs se rabattent sur
un serveur personnalisé ou le serveur local, plutôt que de voir l'application pointer silencieusement vers
un domaine d'espace réservé qui ne fait réellement tourner quoi que ce soit.
---
## 3. Exécution en mode dev```sh
cd apps/desktop
npm install
npm run tauri dev
Cela démarre le serveur de développement Vite, compile le backend Rust en mode débogage, et ouvre une fenêtre native avec le rechargement à chaud côté frontend. La première compilation construit tout l’arbre de dépendances et prend quelques minutes ; les exécutions suivantes sont rapides.
Au premier lancement, Seal demande quel serveur d’annuaire utiliser, dans cet ordre :
127.0.0.1:47100/47101, données dans le répertoire de données applicatives de votre OS),
pratique pour essayer Seal ou tester des instances sur une seule machine, pas une véritable
infrastructure de déploiement. Si une seconde instance constate que ces ports sont déjà pris, elle
réutilise simplement le serveur de la première instance au lieu d’en démarrer une autre,
ce qui permet à deux instances sur une même machine de se trouver. C’est ce qui
est sélectionné automatiquement si « Seal » n’est pas configuré et que vous ne
choisissez rien d’autre.Le choix est enregistré (server.json à côté des autres données locales de l’application) et
réutilisé silencieusement à chaque lancement ultérieur ; modifiez-le via Paramètres → Serveur
d’annuaire, ce qui prend effet au prochain démarrage de l’application plutôt que
de tenter de remplacer à chaud une connexion active. Pour les usages scriptés/développement, une
variable d’environnement permet d’ignorer complètement l’invite :```sh
P2P_CHAT_DIRECTORY_URL=https://directory.example.com npm run tauri dev
### Exécution de deux instances localement (pour tester réellement la messagerie)
Chaque instance a besoin de sa propre identité. Seal prend en charge plusieurs comptes
nativement (Paramètres → Comptes sur cet appareil), mais pour deux *processus
séparés* sur une même machine, `P2P_CHAT_PROFILE` est le chemin le plus rapide : il
crée automatiquement (la première fois) ou reprend automatiquement (à chaque fois) un compte
avec ce nom, sans interaction, en ignorant complètement le sélecteur de compte :```sh
# terminal 1
P2P_CHAT_PROFILE=alice npm run tauri dev
# terminal 2
P2P_CHAT_PROFILE=bob npm run tauri dev
Le choix du serveur (server.json) et la liste des comptes (accounts.json)
sont tous deux partagés entre les processus sur une même machine, et non par profil. La
première instance que vous lancez choisit le serveur, et chaque profil après
cela (y compris bob ici) le réutilise silencieusement. Les deux fenêtres se retrouvent sur le
même serveur d'annuaire embarqué, vous pouvez donc vous ajouter mutuellement comme contacts par ID
et échanger des messages entre elles.
Le serveur de développement de Vite a besoin d'un port réel et fixe pour que la webview de Tauri puisse s'y pointer,
ce qui signifie normalement qu'un seul npm run tauri dev peut tourner à la fois — le
second trouverait le port 1420 déjà occupé et échouerait directement.
npm run tauri est en réalité un petit wrapper (apps/desktop/scripts/tauri.mjs)
qui choisit le prochain port libre (1421, 1422, …) pour chaque instance après la
première et le transmet automatiquement, donc exécuter les deux commandes ci-dessus
dans deux terminaux fonctionne simplement ; vous n'avez rien à faire de différent. Cela
ne modifie le comportement que pour dev — npm run tauri build et tout le reste
passent directement au vrai CLI.
./scripts/run-two-mac-instances.sh # profiles: alice, bob ./scripts/run-two-mac-instances.sh carol dave
Même idée que ci-dessus, mais lance la véritable application compilée (le résultat de `build-mac-app.sh` /
`build-mac-dmg.sh`, ou une copie installée dans `/Applications`) deux fois
avec des valeurs `P2P_CHAT_PROFILE` différentes au lieu de `npm run tauri dev`, plus
proche de ce qu'un utilisateur réel exécute. Affiche les PID et comment arrêter les deux.
### Débogage
- **Logs Rust** : définissez `RUST_LOG` avant le lancement, par ex.
`RUST_LOG=debug npm run tauri dev` (ou `RUST_LOG=p2p_core=debug,net=debug`
pour restreindre la portée). Les champs de journalisation sont limités aux métadonnées (identifiants de pair/groupe/utilisateur,
types d'erreur) ; voir [`docs/SECURITY.md`](https://github.com/emn4tor/seal/blob/HEAD/docs/SECURITY.md) pour savoir
pourquoi il est sûr de laisser ces logs verbeux.
- **Frontend** : la fenêtre de développement est une vraie webview ; clic droit → Inspecter
l'élément (ou ouvrir les outils de développement) fonctionne comme dans un navigateur normal.
- **Crates backend en isolation** : chaque crate dispose de sa propre suite de tests que vous pouvez
exécuter et faire évoluer sans toucher à l'interface du tout ; voir §4.
- **Un serveur d'annuaire autonome**, à la place de celui intégré : voir §5.
---
## 4. Tests```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
Récapitulatif de ce que c'est réellement, car il est facile de surinterpréter : un axum
processus, un fichier SQLite, trois types d'enregistrements (clés publiques, annonces de présence à courte
durée de vie, listes de groupes), toutes les écritures signées par la clé d'identité de
l'appelant. Il n'est jamais sur le chemin d'un message. Voir
docs/THREAT_MODEL.md pour comprendre pourquoi cela est vrai
structurellement, et pas seulement par politique : le Cargo.toml de directory-server ne
dépend même pas des crates qui savent lire le contenu des messages.
sudo ./scripts/setup-backend.sh
Interactif, Linux + systemd uniquement (voir l'en-tête du script pour savoir pourquoi). Il
demande sur quelle famille de distribution vous êtes (Debian/Ubuntu, Fedora/RHEL/Rocky/Alma,
Arch/Manjaro, ou openSUSE, pré-remplie avec une estimation depuis `/etc/os-release`,
donc c'est généralement une confirmation à une touche) et installe les prérequis de
compilation de cette distribution avec une fonction dédiée par famille, propose d'installer Rust
via `rustup` s'il manque, compile le binaire en mode release, crée un
utilisateur système dédié, génère un jeton admin, demande si vous voulez qu'il
configure un domaine avec HTTPS automatique via [Caddy](https://caddyserver.com)
(en installant Caddy lui-même, selon la distribution, en recourant au binaire statique
officiel de Caddy si un paquet de la distribution n'est pas disponible), ou simplement lier
loopback/HTTP simple si vous préférez le gérer vous-même, puis écrit et
active le service systemd. Peut être ré-exécuté sans danger.
Tout ce qui suit est ce qu'il fait réellement, si vous préférez le faire à la main
ou le comprendre avant de l'exécuter.
### macOS: un serveur de test LAN rapide```sh
./scripts/run-mac-test-server.sh
Pas pour un hébergement réel : pour tester l'application sur deux appareils sur le même
réseau (par ex. votre Mac + une autre machine, ou deux personnes sur le même Wi-Fi)
sans configurer de domaine, TLS ou systemd (qui n'existe pas sur macOS
de toute façon). Il compile le binaire de release, génère un jeton d'administration (réutilisé lors des
exécutions ultérieures), lie l'API publique à toutes les interfaces et affiche l'URL à
utiliser : l'adresse IP LAN réelle de votre Mac (via ipconfig getifaddr), pas seulement
127.0.0.1, afin que d'autres appareils puissent y accéder aussi. Le port d'administration reste sur
le loopback uniquement. S'exécute au premier plan ; Ctrl-C l'arrête. Les données sont stockées sous
~/.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 | Requis | Signification |
|---|---|---|
| `DIRECTORY_DB_PATH` | non (défaut `directory.sqlite3`, cwd) | Emplacement du fichier SQLite unique. Le répertoire parent doit exister. |
| `DIRECTORY_PUBLIC_ADDR` | non (défaut `0.0.0.0:8080`) | L'API de rendez-vous utilisée par les applications. Peut être exposée publiquement. |
| `DIRECTORY_ADMIN_ADDR` | non (défaut `127.0.0.1:8090`) | Le point d'accès de purge. Tenez-le hors de l'internet public ; voir ci-dessous. |
| `DIRECTORY_ADMIN_TOKEN` | **oui** | Jeton Bearer pour l'API d'administration. Le processus refuse de démarrer sans celui-ci. Générez-le avec `openssl rand -hex 32` ou similaire ; ne le réutilisez nulle part ailleurs. |
Le processus consigne les adresses auxquelles il s'est lié au démarrage et avertit bruyamment si
`DIRECTORY_ADMIN_ADDR` n'est pas en boucle locale.
### Pointer l'application vers lui
Trois façons, dans l'ordre où vous y auriez normalement recours :
1. **Écran de première exécution** : choisissez « Custom server » et saisissez l'URL. Voir §3.
2. **Réglages → Directory server** : modifiez-le plus tard ; prend effet au prochain
redémarrage.
3. **`P2P_CHAT_DIRECTORY_URL`**, définie avant le lancement : ignore la demande
entièrement et remplace ce qui a été enregistré, utile pour les exécutions de développement ou scriptées : ```sh
P2P_CHAT_DIRECTORY_URL=https://directory.example.com npm run tauri dev
Tous ceux qui veulent se trouver doivent pointer vers la même instance de répertoire ; c'est ainsi qu'ils se repèrent les uns les autres en premier lieu.
[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
Notes:
- `DIRECTORY_PUBLIC_ADDR` est volontairement lié à **loopback** ici ; placez un
proxy inverse devant pour le TLS (ci-dessous) plutôt que d'exposer axum
directement sur Internet.
- Créez d'abord l'utilisateur/groupe système `seal-directory` et
`/var/lib/seal-directory` (`useradd --system --no-create-home
seal-directory && install -d -o seal-directory -g seal-directory
/var/lib/seal-directory`), puis copiez le binaire `directory-server` compilé
(depuis `target/release/`) vers `/usr/local/bin/`.
- Placez le jeton admin dans un `EnvironmentFile` lisible uniquement par root,
pas directement dans le fichier unit (les fichiers unit sont souvent
lisibles par tous).
</details>
### TLS via un proxy inverse
<details>
<summary>Afficher la config Caddy / nginx</summary>
[Caddy](https://caddyserver.com) vous offre du HTTPS automatique avec le moins
de configuration :```
# /etc/caddy/Caddyfile
directory.example.com {
reverse_proxy 127.0.0.1:8080
}
caddy run (ou systemctl enable --now caddy) gère lui-même l'émission
et le renouvellement des certificats. Si vous préférez nginx, terminez le TLS à ce niveau
et utilisez proxy_pass http://127.0.0.1:8080;, car l'application n'a besoin que de HTTP simple
du point de vue du proxy.
Côté pare-feu : seul le port public doit être accessible depuis l'extérieur
(8080 dans les exemples ci-dessus, derrière le 443 via le proxy). Le port d'administration
ne doit jamais être accessible depuis l'extérieur ; accédez-y par redirection de port SSH
(ssh -L 8090:127.0.0.1:8090 your-server) lorsque vous devez exécuter une purge
à distance.
cargo run --release -p directory-server --bin directory-admin --
--admin-url http://127.0.0.1:8090 --token "$DIRECTORY_ADMIN_TOKEN" purge
Ceci supprime le fichier SQLite et recrée un schéma vide : aucune instruction `DELETE`, aucun état partiel. Il est sûr de l'exécuter sans prévenir personne au préalable : chaque enregistrement qu'il contient est un cache de données que chaque client détient déjà localement (son propre enregistrement, sa présence et les listes de groupes dont il est membre), donc les clients ne font que le repeupler en quelques instants après leur prochaine action. Il n'y a volontairement aucune politique de sauvegarde pour cette base de données ; voir [`docs/SECURITY.md`](https://github.com/emn4tor/seal/blob/HEAD/docs/SECURITY.md) pour savoir pourquoi en conserver une irait à l'encontre de tout l'intérêt.
---
## 6. Utilisation de l'application
1. **Premier lancement, première question** : quel serveur d'annuaire utiliser (§3). La valeur par défaut est celle intégrée à la version que vous exécutez (un serveur de test local, sauf si celui qui l'a compilée a configuré un serveur officiel) ; choisissez « Custom server » pour pointer vers un serveur que vous ou une personne de confiance hébergez.
2. **Choisissez un nom d'affichage.** Cela génère une paire de clés privées sur votre appareil (rien à retenir, et rien de récupérable en cas de perte : c'est voulu) et vous guide à travers une courte explication intégrée à l'application sur le fonctionnement réel du chiffrement. Rejouez-la à tout moment depuis les Paramètres. Chaque lancement ultérieur vous ramène directement sans invite ; cela ne se produit qu'une fois par compte. Ajoutez d'autres comptes (identités entièrement séparées) depuis Paramètres → Comptes sur cet appareil, et basculez entre eux sans redémarrer.
3. **Ajoutez quelqu'un** : cliquez sur **+** à côté de « Direct messages » et saisissez son ID (visible dans *ses* Paramètres → Mon identité). Il n'y a volontairement aucun annuaire à parcourir ; vous vous connectez de la même manière que vous partageriez un numéro de téléphone.
4. **Envoyez-leur un message** : choisissez leur nom dans la liste et tapez. Le premier message envoyé à quelqu'un établit automatiquement une session chiffrée.
5. **Créez un groupe** : cliquez sur **+** dans la barre d'icônes, nommez-le, puis invitez des personnes par ID de la même manière. Retirer quelqu'un fait pivoter la clé du groupe afin qu'il ne puisse rien lire de ce qui est envoyé par la suite.
6. **Tout supprimer** : Paramètres → Données et confidentialité. C'est instantané, uniquement local et irréversible : cela détruit vos clés, contacts et historique sur *cet appareil* et n'a aucun effet sur les personnes avec qui vous avez discuté.