PHP の password_hash はパスワードを安全にハッシュ化するための便利な関数です。password_hash は使用アルゴリズムにPASSWORD_DEFAULTを選ぶとbcryptアルゴリズムを使用し、セキュリティを高めるためにソルトとストレッチング(コスト)を自動的に追加します。これのおかげであれこれ自前平文パスワードを加工するより、とりあえず password_hash でハッシュ化して password_verify で認証する仕組みにしておくだけでハッシュ化が弱いことによるセキュリティホールは塞がれます。この記事ではこの password_hash の PASSWORD_DEFAULT アルゴリズムで作られるパスワードの構造について説明します。
PHP: password_hash – Manual
PHP: password_verify – Manual
実際にpassword_hash('パスワード平文', PASSWORD_DEFAULT)
で作られるハッシュ化された文字列は次の形式になります。
// 平文パスワード例。実際のパスワードはランダム文字列を使用する方が安全 $planePassword = 'password'; // PHPのpassword_hash関数をのデフォルトアルゴリズム(bcrypt)使用してパスワードをハッシュ化 $byPasswordHash = password_hash($planePassword, PASSWORD_DEFAULT); echo $byPasswordHash . PHP_EOL; // $2y$10$z2ma6BiDBJY52gYvv2O9hu.H7BcJvKtDExxfrCF.P9KEGxtxZxwmu
この$2y$10$z2ma6BiDBJY52gYvv2O9hu.H7BcJvKtDExxfrCF.P9KEGxtxZxwmu
の中にどのアルゴリズムでハッシュ化されたか、ストレッチングは何回か、ソルトは何か、設定にしたがったハッシュ化結果は何か、が含まれています。この構造を表にすると次のようになります。
役目 | 区切り文字 | アルゴリズム | 区切り文字 | コスト | 区切り文字 | ソルト | ハッシュ値 | |||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
項番 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 |
ハッシュ化文字列 | $ | 2 | y | $ | 1 | 0 | $ | z | 2 | m | a | 6 | B | i | D | B | J | Y | 5 | 2 | g | Y | v | v | 2 | O | 9 | h | u | . | H | 7 | B | c | J | v | K | t | D | E | x | x | f | r | C | F | . | P | 9 | K | E | G | x | t | x | Z | x | w | m | u |
ざっくばらんに言うと $ を区切り文字にしてアルゴリズム、コスト(ストレッチング回数)、ソルト(固定長)が続き、最後に平文を設定にしたがってハッシュ化した値そのものが入る形です。アルゴリズムの部分は password_hash で設定したアルゴリズムによっては2文字より多くなることもあります。こういった文字列を生成することによって PHP はハッシュ文字列の処理に必要なメタ情報をハッシュ文字列内に埋め込んでいます
実際にそれを確かめられるコードが次です。
<?php // 平文パスワード例。実際のパスワードはランダム文字列を使用する方が安全 $planePassword = 'password'; // PHPのpassword_hash関数のデフォルトアルゴリズム(bcrypt)使用してパスワードをハッシュ化 $byPasswordHash = password_hash($planePassword, PASSWORD_DEFAULT); // ハッシュ化されたパスワードを正規表現で部品分け preg_match('/\$(?<algorithm>[^$]*)\$(?<cost>\d{2})\$(?<salt>[A-Za-z0-9.\/]{22})(?<hash>[A-Za-z0-9.\/]{31})/', $byPasswordHash, $matches); $parts = array_filter($matches, fn ($key) => !is_numeric($key), ARRAY_FILTER_USE_KEY); // 結果を出力 echo $byPasswordHash . PHP_EOL; // $2y$10$z2ma6BiDBJY52gYvv2O9hu.H7BcJvKtDExxfrCF.P9KEGxtxZxwmu echo json_encode($parts, JSON_PRETTY_PRINT) . PHP_EOL; // { // "algorithm": "2y", // "cost": "10", // "salt": "z2ma6BiDBJY52gYvv2O9hu", // "hash": ".H7BcJvKtDExxfrCF.P9KEGxtxZxwmu" // } // アルゴリズム、ストレッチング回数、ソルトがあれば crypt 関数で同じことができます $byCrypt = crypt($planePassword,'$'. implode('$', [$parts['algorithm'], $parts['cost'], $parts['salt']])); echo ($byCrypt === $byPasswordHash ? '一致' : '不一致') . PHP_EOL; // 一致 echo (password_verify($planePassword, $byCrypt) ? 'password_verify成功' : 'password_verify不成功') . PHP_EOL; // password_verify成功 // やってることは password_hash と同じなのでコストやソルトを別のものに取り換えても password_verify で認証に成功します。 // 22文字のランダム文字列をソルトとして生成 $salt = substr(str_replace(['+', '='], '', base64_encode(random_bytes(30))), 0, 22); // コストを生成。最低値は4。形式は%02d。大きな値にすると処理が遅くなります $cost = str_pad(rand(4, 15), 2, '0', STR_PAD_LEFT); // 生成したソルトとコストを使って crypt 関数でハッシュ化します。 $byCrypt = crypt($planePassword,'$'. implode('$', [$parts['algorithm'], $cost, $salt])); // 全く別のハッシュ値ですが password_verify で認証に成功します。 echo $byCrypt . PHP_EOL; // $2y$13$HZ0X8Du9Vucyiqz2xVe/Hu97Pu/cIGi48mp4AeO01dklWL8L.r42C echo (password_verify($planePassword, $byCrypt) ? 'password_verify成功' : 'password_verify不成功') . PHP_EOL; // password_verify成功