HN 日本語サマリー

← 一覧へ戻る
Web開発

Hard-Chat – サーバーレス、RAMのみのP2Pターミナルチャット

Hard-Chat – A serverless, RAM-only P2P terminal chat (github.com)

12 pointsby hardlint1 コメント

要約

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、企業ネットワーク)の後ろでも機能します

🚀 使用方法

  1. ページを開く(HTTPSで提供されている必要があります — 例: GitHub Pages経由、ローカルファイルとして開かないこと)
  2. ホスト: [1] INITIALIZE ROOM をクリック → 生成されたルームキーをコピー
  3. ルームキーを連絡先に別のチャネルで送信(対面、音声通話、別の暗号化されたアプリなど)
  4. ゲスト: [2] CONNECT TO ROOM をクリック → 受信したルームキーを貼り付け
  5. 接続を待つ(通常数秒)→ チャットが開きます
  6. 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)」をクリックすると:

  1. ランダムな100文字の文字列が生成されます(generate100CharCode()を使用)。これは、暗号学的に安全な乱数ジェネレータであるcrypto.getRandomValues()を使用します(暗号目的には不向きなMath.random()ではありません)。
  2. 文字セットには、大文字/小文字の英字、数字、および特殊記号(-_!@#$%^&*)が含まれており、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)
  1. ホストはPeerオブジェクトを作成し、ルームキーから導出されたIDを使用して、パブリックPeerJSクラウドブローカーに登録します。
  2. ゲストは、同じルームキーを貼り付けた後、同じIDを計算し、peer.connect(id)を呼び出します。
  3. PeerJSブローカーは、この初期交換(誰が誰と話したいか)のみを仲介します。メッセージの内容は決して見たり送信したりしません。メッセージは、その時点ですでに別のWebRTCチャネルを介して転送されます。
ICEネゴシエーション (NATトラバーサル)

2つのピアがお互いを「紹介」した後、WebRTCは有効なネットワークパスを見つけるためにICEネゴシエーションを開始します:

  1. ホスト候補 — デバイスのローカルIPアドレス
  2. サーバー反射候補 — STUN経由で検出されたパブリックIP
  3. リレー候補 — 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、ポート、転送されたデータ量メッセージ内容(すでに暗号化されて転送されるため)
各ユーザーのISPgithub.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://からは動作しません

📝 主要バージョンの変更履歴

  1. v1 — ネイティブWebRTCと手動SDP交換(オファー/アンサーのコピー&ペースト)
  2. v2 — PeerJSに移行、ルームキーに基づく自動接続、手動SDPフィールドを削除
  3. v3 — 専用Metered.ca TURN + 複数のパブリックSTUN/TURNフォールバックを追加
  4. v4 — 詳細なICE診断(候補ログ、接続状態)— PeerJSの内部イベントハンドラを上書きするバグを修正
  5. v5 — タイムアウトを120秒に延長、手動再生成によるルームキー期限切れシステム、スプラッシュ画面でのVPN警告、TURN認証情報の難読化

💚 プロジェクトをサポートする

Hard-Chatは100%無料、オープンソースで、Hardlint Cybersecurity Teamによってメンテナンスされています。広告は表示せず、データを販売することもありません。私たちのミッションを信じ、将来のセルフホストインフラストラクチャ(カスタムSTUN/TURNサーバー)の資金調達を支援したい場合は、ご支援を検討してください!

Solana (SOL) 寄付アドレス:

GSsqZCtDC7rf53U6gC5cJ4weAYYT9g7twxz9t15mfRDV

📜 ライセンスと免責事項

このソフトウェアは「現状有姿」で提供され、いかなる保証もありません。開発者は、このツールの不適切または違法な使用について責任を負いません。ユーザーは、管轄区域の適用法を遵守する責任を単独で負います。

このドキュメントは、情報提供および技術文書の目的でのみ提供されます。規制遵守、プライバシー、または使用に関する責任について、法的助言を構成するものではありません。


Hardlint Cybersecurity Team