📑

Web Crypto APIについて

6 min read

お品書き

  1. 概要
  2. 歴史
  3. 何に使えるの?
  4. どう使うの?

概要

ブラウザには、Web Crypto APIという暗号ライブラリが入っています。
JavaScriptから使うことができます。
色んなことができます。
これまで暗号化したいなと思ったらnpmから色々暗号用のライブラリを追加していました。それはもういりません。

使える機能

  • 乱数生成
  • ハッシュ計算
  • 共通鍵暗号
  • 鍵ペア生成
  • 公開鍵暗号
  • 署名・検証

生の秘密鍵に触らないようにできるらしいです。
つまり、鍵ペアを生成したときに、ブラウザが鍵の生データを隠し持って置き、アプリケーションにはその参照だけが渡されるようにできます。
そうすると、万が一アプリケーションの開発でミスがあっても秘密鍵が漏洩する可能性を下げられます。

選べるアルゴリズム

暗号には様々なアルゴリズムがあります。
業界の規制などによって使えるものが限られていることがあります。
Web Crypto APIはアルゴリズムを選択することができます。
また、 端末がそのアルゴリズムをつかえるかを検出する機能も備わっています。

歴史

20年位前にさかのぼります。
もともとFirefoxにはcrypto.signTextという非標準の暗号APIが生えていました。
政府や銀行のサイトなどで使われていたようです。
それが、HTML5の流れで標準化していこうという話になったそうです。

それとは別に、当時の主なモチベーションとしては、Webアプリケーションにおける認証機能を良くしたいという事が大きかったようです。
ID/Password方式はサイト運営者にとってもユーザーにとってもダルいものでした。
だけれどもOAuthは大企業にロックインされるのであまり使いたくはない。
そもそもパスワードというものが安全じゃないのではないか?
公開鍵方式ベースでログイン機能の標準化をできないか?
という意識の高まりがあった頃でした。
ちなみに、このころMozillaはBrowserID(後のMozilla Persona)というものを発表しています。

そこで、2011年頃W3CでWeb Cryptography Working Groupが立ち上がりました。
この時に認証に必要なフロントエンド側のAPIを全部作ろうということになりました。

  • Web Identity API
  • Web Identity Sync
  • Web Crypto API

Web Crypto APIはここで初登場します。

2012の時点で、W3Cが『こういうのあったら使う人いますか?』という事を聞いたときに、
手を挙げたのはNetflixと韓国でした。
Netflixは認証などに使いたい。
韓国はすでに個人証明書のインフラが浸透されており、それと接続したい。
そういうUseCaceを発表しています。

2012年ごろから提案が始まりました。
最初の提案者はGoogleとMozillaの方です。
その後はNetflixのマーク・ワトソンさんという方がエディターをされています。
2015年ごろから実装が揃いはじめます。
2017年にRecommendation(W3Cの勧告)になりました。

何に使えるの?

共通鍵暗号

フロントエンドからクラウドストレージに直接保存することがあります。
この時、クラウドストレージに保存するデータもフロントエンドで暗号化できるといいことがあると思います。

公開鍵暗号

公開鍵方式とは

AさんとBさんの会話を、その間の誰が覗き見ても中身がわからないようにできます。

チャットアプリにおいて会話ログの暗号化ができる

チャットアプリを作っているとして、サーバーがユーザー同士のやり取りを見れないようにできます。

趣味の開発や極めてセンシティブなサービスで有用です。

ブラウザの拡張機能を作って既存のチャットアプリ上でE2EEする

ブラウザの拡張機能を作ることで、Slackなどで機密情報のやり取りができます。
どの会社でも、「パスワードをチャットに書き込んではいけない」などのルールがあると思います。
もちろん、そういうルールの存在は当然だと思います。
でも、それはダルいですよね。
そこで、ブラウザの拡張機能を作れば公開鍵方式を使って安全にチャット上で機密情報を送れるものです。

ちなみにこれは僕が作った、Chatwork上で機密情報を送れる君です。

署名・検証

電子署名とは

Aさんの発言を何者かが中継してBさんが受け取ったとき、何者かが改ざんしていないことを確認できる仕組みです。

スター型のP2Pネットワークを作る

WebRTCと相性がいいです。

スター型のネットワークを作っているとします。
Aさんがハブです。AさんはBさんからみんなへのメッセージを中継します。
その時、AさんがBさんのメッセージを改ざんしていないことを保証できます。

使い方

WebCrypto API の実体

window.crypto.getRandomValues()
window.crypto.subtle.digest()
window.crypto.subtle.generateKey()
window.crypto.subtle.encrypt()
window.crypto.subtle.decrypt()
window.crypto.subtle.sign()
window.crypto.subtle.verify()

公開鍵暗号

