JavaScriptでCryptoJSを使って、AESの暗号化と復号を行う


ウェブサイトを見回してみても、正しく実装されていなかったり、良いサンプルが無かったので、記事にしてみました。

→ DEMO&ソースコード:https://jsfiddle.net/hibara/qzono8jb/

CryptJSについては、以下にあります。
https://code.google.com/archive/p/crypto-js/

暗号化するにあたって

CryptJSの本題に入る前に、少し暗号化についてのお作法を知っておく必要があります。詳細は僕が書いた、別記事の「Visual Studio C#でファイルを暗号化してみる」を参照していただきたいですが、一応ざっとおさらい。

暗号化モードではCBCモードを使うのがベター

ブロック暗号方式と呼ばれるものは、その名のとおり、何バイトかずつブロック単位で暗号化していきますが、ここでやりがちなのがECBモードでしょうか。

ECBモード

これの何が問題かといえば、毎回同じデータ、同じパスワードだと、毎回同じ内容の暗号化データがでてきしまうという点です。

あるいは、各ブロックが小さくなるので、暗号化データへの総当たり攻撃(ブルート・フォースアタック)がしやすくなります。

基本的には、CBCモードを使いましょう。

CBCモード/暗号化

冒頭に、乱数による初期化ベクトル(Initialization Vector)を与えて、各暗号化ブロックに捻り合わせて行くイメージでしょうか。

CBCモード/復号する

ちなみに、暗号の大家であるブルース・シュナイアー氏がその著書『暗号技術大全』(日本語訳版は絶版・・・)の中でも、

ファイルを暗号化するのであれば、CBCモードがベストだろう。このモードを使えば、セキュリティは大きく向上するし、保存したデータに多少エラーが発生しても、同期エラーが発生することはまずない。アプリケーションが(ハードウェアではなく)ソフトウェアベースであれば、CBCがほぼ確実にいちばんいい。

と書いています。

パディングモード

ブロック暗号方式では、何バイトかのブロック単位で暗号化することにより、場合によっては、「端数」が出てしまいます。

これは暗号化されると、どこまでが暗号化データだったのか、復号時に正しく判別ができなくなくなるということです。

パディングモードでよく使われるのは、PKCS7のパディングモードでしょうか。

たとえば、以下の例ですと、データ長が8バイトで、実際のデータ列が9バイトあれば、残りの7バイトは、以下のように埋められます。

PKCS7パディングモード

つまり「余り」に埋められた合計サイズが、数値として埋められるというわけです。
これにより、復号時に、データ境界線をプログラムで判別できるようになります。

CryptoJSにはすべて揃っている

ところが、いざ、CBCモードで、PKCS7のパディングモードで暗号化したいと、該当のソースファイルを当たったら、どこにも見当たらない。「おかしい」と思って、本家のページを当たってみたところ、いずれも「Default」であるということが判明。

ユーザーが万一、なにも設定せずに使ってしまっても、黙ってCBCモードでPKCS7パディングモードで暗号化されるという親切設計でした(笑)。

CryptoJS supports the following modes:

CBC (the default)
CFB
CTR
OFB
ECB
And CryptoJS supports the following padding schemes:

Pkcs7 (the default)
Iso97971
AnsiX923
Iso10126
ZeroPadding
NoPadding

暗号化キーは鍵空間を広く使う

これは暗号化ユーティリティを使うすべてのユーザーにも言えることですが、パスワードはなるべく長い文字列で使ってほしいところです。当然、総当たり攻撃がしやすいという問題があるからです。

とはいえ、ユーザビリティを強制するのも、ツールの自由度を下げます。ただ、開発者側もそういった問題に少なからずフォローすることはできます。

たとえば、一文字のみパスワードを入れられても、鍵空間を目一杯使って、毎回異なるパスワードキーを生成してあげれば、多少この問題を和らげることができます(ただし、ブルートフォースアタックのような攻撃には何の訳にも立ちません)。

鍵空間

本題の「CryptoJS」

さて、本題の「CryptoJS」ですが、Google Code Archiveに上がっている、ライブラリです。そのサイトには、こう書かれています。

CryptoJS is a growing collection of standard and secure cryptographic algorithms implemented in JavaScript using best practices and patterns. They are fast, and they have a consistent and simple interface.

CryptoJSは、ベストプラクティス、ベストな形で、JavaScriptにおいての安全な暗号アルゴリズム標準となるべく開発しているものの一つです。これらは高速であり、一貫性とシンプルなインターフェイスを提供しています。

