0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【第028回】サーバーにログを一切残さない!WebRTC DataChannelを用いたエンドツーエンド暗号化P2Pチャットの幕開け

0
Posted at

こんにちは!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など)では、すべての発言データが中央サーバーのデータベースに永続化されます。
しかし、これには運営者・利用者の双方にとって重大な課題が存在します。

  1. プライバシーと法的リスク :
    • データベースに会話ログやアップロードファイルが蓄積されるほど、情報漏洩やサイバー攻撃、捜査令状への対応コストが膨大になります。
  2. インフラ維持コストの増大 :
    • リアルタイムチャットでWebSocket接続を張り続け、膨大なメッセージや大容量ファイルを保存・配信し続けると、サーバーの帯域・CPU・ストレージ費用が指数関数的に爆発します。
  3. 「ログが残らない場」を求めるユーザーニーズ :
    • ネット掲示板の長い歴史の中で、「このスレの住人とリアルタイムに話したいが、痕跡(ログ)は一切残したくない」「その場限りの刹那的な会話やファイル交換を楽しみたい」という強い需要が常に存在していました。

そこで私たちが下した決断が、 「メッセージやファイルをサーバーに一切保存せず、参加者同士のブラウザ間で直接通信(P2P)させる」 というアーキテクチャでした。


2. 全体アーキテクチャ:シグナリングとピア間直接通信

P2P通信を実現するには、参加者同士が互いのグローバルIPやポート番号、暗号化パラメータ(SDP: Session Description Protocol および ICE Candidate)を交換する 「シグナリング」 が必要です。

シグナリングサーバーは「マッチングの電話交換手」としてのみ機能し、P2P接続が確立した瞬間に通信経路から完全に退場します。

アーキテクチャの3大原則

  1. Zero Storage(ゼロストレージ) : サーバー側DBにはテーブルすら作らない。
  2. Zero In-transit Inspection(中間傍受の完全排除) : サーバーは暗号文の復号鍵を持たない。ブラウザ間でエンドツーエンド暗号化される。
  3. 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. この設計が生み出した圧倒的メリット

  1. インフラ費用ゼロ :
    • どんなに長時間の雑談が行われようと、ギガバイト単位の写真やzipファイルがやり取りされようと、サーバーの転送量はシグナリングの数KBのみ。
  2. 完全なるプライバシーの保護 :
    • サーバー運営者であっても、誰がどんな会話を交わしたのかを知る術はありません。ログが存在しないため、漏洩のリスク自体が原理的にゼロです。
  3. 爆速のレスポンス :
    • 東京と大阪のユーザー同士であれば、遅延はわずか数ミリ秒。サーバーを中継しないため、タイピングのライブ感そのままにメッセージが届きます。

7. 次なる技術課題と次回予告

素晴らしいスタートを切ったP2P暗号化チャットですが、掲示板ユーザーからすぐに次のリクエストが届きました。

「チャットは最高だけど、 掲示板のスレッドを読みながら同時にチャットしたい 」
「スレッドのURLをクリックして別画面に遷移すると、P2P接続が切れて部屋から退室扱いになってしまう!」

SPA(シングルページアプリケーション)の中で、WebRTCの接続を維持したままスレッド閲覧とチャット画面をシームレスに行き来するにはどうすればよいのか?

次回は、 【第029回】掲示板を見ながらチャットできる!動的タブUIとマルチタスク体験の構築 をお届けします。お楽しみに!

本番サービス : https://2ch.biz/

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?