Aさんがサーバー経由でBさんにメッセージを送りたいとき

  1. Bさんが鍵ペアを生成
  2. BさんがAさんに公開鍵を送信
  3. AさんがBさんの公開鍵でメッセージを暗号化して送信
  4. Bさんが自分の秘密鍵で復号

電子署名・検証

AさんのメッセージをBさんが確認したいとき

  1. Aさんが鍵ペアを生成
  2. Aさんがメッセージ+公開鍵を秘密鍵で署名してBさんにおくる
  3. BさんがAさんのメッセージを検証

実装方法

ここから「encrypt | decrypt」もしくは「sign | verify」に対応しているアルゴリズムを選びます。

https://github.com/diafygi/webcrypto-examples

あとはサンプル通り鍵ペアを生成して暗号化・復号と電子署名・検証をすればいいです。
後述の理由でextractableはtrueにします。
publicExponentはnew Uint8Array([0x01, 0x00, 0x01])でいいはずです。

鍵の保存方法

一般的にIndexedDBが推奨されています。
シリアライズしなくてもそのまま保存できるようです。

CryptoKey型とJsonWebKey型について


encrypt関数などは引数にCryptoKey型の鍵を取ります。
このCryptoKey型はシリアライザブルではありません。
なので、サーバーに送ったりできません。
window.crypto.subtle.exportKey()で等価なJsonWebKey型に変換できます。
JsonWebKey型はJSON.stringify()できます。
逆にJsonWebKey型をwindow.crypto.subtle.importKey()するとCryptoKey型になります。
つまり、送信するときはexportをして、受け取ったときにimportをします。

stringとArrayBufferについて

window.crypto.subtle.~~~はArrayBuffer型で入出力します。

なので、このようなUtil関数を用意しておきましょう。
ts-ignoreしてるけどどうだっていいぜ問題は無し!!

function arrayBufferToString(buf: ArrayBuffer): string {
  // @ts-ignore
  return window.btoa(String.fromCharCode(...new Uint8Array(buf)));
}
function stringToArrayBuffer(base64: string): ArrayBuffer {
  const binaryString = window.atob(base64);
  const len = binaryString.length;
  const bytes = new Uint8Array(len);
  for (let i = 0; i < len; i++) {
    bytes[i] = binaryString.charCodeAt(i);
  }
  return bytes.buffer;
}

対応しているの?

PC/スマートフォン

少なくとも5年程度前から対応済です。

ブラウザ 対応バージョン
Chrome 37
FireFox 34
Safari 10.3

谷出の古いiPhone6 (OS 12.4)でも動きました。

Node.js

これまではライブラリが必要でしたが、
Node 15から標準ライブラリで対応されました。
ブラウザと同じinterfaceです。

マストドンみたいなものが作りやすくなったかもしれませんね。

Cloudflare

Cloudflare Workersで使えるそうです。
という事は、会員限定コンテンツなどをEdgeに載せることができそうです。

余談

せっかくなので、ハッシュ関数の面白い使い道も紹介しておきます。

ブルームフィルター

集合を扱うアルゴリズムであり、圧縮アルゴリズムでもあります。
JSにも集合を表すSetクラスがあります。
普通の集合はこのようなinterfaceです。


class Set<T> {
  add(element: T);
  has(element: T): boolean;
}

ブルームフィルターのインターフェース

それに対してブルームフィルターはこうです。


class BloomFilter<T> {
  add(element: T);
  has(element: T): false | undefined; // <-  ここ
}
					    

このように、集合の中にその要素が存在するかどうかを、
「存在しない」または「分からない」の二択で返事をしてくれます。

例えば、サッカー選手の中に谷出陸はいますか? という質問に対してはfalseを返してくれますが、
サッカー選手の中に本田圭佑はいますか? という質問に対しては知らないを返します。
つまり、偽陰性は起こらなくて偽陽性だけが起きます。

何がうれしいか

これだけならただの劣化コピーです。
しかし、集合のサイズ(空間計算量)が小さいというメリットがあります。

サッカー選手を表す集合があった場合、その中に(おそらく)数万人程度の情報を持たなければいけません。
その点、ブルームフィルターはそれに比べてかなり小さいサイズにできます。

何に使えるか

この性質はキャッシュととても相性がいいです。
こういうことができます。

  • フロントエンドが自分の持っているキャッシュをブルームフィルターで圧縮してサーバーに送信
  • サーバーが必要なリソースをPush
  • フロントエンドはリソースが足りなければサーバーにリクエスト

どうやっているのか

実装の理屈は簡単です。
各要素をhash化して論理和を取っているだけです。
この時、ハッシュの取り方と重ね方を工夫するとあんまり衝突しなくなります。
digest()が使えるという事です。

以上

ありがとうございました。