2. Web Crypto APIによる暗号化

JavaScriptで使える汎用の暗号ソフトとして、Web Crypto APIというものがあり、ブラウザ上で実行できる。 多くのブラウザで使えるようになっているが、まだ比較的新しく、いくつかのブラウザでは未対応のものもあるようである。

2.1 window.crypto

まず、乱数発生プログラムwindow.crypto.getRandomValues()を使ってみよう。

このプログラムは、Math.random()関数と似たようなものだが、暗号強度の点で違いがある。 Math.random()関数は疑似乱数と呼ばれ、まったく同じ条件の場合に同じ値を返す欠点があり、暗号解読者から予測されてしまう問題があった。 例えば、1000回 Math.random()を繰り返しても1000回目の乱数値を完全に予測できるため、プログラムが知られていれば乱数は乱数でなくなり、固定値となってしまう。 (とは言っても、統計上の一様乱数と言う意味では、Math.random()は十分な性質を持つ乱数ではある。また、 乱数値が予測できるのはCやC++言語の場合で、Webブラウザ上で実行されるJavascriptの場合は少し様子が異なる。このことについては後で触れる。)

その点、window.crypto.getRandomValues()は、十分なエントロピーを有するシード値を用いた疑似乱数発生器を使用しているため、 暗号解読者でも予測不能であり、暗号目的として有用という意味合いの乱数を発生する。

function get_random_values() {
 let array = new Uint32Array(10);
 window.crypto.getRandomValues(array);
 let str = "getRandomValues:\n";
 for (var i = 0; i < array.length; i++) {
  str += array[i];
 }
 alert(str);
}
function get_Mathrandom_values() {
 let array = new Float64Array(5);
 let str = "Math.random() values:\n";
 for (var i = 0; i < array.length; i++) {
  array[i] = Math.random();
  str += array[i];
 }
 alert(str);
}

上の各ボタンはそれぞれの乱数生成部分のみを200万回繰り返した時の計算時間を計測するものであるが、 両方とも高速であり、私のPC(google chrome)ではそれぞれ1秒と0.15秒程度であった。 当然ではあるが、Math.random()の方が6~7倍程度高速であったが、getRandomValues()も十分高速である印象である。

両者とも、ほぼビット数を合わせたつもりであるが、Math.random()は0~1の浮動小数点数を生成するので、 微妙な違いがあるかもしれないが、おそらく、誤差の範囲内だろう。

Math.random()も毎回異なる乱数値を表示し、Webブラウザを閉じてから再読み込みしても再現性はなく、毎回異なっている。 もしかすると、PCそのものを再起動すれば再現性が出てくるのかもしれないが、後で調査してみようと思う。

しかし、MDN Web Docs References & Guides : Math.randomn() によると、「暗号に使用可能な安全性を備えていない。セキュリティに関連する目的では代わりにwindow.crypto.getRandomValues() を使用してください。」とある。 おそらく、暗号解読者にとっては予測可能な範囲内の乱数値が出現する可能性が大きいということなのだろう。

とりあえず、このMath.random()の暗号目的での安全性の問題については後で議論するということにして、「Math.random()は暗号目的では使用できない」ということを頭ごなしに信じることにしよう。

getRandomValues()で利用可能なTypedArrayは、Int8Array または Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Arrayの6種類である。

「暗号鍵を生成するためには generateKey() (en-US) を使用してください」とあり、getRandomValues()の利用目的は意外と少なく、パスワードの自動生成やGUID、UUIDなどの識別子の生成などが考えられる。

ほとんどの暗号関連のプログラムはCrypto.subtleにあり、SubtleCryptoインターフェイスを使用する。

2.2 SubtleCrypto

2.2.1 Web Cryptography API Tutorialから

少し古いが、2015年頃のWeb Cryptography API Tutorial (現在、利用不可になっている模様…)を参考にして、少し勉強してみようと思う。

「Web Cryptography API」の一般的によく使われる利用法は、以下のとおりである。

  1. 多要素認証(Multi-factor Authentication)
    パスワードだけの認証ではなく、様々な認証方式を取り入れたより強固な認証方式。
  2. 保護されたドキュメント交換(Protected Document Exchange)
    許可された利用者のみが暗号化されたドキュメントを閲覧できる。
  3. クラウドストレージ(Cloud Storage)
    クラウドサービスやレンタルサーバなどに暗号化して保存し、保存した利用者のみが復号して閲覧できる。
  4. ドキュメントの署名(Document Signing)
    電子署名のことであり、ユーザが該当するドキュメントを受諾したことを証明する。
  5. データ整合性保護(Data Integrity Protection)
    データが改竄されていないことを検証する。
  6. 安全なメッセージング(Secure Messaging)
    暗号化された通信による、関係のない他者に知られることなく安心にメッセージを送受信する。

