PHPメモ041:パスワードのSalt付きハッシュ値
Salt(ソルト)とは
Saltはパスワードのハッシュ値を生成する際に元のデータに付与する文字列である。
ハッシュ値 = ハッシュ関数(パスワード+Salt)
Saltをユーザ毎に異なる値にすれば、同じパスワードでもハッシュ値はそれぞれ異なる値となる。
Saltを付与してハッシュ値を生成した場合、ハッシュ値だけでなくSaltも保存しておかなければならない。入力されたパスワードの検証では
ハッシュ関数(入力されたパスワード+Salt)
でハッシュ値を求めて、システムが持つパスワードのハッシュ値と比較する。よって、Saltがないとパスワードの検証ができない。
「パスワード+Salt」から生成したハッシュ値の利点はレインボーテーブルによるオフライン攻撃に対して強くなることである。レインボーテーブルとはハッシュ値とパスワードの、巨大な対応表(逆引き表)である。インターネットで公開されているものもある。
Saltを付与しないで生成したハッシュ値では、このレインボーテーブルを使って高速にハッシュ値から平文のパスワードを見つけられる可能性がある。
対して「パスワード+Salt」のハッシュの場合、Saltはシステムやユーザ毎に違うのでレインボーテーブルの有効性が著しく下がる。
ただし、オフラインの総当り攻撃(ブルートフォース攻撃)にはほとんど効果がない。オフライン攻撃、総当り攻撃については参考サイト1を参照。
PHPでの実装
PHPでハッシュ値を得るにはPHP 5.5以降では password_hash() という関数が用意されているのでそれを使う。5.5より前のPHPでは crypt() を使用する。
どちらの関数も戻り値にSaltが含まれているので、別途Saltを保存しておく必要はなく関数の戻り値を保存しておけばよい。
戻り値の形式は参考サイト2の下の方で説明されている。簡単に説明すると最初の4桁がハッシュを求めるためのアルゴリズムの指定、次の3桁がコスト(ストレッチングの回数)の指定、その後がSaltとハッシュ値になっている。ストレッチングについては参考ページ1を参照。
前記のようにこの値は「<ハッシュを生成する方法の指定>+<Salt>+<ハッシュ値>」となっているが、別にコード中で分解したりくっつけたりする必要なく全体をハッシュ値として扱えばよい。
crypt() でアルゴリズムにBlowfishを指定する場合、最初に渡すSaltは以下のような形式である。
$2y$<2桁数値>$<22桁の文字列>
先頭の4桁 "$2y$" はPHP 5.3.7以降で使用できる。それ以前のPHPでは "$2a$" と指定できるが、セキュリティ上問題があるとPHPのドキュメントでは説明されている。
ただし、自分が使用できる環境(OSはCentOS)では、PHPが5.3.7より古いが "$2y$" でもcrypt() が値を返しており、エラーやワーニングも出ず表立って不都合はなかった。ハッシュ化に関連するモジュールが更新されているんだろうか?
続くコストの指定には4~31を指定できる。固定長2桁で、10未満の値を指定する場合は前ゼロを付ける(4の場合は"04")。
最後の22桁の文字列に使用できる文字は "./0-9A-Za-z" である。
以下のコードで ctypt() を試してみた。
define('SALT_HEADER', '$2a$04$'); function create_salt() { // Blowfishのソルトに使用できる文字種 $chars = array_merge(range('0', '9'), range('a', 'z'), range('A', 'Z'), array('.', '/')); // ソルトを生成(上記文字種からなるランダムな22文字) $salt = ''; for ($i = 0; $i < 22; $i++) { $salt .= $chars[mt_rand(0, count($chars) - 1)]; } return SALT_HEADER . $salt; } $password = 'abcd1234'; $salt = create_salt(); echo "salt={$salt}\n"; $hashed_password = crypt($password, $salt); echo "{$password}=>{$hashed_password}\n"; $str1 = 'abcd1234'; $hashed_str1 = crypt($str1, $hashed_password); echo "{$str1}=>{$hashed_str1}: "; if ($hashed_str1 === $hashed_password) { echo "password OK\n"; } else { echo "password NG\n"; } $str2 = 'wxyz7890'; $hashed_str2 = crypt($str2, $hashed_password); echo "{$str2}=>{$hashed_str2}: "; if ($hashed_str2 === $hashed_password) { echo "password OK\n"; } else { echo "password NG\n"; }
出力は以下のようになる。
何度も実行するとほとんどの場合でSaltの最後の1文字が crypt() の戻り値に含まれていない。Saltのアルゴリズムとコストの指定に続く22文字の文字列のうち、実際には21文字しか使われない場合があるということだろうか?
salt=$2a$04$6LX9dypo2YY6IiBEu8Z25E abcd1234=>$2a$04$6LX9dypo2YY6IiBEu8Z25.Ifd.t6Ai3/xECpUME.B89TOenc9t9Z6 abcd1234=>$2a$04$6LX9dypo2YY6IiBEu8Z25.Ifd.t6Ai3/xECpUME.B89TOenc9t9Z6: password OK wxyz7890=>$2a$04$6LX9dypo2YY6IiBEu8Z25.rupqqWpAbJmZzQsm4tOMaASf5ngh41i: password NG
参考ページ:
1.本当は怖いパスワードの話 - @IT
2.PHP: パスワードのハッシュ - Manual
3.PHP: crypt - Manual
4.PHP: password_hash - Manual
5.PHP でパスワードを保存するなら Blowfish でハッシュしよう | バシャログ。
6.PHP - [メモ] パスワードのハッシュを強力にする - Qiita
« PostgreSQLの文字列のエスケープ | Main | BASIC認証の設定のAuthNameとRequire »
「PHP」カテゴリの記事
- PHPStorm 2018.1.7 に更新(2018.12.09)
- PHPメモ051:includeとrequireの使い分け(2015.06.19)
- CakePHPのインストール(2015.06.14)
- PHPからPDOでPostgreSQLに接続する(2015.06.09)
- PHPでメールを送信したら一部のOutlookで受信したメールでヘッダがおかしくなった(2015.05.31)
「セキュリティ」カテゴリの記事
- PHPメモ041:パスワードのSalt付きハッシュ値(2014.08.09)
- ファイルインクルード脆弱性(2014.05.18)
- ファイルダウンロードによるXSS その3(2014.03.27)
- ファイルダウンロードによるXSS その2(2014.03.23)
- ファイルダウンロードによるXSS その1(2014.02.24)
The comments to this entry are closed.
« PostgreSQLの文字列のエスケープ | Main | BASIC認証の設定のAuthNameとRequire »
Comments