なにを目指しているか?
多くのクラウドストレージやメールを使ったファイル転送ではクライアントとサーバー間の暗号化はされています。ですが、その多くの場合サーバー内では生のデータもしくは暗号化されていてもサーバー内で復号可能の場合が多いと思います。
そこで送受信を行う端末間でエンドツーエンド暗号化(E2E暗号化)してサーバー側にも分からない形してサーバーを信じなくても良い、より高度なセキュリティを目指したいです。
それと同時に安全性かつ手軽にファイルを転送を重視したいです。
一般にセキュリティを強めると不便になったりして、安全性と利便性にトレードオフがあると思います。このトレードオフをなるべく解決して、安全性と利便性の両立して安全性と手軽さを両立することを考えました。ユーザーのパスワードの入力不要で、手軽にセキュアに転送するために使った技術に関して後述します。
アプリケーション
Piping UIと呼んでいて以下のURLでスマホやPCなどブラウザが使えるデバイスで利用できます。
https://piping-ui.org
GitHub: https://github.com/nwtgck/piping-ui-web
以下に暗号化以外の話があります。
https://qiita.com/nwtgck/items/2dd47031ba729256f5eb
埋め込みアプリ
See the Pen Piping UI Embed by Ryo Ota (@nwtgck) on CodePen.
Piping UIにおけるパスワードなしのE2E暗号化
パスワード不要というのはファイルを暗号化するパスワードが一切不要ということです。よく話題になるパスワード付きZIPのパスワードのようなものが一切不要で、転送ごとに暗号化用の鍵がランダムに変わります。その鍵は256ビットなので$2^{256} = 115792089237316195423570985008687907853269984665640564039457584007913129639936$の組みわせがあります。
つまり通信ごとに変わる一度しか使われない鍵に115792892無量大数3731不可思議6195那由他4235阿僧祇7098恒河沙5008極6879載785正3269澗9846溝6564穣564𥝱394垓5758京4007兆9131億2963万9936の膨大な鍵の組み合わせが考えられます。ユーザーが記憶できるパスワードの数や長さの限界を超えたセキュリティが実現できるのではと思います。
そのため、例え暗号データを傍受した悪い人にパスワードを言うように脅されたとしても、原理的に
誰もその鍵を教えることはできなく安心です(脅されるとか多分ないけど)。
この鍵を使ってブラウザ側で暗号化を施すためサーバー側を信用できなくても安心して転送が可能です。速度の心配もあるかもしれないので、ブラウザ側での暗号化周りの速度に関して後述します。
パスワード不要でE2E暗号化する技術
楕円曲線ディフィー・ヘルマン鍵共有 (ECDH)
ユーザーのパスワード入力不要で信頼できない通信路で暗号化する方法は様々あり、その中でも楕円曲線ディフィー・ヘルマン鍵共有 (ECDH)を利用しています。
ECDHでは、送信者と受信者が互いに公開鍵と秘密鍵を作って、そのうちの公開鍵同士を交換します。互いに公開鍵とそれぞれ持っている別々の秘密鍵から送信者と受信者で同じ乱数値が生成できるようになります。その送信者と受信者で同じ値になる乱数値を共有鍵として利用できます。楕円曲線を使わない素数とmod演算を使ったディフィー・ヘルマン鍵共有に関しての数学的説明は「ディフィー・ヘルマン鍵共有の仕組み - 小人さんの妄想」が分かりやすかったです。
楕円曲線における足し算のざっくりとした数学的なイメージを掴むには「仮想通貨ファンなら楕円曲線暗号も好きになろう | ALIS」が分かりやすかったです。
Web Crypto API
どのように楕円曲線ディフィー・ヘルマン鍵共有 (ECDH)をブラウザ上で実現するかに関してです。
Web標準でWeb Crypto APIが用意されていて多くのブラウザ上で利用可能です。
ECDHのキーペアは以下のようにシンプルに生成可能です。
const keyPair = await window.crypto.subtle.generateKey(
{ name: 'ECDH', namedCurve: 'P-256'},
false,
['deriveKey', 'deriveBits']
);
またcrypto.subtle.exportKey()
を使えばJSON Web Key (JWK)として取得可能で、通信の時も扱いやすいJSONが手に入ります。Web Crypto APIを使った暗号化の流れのサンプルは「WebCrypto APIでECDH鍵交換を用いた暗号化を使ってみる - Qiita」などにあります。鍵生成などの速度に関して後述していて、その中のCodepenもサンプルコードになるかもしれません。
OpenPGP.js
ECDHで送信者と受信者で共通鍵の共有ができた後、OpenPGPの形式で共通鍵での暗号化をしています。その際にOpenPGP.jsを利用しています。パスワード付きで暗号化する機能をgpg
コマンドとcurl
コマンドでCLIからでもブラウザからでも扱いやす
くするためにOpenPGPを使っています。
詳しくは「Piping UI: セキュアにOpenPGPで暗号化してファイル転送する技術周り - nwtgck / Ryo Ota」にまとめています。
どれぐらいブラウザでの暗号化などは速いのか?
最初、ブラウザで鍵生成したり暗号化・復号って時間かかりそうと思っていましたが、実際のところかなり速いです。
僕の環境では、Web Cryptoで以下の一連の処理を100回しても220ミリ秒ぐらいです。
iPad miniでも624ミリ秒でした。
- ECDHのキーペアの生成 x 2
- 公開鍵をJSONにエクスポート x 2
- ランダムな初期化ベクトル(IV)の生成
- ECDHによる共通鍵の導出 x 2
- AES-GCMでの暗号化
- 復号
- 生データと復号後のデータの一致の比較
-
JSON.stringify()
を使った遅そうな比較処理
-
お試し用Codepen: https://codepen.io/nwtgck/pen/NWWeLQg?editors=0011
上記のCodepenのJavaScript欄はWeb CryptoでのECDHの鍵生成やJSONへのエクスポートなど方法の参考にもなると思います。
「WebCrypto APIでECDH鍵交換を用いた暗号化を使ってみる - Qiita」を参考にしています。
この速度ならWebフロントエンドで暗号化/復号しても良さそうだと思いました。OpenPGP.jsだとReadableStream
を暗号化/復号可能で、たとえばダウンロードと復号を同時に行うことが可能です。
Piping UIにおける鍵共有と暗号化の流れ
- ECDHの公開鍵を送信者と受信者間で交換する。
- 送信者と受信者がそれぞれ公開鍵から共有鍵を算出する。
- 送信するユーザーが受信者の確認コードを確認する。
- 送信するユーザーの確認して送信を開始する。
以下が鍵共有に対応するコードです。
https://github.com/nwtgck/piping-ui-web/blob/37e302e031ec11582401684194419268fd5adabc/src/piping-ui-utils.ts#L96-L149
確認コードがある理由は中間者攻撃が防ぐためです。たとえ暗号化していても悪意のある中間者に向かって暗号化して送信する可能性があるため、送信者が想定している受信者なのかを確認できるようにしています。一番防ぎたいのはデータが想定してない受信者に漏れることなので送信者が受信者を確認するようになっています。
ただクライアントとサーバー間はHTTPSで通信路は守られていて、中間者攻撃するためにはサーバーをクラックするか、もともと悪意のあるサーバーでないといけないなど中間者攻撃の可能性は低いかもしれません。
確認コードは送信者と受信者のJWKの公開鍵のthumbprintをハッシュ化して合わせたものです (https://github.com/nwtgck/piping-ui-web/blob/37e302e031ec11582401684194419268fd5adabc/src/piping-ui-utils.ts#L151-L159)。
Piping UIの転送は、自分の他のデバイスや物理的に近くの人や電話越しの相手など想定しているため、その時に確認コードを伝えること想定しています。その場で転送をするという点でAirDropのマルチデバイス間で使える版に近いかもしれません。
Piping Server
Piping UIの転送の基盤にPiping Serverを使っています。詳しい説明やセルフホストして自前でサーバーを立てて更にセキュアにしたい場合は「ネットワーク越しでパイプしたり、あらゆるデバイス間でデータ転送したい! - Qiita」が参考になると思います。Herokuやdocker run
ですぐに立てる方法を紹介しています。
最後に
パスワードを決めたり覚えたりする必要がなく安全に便利にPiping UIで転送できるようになりました。
ですが、パスワードなしでE2E暗号化するということは特に新しくなく枯れた技術なのではないかと思います。よく広まっているHTTPSも、当たり前ですがデータを暗号化するためにユーザーのパスワードはなく、クライアントとサーバー間で暗号化していて同じことなのではと思います。