こんにちは!2ちゃんねる互換掲示板システム「 つゔぁいちゃんねる(2ch.biz) 」の開発録です。
前回(第27回)をもって、第1部「掲示板コア機能・初期セキュリティ・管理運用基盤編」が無事に完結しました。
今回から、本連載はまったく新しい挑戦となる 「第2部:WebRTCによる完全サーバーレスP2P暗号化チャット開発録」 へと突入します!
その第1弾となる本稿では、中央サーバーにメッセージやファイルのログを1文字・1バイトたりとも残さない、 WebRTC DataChannel(Full-Mesh) と AES-GCM 256bit エンドツーエンド暗号化(E2EE) を組み合わせた完全匿名P2Pチャットシステムの立ち上げについて、アーキテクチャから実装コードまで徹底解説します。
1. なぜ掲示板に「サーバーレスP2Pチャット」が必要だったのか?
一般的なウェブ掲示板やチャットサービス(Discord, LINE, Slackなど)では、すべての発言データが中央サーバーのデータベースに永続化されます。
しかし、これには運営者・利用者の双方にとって重大な課題が存在します。
-
プライバシーと法的リスク :
- データベースに会話ログやアップロードファイルが蓄積されるほど、情報漏洩やサイバー攻撃、捜査令状への対応コストが膨大になります。
-
インフラ維持コストの増大 :
- リアルタイムチャットでWebSocket接続を張り続け、膨大なメッセージや大容量ファイルを保存・配信し続けると、サーバーの帯域・CPU・ストレージ費用が指数関数的に爆発します。
-
「ログが残らない場」を求めるユーザーニーズ :
- ネット掲示板の長い歴史の中で、「このスレの住人とリアルタイムに話したいが、痕跡(ログ)は一切残したくない」「その場限りの刹那的な会話やファイル交換を楽しみたい」という強い需要が常に存在していました。
そこで私たちが下した決断が、 「メッセージやファイルをサーバーに一切保存せず、参加者同士のブラウザ間で直接通信(P2P)させる」 というアーキテクチャでした。
2. 全体アーキテクチャ:シグナリングとピア間直接通信
P2P通信を実現するには、参加者同士が互いのグローバルIPやポート番号、暗号化パラメータ(SDP: Session Description Protocol および ICE Candidate)を交換する 「シグナリング」 が必要です。
シグナリングサーバーは「マッチングの電話交換手」としてのみ機能し、P2P接続が確立した瞬間に通信経路から完全に退場します。
アーキテクチャの3大原則
- Zero Storage(ゼロストレージ) : サーバー側DBにはテーブルすら作らない。
- Zero In-transit Inspection(中間傍受の完全排除) : サーバーは暗号文の復号鍵を持たない。ブラウザ間でエンドツーエンド暗号化される。
- Room Auto-Destruction(部屋の即時自動消滅) : 参加者が 0 人になった瞬間、シグナリング用メモリは即座に解放される。
3. Web Crypto API による AES-GCM 256bit 暗号化
WebRTC 自体も DTLS(Datagram Transport Layer Security)によって伝送路上で暗号化されますが、万が一の中間者攻撃(MITM)やシグナリング傍受に備え、アプリケーション層でも Web Crypto API を用いた二重暗号化を行っています。
ルームパスコード(またはURLフラグメント内のシード)から PBKDF2 で 256bit の共通鍵を導出し、メッセージごとにランダムな 96bit IV(初期化ベクトル)を生成して AES-GCM で暗号化します。
/**
* Web Crypto API を用いた E2EE 暗号化・復号モジュール
*/
class P2PCrypto {
constructor() {
this.key = null;
}
// パスフレーズとソルトから AES-GCM 256bit 鍵を導出 (PBKDF2)
async deriveKey(passphrase, salt) {
const enc = new TextEncoder();
const baseKey = await crypto.subtle.importKey(
'raw',
enc.encode(passphrase),
{ name: 'PBKDF2' },
false,
['deriveKey']
);
this.key = await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: enc.encode(salt),
iterations: 100000,
hash: 'SHA-256'
},
baseKey,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
}
// テキストやバイナリの暗号化 (IV + 暗号文)
async encrypt(plainText) {
if (!this.key) throw new Error('鍵が初期化されていません');
const enc = new TextEncoder();
const iv = crypto.getRandomValues(new Uint8Array(12)); // 96bit IV
const cipherBuffer = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: iv },
this.key,
enc.encode(plainText)
);
// IV(12byte) + 暗号化データ を結合してBase64化
const combined = new Uint8Array(iv.byteLength + cipherBuffer.byteLength);
combined.set(iv, 0);
combined.set(new Uint8Array(cipherBuffer), iv.byteLength);
return btoa(String.fromCharCode.apply(null, combined));
}
// 暗号文の復号
async decrypt(base64Data) {
if (!this.key) throw new Error('鍵が初期化されていません');
const binary = atob(base64Data);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
const iv = bytes.slice(0, 12);
const data = bytes.slice(12);
const decryptedBuffer = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: iv },
this.key,
data
);
return new TextDecoder().decode(decryptedBuffer);
}
}
4. 2ch伝統文化の完全再現:クライアント側トリップ生成(名前#パスワード)
つゔぁいちゃんねるのアイデンティティである「匿名性」と「個人識別(固定ハンドル)」を両立するため、P2Pチャット内でも掲示板本体と 100%同一のトリップ(SHA-1計算) をクライアント完結で生成できるようにしました。
入力欄に 名無しさん#secretKey と入力すると、リアルタイムに以下のように分解・変換されます。
/**
* 2ch互換のトリップ生成(クライアント完結版)
*/
async function generateTrip(inputName) {
const hashIndex = inputName.indexOf('#');
if (hashIndex === -1) {
return { name: inputName || '風吹けば名無し', trip: '' };
}
const name = inputName.substring(0, hashIndex) || '風吹けば名無し';
const key = inputName.substring(hashIndex + 1);
if (!key) {
return { name: name, trip: '' };
}
// SHA-1 ハッシュ生成
const enc = new TextEncoder();
const hashBuffer = await crypto.subtle.digest('SHA-1', enc.encode(key));
const hashArray = Array.from(new Uint8Array(hashBuffer));
const base64 = btoa(String.fromCharCode.apply(null, hashArray));
// 先頭8文字をトリップとして抽出
const trip = '◆' + base64.substring(0, 8);
return { name, trip };
}
これにより、ユーザーはサーバーに秘密鍵(パスワード)を送信することなく、自分の固定ハンドルをP2Pルーム内で証明できます。
5. UI/UXデザイン:XChat (IRC) 伝統の2カラムレイアウト
UIは、古き良きIRCクライアント(XChat / HexChat)の機能美をリスペクトした設計に仕上げました。
-
自由可変サイズウィンドウ(
resize: both)&最大化機能 :- デスクトップでは右下のつまみで自由にリサイズ可能。最大化ボタン(
🗖)を押せばフルスクリーンチャットに変貌。
- デスクトップでは右下のつまみで自由にリサイズ可能。最大化ボタン(
-
伝統の2カラム・メッセージログ :
- 左列(右揃え発言者名+トリップ) | 縦セパレータ(
|) | 右列(メッセージ本文)。 - 発言がどれだけ高速に流れても、誰の発言かが一目で視認できます。
- 左列(右揃え発言者名+トリップ) | 縦セパレータ(
-
ニックネームサイドバー :
- 部屋の参加者一覧を右側に常時表示。ホスト(部屋主)には
@プレフィックスが付与されます。
- 部屋の参加者一覧を右側に常時表示。ホスト(部屋主)には
6. この設計が生み出した圧倒的メリット
-
インフラ費用ゼロ :
- どんなに長時間の雑談が行われようと、ギガバイト単位の写真やzipファイルがやり取りされようと、サーバーの転送量はシグナリングの数KBのみ。
-
完全なるプライバシーの保護 :
- サーバー運営者であっても、誰がどんな会話を交わしたのかを知る術はありません。ログが存在しないため、漏洩のリスク自体が原理的にゼロです。
-
爆速のレスポンス :
- 東京と大阪のユーザー同士であれば、遅延はわずか数ミリ秒。サーバーを中継しないため、タイピングのライブ感そのままにメッセージが届きます。
7. 次なる技術課題と次回予告
素晴らしいスタートを切ったP2P暗号化チャットですが、掲示板ユーザーからすぐに次のリクエストが届きました。
「チャットは最高だけど、 掲示板のスレッドを読みながら同時にチャットしたい 」
「スレッドのURLをクリックして別画面に遷移すると、P2P接続が切れて部屋から退室扱いになってしまう!」
SPA(シングルページアプリケーション)の中で、WebRTCの接続を維持したままスレッド閲覧とチャット画面をシームレスに行き来するにはどうすればよいのか?
次回は、 【第029回】掲示板を見ながらチャットできる!動的タブUIとマルチタスク体験の構築 をお届けします。お楽しみに!
本番サービス : https://2ch.biz/