
Seal v0.2.3
ピアツーピア、エンドツーエンド暗号化チャット。受信トレイなし。復元するアカウントもなし。誰も盗聴していない — 私たちでさえも。
メッセージはlibp2p経由でピア間を直接送信され、デバイスから送信される 前に、SignalスタイルのOlm/Megolmプロトコル( vodozemac経由)で 暗号化されます。関与する唯一のサーバーは、ピアがお互いの現在の アドレスを見つけるための小さなディレクトリです。サーバーはメッセージ内容を 見ることはなく、1コマンドで削除できます。
実際に何から保護されているか、またその仕組みについては、
docs/THREAT_MODEL.mdと
docs/SECURITY.mdを参照してください。
Contents
- スクリーンショット
- 機能
- 仕組み
- プロジェクト構成
- 1. 前提条件
- 2. ビルド
- 3. 開発モードで実行する
- 4. テスト
- 5. バックエンド(ディレクトリサーバー)のセットアップ
- 6. アプリの使い方
スクリーンショット
初回起動 — 名前を選ぶだけ。他に設定は不要です。
会話 — グループレール、連絡先一覧、エンドツーエンド暗号化チャットペイン。
設定 — マイク感度、プッシュトーク、ログイン時に起動、ネットワーク到達可能性。
機能
- 常にエンドツーエンド暗号化 — すべてのメッセージは、デバイスから送信され る前にOlm(1:1)またはMegolm(グループ)で封印され、 vodozemacのダブルラチェット方式を 使用します。すべてのメッセージに独自の鍵が割り当てられます。
- 受信トレイは存在しない — メッセージは直接のピアツーピア接続で (libp2p: QUIC/TCP + Noise、リレーとNAT用の ホールパンチング付き)送信されます。受信者がオフラインの場合、メッセージは ローカルで待機して再試行されます。誰かのインフラにキューイングされることはありません。
- ディレクトリであってデータベースではない — 関与する唯一のサーバー
(
crates/directory-server)は、ユーザーIDを現在のネットワークアドレスに マッピングするだけで、それ以外は何もしません。構造的にメッセージ内容を 読めません。そのCargo.tomlは、読み取り方法を知っているcrateに依存すらしていません。 - 1台のデバイスで複数アカウント — 完全に分離されたアイデンティティ(鍵、 連絡先、メッセージ)を、再起動せずに切り替えられます。
- 実際のメンバー変更に対応したグループ — グループごとにテキストと音声の チャンネルを用意。メンバーを削除するとグループの鍵がローテーションされ、 それ以降に送信されたメッセージを読めなくなります。
- 音声機能を内蔵 — システム全体のショートカットでのプッシュトーク(Seal だけでなく、任意のアプリから操作可能)、調整可能なマイク感度、オプションの ボイスチェンジャーを搭載。
- メタデータなしの添付 — EXIFデータ(GPS位置情報、 カメラ/デバイス情報)は、画像が送信される前に削除されます。デフォルトで 有効です。
- 本物のパニックボタン — 設定 → データとプライバシー を実行すると、 このデバイス上のすべての鍵、連絡先、メッセージが即座にかつ不可逆的に削除され、 会話した相手には一切影響を与えません。
- 必要に応じてログイン時に起動 — デフォルトでオン。設定でワンタップ切り替え。
- 1つのコードベース、3つのプラットフォーム — macOS、Windows、Linuxで ネイティブウィンドウを実現(Tauri 経由)。
仕組み
このアプリには2種類のアイデンティティがあり、意図的に分離されています。
- チャットアイデンティティは、ローカル生成されるEd25519/Curve25519鍵ペア
で、初回起動時に
vodozemacによって作成されます
(
identity::Identity)。公開「ユーザーID」は、その鍵のフィンガープリント にすぎません(wire_proto::user_id_from_ed25519)。サーバーによる発行・ 失効はできません。作成にサーバーが関与しないからです。 - ネットワークアイデンティティは、別のlibp2p鍵ペア(
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 はネットワークと直接通信することはありません。