1. 課題研究の背景と目的
当初の目的は純粋なJavaScriptを復習するでした。
その為、WebSocketを使ったチャットアプリをクライアントはJavaScript、HTML、CSS、サーバはJava、SpringBootで開発していました。
完成後、認証や暗号通信がないことに満足できずサーバ証明書とTLSを使った認証、暗号化通信を試みたが、使用しているVSCodeの設定がおかしくなったのか証明書をロードできずVSCodeを再インストールすることも考えたが、これまでPythonで作成したアプリの挙動が変わることを恐れVSCodeの再インストールを諦め、代替手段で認証、認可、暗号化通信が可能かを試みました。
サーバ証明書、TLSを使えない状況であり、パスワードレス認証も試みたかったので、認証はユーザID、EC鍵ペア1組を用いることにしました。
2. 技術選定と構成概要
今回のチャットアプリは以下の技術スタックで構築しました。
- クライアント: JavaScript / HTML / CSS(HTTP、WebSocketでリアルタイム通信)
- サーバ: Java / SpringBoot(REST API+HTTP、WebSocket対応)
-
鍵: RSAではなくEC(楕円曲線暗号)を採用。軽量性と高速な鍵生成が理由。
クライアントでチャレンジの署名・検証で汎用EC鍵を元にECDSA鍵を生成し、
クライアントとサーバで暗号通信用の共通鍵として汎用EC鍵を元にECDH鍵を生成してから、それを元にAES鍵を生成しました。 - 認証方式: パスワードレス認証(ユーザーID+EC鍵ペア)
- 認可方式: JWTによるアクセストークンの生成・検証
- 暗号技術: ECDHによる鍵共有、AES-GCMによる暗号通信
- データの記録: 検証段階なのでDB、REDIS、Cookieは使わず、SessionStrageを使しました。
基本的な構成としては、クライアントが認証、認可を経てWebSocketを通じてリアルタイムに通信を行い、
メッセージは暗号化されて送受信されるようにしました。
3. TLSを使えない状況での代替設計
今回の環境では、TLSおよびサーバ証明書が利用できないという制約がありました。
これにより、HTTPSによる暗号化やサーバ証明の仕組みは使えず、アプリケーションレイヤーで独自の認証・認可・暗号化通信を設計する必要がありました。
そこで、以下のような代替設計によって安全性の担保を試みました。
🔐 認証方式(パスワードレス)
- パスワードの代わりに ユーザーID+EC鍵ペア を使用
- サーバはユーザーの公開鍵を保持し、署名によって本人確認を行う
チャレンジ応答の流れ:
- クライアントがログインリクエストを送信
- サーバが チャレンジ(ランダム文字列) を生成してクライアントへ返却
- クライアントがチャレンジに対して ECDSA署名 を生成
- サーバが公開鍵で署名を検証 → 認証成立
🛡 認可方式(JWT)
- 認証後、サーバ側が JWTトークン を発行
- トークンにはユーザーIDやロール情報や有効期限を含み、署名済み
- WebSocket接続や操作時に、このトークンを検証することでアクセス制御を実現
🔐 暗号化通信(ECDH + AES-GCM)
- クライアントとサーバがそれぞれ EC鍵ペア を保持
- 双方で交換した公開鍵と秘密鍵により ECDHで共通鍵を生成
- この共通鍵から AES-GCM鍵 を導出し、メッセージを暗号化・復号
暗号化の流れ:
- クライアントがメッセージを送信する前に AES-GCMで暗号化
- 暗号文+IVを JSON でサーバへ送信
- サーバ側で クライアントとの共通鍵 を使って復号処理
- 各接続ユーザーの共通鍵を使って 再暗号化し、WebSocketで配信
🔁 再暗号化によるブロードキャスト
- サーバは受信メッセージを一度復号し、内部の平文を取得
- 接続中の各ユーザーと保持している共通鍵を使って、新しい iv を生成
- sender, timestamp, msg を個別に暗号化
- WebSocketを通じて各ユーザーへ送信
🔐 この方式により、各ユーザーの秘密鍵が漏洩しても他のユーザーの通信には影響せず、
TLS非使用環境でもセキュアなメッセージ通信が可能となった
この代替設計により、TLSやサーバ証明書が使えない制約下でも、認証・認可・暗号通信の一連の処理を安全に実現できることを検証することができました。
4. 実装の苦難ポイント
この課題研究では、TLSを使わないがゆえにアプリ層でセキュリティ処理を全て自前で実装する必要があり、多くの技術的困難に直面しました。ここでは特に印象深かった苦難とその解決策を紹介します。
(1) チャレンジ署名と検証のトラブル
今回は使う鍵が多いので署名や検証で使う鍵を間違え処理が正常終了しない失敗をしていました。
クライアントが使う鍵:
openSSHで手動作成した汎用EC秘密鍵、公開鍵
↓
ECDA秘密鍵、公開鍵、ECDH秘密鍵、公開鍵、サーバEC公開鍵
↓
AES共通鍵をアプリが生成
(2) 共通鍵の生成のトラブル
(1)と同様に、使う鍵を間違えクライアントとサーバで同じ鍵を生成できない苦戦もありました。
(3) 送信者以外のチャットメッセージの復号失敗
事象:送信者以外が受信メッセージを復元できない。
原因:送信者がメッセージの暗号化で使用した鍵とその他のユーザが複合で使用する共通鍵が異なる。
対処:サーバがメッセージを受信すると、送信者の共通鍵で複合し各送信者以外のユーザごとの共通鍵で再暗号化し、ブロードキャスト送信する。
5. 成果としてのブレイクスルー
- 複数ユーザー(3名)による暗号通信チャットが実現
- TLSや証明書なしでも、ECDHとAES-GCMによる安全な暗号化
- パスワードレスログインが成功
- セッションごとに暗号鍵が異なり、復号も成功
秘密鍵はサーバに送信せず、クライアントの内部処理で使用します。
クライアント側のログイン成功までのログ
login.js:332 受け取ったチャレンジ: Uint8Array(32) [81, 89, 88, 73, 88, 67, 119, 113, 118, 103, 51, 106, 103, 82, 117, 77, 66, 48, 52, 107, 89, 102, 53, 70, 122, 70, 86, 100, 109, 54, 109, 74, buffer: ArrayBuffer(32), byteLength: 32, byteOffset: 0, length: 32, Symbol(Symbol.toStringTag): 'Uint8Array']0: 811: 892: 883: 734: 885: 676: 1197: 1138: 1189: 10310: 5111: 10612: 10313: 8214: 11715: 7716: 6617: 4818: 5219: 10720: 8921: 10222: 5323: 7024: 12225: 7026: 8627: 10028: 10929: 5430: 10931: 74buffer: ArrayBuffer(32)byteLength: 32byteOffset: 0length: 32Symbol(Symbol.toStringTag): "Uint8Array"[[Prototype]]: TypedArray
login.js:379 受け取ったトークン: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJha2lrbyIsInJvbGUiOiJVU0VSIiwiaWF0IjoxNzUxNjI2Mjk4LCJleHAiOjE3NTE2MjY4OTh9.elky4FrlprRuBxVlWV3N5_kb_hxXUF-z4yJFmniJujY
login.js:385 受け取ったサーバ公開鍵: Uint8Array(91) [48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 3, 66, 0, 4, 159, 175, 200, 184, 183, 113, 147, 47, 102, 201, 28, 32, 127, 203, 247, 35, 226, 130, 2, 247, 92, 50, 226, 112, 8, 52, 153, 202, 107, 8, 34, 245, 46, 150, 234, 44, 196, 196, 114, 218, 235, 27, 193, 234, 201, 145, 244, 235, 187, 105, 50, 79, 151, 101, 218, 144, 61, 170, 205, 79, 120, 236, 93, 124, buffer: ArrayBuffer(91), byteLength: 91, byteOffset: 0, length: 91, Symbol(Symbol.toStringTag): 'Uint8Array']
login.js:394 暗号通信鍵: 1d81d83ede5f1feb1e9d999c4621fc009fa72911896a360a56f12cf179e9d91b
login.js:423 トークン検証結果: true
サーバ側のログイン成功までのログ
受け取った公開鍵:3059301306072a8648ce3d020106082a8648ce3d030107034200047172aeac8a8fe176f7b77bf1b4e149a12f5c3d98e4953f53f95253c0e362ff53ff003d42fc2bb2c8bdcbd2b56fa598e892bc3285cc99e1861c57b43fd4bf2484
受け取った署名:b8VXblfkwE+txBR5LV00YmNsnQ4JfsOTJ5UAa10meftibx9fPXn7Ni/c3NEIYVKwkAar58A9m4fv81Czc6sx4w==
署名検証結果:true
暗号通信鍵:1d81d83ede5f1feb1e9d999c4621fc009fa72911896a360a56f12cf179e9d91b
作成したトークン:eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJha2lrbyIsInJvbGUiOiJVU0VSIiwiaWF0IjoxNzUxNjI2Mjk4LCJleHAiOjE3NTE2MjY4OTh9.elky4FrlprRuBxVlWV3N5_kb_hxXUF-z4yJFmniJujY
トークン検証結果:true
サーバ側でメッセージを中継・ブロードキャスト送信しているログ
接続ID:895eb031-e6e5-e762-a4fc-7bbe46846cb4
接続ユーザーID:gogotokyo
オリジン:http://127.0.0.1:5500
make session:895eb031-e6e5-e762-a4fc-7bbe46846cb4
受信メッセージ:{"iv":"JVkk+VSeFWef+U9w","sender":"KEKHeA1vIR9cmCJnECzzh4dSUz9P","timestamp":"exncJk/meu9oJ6olTyzU7CI0IWM7jPTUXDygYgOAGIJiItlyhNmVaw==","msg":"rZJk9fVzrkP38H+9nZd3P5ms/s2sWU4CbtF8MAWOV+0feTPqMndJnYEjC+apUW4qLvaCk7dLp6C0yYDbCsb2gw=="}
受信メッセージ:{"iv":"UVt7SKSsjnEnByMw","sender":"sg7zm/6HvCexQbjmWa1bJC4jxK+H","timestamp":"9F+zwabBaKG23Q8Inr6jvoFaxUUH7QInrA9jG5XUgg7NdURMbvBM+Q==","msg":"JewQFwhwvA81CtmK4wUUyuT5lXKOj1Mm0t5Vxg=="}
受信メッセージ:{"iv":"qEAEgJnIgS0orKFs","sender":"1NbMbipNm2ZZvUeQdQLSQDnGReg5","timestamp":"jI+MNHLAl/4ryEQGW0GN6tGD36o6IXBB83XYZIMTVr0iZQGjNTytcQ==","msg":"XT0T4txTQ1CLH5KYifgUMbCZ5mWWD6t6HxTaopv1tA=="}
ivはbase64変換してるだけで、sender(投稿者)、timestamp(投稿時刻)、msg(投稿内容)はivと共通鍵で暗号化した上でbase64変換しています。
WebSokect接続時に、認証済か?期待したオリジンか?トークンの有効期限を過ぎてないか?をチェックし、NGの場合、切断しています。
6.まとめ
ここまで読んで頂きありがとうございます。
1週間前の夜にこの課題研究に取り組み始めました。
当初は先月末でアプリの実装と動作確認を終え、記事の投稿を今月1日にするスケジュールを組んでいましたが、TLSとサーバ証明書を使えない制約により予定を3日過ぎての完了となりました。
予想していた機能の実装には成功しましたが、かかった時間やセキュリティとしては、やはり、TLSとサーバ証明書を使う方が盤石であり余分な時間を節約できる効果もあると感じました。
この検証での暗号通信はクライアントとサーバで共通鍵を生成できたタイミング以降で可能となります。
この記事が皆様の参考になれば幸いです。
以上

