追記(2013/11/23):Twitterからのご指摘があり、ソースコードも含め大幅に改訂しました。
日本でも大きく報道されていましたが、先だってアドビが顧客パスワードを大量に流出したという事件がありました。
Adobeから盗まれたユーザー情報がネット上で公開される
http://itpro.nikkeibp.co.jp/article/IDG/20131031/515124/
Adobe – お客様情報のセキュリティに関する重要なお知らせ
http://blogs.adobe.com/japan-conversations/セキュリティに関する重要なお知らせ/
このニュースを読んだとき、僕は素朴に、「どうしてパスワードを生のまま保存しておくかなー」なんて
不思議に思ったものです。Adobeという大企業ともあれば、それなりの技術者はいるだろうし、
それなりの知識を持ち合わせているものだと思っていたからです。
ところが、以下のサイトで、詳細が説明されていて納得。
Anatomy of a password disaster – Adobe’s giant-sized cryptographic blunder
http://nakedsecurity.sophos.com/2013/11/04/anatomy-of-a-password-disaster-adobes-giant-sized-cryptographic-blunder/
要約すると、
- Adobeはパスワードを暗号化していると主張しているが、ハッシュ値に見えなくもない。
- なぜなら、その暗号化データのサイズはわずか8バイト(データサイズが小さいので解読される恐れがある)。
- Adobe独自の暗号アルゴリズムが使われていた可能性がある。
もしそうなら、極めて危険で、考えられない仕様(※DES または3DESの可能性もあるが)。- その暗号化データサイズと、同じデータ列のものが多く見られることから、IVもなく、ECBモードで暗号化されている。
- さらに、ユーザーがパスワードを忘れたときのために入力する「パスワードヒント」に、
パスワード文字列をそのまま書き込む人が多くいた(笑)。
※しかもそのパスワードヒントはまったく暗号化されていなかった。- その他にも「qwerty」「123456」とか、ありきたりで、よくある、簡単なパスワードを設定する人が多くいた。
- あとは大量にある同じデータ配列の「暗号化データ」から、
クロスワードパズルを解くように大量のパスワードを導き出した(笑)。
「生パスワードをそのまま保存」という愚はおかしていなかったものの、
でもやっぱり残念な実装だった、というのは否めないようです。
Twitter上で、ユーザーパスワードを暗号化しておくよりも、
「ユーザーごとに固有のsaltを加えたハッシュ値を出すのがベストプラクティス」というご指摘をいただきました。
僕の方で、saltの解釈を誤解していたようで、ほぼCBC暗号でのIVの役割と同じことをするようです。
PHPでソースコードを書くと以下のようになります。
[php]
<?php
$user_id="m@hibara.org";
$password = "qwerty";
echo "UserID: ", $user_id, "<br />\n";
echo "Password: ", $password, "<br />\n";
//SHA-256の場合、salt文字列は16文字まで。
//それ以上が指定されると自動的にその長さに詰められます。
echo ‘SHA-256:’ . crypt($password, "$5$".$user_id);
?>
[/php]
出力結果は、このような感じになります(毎回出力結果は変わります)。
UserID: m@hibara.org
Password: qwerty
SHA-256:$5$m@hibara.org$5ehqfKQFfy6lKz8kbDl2EB/aQ3JHyLCMA5W/rdT2LE.
なお、saltにメールアドレスを指定した場合、変更があったときに、
パスワードハッシュも変えなくてはならないので、そこだけは要注意。
PHP ver.5.5~が使えるなら、crypt()のラッパーで、互換性のある、
password_hash()を使うと、saltの自動生成を行ってくれるようです。
[php]
<?php
$password = "qwerty";
echo "Password: ", $password, "<br />\n";
echo "password_hash(): " . password_hash($password, PASSWORD_DEFAULT)."\n";
?>
[/php]
出力結果は以下の通りです(毎回出力結果は変わります)。
Password: qwerty
password_hash(): $2y$10$Pw6tUB4HllZFM.oSZc5ZVu1y1xSUMw4E8ecfic/8utKdxlAheSOq2
Twitter上でのツッコミありがとうございました!
前述のサイトでも指摘されていましたが、少なくとも初期化ベクトル(IV)を利用した、
CBCモードで格納しておくのが解決策といえそうです。
改めてCBCモードとは何か?
前にも記事
にしましたが、少しおさらいを。
毎回つくられる乱数(初期化ベクトル=IV)を与えることで、毎回暗号結果が異なるようになる仕組みのことでした。
今回問題だったのは、ECBモードです。
同じパスワードだと、同じ内容のデータ。そのまま変換されているだけですので、当然同じデータになります。
そこに、Adobeの件では、わずか8バイトのデータサイズで大量解析しやすかったこと、
また、他のユーザーが入力したヒントや、名前から、パスワードが推測され、ひもづけられたことで、
大量流出へとつながりました。
ですので、大本である、「同じパスワードを入力されても、ちがうデータになる」としておけば、
そこまでの被害にはならなかったはずです。
ためしに、PHPでどういった実装ができるか、調べてみました。
本家PHPサイトのドキュメントにあったソースコードのほぼ流用ですが、
http://us1.php.net/manual/ja/function.mcrypt-encrypt.php
[php]
<?php
//ユーザー登録(パスワードを暗号化する)
$password = "qwerty";
echo "password: ", $password, "<br />\n";
$email = "m@hibara.org";
echo "E-mail: ", $email, "<br />\n";
srand();
$iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
$iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
$password_hash = sha1($password);
echo "Password SHA-1: ", $password_hash, "<br />\n";
//パスワードから、SHA-1ハッシュを取得して暗号化(暗号アルゴリズムはAES:Rijndael)
$ciphertext = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $password, $password_hash, MCRYPT_MODE_CBC, $iv);
//出力するデータにIVをつけることを忘れずに
$ciphertext = $iv . $ciphertext;
//BASE64エンコード
$ciphertext_base64 = base64_encode($ciphertext);
echo "encrypted data: ", $ciphertext_base64 , "<br />\n";
//————————————————————————
//認証(復号する)
$ciphertext_dec = base64_decode($ciphertext_base64);
//IVサイズは128bit(16バイト)
$iv_dec = substr($ciphertext_dec, 0, $iv_size);
$ciphertext_dec = substr($ciphertext_dec, $iv_size);
$plaintext_dec = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $password, $ciphertext_dec, MCRYPT_MODE_CBC, $iv_dec);
echo "Password SHA-1: ", $plaintext_dec, "<br />\n";
?>
[/php]
ただ、何万人もいるようなサイトで、これだけの処理をログイン時にやるには、
ちょっとサーバ負荷が高そうですね。ユーザー増加によるデータベースサイズも気になります。
やはり、saltつきハッシュで格納するのがベストのような気がします。
このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください。
日々の開発作業で気づいたこと共有を。同じところで躓いている人が、 検索で辿り着けたら良いな、というスタンスで記事を書くので不定期更新になります。
コメントする