(参考:W3C勧告:Web Cryptography API (W3C Recommendation 26 January 2017)

2.2.2 ハッシュ

ハッシュ(hash)はダウンロードされたファイルが正しくダウンロードされたのかどうかをチェックするためによく使われている。 入力データが少しでも異なると、全く異なるハッシュ値が表示されるようになっているため、通信時にファイルが破損していないかどうかのチェックができる。 または、悪意を持つ他者によって改竄されていないかどうかのチェックもできる。 MD-5、SHA-1など、今でも使われたりするが、暗号学的には脆弱性が判明し、他者からの改竄攻撃が可能とされ、 今では、SHA-2以上のハッシュ関数の使用が推奨されている。

ハッシュ関数そのものは様々な利用目的に活用され、例えば、連想配列(ハッシュ配列とも言う)などの超高速データ管理ツールとして、 データベース管理に利用されたりする。 膨大なデータを管理する場合の必須アイテムであり、短時間で莫大な量のデータの中から必要情報を抽出するのにハッシュ配列は重要である。

上述したように、ハッシュ値は入力データに対して非常にユニークな数値を与え、 同じハッシュ値を与える別の入力データを探すことが非常に困難であることから、悪意の他者によるデータの改竄が非常に難しい。 そのため、暗号学的ハッシュ関数とも呼ばれ、改竄に対する強度が高いハッシュ関数がとても重要になっている。

Web Crypto APIでhash値を求めることができるが、SHA-2シリーズのみ利用できる。 SHA-1もサポートされているが、脆弱性が存在するため非推奨となっている。 サポートされているのは、SHA-2シリーズのSHA-256、SHA-384、SHA-512の3つである。 SHA-3シリーズは未実装であるが、今のところSHA-2で十分との報告があり、SHA-256がよく利用されているようである。 SHA-256は32ビットCPU用、SHA-384とSHA-512は64ビットCPU用ということであるが、 Javascriptは通常53ビット整数までしか扱えないので、64ビット整数はBigInt扱いとなり、遅い可能性がある。 最も耐性が高いSHA-512が良いと思われるが、処理速度がどの程度か比較してみたほうがいいだろう。

SHAシリーズは米NISTが認可しているもので、SHA-2シリーズのSHA-256がよく利用されている。 一時期、SHA-1が破られたことからSHA-3が策定されたが、SHA-2がいまだに破られていないことから、SHA-2は現在もなお推奨されている。

MDN Web Docs:SubtleCrypto.digest()を参考にすると、構文は、

[Syntax]
const digest = crypto.subtle.digest(algorithm, data);

以下にhash値を求めるプログラム例を示す。

const text = 'An obscure body in the S-K System, your majesty. The inhabitants refer to it as the planet Earth.';
async function digestMessage(message) {
 const msgUint8 = new TextEncoder().encode(message);  // encode as (utf-8) Uint8Array
 const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8);  // hash the message
 const hashArray = Array.from(new Uint8Array(hashBuffer));  // convert buffer to byte array
 const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');  // convert bytes to hex string
 return hashHex;
}
const digestHex = await digestMessage(text);
alert(digestHex);

比較のため、SHA-256とSHA-512の計算を20000回繰り返した場合の計算時間を求めてみる。

奇妙なことに、SHA-256とSHA-512の計算時間はgoogle chromeでほとんど同じだった。 入力データが同じものだったせいかも知れない。

とりあえず、SHA-512も高速に動作することがわかった。

2.2.3 Symmetric Encryption(対称暗号or共通キー暗号)

共通キー暗号のことを対称暗号(Symmetric Encryption)とも言う。 暗号化と復号を同じキーを使って行うので、共通キー暗号もしくは対称暗号と呼ばれる。 1.1で述べたAESを使う話である。

AESは、 AES-CTR, AES-CBC, AES-GCM の 3種類が利用可能となっている。

CTRとCBCは通常の対称暗号方式であるが、GCMは認証機能付き対称暗号である。 認証機能付きのほうが改竄攻撃に強いと言われているので、認証機能付き対称暗号であるGCMを使用するのが望ましいように思われる。

CBCに関しては、初期化ベクトル initialization vector(IV)に対する攻撃とパディングオラクル攻撃が知られてるが、認証機能を併用すると防ぐことができる。

CTRに関しては、IVを除いてCBCと同様に改竄の可能性があるが、並列処理できるという利点がある。 CTRに認証機能を付与したものがGCMであり、同様に並列処理により高速化可能である。

その他にAES-KWというものがあるが、現在調査中。

2.2.4 AES-GCM

通常の対称暗号 AES-CTR や AES-CBC では通信途中で改竄されても気付かないことがあるが、AES-GCMを使うとそのような改竄をされるとエラーとなる。 暗号化したデータのハッシュ値を送信しても、改竄したデータのハッシュ値を求めて送信されると、結局、改竄されているのかどうかの区別がつかない。 元データを暗号化した本人のデータかどうかのチェックができればよいので、本人確認用のコードを埋め込み後に暗号化し送信すればよいことになる。 悪意の改竄者は復号キーを持たないので、暗号文を復号しないで改竄することしかできないため、そのたぐいの攻撃(IV攻撃、パディングオラクル攻撃など)を防ぐことができるようになる。

ボブからアリスへ暗号データを送信する際、悪意の中間者マロリーがman-in-the-middle攻撃(MITM攻撃)をした場合、この攻撃を避けることが非常に難しいことが知られている。 暗号化しているのがマロニーだった場合、そしてボブとアリスがマロニーを本当の相手だと信じていた場合は、どうしようもない。GCMでは暗号化した本人かどうかのチェックができるだけである。

MITM攻撃を避ける方法は、公開キーの証明書を利用したり、フィッシングメールのアドレスを無暗に信じないことなど、いろいろであるが、 完全に防ぐのは難しいので、常に最前線の情報に敏感になっていることが重要だろう。

作成日: 更新日:



back   next