password_hash関数はcrypt関数のラッパーです。パスワードを簡単かつ安全にハッシュ化するための関数です。現在のPHPマニュアルにはpassword_hash関数の重要な制限が未記載であったため追加しました。
password_hash関数はcrypt関数のラッパー
password_hash関数は string password_hash ( string $password
, integer $algo
[, array $options
] ) の書式を持ち、$algoにはDEFAULTまたはPASSWORD_BCRYPTが指定できます。現時点ではPASSWORD_BCRYPTしか指定できないのでどちらを指定しても同じです。DEFAULTを利用することにより、将来より強固なハッシュ関数が追加された場合に自動的な更新が可能となります。(password_hash関数で再度ハッシュされた場合に更新されます)
PASSWORD_BCRYPTはcrypt関数でCRYPT_BLOWFISHを指定した場合と同じ動作をします。CRYPT_BLOWFISHには重要な制限があり、crypt関数のページにはその解説があるのですが、password_hash関数にはありませんでした。 このため、PHPマニュアルに説明を追加しました。
<simpara> <constant>CRYPT_BLOWFISH</constant> has length limitation. It trancates string longer than 72 bytes and ignored. To workround this limitation, users may use other hash functions such as SHA256/SHA512 prior to <function>password_hash</function>, apply the same hash function before <function>password_verify</function>. </simpara>
CRYPT_BLOWFISHの制限
CRYPT_BLOWFISHは72文字(72バイト)より先の文字列を削除し、無視します。これは長いパスワードを設定した場合に問題となります。
この仕様は簡単に実験可能です。
[yohgaki@dev tmp]$ php -r 'var_dump(password_verify("123456789012345678901234567890123456789012345678901234567890123456789012", password_hash("12345678901234567890123456789012345678901234567890123456789012345678901234567890", PASSWORD_DEFAULT)));' bool(true) [yohgaki@dev tmp]$ php -r 'var_dump(password_verify("12345678901234567890123456789012345678901234567890123456789012345678901", password_hash("12345678901234567890123456789012345678901234567890123456789012345678901234567890", PASSWORD_DEFAULT)));' bool(false)
CRYPT_BLOWFISH制限の回避
制限の回避は簡単です。以下のようにします。
<?php // Use sha512 output $h = password_hash(hash('sha512', 'my long password'), PASSWORD_DEFAULT); // Verify $r = password_verify(hash('sha512', 'my long password'), $h); var_dump($r); // TRUE
※ blowfishはchar *をそのまま使っている非バイナリセーフ関数です。当初、hash()でバイナリ値を返していたのですが単純にデフォルトのHEX値を使うようにしました。 参考: https://bugs.php.net/bug.php?id=76842 こういった感じのバグレポートはcrypt()にもあったのですが、blowfishはハッシュ関数でも非バイナリセーフである件を忘れていました。
複数のハッシュ関数を利用することには様々な考え方があります。暗号理論的に安全なハッシュ関数は適切に分散し、衝突がない、不可逆のハッシュ値を作るものです。SHA512も暗号理論的に安全とされています。しかし、現実にはハッシュ関数にはそれぞれクセ/仕様があります。このため、複数のハッシュ関数を利用すると、単一のハッシュ関数を利用した場合に比べ弱くなることがあります。(論理的には必ずと言って構わないほど弱くなる)
しかし、ハッシュ化されたパスワードの安全性を考える場合に重要なことはエントロピーです。入力可能なアスキー値は1文字あたり凡そ6.5bitです。上記の例ではSHA512のバイナリ値を利用しているので、論理的には1文字あたり8bitになります。SHA512のバイナリ値は64バイトです。Blowfishの最大入力値である72バイトに入力可能なアスキー文字を利用した場合と比べても、SHA512のバイナリ値は遜色ない入力であると考えられます。
予めSHA512でハッシュする場合、秘密の固定saltを追加する事も可能です。仮にパスワードデータベースをSQLインジェクションで盗まれたとしても、秘密の固定saltが予測不可能な文字列の場合、秘密の固定saltが盗まれなければ、簡単(脆弱)なパスワードをユーザーが利用していても攻撃者が総当たり攻撃でパスワードを解析することはほぼ不可能です。複数のハッシュ関数を利用する場合に発生する「弱さ」は実用的には無視しても構わないと言えます。
ただし、password_hash()が更に強固なハッシュアルゴリズム(例えば1024bitのハッシュなど)を採用した場合、パスワードデータベースをより弱くする事になります。新しいハッシュ関数に長さ制限がない場合、秘密の固定saltは直接文字列連結するように変えた方が良いです。
まとめ
password_hash()のみで全て完結できれば良いのですが、72文字(バイト)より長いパスワードを許可する場合には注意が必要です。
この対策を行うとpassword_hash()をPASSWORD_DEFAULTで利用していても、常に同じハッシュ関数で予めハッシュしておかなければならなくなります。私の考えとしては「パスワードの長さに制限を設けるべきでない。数百文字でも受け入れるべき」ですが、将来を考えて72文字より長いパスワードの設定を許可しない仕様にしても構わないと思います。
PHP 7.3からはArgon2idがpassword_hash()のアルゴリズムとして利用できます。こちらには極端な長さ制限やバイナリセーフ問題はありません。 argon2iはサイドチャネル攻撃耐性があり、argon2idはGPUクラック/サイドチャネル攻撃耐性があります。Argon2が利用できる場合、argon2idを利用すると良いでしょう。