「車輪の再発明」ですが、javascriptでMD5を計算するスクリプトを作成しました。そして2049通りのテストケースを作成し、各ブラウザでテストしました。おまけに各ブラウザのjavascript実行速度のベンチマークもしました。
はじめに
さくらレンタルサーバーのApacheはmod_digestが入っていないようで、Digest認証ができません。
パスワードを平文で毎回送るベーシック認証も如何なものだろう?と思っていました。(まあSSLにすればよいんですが。)
ふと、「ブラウザ側で、javascriptにてDigest認証と同様にハッシュを生成してやればよいのでは?」と思いつきました。
そこで、javascriptでMD5が実装されているか調べてみました。
尚、今回作成した記事中で参照しているファイル(yjdmd5.js, testcase.js, md5test.html)の再利用や改変は、ご自由に行って下さい。m(_ _)m)
(2015年1月19日追記:リンクしているyjdmd5.jsを更新しました。これを利用したダイジェスト認証について「javascriptのMD5を用いたダイジェスト認証」を書きましたので、こちらも合わせてご参照下さい。)
(2015年1月25日追記:「javascriptでSHA-512」では、ハッシュ関数SHA-512をjavascriptで実装し、それをスクリプトによるダイジェスト認証にて使うことを提案しています。)
調査
javascriptにmd5関数があることを期待してGoogle先生に聞いてみたのですが、どうやらjavascriptの組み込み関数にはないみたいです。代わりに、沢山の実装事例がありました。
実装事例
特に参考にさせて頂いたページは以下です。
- 「JavaScript で MD5」sanakiさん : RFC1321のサンプルに忠実な実装があります。
- 「md5.js」mitsunari@cybozu labsさん : 「同種のライブラリに比べて3~7倍ほど高速」なコードが有ります。
- 「RFC1321」(の日本語訳サイト(^_^;)。) : アルゴリズムの解説とC言語のサンプルコードがあります。
(sanakiさんのページには、既に「javascriptでチャレンジ/レスポンス認証」のアイデアが書かれています。)
以上のページは古めです。2つのソースコードを見させていただいて、「現在でもビット演算を16bitに区切って行う必要があるのか?」と疑問に思いました。(mitsunari@cybozu labsさんはFirefoxの場合のみ16bit区切り。)
またPHPと互換にするために「文字列はUTF-8で処理したい」とも思い、RFC1321を見ると比較的簡単に実装できそうだったので作ってみることにしました。(本来であれば車輪の再発明はしないに限るのですが…。)
(2015年1月22日追記:code.google.comに、暗号ライブラリ「crypto-js」がありました。コミットの日付が2012-2013年の、新し目の実装です。リンク先のページには使い方がありますが、googleさんに置かれたjsを読み込むだけなので、お手軽です。)
javascriptでバイナリ操作
javascriptにはビット演算子があり、32bit整数としてビット演算ができるそうです。(私はjavascriptをいじり始めてから若干ヶ月です。)
しかしMD5のアルゴリズムでは足し算が多用されています。C言語ならunsigned longを使えば良いので簡単ですが、javascriptではどうでしょう?足し算のたびに32bit整数にまるめてあげる( & 0xFFFFFFFFする)必要があるでしょうか?
疑問点をざっくり調べてみたところ、javascriptでは以下のようです。(ECMA Scriptの仕様書は見ていません(^_^;)。)
- Number型(64bitの浮動小数点型)は2^53までなら整数を正確に表せる。
- ビット演算子を使うと、Number型は32bit整数になる。(下位32bitのみに丸められるイメージ。)
- Uint32Arrayを使うと高速。(javascriptの配列はハッシュテーブルなので遅そう。PHPもだけど。)
- Uint32Arrayに代入するときも適当な32bit整数になる。
上記より、2^20回(くらい)以上連続で足し算しない限りは、ビット演算があったときに自動で32bitになるので、最後以外は意識してまるめてあげる必要はなさそうです。
Firefoxについての特記事項もありませんでしたので、とりあえず32bit演算を基本に作ってみることにしました。
(根本的にアセンブラ・C言語ならスマートに実装できるアルゴリズムをスクリプト上で、しかも浮動小数点型で実装するって、何か残念な気がします。)
UTF-8への変換
javascriptのstringをUTF-8に変換するには、「Blobのコンストラクタに渡してFileReaderで読めば早い」という記事がありました。
しかしコールバック関数(あるいはワーカー)が必要で、またjavascriptにはミューテックスがなく関数内で同期ができなさそうなので、「よくある方法」で変換することにしました。
コードの作成
要件
要件としては以下を考えました。
- 文字列のハッシュ値が得られればよい。(今のところ、巨大なファイルのハッシュ値を得ることはない。)
- 従って、RFCのサンプルコードにある、Updateのような関数は(今のところ)必要ない。
- 文字列はUTF-8として処理。
あと、速度はそれほど必要ありません。そこそこの速度と、そこそこのコードのきれいさを勘案します。
実装
実装したコードはこちらをご参照下さい。:yjdmd5.js (文字コードはUTF-8です。)
Uint32Arrayを使っているので古いブラウザ(IE9以前とか)は未対応です。m(_ _)m
Transformメソッドはmitsunari@cybozu labsさんのコードに似ていますが、RFCのサンプルコードを参考にF, G, H, I, FF, GG, HH, II, ROTATE等の関数を作成し、スクリプトを使ってインライン展開したらこうなりました。
実装の特徴は、バッファにUint32Arrayを使ってダイレクトに32bitリトルエンディアンを扱っていることでしょうか。Uint32Arrayはコンストラクタで0で初期化されるので、1byteのキャラクターを「シフトして”|=”(OR代入)」でセットできます。(byteのセットもインライン展開したのでコードは汚いです。)
テスト
テストの入力は0文字から1024文字までのランダムなASCIIと日本語の文字列(2049ケース)としました。テストケース(入力と正解)はPHPで書いたスクリプトで生成しました。
テストケースはこちらをご参照下さい。:testcase.js (文字コードはUTF-8です。)
テストドライバはこちらです。:md5test.html
ボタンを押してもテスト実行時間が表示されるだけなので面白くありませんが、ソースを見るとちゃんとテストを実行していることがお分かり頂けるかと思います。
しかしUTF-8のテストは日本語だけなので、4byteコードの検証はなされていません。
テスト結果
Windows7にて、以下のブラウザでテストした結果、OKでした。
- Chrome 35.0.1916.153 m
- FireFox 30.0
- IE 11.0.9600.17207
- Safari 5.1.7
- Opera 22.0.1471.70
おまけ:ブラウザのベンチマーク
テストドライバでは、ボタンを連打すると繰り返しテストの実行時間が表示されます。
そこでテストのついでに10回実行時間を測定して、平均値を出してブラウザのベンチマークをしてみました。
ブラウザ | バージョン | 平均実行時間 [msec] |
---|---|---|
Chrome | 35.0.1916.153 m | 420.8 |
FireFox | 30.0 | 80.4 |
IE 11 | 11.0.9600.17207 | 218.5 |
Safari | 5.1.7 | 765.1 |
Opera | 22.0.1471.70 | 412.1 |
何となく「ChromeのV8が速い」と思い込んでいたので、以外な結果でした。
Firefoxに至っては、「きっと途中でエラーが起きて、すべてのテストを実行してないに違いない」とさえ思い、デバッガで確認しました。
Firefoxに関しては「Firefox JavaScript実行速度、C++に迫る」(マイナビニュース)という昨年末(2013年)の記事がありました。これによると、「C++より1.5倍遅い」だけだとのことです。(このテストであれば、実行エンジン内ではほとんどの変数が整数として動いているのかもしれません。)
Safariはがんばりが必要ですね。
もちろんこのベンチマークはjavascriptの様々な要素を測定していません。ご参考程度に御覧ください。
まとめ
javascriptでMD5を計算するスクリプトを作成し、テストしました。
おまけに各ブラウザのjavascript実行速度のベンチマークもしました。
バイナリ操作の仕方もわかったので、「DH鍵交換して、ストリーム暗号でAJAX」も実装可能ですが、さすがにそれだったらSSLを使えって感じでしょうか。