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分で失効します。

全文翻訳

🔒 ゼロトレース・ターミナル エンドツーエンドで暗号化されたP2Pチャットを、ブラウザ上で直接利用できます。サーバーなし、アカウントなし、履歴保存なし。 Hardlint Cybersecurity Team による ⚠️ 重要なお知らせ このプロジェクトは、教育およびセキュリティ研究の目的で配布されています。ネットワークレベルの匿名性を保証するものではありません。会話の内容は保護しますが、接続している相手を必ずしも保護するものではありません。機密性の高い通信に使用する前に、攻撃対象領域と既知の制限セクションをお読みください。両方のデバイスで信頼できるVPNを使用することを強く推奨します。 ✨ 特徴 🔐 エンドツーエンド暗号化 — AES-GCM 256ビット、PBKDF2(100,000イテレーション)経由で導出された鍵 🌐 真のP2P接続 — 2つのデバイス間の直接WebRTCリンク、メッセージを中継する中央サーバーなし 🚫 ゼロ永続性 — クッキーなし、localStorageなし、データベースなし:タブを閉じれば何も残りません 🔑 単一共有シークレット — ランダムに生成されたルームキー(100文字)、手動の技術設定は不要 🧹 パニックパージ — ボタン1つで鍵、接続状態、表示されているチャット履歴を即座に消去 📡 信頼性の高い接続 — 18のSTUN/TURNサーバーがフォールバックとして設定されており、制限の厳しいNAT(4G/5G、企業ネットワーク)の後でも機能します 🚀 使用方法 ページを開きます(HTTPSで提供される必要があります — 例:GitHub Pages経由、ローカルファイルとして開かないでください) ホスト:[1] ルームの初期化 をクリック → 生成されたルームキーをコピーします ルームキーを別のチャネル(対面、音声通話、別の暗号化アプリ)で連絡先に送信します ゲスト:[2] ルームに接続 をクリック → 受信したルームキーを貼り付けます 接続を待ちます(通常数秒)→ チャットが開きます 2分以内に接続が確立されない場合、ルームキーは自動的に失効します:専用ボタンで新しいものを生成してください 📋 要件 WebRTCとWeb Crypto APIをサポートする最新のブラウザ(最近のChrome、Firefox、Edge、Safari) 両方のデバイスでのインターネットアクセス ページはHTTPSで提供される必要があります(Web Crypto APIとWebRTCにはセキュアコンテキストが必要です)— ローカルファイルとして開いた場合は機能しません 接続試行中に両当事者が同時にページを開いている必要があります ルームキーは、余分なスペースや改行なしで、正確に100文字すべてをコピーする必要があります 🏗️ アーキテクチャ概要 ゼロトレース・ターミナルは、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)に使用されます。 🔄 運用フロー ルームキー生成 ユーザーが「ルームの初期化(ホスト)」をクリックすると: 暗号学的に安全な乱数ジェネレーター(Math.random()ではなく、暗号目的には不向き)であるcrypto.getRandomValues()を使用して、ランダムな100文字の文字列(generate100CharCode())が生成されます。文字セットには、大文字/小文字の英字、数字、特殊記号(-_!@#$%^&*)が含まれており、100文字内でエントロピーを最大化しています。この文字列(ルームキー)は、両当事者が交換する必要のある唯一の共有シークレットであり、帯域外(例:音声メッセージ、対面、別の暗号化チャネル)で交換されます。 ルームキーからの鍵の導出 ルームキーから、異なる目的を持つ2つの独立した値が導出されます。 A. メッセージ暗号化鍵(PBKDF2 → AES-GCM) PBKDF2( password = ルームキー, salt = "p2p-zero-trace-salt-v1"(固定、ハードコード済み), iterations = 100,000, hash = SHA-256 ) → 256ビットAES-GCM鍵 B. PeerJS識別子(切り捨てられたSHA-256) SHA-256(ルームキー) → 最初の32文字の16進数、"ztt-"をプレフィックスとして付与 このIDは、ルームキー以外のものを交換することなく、ホストとゲストがお互いをPeerJSシグナリングブローカーで見つけられるようにするためにのみ使用されます。暗号学的な役割は果たしません。 固定ソルトに関する注意:PBKDF2ソルトはハードコードされており、すべてのセッションで同一です。これは、ルームキー自体が非常に高いエントロピー(100文字のランダム文字列)を持っているため許容されます。固定ソルトは、弱いパスワードが再利用されるシナリオではセキュリティを低下させますが、このケースには当てはまりません。 シグナリングフェーズ(PeerJS) ホストはPeerオブジェクトを作成し、ルームキーから導出されたIDを使用してパブリックPeerJSクラウドブローカーに登録します。ゲストは、同じルームキーを貼り付けた後、同じIDを計算し、peer.connect(id)を呼び出します。PeerJSブローカーは、この初期交換(誰が誰と話したいか)のみを仲介します。メッセージコンテンツは、その時点では別のWebRTCチャネルを介して送信されるため、ブローカーはメッセージコンテンツを見ることも送信することもありません。 ICEネゴシエーション(NATトラバーサル) 2つのピアがお互いを「紹介」した後、WebRTCは有効なネットワークパスを見つけるためにICEネゴシエーションを開始します。 ホスト候補 — デバイスのローカルIPアドレス サーバー反射(srflx)候補 — STUN経由で検出されたパブリックIP リレー候補 — TURN経由で割り当てられ、直接接続が失敗した場合にのみ使用されます 設定されたICEサーバー(優先順位順): 専用メータード.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秒以内に接続が確立されない場合、セッションは失効したと見なされます。 PeerおよびDataConnectionは破棄されます(peer.destroy()、conn.close())。 ステータスは[EXPIRED]と表示されます。 ルームキーは無効になります。 新しいルームキーを生成するためのボタン(ホスト)または新しいルームキーを入力するためのボタン(ゲスト)が表示されます。 これにより、ルームキーがパブリックブローカーで無期限に「リスニング」状態になるのを防ぎます。 🔐 メッセージ暗号化モデル すべてのメッセージは、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」ボタンは明示的に以下を強制します。 接続のクローズ