今回よりここで連載をさせていただくことになった小山 (koyhoge)です。フリーランスでエンジニアをしておりまして、他にニフティクラウド mobile backend (NCMB) という mBaaS サービスのエバンジェリストもやっているので、それがきっかけでここに書かせてもらうことになりました。
さて第1回目は、PHP 5.5.0 から新たに導入された password_hash() 関数について深掘りしていきたいと思います。
ハッシュ関数とは
このテーマを取り上げるきっかけは、今年の1月31日に開催されたPHP勉強会@東京での、長谷川智希さんの発表です。
ハッシュと暗号は違うぞ! / Do not confuse hashes and ciphers.
ハッシュ関数は以下の特徴を持ちます。
- ある値に処理を行った際に、もとの値とは関係く見える別の値に変換される。
- 変換後の値が同じなのに、そっくりだが実は異なるという変換前の値を得ることは不可能。
- 変換後の値から変換前の値を推測することは極めて難しい。
これらの特性のため、ユーザが入力されたパスワードをDBなどに保存するためによく使用されます。ユーザから入力された文字列は、ハッシュ関数によって変換されてDBに格納され、しかも変換後の文字列から入力されたパスワードを推測するのは困難なので、仮にDBの内容が流出してもパスワードそのものは守られるという仕組みです。
先日 2月24日に、ハッシュ関数の一種である「SHA-1」が破られたという発表がありました。
Webブラウザのセキュリティ対策など幅広い用途に使われてきたハッシュアルゴリズムの「SHA-1」について、米Googleは2月23日、理論上の可能性が指摘されていたSHA-1衝突を初めて成功させたと発表した。これでSHA-256やSHA-3のような、安全な暗号ハッシュへの移行を急ぐ必要性がこれまで以上に高まったと強調している。
「SHA-1衝突攻撃がついに現実に、Google発表 90日後にコード公開」
http://www.itmedia.co.jp/enterprise/articles/1702/24/news067.html
これはSHA-1を用いてPDFを変換した値が、よく似ているが異なる2つのファイルで同一にする手法が発見されたということです。データが改竄されていないことの確認のために、元データのハッシュ値を公開するという方法はよく行われていますが、そのハッシュ関数がSHA-1の場合は、もはや改竄されていないことは保証できないことを意味します。
とはいえSHA-1はすでに様々な問題が指摘されており、ハッシュアルゴリズムとしてはすでに使用中止が提言されています。これらを改良したSHA-2などが、より安全なハッシュ関数としてすでに使われています。
大事なのは、何が安全なハッシュ関数なのかという点は、時代とともに変化していくということです。現在のSHA-1が発表されたのは1995年ですが、当時の計算速度では十分に安全とされたので広く使用されました。その後計算速度はどんどん上がり、SHA-1変換前の値を効率的に求める手法が発見されたこともあり、現在ではSHA-1は安全なハッシュ関数とは見なされなくなっています。
現在はSHA-1の移行先として、SHA-2のバリエーションの一つであるSHA-256が使われ始めています。しかしこれも現在の計算速度と、今のところ攻撃手法が見つかっていないということでとりあえず安全だろうと見なされているにすぎません。
password_hash() はなぜ生まれたか
このように時代とともに変化する「安全なハッシュ関数」を実現するにはどうしたら良いでしょうか。PHPでハッシュ関数を用いるには、大昔では
$out = sha1($input);
や
$out = md5($input);
などのようにアルゴリズム名の関数を直接呼び出していました。これはさすがにアルゴリズムを変更したいと思った時に不便だということで、PHP-5.1.2 から hash() 関数が作られました。hash() 関数は
$out = hash(‘sha1’, $input);
とハッシュアルゴリズムを文字列として指定する汎用関数になりました。これならアルゴリズムを設定値として外部に持たせることができます。
ただその場合の問題は、ハッシュとして変換された結果に後方互換性がないことです。SHA-1で変換した値とSHA-256で変換した値は当然ながら異なります。つまりパスワードをSHA-1変換した値としてDBに保持してあった場合、ハッシュアルゴリズムをSHA-256に変更する際にはDBの中の値をすべてSHA-256で変換し直さなければいけません。これをサービスを動かしながら行うのは、間違いが起こりやすく大変な労力です。
その問題を解決するために、PHP-5.5.0/PHP-7.0 からpassword_hash()関数が新たに作られました。以下のように使用します。
// ハッシュ値生成 $out = password_hash($input, PASSWORD_DEFAULT); // 登録されているハッシュと入力が合っているかチェックする $valid = password_verify($input, $out);
入力が正しいかどうかの比較には、文字列そのものの比較ではなく password_verfy() という専用の関数を使います。その理由は後述します。
password_hash() に渡すアルゴリズムには、通常 PASSWORD_DEFAULT を指定します。これはそのPHPがリリースされた時点で十分に安全なハッシュアルゴリズムが選択されるという意味です。現在リリースされているPHPでは内部的にはPASSWORD_BCRYPT と同一で、Blowfishアルゴリズムを使用します。将来的にはその意味するところが変わる可能性は大いにあります。
どうやって互換性を保っているのか
password_hash()関数が返す変換後の文字列は、例えば以下の様なものです。
$2y$10$vjrTmLpZIMYAvEbalTg00uMyJs8W8Ij4maSGwvBlveyn6XXT6rSZO
文字列の先頭に「$2y$10$」といういかにも意味がありそうなものが付いています。実際にこれには意味があって、
$ハッシュアルゴリズムを表す文字列$コスト$
を表しています。この例の場合の「2y」はBlowfishをハッシュアルゴリズムとして使用したことを意味しています。続く「$10$」の10はBlowfishアルゴリズムで使用される「コスト」を意味していて反復回数の計算に用いられます。
このようにpassword_hash()が出力する文字列には、
- どのアルゴリズムを用いて
- 何回反復して計算して
- ソルトにはどういう文字列を用いたか(後述)
がすべて含まれていますので、たとえ標準となるハッシュアルゴリズムが変更になったとしても、以前に保存されたものを正しく扱うことが可能なのです。そのためには、出力される文字列を単に比較する従来の方法ではダメで、どんな状態で保存されたのかを加味しながらチェックすることが必須となります。これが password_verify() 関数が生まれた理由です。
ソルトについて
どのようなハッシュアルゴリズムを用いていたとしても、ある文字の入力から出力される値は常に同じです。そのため、パスワードによく使われるであろう一般的な単語を組み合わせた、いわゆる「辞書攻撃」で使用される文字列のハッシュ値を、あらかじめ計算して用意しておく手法があります。さらには指定した長さの文字列の組み合わせすべてを計算して保持しておくレインボーテーブルという手法もあります。もし何らかの原因で、DBに保存されているハッシュ値が漏洩した場合に、レインボーテーブルと比較することでオリジナルのパスワード文字列を割り出すことが可能になってしまうわけです。
これを回避するために、パスワードのハッシュを求める場合は、元のパスワードに対して意味のない文字列を追加してから処理を行うことが一般に行われています。この追加する文字列のことを「ソルト」といいます。
ソルトは乱雑な文字列なので、それを追加した文字列全体に対しては辞書攻撃は無効になります。またソルトを十分な長さにすれば、パスワードの文字列が短くてもレインボーテーブルの対象にはなりにくくなります。
password_hash() 関数は、オプションで自分で用意したソルトを指定することも可能ですが、現在では非推奨で password_hash() 関数が内部で自動生成するソルトを使うことが推奨されています。
もし password_hash() の値が漏洩した場合、ハッシュアルゴリズムとソルトは漏洩者に分かってしまいますが、総当り攻撃をするにはゼロから計算をやり直さなければいけないので、出来合いのレインボーテーブルは役に立たない点は重要なポイントです。
おわりに
ユーザが入力するパスワードをどのように保存するかは、ウェブサービスを作る上での古くからの課題でした。ハッシュを用いるという手法は一般に広まってきているように思いますが、より安全なハッシュを用いるという視点がこれからは求められています。
世界最高速クラスの仮想マシン「KUSANAGI」は、WordPress専用ではなく一般的なPHP環境を高速化できます。PHPを使用するサービス開発者の方々の選択肢としてご考慮いただければ幸いです。
画像参照URL:https://www.flickr.com/photos/143601516@N03/29723649810/