
ピアツーピア、エンドツーエンド暗号化チャット。受信トレイなし。復元するアカウントもなし。誰も盗聴していない — 私たちでさえも。
メッセージはlibp2p経由でピア間を直接送信され、デバイスから送信される 前に、SignalスタイルのOlm/Megolmプロトコル( vodozemac経由)で 暗号化されます。関与する唯一のサーバーは、ピアがお互いの現在の アドレスを見つけるための小さなディレクトリです。サーバーはメッセージ内容を 見ることはなく、1コマンドで削除できます。
実際に何から保護されているか、またその仕組みについては、
docs/THREAT_MODEL.mdと
docs/SECURITY.mdを参照してください。
初回起動 — 名前を選ぶだけ。他に設定は不要です。
会話 — グループレール、連絡先一覧、エンドツーエンド暗号化チャットペイン。
設定 — マイク感度、プッシュトーク、ログイン時に起動、ネットワーク到達可能性。
crates/directory-server)は、ユーザーIDを現在のネットワークアドレスに
マッピングするだけで、それ以外は何もしません。構造的にメッセージ内容を
読めません。そのCargo.tomlは、読み取り方法を知っているcrateに依存すらしていません。このアプリには2種類のアイデンティティがあり、意図的に分離されています。
identity::Identity)。公開「ユーザーID」は、その鍵のフィンガープリント
にすぎません(wire_proto::user_id_from_ed25519)。サーバーによる発行・
失効はできません。作成にサーバーが関与しないからです。PeerId)で、
トランスポート層でのみ使用されます。再起動してもチャットアイデンティティには
まったく影響しません。2つは、自分で署名するプレゼンスレコードによってのみ
結び付けられます。誰かを見つけることと、実際に会話することは、別々のステップです。``` ┌────────────────────────┐ │ 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. アリスはディレクトリでボブをユーザーIDで検索する。これにより、ボブの
公開鍵と最後に通知されたネットワークアドレスが返される。ディレクトリが保持しているのはこれだけだ。
すなわち、公開鍵、表示名、グループ
メンバーシップリスト、そして短期間のアドレス通知
(`crates/directory-server`)。
2. アリスは libp2p 経由でボブに直接ダイヤルする(QUIC または TCP+Noise。リレー+
ホールパンチングで NAT の背後にあるピアに対応。`crates/net` を参照)。ディレクトリは
ここから先は完全に登場しない。
3. 実際のメッセージは、1対1チャットでは **Olm** で暗号化され、グループでは
**Megolm** で暗号化されます(`crates/crypto-session`)。これはダブルラチェット式の
仕組みで、すべてのメッセージが独自の鍵を取得してから、
libp2p 接続に載せられます。サーバー側のインボックスはありません。ボブがオフラインの場合、
メッセージはローカルで待機して再試行され、他の誰かのインフラには
保存されません。
上記のすべては、`crates/core` の `AppService` によって調整され、これは
Tauri アプリの Rust バックエンド(`apps/desktop/src-tauri`)が実際に呼び出す
対象です。UI はネットワークと直接通信することはありません。
## プロジェクト構成```
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)
すべてのプラットフォームで Rust と Node.js が必要です。さらに、Tauri がネイティブウィンドウをビルドするために必要なプラットフォーム固有のツールチェーンも必要です。また、storage と directory-server は SQLite をソースからバンドルコンパイルするため、プレーンな C コンパイラが必要です(このプロジェクトのどこにも OpenSSL やその他のネイティブ暗号ライブラリは必要ありません)。
すべてのプラットフォームに共通:
Cコンパイラ、pkg-config、および Tauri の Linux バックエンドがリンクする
WebKitGTK/AppIndicator 開発パッケージをインストールします。
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"
アーキテクチャ:```sh
sudo pacman -S --needed webkit2gtk-4.1 base-devel curl wget file openssl
appmenu-gtk-module libappindicator-gtk3 librsvg pkgconf
(Package names shift between Tauri releases: if a build fails looking for a
missing `.pc` file, check the
[current Tauri Linux prerequisites](https://v2.tauri.app/start/prerequisites/)
for your distro.)
(パッケージ名はTauriのリリースごとに変わります。ビルドが欠落した `.pc` ファイルを探して失敗する場合は、お使いのディストリビューション向けの[現在のTauri Linux前提条件](https://v2.tauri.app/start/prerequisites/)を確認してください。)
</details>
<details>
<summary><strong>Windows</strong></summary>
1. **Microsoft C++ Build Tools**をインストールします(Visual Studio Installer → "Desktop development with C++" ワークロード)。これはTauriのネイティブシェルと、バンドルされたSQLiteをコンパイルする両方に必要です。
2. **MSVC** Rustツールチェーンをインストールします: `rustup default stable-msvc`。
3. **WebView2**: Windows 11とほとんどの最新のWindows 10インストールには既に存在しています。ない場合は、TauriのビルドがEvergreenランタイムのインストールを促します。
</details>
---
## 2. ビルド
リポジトリのルートから:```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 は、リポジトリルートの target/release/bundle/ 配下にプラットフォームネイティブなインストーラを生成します(これは Cargo ワークスペースであるため、Tauri アプリを含むすべてのクレートが単一のトップレベル target/ ディレクトリを共有します)。クロスコンパイル(例: macOS から Windows インストーラをビルドする)は設定されていません。各ターゲットプラットフォームでビルドするか、CI ビルドのリリースが必要な場合は Tauri の GitHub Actions ワークフローを使用してください。
scripts/ には、プラットフォーム/出力ごとに1つのビルドスクリプトがあり、それぞれ独立して実行可能で、実際に動作するアーティファクトを生成することが確認されています。
| スクリプト | 生成物 |
|---|---|
scripts/build-mac-dmg.sh | macOS .dmg インストーラ |
scripts/build-mac-app.sh | macOS .app バンドル(そのまま、インストーラなし) |
scripts/build-linux.sh | Linux .AppImage + .deb |
scripts/build-windows.ps1 | Windows .msi + .exe (NSIS) |
各スクリプトは単に npm run tauri build --bundles <...> を適切なフラグとプラットフォームチェックでラップしているだけです。異なるバンドル組み合わせが必要な場合は、自分で生のコマンドを実行してください(apps/desktop から npx tauri build --help)。
scripts/release.sh vX.Y.Z は、必要なすべての場所でバージョンを更新し、コミットにタグを付けます — docs/RELEASING.md を参照してください。macOS と Linux で実行できます。コミットやプッシュは行いません。
サーバー選択画面(§3)には常に3つのオプションが表示されます: Seal(独自の公式ネットワーク)、カスタムサーバー、および下部にある小さな ローカルテストサーバー リンクです。「Seal」は、ビルド 時に URL を組み込むまで無効(グレーアウトされ、「このビルドではまだ設定されていません」と表示)になっています:```sh SEAL_DEFAULT_DIRECTORY_URL=https://directory.example.com npm run tauri build
§5で自分のサーバーを立ち上げ、実際のドメインをそこに向けたら、これを設定して再ビルドしてください:以降に配布するすべてのコピーでは、他のコードに一切手を加えることなく、そのURLを使用して "Seal" が実際の選択可能なオプションとして表示されます。通常のビルド/開発ビルドでは未設定のままにしてください:このリポジトリには公式サーバーがホストされていないため、"Seal" は無効のままであり、ユーザーはカスタムサーバーまたはローカルサーバーにフォールバックすることになります。アプリが実際には何も実行していないプレースホルダードメインを黙って指すことはありません。
---
## 3. 開発モードで実行する```sh
cd apps/desktop
npm install
npm run tauri dev
これにより Vite 開発サーバーが起動し、Rust バックエンドがデバッグモードでコンパイルされ、 フロントエンドがホットリロードされるネイティブウィンドウが開きます。初回ビルドでは、 依存ツリー全体をコンパイルするため数分かかりますが、以降の実行は高速です。
初回起動時、Seal はどのディレクトリサーバーを使うかを次の 順序で尋ねます:
127.0.0.1:47100/47101 にバインドされ、データは OS のアプリデータディレクトリ配下)。
Seal を試したり、1 台のマシンでインスタンスをテストするのには適していますが、
実際のデプロイではありません。2 番目のインスタンスがポートを使用済みと
検出した場合、新しいサーバーを起動する代わりに最初のインスタンスのサーバーを再利用します。
これが、1 台のマシン上の 2 つのインスタンスが互いを発見できる仕組みです。
"Seal" が設定されておらず、他に何も選択しない場合、
自動的にこれが選択されます。選択内容は保存され(アプリの他のローカルデータの隣にある server.json)、
以後の起動のたびに静かに再利用されます。変更するには Settings → Directory
server で行いますが、これは実行中の接続をホットスワップしようとするのではなく、
次回アプリを起動したときに有効になります。スクリプトや開発用途では、
環境変数を使うとプロンプトを完全にスキップできます:```sh
P2P_CHAT_DIRECTORY_URL=https://directory.example.com npm run tauri dev
### 2つのインスタンスをローカルで実行する(実際にメッセージングをテストするため)
各インスタンスには独自のIDが必要です。Seal は複数のアカウントをネイティブにサポートしています(このデバイスの Settings → Accounts)が、1台のマシン上で2つの *別々のプロセス* を実行するには、`P2P_CHAT_PROFILE` がより迅速な方法です。これは、その名前のアカウントを(初回は)自動作成し、(それ以降は毎回)自動再開して、非対話的にアカウントピッカーを完全にスキップします:```sh
# terminal 1
P2P_CHAT_PROFILE=alice npm run tauri dev
# terminal 2
P2P_CHAT_PROFILE=bob npm run tauri dev
サーバーの選択 (server.json) とアカウントリスト (accounts.json)
は、1台のマシン上のプロセス間で共有され、プロファイルごとではありません。最初に
起動するインスタンスがサーバーを選択し、それ以降のすべてのプロファイル
(ここでの bob も含む) はそれを黙って再利用します。両方のウィンドウは
同じ組み込みディレクトリサーバー上に配置されるため、IDでお互いを連絡先として追加し、
相互にメッセージを送信できます。
Vite の開発サーバーは、Tauri の WebView が指す実際の固定ポートを必要とするため、
通常は一度に1つの npm run tauri dev しか実行できません — 2つ目の
インスタンスはポート 1420 がすでに使用されているのを見つけて即座に失敗します。
npm run tauri は実際には小さなラッパー (apps/desktop/scripts/tauri.mjs) であり、
最初のインスタンス以降は次に空いているポート (1421, 1422, …) を選択して
自動的に接続します。そのため、上記の2つのコマンドを2つのターミナルで実行するだけで
機能します。特別な操作は必要ありません。これは
dev の場合のみ動作を変更します — npm run tauri build など、その他のコマンドは
実際の CLI にそのまま渡されます。
./scripts/run-two-mac-instances.sh # profiles: alice, bob ./scripts/run-two-mac-instances.sh carol dave
上記と同じアイデアだが、実際にビルドされたアプリ(`build-mac-app.sh` /
`build-mac-dmg.sh`の出力、または`/Applications`にインストールされたコピー)を
`npm run tauri dev`の代わりに異なる`P2P_CHAT_PROFILE`で2回起動し、
実際のユーザーが実行する状況に近い。PIDと両方の停止方法を出力する。
### デバッグ
- **Rustログ**: 起動前に`RUST_LOG`を設定します。例:
`RUST_LOG=debug npm run tauri dev`(または`RUST_LOG=p2p_core=debug,net=debug`
で範囲を絞る)。ログに記録されるフィールドはメタデータ(ピア/グループ/ユーザー
ID、エラータイプ)のみです。詳細なままでも安全な理由は
[`docs/SECURITY.md`](https://github.com/emn4tor/seal/blob/HEAD/docs/SECURITY.md)を参照してください。
- **フロントエンド**: 開発ウィンドウは実際のWebViewです。右クリック → 要素の検査
(または開発者ツールを開く)は通常のブラウザと同じように動作します。
- **バックエンドクレートを単独で**: 各クレートには独自のテストスイートがあり、UIにまったく触れずに
実行・反復できます。§4を参照してください。
- **スタンドアロンのディレクトリサーバー**(組み込みの代わり): §5を参照してください。
---
## 4. テスト```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
これが実際に何なのかをまとめると(大げさに想像しがちなので):1つの axum プロセス、1つの SQLite ファイル、3種類のレコード(公開鍵、短命のプレゼンス通知、グループ名簿)、そしてすべての書き込みは呼び出し元自身のアイデンティティ鍵で署名されます。メッセージの経路上には決して存在しません。それがポリシーだけでなく構造的にそうである理由については、docs/THREAT_MODEL.md を参照してください:directory-server の Cargo.toml は、メッセージコンテンツの読み方を知っているクレートに依存すらしていません。
sudo ./scripts/setup-backend.sh
Interactive、Linux + systemd のみ対応(理由はスクリプトのヘッダーを参照)。どのディストリビューション系かを尋ねる(Debian/Ubuntu、Fedora/RHEL/Rocky/Alma、Arch/Manjaro、openSUSE。`/etc/os-release` からの推測で事前入力されるので、通常は1回のキー入力で確認できる)。そして、ファミリーごとの専用関数でそのディストリのビルド前提条件をインストールし、Rust が無い場合は `rustup` 経由でインストールするか尋ね、リリースバイナリをビルドし、専用システムユーザーを作成し、管理トークンを生成し、[Caddy](https://caddyserver.com) による自動HTTPS付きドメインを設定するか(Caddy自体もディストリごとにインストールし、ディストリのパッケージが無い場合はCaddy公式の静的バイナリにフォールバック)、それとも自分で前面に出す場合はループバック/平文HTTPにバインドするだけにするかを尋ね、その後 systemd サービスを書き込んで有効化する。再実行しても安全。
以下は、実際にこのスクリプトが行っている内容です。手動で行いたい場合や、実行前に理解したい場合にどうぞ。
### macOS: 手軽なLANテストサーバー```sh
./scripts/run-mac-test-server.sh
実運用向けではありません。ドメイン、TLS、systemd(macOSにはそもそも存在しない)を設定せずに、同じネットワーク上の2つのデバイス(例: Macともう1台のマシン、または同じWi-Fi上の2人)でアプリをテストするためのものです。リリースバイナリをビルドし、管理トークンを生成し(後の実行で再利用)、パブリックAPIをすべてのインターフェースにバインドして、使用するURLを出力します。それはMacの実際のLAN IP(ipconfig getifaddr による)であり、127.0.0.1 だけではないので、他のデバイスからも到達できます。管理ポートはループバックのみに留まります。フォアグラウンドで実行され、Ctrl-C で停止します。データは ~/.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
| 変数 | 必須 | 意味 |
|---|---|---|
| `DIRECTORY_DB_PATH` | 任意(デフォルト `directory.sqlite3`、カレントディレクトリ) | 単一のSQLiteファイルが配置される場所。親ディレクトリが存在している必要があります。 |
| `DIRECTORY_PUBLIC_ADDR` | 任意(デフォルト `0.0.0.0:8080`) | アプリが通信するランデブーAPI。公開しても問題ありません。 |
| `DIRECTORY_ADMIN_ADDR` | 任意(デフォルト `127.0.0.1:8090`) | パージエンドポイント。これをパブリックインターネットに公開しないでください。以下を参照。 |
| `DIRECTORY_ADMIN_TOKEN` | **必須** | 管理API用のBearerトークン。プロセスはこれなしでは起動を拒否します。`openssl rand -hex 32` または類似のコマンドで生成してください。他の場所で再利用しないでください。 |
プロセスは起動時にバインドしたアドレスをログに記録し、
`DIRECTORY_ADMIN_ADDR` がループバックでない場合は大きく警告します。
### アプリの接続先を指定する
通常使うであろう順に、3つの方法があります:
1. **初回起動画面**: 「カスタムサーバー」を選択し、URLを入力します。§3 を参照。
2. **設定 → ディレクトリサーバー**: 後で変更できます。次の再起動時に
有効になります。
3. **`P2P_CHAT_DIRECTORY_URL`** を起動前に設定すると、入力を完全にスキップし、
保存済みの設定を上書きします。開発用やスクリプト実行に便利です: ```sh
P2P_CHAT_DIRECTORY_URL=https://directory.example.com npm run tauri dev
お互いを見つけたい人は皆、同じ ディレクトリインスタンスを指す必要があります。それがそもそもお互いを探す方法だからです。
[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
注記:
- `DIRECTORY_PUBLIC_ADDR` はここでは意図的に **loopback** にバインドされています。TLS 用のリバースプロキシを
前面に置いてください(下記参照)。axum を直接インターネットに公開するのではなく。
- 先に `seal-directory` システムユーザー/グループと `/var/lib/seal-directory` を作成し
(`useradd --system --no-create-home seal-directory && install -d -o seal-directory -g seal-directory
/var/lib/seal-directory`)、ビルドした `directory-server` バイナリを (`target/release/` から)
`/usr/local/bin/` にコピーしてください。
- 管理者トークンは、ルートのみが読み取り可能な `EnvironmentFile` に配置し、ユニットファイルに直接
記述しないでください(ユニットファイルはしばしば誰でも読み取り可能です)。
</details>
### リバースプロキシ経由のTLS
<details>
<summary>Caddy / nginx 設定を表示</summary>
[Caddy](https://caddyserver.com) を使えば、最小限の設定で自動的に HTTPS を利用できます:```
# /etc/caddy/Caddyfile
directory.example.com {
reverse_proxy 127.0.0.1:8080
}
caddy run(または systemctl enable --now caddy)は証明書の
発行/更新を自動で処理します。nginx を使いたい場合は、そこで TLS を終端し、
proxy_pass http://127.0.0.1:8080; とします。アプリはプロキシから見て平文 HTTP のみを
必要とするためです。
ファイアウォールに関して:外部から到達できる必要があるのはパブリックポートのみです
(上記の例では 8080 で、プロキシ経由で 443 が前面にあります)。管理ポートは
外部から到達可能にしてはいけません。リモートでパージを実行する必要がある場合は、
SSH ポートフォワーディング(ssh -L 8090:127.0.0.1:8090 your-server)経由で
アクセスしてください。
cargo run --release -p directory-server --bin directory-admin --
--admin-url http://127.0.0.1:8090 --token "$DIRECTORY_ADMIN_TOKEN" purge
これによりSQLiteファイルが削除され、空のスキーマが再作成されます。`DELETE`
文も部分的な状態もありません。事前に誰かに警告せずに実行しても安全です。
この中のすべてのレコードは、各クライアントがすでにローカルに保持しているデータの
キャッシュです(自身の登録情報、プレゼンス、所属するグループのメンバー
名簿など)。そのため、クライアントは次のアクションの直後にそれを再入力するだけです。
このデータベースには意図的にバックアップポリシーがありません。
[`docs/SECURITY.md`](https://github.com/emn4tor/seal/blob/HEAD/docs/SECURITY.md) に、バックアップを保持することが
全体の趣旨を損なう理由が説明されています。
---
## 6. アプリの使用
1. **初回起動時、最初の質問**: 使用するディレクトリサーバー(§3)。
デフォルトは、実行中のビルドに組み込まれているものです(ローカル
テストサーバー。ただし、ビルドした人が公式サーバーを設定した場合を除く)。
「カスタムサーバー」を選択して、自分または信頼できる誰かがホストするサーバーを指定します。
2. **表示名を選択します。** これにより、デバイス上に秘密鍵ペアが生成され
(覚えるものはなく、失われた場合に回復できるものもありません。これは
意図的です)と、暗号化が実際にどのように機能するかについてのアプリ内の
短い説明が表示されます。設定からいつでも再生できます。以降の
起動時は、プロンプトなしで直接戻ります。これはアカウントごとに1回だけ
発生します。設定 → アカウントから、このデバイス上でさらにアカウント
(完全に別々のアイデンティティ)を追加し、再起動せずにそれらを切り替えることができます。
3. **誰かを追加する**: 「ダイレクトメッセージ」の横にある**+**をクリックし、
相手のID(*相手の*設定 → マイアイデンティティにあります)を入力します。閲覧できるディレクトリは
設計上ありません。電話番号を共有するのと同じ方法で接続します。
4. **メッセージを送信する**: リストから名前を選んで入力します。誰かに送る最初の
メッセージは、自動的に暗号化セッションを確立します。
5. **グループを開始する**: アイコン欄の**+**をクリックし、名前を付けてから、
同じ方法でIDで人を招待します。メンバーを削除するとグループのキーがローテーションされるため、
その後送信されたものを読めなくなります。
6. **すべて削除する**: 設定 → データとプライバシー。これは即時で、
ローカルのみに影響し、取り消し不可能です。キー、連絡先、
*このデバイス*上の履歴を破壊し、会話した相手には影響しません。