Agoraを使ってちょっとセキュアなビデオ通話
Agoraを使って、Webでできるだけセキュアなビデオ通話を試みたいと思います。
こちらは下記の記事の続きです。
Agoraのチャンネル暗号化機能
下記のページにAgoraの暗号化について記述があります。
その中に暗号化データの処理のフローが書いてありました。
暗号化にはDTLSというプロトコルを使用しているようです。
調べたところ、DTLSはSSL/TLSのUDP版に当たるそうです。
TCPならSSL/TLS、UDPならDTLSみたいなイメージだと思います。
Agoraの雲の中を見るとちゃんと「Encrypted Data」が通っているのでE2EEになってますね。
実装
暗号化するにはフロントエンドでAgoraクライアントを初期化した直後の行に1行追加するだけでOKです。
const client = AgoraRTC.createClient({mode: "rtc", codec: "vp8"});
client.setEncryptionConfig(encryptionMode, secret); # 追加
encryptionMode
には暗号化方式、secret
にはチャンネル固有の暗号化キーが入ります。
例えばこんな感じです。
client.setEncryptionConfig("aes-128-xts", "Thiesh0sooFef9ae7OofN9ahBsf0pheof5");
これはAES128のXTSモードで暗号化キー"Thie..."で暗号化するという意味になります。
案外簡単でしたね。
っと思いそうでしたが、この暗号化キーはどうやって共有すればいいのでしょう!?
All users in the same channel must use the same encryption mode and encryption secret.
このような記載があり、チャンネルに参加しているユーザは同じ暗号化方式と暗号化キーを設定していなければなりません。(当たり前)
暗号化キーの共有方法はAgoraの書かれていませんでした。。。
暗号化キーの共有
注) これ以降は実験的プロジェクトなのでコードを使用される方は自己責任でお願いします。
暗号化キーの共有には楕円曲線ディフィー・ヘルマン鍵共有(ECDH)という手法が有名です。
フロントエンドJavascriptのWeb Crypto APIはECDHに対応してるのでこれを使ってみましょう。
ECDHのアルゴリズムをめちゃ雑に説明するとこうなります。
ECDHアルゴリズム(私の秘密鍵, あなたの公開鍵) = 共有鍵
ECDHアルゴリズム(あたなの秘密鍵, わたしの公開鍵) = 共有鍵
それぞれが公開鍵・秘密鍵を生成して、公開鍵のみを共有すれば、同じ共有鍵を生成することができます。
実装
- まず、秘密鍵と公開鍵を生成します。
- 公開鍵を共有
- 自分の秘密鍵と相手の公開鍵を使って、共有鍵を生成
- Agoraのチャンネルの通信の暗号化キーに生成された共有鍵を使用
最終的なソースは下記リンクになります。
1.まず、秘密鍵と公開鍵を生成します。
crypto.subtle.generateKey
がWeb Crypto APIの鍵生成のメソッドです。
これで生成される鍵オブジェクト(CryptoKey)はそのまま通信に載せれないので公開鍵はbase64エンコーディングしています。
async function generateKey(){
const ec = {
name: "ECDH",
namedCurve: "P-521"
};
const usage = ["deriveKey"];
const keys = await crypto.subtle.generateKey(ec, true, usage);
const exportedPublicKey = await crypto.subtle.exportKey("spki", keys.publicKey);
const base64PublicKey = btoa(String.fromCharCode.apply(null, new Uint8Array(exportedPublicKey)));
return {
privateKey: keys.privateKey,
base64PublicKey: base64PublicKey
}
}
2. 公開鍵を共有
今回は自分のサーバ上のsocket.ioで共有しました。
要は前項で作ったbase64PublicKeyを相手に共有できればOKです。
詳細は省略します。
3. 自分の秘密鍵と相手の公開鍵を使って、共有鍵を生成
下記のメソッドにのmyPrivateKey
に 1. で生成した秘密鍵オブジェクトをotherBase64PublicKey
に 2. で相手から受け取ったbase64化された公開鍵を入れると、共通鍵を生成してくれます。
コアになるのはcrypto.subtle.deriveKey
というメソッドでこれが共有鍵を作ってくれています。
async function deriveKey(myPrivateKey, otherBase64PublicKey){
function base64ToBuffer(base64Text) {
let binary = atob(base64Text);
let len = binary.length;
let bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
async function keyImport(base64Key){
const key = base64ToBuffer(base64Key).buffer;
const ec = {
name: "ECDH",
namedCurve: "P-521"
};
return await crypto.subtle.importKey("spki", key, ec, false, []);
}
const pub = await keyImport(otherBase64PublicKey)
const aes = {
name: "AES-GCM",
length: 256
};
const ec = {
name: "ECDH",
public: pub
};
const usage = ["encrypt", "decrypt"];
const key = await crypto.subtle.deriveKey(ec, myPrivateKey, aes, true, usage);
const buffer = await crypto.subtle.exportKey("raw", key);
return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)));
}
4. Agoraのチャンネルの通信の暗号化キーに生成された共有鍵を使用
aesKey
の部分に3.で作った鍵を入れれば完成です。
const client = AgoraRTC.createClient({mode: "rtc", codec: "vp8"});
client.setEncryptionConfig("aes-128-xts", aesKey);
3人以上の場合、
薄々、気づいている方もいるかもしれませんが、上記のコードでは2人の間でしか鍵を共有できません。
ちょっとややこしいですが、頑張ればECDHを使って3人以上の共有鍵を生成することも可能です。
その場合、参加者が変わるたびに自分の秘密鍵と自分以外の公開鍵を使って鍵を作り直すことになります。そのため、人数が増えてくると負荷が増えてしまうという問題があります。
これはモダンなE2EEなの?
そうは言えないと思います。
最近のE2EEプロダクトは暗号化チャットアプリのSignalが開発したSignalプロトコルを利用しているものが多く、実際にSignalプロトコルはとてもセキュアです。
参考) Signalプロトコルを利用しているプロダクト
Signal
WhatsApp
Google Allo
Facebook Messenger
Skype
今回のコードはSignalプロトコルを利用していません。Web Crypto APIがSignalプロトコルの暗号化手法に対応していないためです。そのため、最新のモダンなE2EEよりはセキュリティ的に劣ると思います。
そのため、あくまで使用は自己責任でお願いします。
資料
終わりに
最後の方、言い訳が多くなってしまいましたが、何かの参考になれば幸いです。
Discussion