以前javascriptでMD5を計算するスクリプトを紹介しましたが、今回はそれを用いて、WEBアプリとjavascriptでダイジェスト認証を実装する方法をご紹介します。
「なぜそんなことをするのか?」という理由も記します。
はじめに
以前「javascriptでMD5」という記事を書きました。案外とアクセスしてくださる方がいらっしゃいます。
本稿ではそれを利用し、PHPで実装したWEBアプリでダイジェスト認証と同様の認証処理を実装する方法をご紹介いたします。
読者としてはHTML、PHP、javascriptとjQueryを知っている技術者を想定しています。掲載するコードは説明のためにシンプルにしてありますので、必要に応じて補完して下さい。
javascriptでダイジェスト認証を実装する意味
最初に、javascriptを使ってまでダイジェスト認証を実装する意味から記します。以下の4点です。
- BASIC認証より安全。特にSSLでない場合。
- Apacheにmod_digest(ダイジェスト認証のモジュール)がインストールされていなくてもよい。
- HTMLでログインフォームをデザインできる。
- もし内部に盗聴者がいたとしても、ユーザーのパスワードそのものを取得することはできない。
1点目、2点目は、SSLの時は対して意味はありません。しかし3点目、4点目はSSLの時にも意味を持ちます。
ダイジェスト認証の概要と意味
次に、ダイジェスト認証の仕組みを簡単に説明します。
ユーザー登録の処理
ユーザー登録時の流れは以下のようになります。
登録する時に、ユーザーはユーザー名及びパスワードを入力し、送信ボタンを押します。するとブラウザのjavascriptは、ユーザー名、パスワード、及びサーバーが送ったrealmからハッシュ値を計算します。ハッシュ値とは、入力がちょっとでも違うと出力値が大きく違ってくる関数、尚且つ逆関数的に入力を推測するのが難しい関数の計算結果です。またrealmは認証の範囲のことですが、私は「サービスの名前」くらいに捉えています。
そしてブラウザは、サーバーに対してパスワードそのものを送信することはせず、ユーザー名とハッシュ値を送信します。
もし通信が盗聴され、盗聴者がハッシュ値を取得したとしても、パスワードを推測するのはすごぶる難しくなります。またサービス提供者の内部に盗聴者がいたとしても、パスワードそのものは得られません。
とはいえ、盗聴者が、ハッシュ値から総当り(線形アタック、プルートフォースアタック)でパスワードを解析する可能性はありえます。それが難しいように、ユーザーは十分に複雑な(長い)パスワードにする必要があります。
サービス提供側にとっては、パスワードを使い回すユーザーに対して「内部に盗聴者がいたとしても、パスワードを取得して他のサイトでそのユーザーになりすます、という行為ができないように頑張っています」という主張ができます。
ログイン時の処理
ユーザーがログインするときの処理は以下のようになります。
ここでも、ブラウザはユーザーが入力したパスワードをそのまま送信することはしません。
ブラウザのjavascriptは、サーバーが送信してきた、毎回異なるランダムな文字列であるnonceと、自らが生成したcnonceを結合してダイジェスト(これもハッシュ値)を生成します。ダイジェストは毎回異なりますので、盗聴者がこれを取得したとしても次回に使うことはできません。
ということで、SSLでないときに通常のBASIC認証のようにパスワードを直接送る方法と比較すると、より安全な方法と言えます。
しかしここでも、盗聴したダイジェストを総当り解析してパスワードを推定する、という行為が考えられないわけではありません。
ライブラリ
続いて、コードの例を紹介いたします。
まずは、再利用可能にライブラリにしている2つのコード、javascript側のyjdmd5.js、PHP側のdigest.phpです。これらのソースについては、リンクよりご参照下さい。(文字コードはUTF-8です。またphpは拡張子.txtを追加しています。)
ここに、yjdmd5.jsは実際に使っているものですが、digest.phpの方は、説明のためにシンプルに改変したものです。
もしかしたら、digest.phpには間違いがあり、実際には動作しないかもしれません。その場合はご容赦下さい。(以降に示すサンプルコードについても同様です。)
尚、ライブラリ及び本稿のサンプルコードはご自由に利用、編集、再配布して頂けます。
yjdmd5.js
MD5を計算するjavascriptのスクリプトです。
詳細は「javascriptでMD5」およびyjdmd5.jsのコードとコメントをご参照下さい。最後の方に書いてある、以下の3つの関数を使います。
yjd_md5(m_arg)
MD5を計算する関数です。パスワード登録時に、ブラウザ側でハッシュ値を計算する際に使います。
yjd_get_digest(s_user, s_realm, s_passwd, s_method, s_uri, s_nonce, s_cnonce, s_nc, s_qgp)
ログイン時に、ブラウザ側でダイジェストを計算する関数です。内部でMD5を計算しています。
yjd_create_nonce(i_length)
nonce(ランダムな文字列)を作成する関数です。作成したnonceはyjd_get_digestに渡します。
digest.php
ダイジェスト認証のための、PHPのライブラリです。
詳細はdigest.phpのコードとコメントをご参照下さい。PHPスクリプトにてrequireにて読み込みます。
GetUrl()
URLを取得します。
プロトコル部が「http://」決め打ちになっています。SSLを使っており、もし気になる場合は修正してください。(認証には影響しません。)ポートやHTTPSの値を見て決定もできますが、さくらインターネットの共有SSLの場合はご注意を!(参考「さくらレンタルサーバーの共有SSLを使う」)
CreateNonce($i_length)
nonceを作成する関数、yjd_create_nonceのPHP版です。
AuthDigest($s_digest, $s_user_hash, $s_nonce, $s_cnonce)
ダイジェスト認証の本体です。
ユーザー登録ページ
続いて処理をするスクリプトの例を示します。
まずはユーザー登録ページの例からです。
ユーザー登録ページのHTML(或いはPHP)の一部、javascriptのコードは以下になります。javascriptについては事前にyjdmd5.jsを読み込んでおいて下さい。
HTML(PHP)
<form id="register_form" method="POST" action="#"> <p>ユーザー:<input type="text" name="f_user" value="" /></p> <p>パスワード:<input type=" password " name="f_password " value="" /></p> <input type="hidden" name="f_realm" value="realm" /> <input type="hidden" name="f_hash" value="" /> <button type="submit">ユーザー登録</button> </form>
ここではレルムは”realm”と決め打ちしています。以降もrealmですが、適当なサービスの名前に書き換えてください。(実際は、定数、変数あるいは関数コールで取得することになると思います。)
フィールドf_hashの値は、次のjavascriptにて生成します。
javascript
jQuery(function(){ $('#register_form').submit(function(){ var s_user = $('[name=" f_user "]').val(); var s_realm = $('[name="f_realm"]').val(); var s_passwd = $('[name="f_password"]').val(); var s_hash = yjd_md5(s_user+':'+s_realm+':'+s_passwd); $('[name="f_hash"]').val(s_hash); $('[name="f_password"]').val(''); }); });
6行でハッシュ値を計算し、7行でフィールドに設定しています。
8行では、パスワードを送信しないようにクリアしています。
実際には、確認のためのパスワード再入力のinputもあるでしょうから、その判定とクリアの処理も必要でしょう。
jQueryのセレクタはname属性で指定していますが、idやclassでの指定でももちろん構いません。(以降同様。)
PHP
サーバー側のPHPコードは記述するまでもありません。$_POST[‘f_user’](ユーザー名)、及び$_POST[‘f_hash’](ハッシュ値)を、データベースやファイルに記憶します。
ハッシュ値は、ユーザー名より検索できるようにしておきます。
ログインページ
ログインページのPHP、javascript、およびログインのPOSTを受け取るPHPのコードは以下になります。
ログインページのPHP
<?php $s_nonce = CreateNonce(); /* セッション情報として$s_nonce(の情報)を格納しておく */ ?> <form id="login_form" method="POST" action="#" > <input type="hidden" name="f_realm" value="realm" /> <input type="hidden" name="f_url" value="<?php echo GetUrl() ?>" /> <input type="hidden" name="f_nonce" value="<?php echo $s_nonce ?>" /> <input type="hidden" name="f_cnonce" value="" /> <input type="hidden" name="f_nc" value="00" /> <input type="hidden" name="f_qgp" value="auth" /> <input type="hidden" name="f_digest" value="" /> <p>ユーザー: <input type="text" name="f_user" value="" /></p> <p>パスワード:<input type="password" name="f_passwd" value="" /></p> <button type="submit">ログイン</button> </form>
nonceは毎回ランダムに生成する必要がありますので、2行にてライブラリの関数をコールして取得しています。このnonceの(ハッシュ値等の)情報はセッション情報のどこかに格納しておきます。
7行にて、URLをライブラリの関数にて取得しています。決め打ちでも構いません。
フィールドf_nc、f_qgpの値(ダイジェスト認証のNC、QGP)は決め打ちにしています。
9行と12行の、フィールドf_cnonceおよびf_digestの値は、次のjavascriptにて生成します。
javascript
jQuery(function(){ $('#login_form').submit(function(event) { var s_user = $('[name="f_user"]').val(); var s_realm = $('[name="f_realm"]').val(); var s_passwd = $('[name="f_passwd"]').val(); var s_method = $('#login_form').attr('method'); var s_uri = $('[name="f_url"]').val(); var s_nonce = $('[name="f_nonce"]').val(); var s_cnonce = yjd_create_nonce(); var s_nc = $('[name="f_nc"]').val(); var s_qgp = $('[name="f_qgp"]').val(); var s_digest = yjd_get_digest(s_user, s_realm, s_passwd, s_method, s_uri, s_nonce, s_cnonce, s_nc, s_qgp); $('[name="f_digest"]').val(s_digest); $('[name="f_cnonce"]').val(s_cnonce); $('[name="f_passwd"]').val(''); }); });
9行でcnonceを計算し、14行でサーバーに渡すための設定をしています。
12行でダイジェストを計算し、13行で設定しています。
15行ではパスワードは送信しないように空白にしています。
PHPでの認証
PHPでの認証は以下の様なコードを書きます。実際にはメソッドか関数になると思いますが、ここではベタで書いています。
$s_hash = /* $_POST['f_user']より、ハッシュ値を検索して取得する。 */; $s_digest = $POST['f_digest']; $s_nonce = $POST['f_nonce']; /* セッション情報に格納しているnonce情報にて要検証 */ $s_cnonce = $POST['f_cnonce']; $s_uri = $POST['f_url'] ; if( AuthDigest($s_digest, $s_hash, $s_nonce, $s_cnonce, $s_uri) ) { /* 認証が成功した時の処理 */ } else { /* 認証が失敗した時の処理 */ }
1行は、$_POST[‘f_user’]より格納してあるハッシュ値を検索して取得するコードを記述して、$s_hashに当該ユーザーのハッシュ値を代入します。
3行では、nonceを$POST[‘f_nonce’]より取得しています。しかし攻撃者が自分の知っているnonceを勝手に設定できないよう、セッション情報に格納したnonce情報との検証が必要になります。
6行目でライブラリのAuthDigestをコールし、ハッシュ値から計算した正解のダイジェストが、送られてきたダイジェストと同じかどうか判定し、処理を分岐します。
まとめ
javascriptを利用してダイジェスト認証を行う方法をレポートしました。
「SSLの場合は必要ない」と思われるかもしれませんが「内部の盗聴者を防止できる」というメリットがあります。(とはいえ、これで万全ではありません。)
(2015年1月25日追記:次稿「javascriptでSHA-512」では、ハッシュ関数SHA-512をjavascriptで実装し、それを本稿におけるダイジェスト認証にて使うことを提案しています。攻撃者にとっては、MD5よりパスワードの解析が困難になります。)