実際、オープンソースで、CDNやGitHubにも上がっています。

使い方は、たしかに簡単で、実際に使いたいjsファイルをhtmlヘッダ内で定義するだけです。

CryptoJSのAESを使ってみる

<script src="http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/aes.js"></script>
  <script src="http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/pbkdf2.js"></script>

通常なら、「aes.js」だけで行けますが、今回は鍵空間を広げるためのライブラリを使うので、「pbkdf2.js」を含めました。

CryptoJS を使う上での注意点は、やはり「バイナリ」の扱いでしょうか。

おそらくウェブ上(サーバ間)でのデータのやりとりも考慮されているのか、データをバイナリデータ(Hex = 16進文字列)や、Base64エンコーディングして渡す場面が何度かあります。

ただ、CryptoJS では、それらを適宜、必要なデータへコンバートするためのメソッドも用意されています。

CryptoJS.enc.Hex.parse()
CryptoJS.enc.Base64.parse()
CryptoJS.enc.Utf8.parse()

などを駆使して暗号化します。

まずは、暗号化から。この程度のソースコードにjQueryを使っているのはご容赦ください。

  $('#encrypt').on('click', function () {
    //パスワードはUTF-8エンコーディング
    var secret_passphrase = CryptoJS.enc.Utf8.parse($('#encrypt-password').val());
    //alert(secret_passphrase.toString(CryptoJS.enc.Utf8));
    var salt = CryptoJS.lib.WordArray.random(128 / 8);
    var key128Bits500Iterations =
      CryptoJS.PBKDF2(secret_passphrase, salt, {keySize: 128 / 8, iterations: 500 });
    //初期化ベクトル(ブロック長と同じ)
    var iv = CryptoJS.lib.WordArray.random(128 / 8);
    //暗号化オプション(IV:初期化ベクトル, CBCモード, パディングモード:PKCS7
    var options = {iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7};
    //暗号化内容のエンコーディングは「UTF-8」
    var message_text = CryptoJS.enc.Utf8.parse($('#encypt-text').val());</p>

    //----------------------------------------------------------------------
    //暗号化
    var encrypted = CryptoJS.AES.encrypt(message_text, key128Bits500Iterations, options);
    //----------------------------------------------------------------------

    //暗号結果データをカンマ(&quot;,&quot;)で結合してまとめる(復号時にわかるように)
    //(salt + iv + ciphertext)
    var binary_data = CryptoJS.enc.Hex.stringify(salt);
    binary_data += (',' + CryptoJS.enc.Hex.stringify(iv));
    binary_data += (',' + encrypted);
    $('#encypted-data').text(binary_data);
});

暗号化の際の注意点としては、暗号に必要なsaltやIVなどを、暗号化データに含めないといけない点です。

ここでは単に、カンマ区切りとしていますが、他に方法があるのなら、どのような手段でも良いでしょう。

そして、復号はこちら。

  $('#decrypt').on('click', function () {
    // あからじめ仕込んでおいた暗号化データのカンマ","を使って文字列をそれぞれに分割
    var array_rawData = $('#encypted-data').text().split(',');</p>

    var salt = CryptoJS.enc.Hex.parse(array_rawData[0]);  // パスワードSalt
    var iv = CryptoJS.enc.Hex.parse(array_rawData[1]);    // 初期化ベクトル(IV)
    var encrypted_data = CryptoJS.enc.Base64.parse(array_rawData[2]); //暗号化データ本体

    //パスワード(鍵空間の定義)
    var secret_passphrase = CryptoJS.enc.Utf8.parse($('#decrypt-password').val());
    var key128Bits500Iterations =
      CryptoJS.PBKDF2(secret_passphrase, salt, {keySize: 128 / 8, iterations: 500 });

    //復号オプション(暗号化と同様)
    var options = {iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7};

    //復号
    var decrypted = CryptoJS.AES.decrypt({&quot;ciphertext&quot;:encrypted_data}, key128Bits500Iterations, options);
    // 文字コードをUTF-8にする
    $('#decrypt-text').val(decrypted.toString(CryptoJS.enc.Utf8));
});

以上です。

実際のデモは、先にも書きましたが、
https://jsfiddle.net/hibara/qzono8jb/

で、見られます。

コメント

お気軽にコメントをどうぞ〜

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

コメントフィード

s