Hard-Chat – サーバーレス、RAMのみのP2Pターミナルチャット
Hard-Chat – A serverless, RAM-only P2P terminal chat (github.com)
要約
Hard-Chatは、ブラウザ上で動作するエンドツーエンド暗号化されたP2Pチャットアプリケーションです。サーバーやアカウント、履歴の保存がなく、WebRTCを使用してデバイス間で直接通信します。AES-GCM 256ビット暗号化とPBKDF2による鍵導出を採用し、STUN/TURNサーバーでNAT越えをサポートします。ルームキーは100文字のランダムな文字列で、接続確立後2分で失効します。
全文翻訳
🔒 Zero-Trace Terminal
エンドツーエンドで暗号化されたP2Pチャットをブラウザで直接利用。サーバーなし、アカウントなし、履歴保存なし。
by Hardlint Cybersecurity Team
⚠️ 重要なお知らせ
このプロジェクトは、教育およびセキュリティ研究の目的で配布されています。ネットワークレベルの匿名性を保証するものではありません。接続している誰であるかではなく、会話の内容を保護します。機密性の高い通信に使用する前に、攻撃対象領域と既知の制限事項セクションをお読みください。
両方のデバイスで信頼できるVPNの使用を強く推奨します。
✨ 特徴
- 🔐 エンドツーエンド暗号化 — AES-GCM 256ビット、PBKDF2(10万回反復)で導出されるキー
- 🌐 真のP2P接続 — メッセージを中継する中央サーバーなし、2つのデバイス間の直接WebRTCリンク
- 🚫 ゼロ永続性 — クッキーなし、localStorageなし、データベースなし:タブを閉じれば何も残りません
- 🔑 単一共有シークレット — ランダムに生成されるルームキー(100文字)、手動での技術的な設定は不要
- 🧹 パニックパージ — ボタン1つでキー、接続状態、表示されているチャット履歴を即座に消去
- 📡 信頼性の高い接続性 — 18のSTUN/TURNサーバーがフォールバックとして設定されており、制限の厳しいNAT(4G/5G、企業ネットワーク)の後ろでも機能します
🚀 使用方法
- ページを開く(HTTPSで提供されている必要があります — 例: GitHub Pages経由、ローカルファイルとして開かないこと)
- ホスト:
[1] INITIALIZE ROOMをクリック → 生成されたルームキーをコピー - ルームキーを連絡先に別のチャネルで送信(対面、音声通話、別の暗号化されたアプリなど)
- ゲスト:
[2] CONNECT TO ROOMをクリック → 受信したルームキーを貼り付け - 接続を待つ(通常数秒)→ チャットが開きます
- 2分以内に接続が確立されない場合、ルームキーは自動的に期限切れになります:専用ボタンで新しいものを生成してください
📋 要件
- WebRTCおよびWeb Crypto APIをサポートするモダンブラウザ(最近のChrome、Firefox、Edge、Safari)
- 両方のデバイスでのインターネットアクセス
- ページはHTTPSで提供されている必要があります(Web Crypto APIとWebRTCにはセキュアコンテキストが必要)— ローカルファイルとして開いた場合は機能しません
- 接続試行中は、両当事者が同時にページを開いている必要があります
- ルームキーは正確に100文字、余分なスペースや改行なしで、すべてコピーする必要があります
🏗️ アーキテクチャ概要
Zero-Trace Terminalは、2つのデバイスがWebRTC経由で直接ピアツーピア接続を確立し、エンドツーエンドで暗号化されたテキストメッセージを交換できる静的Webアプリケーション(HTML/CSS/JS、独自のバックエンドなし)です。
主要コンポーネント:
| コンポーネント | 役割 | 技術 |
|---|---|---|
| ユーザーインターフェース | レトロなターミナルUI | プレーンHTML/CSS |
| シグナリング | 2つのピアがお互いを「見つける」のを助ける | PeerJS(パブリッククラウドブローカー) |
| データ転送 | 暗号化されたP2Pチャネル | WebRTC DataChannel |
| NATトラバーサル | ファイアウォール/NATを通過する | STUN + TURN (ICE) |
| 暗号化 | メッセージ内容の保護 | AES-GCM 256ビット + PBKDF2 |
| ホスティング | コード配布 | GitHub Pages(静的) |
独自のアプリケーションサーバーはありません。コードはすべて各ユーザーのブラウザで実行されます。関与する唯一の外部インフラストラクチャは、2つのデバイスがお互いを「紹介」するため(シグナリング)、および直接接続が不可能な場合にトラフィックを中継するため(TURN)に使用されます。
🔄 運用フロー
ルームキー生成
ユーザーが「INITIALIZE ROOM (HOST)」をクリックすると:
- ランダムな100文字の文字列が生成されます(
generate100CharCode()を使用)。これは、暗号学的に安全な乱数ジェネレータであるcrypto.getRandomValues()を使用します(暗号目的には不向きなMath.random()ではありません)。 - 文字セットには、大文字/小文字の英字、数字、および特殊記号(
-_!@#$%^&*)が含まれており、100文字内でエントロピーを最大化しています。
この文字列(ルームキー)は、両当事者が交換する必要がある唯一の共有シークレットです(帯域外、例: 音声メッセージ、対面、別の暗号化チャネル)。
ルームキーからのキー導出
ルームキーから、それぞれ異なる目的を持つ2つの独立した値が導出されます:
A. メッセージ暗号化キー (PBKDF2 → AES-GCM)
PBKDF2(
password = Room Key,
salt = "p2p-zero-trace-salt-v1" (固定、ハードコード済み),
iterations = 100,000,
hash = SHA-256
) → 256ビットAES-GCMキー
B. PeerJS識別子(短縮SHA-256)
SHA-256(Room Key) → 最初の32文字の16進数、"ztt-"をプレフィックスとして付与
このIDは、ルームキー以外のものを交換することなく、ホストとゲストがお互いをPeerJSシグナリングブローカーで見つけられるようにするためだけに、単独で使用されます。暗号学的な役割はありません。
固定ソルトに関する注意: PBKDF2ソルトはハードコードされており、すべてのセッションで同一です。これは、ルームキー(パスワード)が既に非常に高いエントロピー(100文字のランダム文字列)を持っているため許容されます。固定ソルトは、弱い、再利用されたパスワードが関与するシナリオでのみセキュリティを低下させますが、このケースには当てはまりません。
シグナリングフェーズ (PeerJS)
- ホストは
Peerオブジェクトを作成し、ルームキーから導出されたIDを使用して、パブリックPeerJSクラウドブローカーに登録します。 - ゲストは、同じルームキーを貼り付けた後、同じIDを計算し、
peer.connect(id)を呼び出します。 - PeerJSブローカーは、この初期交換(誰が誰と話したいか)のみを仲介します。メッセージの内容は決して見たり送信したりしません。メッセージは、その時点ですでに別のWebRTCチャネルを介して転送されます。
ICEネゴシエーション (NATトラバーサル)
2つのピアがお互いを「紹介」した後、WebRTCは有効なネットワークパスを見つけるためにICEネゴシエーションを開始します:
- ホスト候補 — デバイスのローカルIPアドレス
- サーバー反射候補 — STUN経由で検出されたパブリックIP
- リレー候補 — TURN経由で割り当てられ、直接接続が失敗した場合にのみ使用されます
設定されたICEサーバー(優先順位順):
- 専用Metered.ca TURN(独自の認証情報、共有されていません)—
stun.relay.metered.ca/global.relay.metered.ca - 7つのパブリックSTUNフォールバック(Google ×3、Cloudflare、Twilio、Nextcloud、stunprotocol.org、freestun)
- 10の追加パブリックTURNフォールバックエンド(OpenRelay、freestun、numb.viagenie、ExpressTurn)— 専用TURNも失敗した場合にのみ使用されます
ブラウザは自動的にすべての組み合わせを試行し、動作するチャネルを確立した最初のものを選択します(標準ICEアルゴリズム、WebRTCによって内部的に処理されます)。
タイムアウトとセッション期限切れ
- 接続が120秒以内に確立されない場合、セッションは期限切れと見なされます:
PeerConnection/DataConnectionは破棄されます(peer.destroy(),conn.close())。- ステータスは
[EXPIRED] Room Key no longer validと表示されます。 - 新しいルームキーを生成するためのボタン(ホスト)または新しいキーを入力するためのボタン(ゲスト)が表示されます。
- これにより、ルームキーがパブリックブローカーで無期限に「リスニング」状態になるのを防ぎます。
🔐 メッセージ暗号化モデル
すべてのメッセージは、DataChannel経由で送信される前に個別に暗号化されます:
1. ランダムな12バイトのIVを生成します(crypto.getRandomValues)
2. ciphertext = AES-GCM-Encrypt(key, IV, plaintext)
3. payload = IV || ciphertext (連結、IVは平文で先頭に配置)
4. payloadをUint8Arrayとしてconn.send()で送信します
受信時:
1. 最初の12バイトをIVとして抽出します
2. 残りは暗号文です(末尾に16バイトのGCM認証タグが含まれます)
3. plaintext = AES-GCM-Decrypt(key, IV, ciphertext)
AES-GCMによって保証されるセキュリティプロパティ:
- 機密性 — キーを持たない誰も内容を読むことはできません
- 完全性/認証 — 転送中のパケットの改ざんはいかなるものも、サイレントに破損した出力を生成するのではなく、復号化を失敗させます(
[ERR: DECRYPTION_FAILED])。
このスキームでカバーされていないこと:
- セッション間のフォワードシークレット — 同じルームキーが複数のセッションで再利用された場合(新しいキーが毎回生成される通常のフローではありません)、それらのセッションはすべて同じ導出キーを共有します。
- ピアID認証 — ルームキーを知っている人は誰でも接続できます。共有キーの所有権を超えて、「誰」が相手であるかの暗号学的な検証はありません。
💾 データ永続性(クライアントサイド)
| データ | 永続性 | メモ |
|---|---|---|
| ルームキー | なし | JS変数にのみ存在、クローズ/リロードで消去 |
| 導出されたAES-GCMキー | なし | 同上、ディスクに書き込まれることはありません |
| チャットメッセージ | なし | DOM/RAMにのみ存在、localStorage/IndexedDBなし |
| クッキー | なし | プロジェクトでは一切使用していません |
| アプリケーションログ | ローカルDevToolsコンソールのみ | どこにも送信されず、タブを閉じると消去されます |
「PANIC: PURGE SESSION」ボタンは明示的に以下を強制します:
PeerConnection/DataConnectionのクローズ- メモリ内の暗号化キーのゼロ化
- すべてのUIフィールドと表示されているメッセージ履歴の消去
👁️ 外部インフラストラクチャが見ることができるもの(メタデータ)
理解すべき重要な点:暗号化は内容を保護しますが、接続メタデータは保護しません。
| サービス | 見ることができるもの | 見ることができないもの |
|---|---|---|
| GitHub Pages | ページをロードした人のIPとタイムスタンプ | メッセージ内容、ルームキー |
| PeerJSブローカー(パブリッククラウド) | ホストとゲストのIP、接続した日時、ピアID(ルームキーのハッシュであり、キー自体ではない) | メッセージ内容 |
| Metered.ca TURN(リレーとして使用される場合) | 送信元/宛先IP、ポート、転送されたデータ量 | メッセージ内容(すでに暗号化されて転送されるため) |
| 各ユーザーのISP | github.io / metered.ca / PeerJSサーバーへの接続が行われていること | メッセージ内容 |
推奨される緩和策(コード外): 両方のデバイスで信頼できるVPN(例: 匿名で作成したアカウントを持つMullvad)を使用し、これらのサードパーティサービスに実際のIPアドレスを公開しないようにしてください。プロジェクトは、スプラッシュ画面でこの効果に関する明示的な警告を表示します。
🎯 攻撃対象領域と既知の制限事項
| リスク | 説明 | 緩和策あり? |
|---|---|---|
| メッセージ内容の傍受 | TURN/ネットワークトラフィックのMITM | ✅ はい — エンドツーエンドAES-GCM |
| 転送中のメッセージ改ざん | パケット操作 | ✅ はい — GCM認証タグ |
| IPメタデータによる匿名化解除 | サードパーティログを介したIP↔ID相関 | ⚠️ 部分的 — クライアントサイドでVPNが必要、コード自体では解決しない |
| Metered.caアカウントの侵害 | TURN認証情報は公開コード内にある(base64で難読化、暗号化されていない) | ⚠️ 最小限の抑止力、実際のセキュリティではない |
| サードパーティサービスへの依存 | GitHub Pages、PeerJSブローカー、Metered TURN — 停止した場合、アプリは動作しなくなる | ⚠️ 緩和策なし(完全なセルフホスティングが必要になる) |
| ルームキーの再利用 | それを再利用するセッション間でフォワードシークレットを侵害する可能性がある | ✅ 通常フローでは適用されない(セッションごとに新しいキー) |
🛠️ 完全な技術スタック
- フロントエンド: HTML5、CSS3(フレームワークなし)
- 暗号化: ブラウザネイティブWeb Crypto API(
crypto.subtle)— PBKDF2、AES-GCM、SHA-256 - P2P/シグナリング: PeerJS v1.5.4(ネイティブWebRTCのラッパーライブラリ)、パブリックCDN(unpkg.com)からロード
- NATトラバーサル: WebRTC ICE(STUN/TURN)— 合計18エンドポイントを設定
- ホスティング: GitHub Pages(静的、自動HTTPS)
- ブラウザ要件: WebRTCサポート、Web Crypto API、ES6+ — セキュアコンテキスト(HTTPS)が必要。
file://またはcontent://からは動作しません
📝 主要バージョンの変更履歴
- v1 — ネイティブWebRTCと手動SDP交換(オファー/アンサーのコピー&ペースト)
- v2 — PeerJSに移行、ルームキーに基づく自動接続、手動SDPフィールドを削除
- v3 — 専用Metered.ca TURN + 複数のパブリックSTUN/TURNフォールバックを追加
- v4 — 詳細なICE診断(候補ログ、接続状態)— PeerJSの内部イベントハンドラを上書きするバグを修正
- v5 — タイムアウトを120秒に延長、手動再生成によるルームキー期限切れシステム、スプラッシュ画面でのVPN警告、TURN認証情報の難読化
💚 プロジェクトをサポートする
Hard-Chatは100%無料、オープンソースで、Hardlint Cybersecurity Teamによってメンテナンスされています。広告は表示せず、データを販売することもありません。私たちのミッションを信じ、将来のセルフホストインフラストラクチャ(カスタムSTUN/TURNサーバー)の資金調達を支援したい場合は、ご支援を検討してください!
Solana (SOL) 寄付アドレス:
GSsqZCtDC7rf53U6gC5cJ4weAYYT9g7twxz9t15mfRDV
📜 ライセンスと免責事項
このソフトウェアは「現状有姿」で提供され、いかなる保証もありません。開発者は、このツールの不適切または違法な使用について責任を負いません。ユーザーは、管轄区域の適用法を遵守する責任を単独で負います。
このドキュメントは、情報提供および技術文書の目的でのみ提供されます。規制遵守、プライバシー、または使用に関する責任について、法的助言を構成するものではありません。
Hardlint Cybersecurity Team