Closed16
ハッシュ ( + PHP ) のお勉強
データベース移行時におけるハッシュ値の扱いをどうしたら良いかよくわからないので、ハッシュ ( + PHP ) のお勉強
アルゴリズム
- アルゴリズムの違いで変わる要素 → Secure Hash Algorithm ( wikipedia )
- 今回は細かいことは後回し
ソルト
- ハッシュ化前に対象文字列に付加するランダム文字列
- 同一文字列のハッシュ衝突を防ぐために用いる
- 複数のユーザで同一のソルトを使っていて、かつ同一のパスワードだった場合に、同じパスワードであることがわかってしまう
- ユーザごとに異なるソルトを設定することが推奨される
ストレッチング
- ハッシュ化を何重も繰り返すこと
- 解析に要する時間を長期化するために用いる
- 当然ハッシュ化自体も高負荷化する
PHP におけるパスワードハッシュ
参考
ハッシュ化
概要
-
password_hash
は同じ値を渡しても結果が毎回異なる- あらかじめ先に対応表を作っておく攻撃を防ぐため
- 違うユーザの同じパスワードを同じだと認識できなくするため
- ソルトをランダムで生成しているから
アルゴリズム
-
PASSWORD_DEFAULT
- 強力なアルゴリズムが追加されると値は追従する
- 現状は
PASSWORD_BCRYPT
と同値
-
PASSWORD_BCRYPT
-
crypt
互換
-
-
PASSWORD_ARGON2I
- PHP が Argon2 を有効にしてコンパイルされている場合のみ
- 今は調査対象外
-
PASSWORD_ARGON2ID
- 同上
crypt
- 不可逆ハッシュ化をする
- ハッシュ方式はソルトで決まる
- 察するに現状は
CRYPT_BLOWFISH
に該当する? - これは SHAxxx とはまた別の何か?
-
bcrypt - Wikipedia
- 生成結果が
$2y$
で始まっているので、これかな
- 生成結果が
- 察するに現状は
-
crypt
のソルト引数の生成とcrypt
のストレッチをよしなにするラッパーがpassword_hash
認証
-
password_verify
を用いて、生値
とハッシュ値
を比較する - ハッシュ化するときに使ったソルトは
ハッシュ値
を見ればわかる、ということかな?
main.php
public function main()
{
var_dump(sha1('miraito')); // c7592fc9ef439e854e4fa535689e2fc9a16e4465
var_dump(sha1('miraito')); // c7592fc9ef439e854e4fa535689e2fc9a16e4465
var_dump(sha1('miraito')); // c7592fc9ef439e854e4fa535689e2fc9a16e4465
var_dump(password_hash('miraito', PASSWORD_DEFAULT)); // $2y$10$Rcngip7pwJKPSt9KTmauVesZoYopvx9Mco41VtGF6vaD4WaIJTYW6
var_dump(password_hash('miraito', PASSWORD_DEFAULT)); // $2y$10$N6shLCKJyMFPcGM1abP/NuTwui0xlxR3HdCl9Y9BL8gdq6D7adDs6
var_dump(password_hash('miraito', PASSWORD_DEFAULT)); // $2y$10$oguCt3vVe5FE.7ZIuZz4MOZPlLV0O5apUZaC753c2Kq.zmE8ROPsW
var_dump(password_verify('miraito', '$2y$10$Rcngip7pwJKPSt9KTmauVesZoYopvx9Mco41VtGF6vaD4WaIJTYW6')); // true
var_dump(password_verify('miraito', '$2y$10$N6shLCKJyMFPcGM1abP/NuTwui0xlxR3HdCl9Y9BL8gdq6D7adDs6')); // true
var_dump(password_verify('miraito', '$2y$10$oguCt3vVe5FE.7ZIuZz4MOZPlLV0O5apUZaC753c2Kq.zmE8ROPsW')); // true
}
crypt
crypt( string $string, string $salt ) : string
-
$salt
は php8 から必須っぽい、まぁ直接crypt
使うことはないのであんま興味なし - ハッシュ方式は
$salt
で決まる
サポートされているハッシュ形式と $salt
例
main.php
/**
* CRYPT_STD_DES
*
* 文字 2
*/
var_dump(crypt('miraito', 'xx'));
// result xxUyV34mHYZIQ
/**
* CRYPT_EXT_DES
*
* _ 反復回数4 文字4
*/
var_dump(crypt('miraito', '_1234abcd'));
// result _1234abcdTQJlZ5MGkIg
/**
* CRYPT_MD5
*
* $1$ 文字9
*/
var_dump(crypt('miraito', '$1$abcdefgh$'));
// result $1$abcdefgh$w7VHhLLGfEYk7t2gy4rbF1
/**
* CRYPT_BLOWFISH
*
* $2a$ | $2x$ | $2y$ コスト2 $ 文字22 ( manual やいろいろ叩いたっ結果的に 文字25 な気が... )
*/
var_dump(crypt('miraito', '$2y$04$abcdefghijklmnopqrstu$'));
// result $2y$04$abcdefghijklmnopqrstu.3Un6BHJgkr32hgzhQzw.Hkj4rmbeQgK
/**
* CRYPT_SHA256
*
* $5$ rounds={n} $ 文字25 $
*/
var_dump(crypt('miraito', '$5$rounds=5000$abcdefghijklmnopqrstuvwxy$'));
// result $5$rounds=5000$abcdefghijklmnop$eRm7RIwBZsWcUJlhZNNSDIoExbJH63E4xN/eNxHU3g0
/**
* CRYPT_SHA512
*
* $6$ rounds={n} $ 文字25 $
*/
var_dump(crypt('miraito', '$6$rounds=5000$abcdefghijklmnopqrstuvwxy$'));
// result $6$rounds=5000$abcdefghijklmnop$qq9uN1d.klE/OGf/7Ki9j3BvdX.mZ6/W6EUTVGxYIjsaEbPzFCLhK2ku/qelK1BzIsFbW.4bAYLj.WTwDyQqe0
$salt
のある位置より後ろは結果に影響しないのか...
main.php
var_dump(crypt('miraito', '$5$rounds=5000$abcdefghijklmnopxxxxxxxxx$'));
// result $5$rounds=5000$abcdefghijklmnop$eRm7RIwBZsWcUJlhZNNSDIoExbJH63E4xN/eNxHU3g0
var_dump(crypt('miraito', '$5$rounds=5000$abcdefghijklmnopyyyyyyyyy$'));
// result $5$rounds=5000$abcdefghijklmnop$eRm7RIwBZsWcUJlhZNNSDIoExbJH63E4xN/eNxHU3g0
var_dump(crypt('miraito', '$5$rounds=5000$abcdefghijklmnopzzzzzzzzz$'));
// result $5$rounds=5000$abcdefghijklmnop$eRm7RIwBZsWcUJlhZNNSDIoExbJH63E4xN/eNxHU3g0
わかってきた
で、$salt
が同じならハッシュ値のとある位置までは同じ結果になる
main.php
var_dump(crypt('hogesan', '$5$rounds=5000$abcdefghijklmnopxxxxxxxxx$'));
// result $5$rounds=5000$abcdefghijklmnop$caU8JoEROwtHH/JXTCAEeCUq07s02vwAbKffghJzCz0
var_dump(crypt('miraito', '$5$rounds=5000$abcdefghijklmnopxxxxxxxxx$'));
// result $5$rounds=5000$abcdefghijklmnop$eRm7RIwBZsWcUJlhZNNSDIoExbJH63E4xN/eNxHU3g0
-
$salt
のとある位置までがハッシュ値
に含まれている -
$salt
のとある位置より後ろはハッシュ値
に影響しない
ということは ハッシュ値
自体をそれを生成した時の $salt
としてそのまま使える
main.php
var_dump(crypt('miraito', '$5$rounds=5000$abcdefghijklmnopxxxxxxxxx$'));
// result $5$rounds=5000$abcdefghijklmnop$eRm7RIwBZsWcUJlhZNNSDIoExbJH63E4xN/eNxHU3g0
var_dump(crypt('miraito', '$5$rounds=5000$abcdefghijklmnop$eRm7RIwBZsWcUJlhZNNSDIoExbJH63E4xN/eNxHU3g0'));
// result $5$rounds=5000$abcdefghijklmnop$eRm7RIwBZsWcUJlhZNNSDIoExbJH63E4xN/eNxHU3g0
つまりこういう流れで検証されている
write.php
$plain = 'miraito'; // ユーザ入力
$salt = '$5$rounds=5000$abcdefghijklmnopxxxxxxxxx$'; // なんやかや生成
$hashed = crypt($plain, $salt);
var_dump(crypt($plain, $salt) === $hashed); // 当然 true
db_write($hashed); // 結果だけ保存
check.php
$plain = 'miraito'; // ユーザ入力
$hashed = db_read(); // なんやかや参照
var_dump(crypt($plain, $hashed) === $hashed); // 使った $salt がなくても true
タイミング攻撃
要約すると
- 普通の文字列比較は 1 文字ずつ比べてずれたら即 false になる
-
axx === abc
はxxx === abc
より長くかかる
-
- 当然性能のためだが、一致具合によって処理時間が変わるという脆弱性がある
-
axx === ???
とbxx === ???
とcxx === ???
でa
だけ長れば、先頭がa
だとわかる
-
- なのでパスワードをチェックする時は不一致具合によって処理時間の変わらない方法でやれ
php では
-
hash_equals が用意されているので、
===
ではなくこれを使え-
===
との違いは処理時間に関するもののみ -
hash_equals
自体に何らかの変換処理などが含まれているわけではない
-
参考
整理
crypt
-
$plain
と$salt
を渡すと$hashed
ができる - ハッシュ方式は
$salt
の値自体で決まる -
$hashed
の先頭と$salt
の先頭は一致するので、$hashed
自身も$salt
である - 再入力された
$plain
と$hashed
で算出した結果が$hashed
になれば認証が成功する
password_hash
-
$plain
と$algorithm
を渡すと$hashed
ができる -
crypt
のラッパーである-
crypt
のストレッチをする -
crypt
に渡す$salt
のランダム生成をする
-
- ハッシュ方式は
$algorithm
定数で指定する-
PASSWORD_DEFAULT
はcrypt
のCRYPT_BLOWFISH
に相当する
-
hash_equals
-
string
とstring
が一致するか確認できる - タイミング攻撃に対して安全である
password_verify
-
$plain
と$hashed
が合致するか確認できる - 察するに
hash_equals
とcrypt
のラッパーである-
$hashed
を$salt
としてcrypt
の実行 - その結果を
hash_equals
で安全に比較
-
結論
-
password_hash
とpassword_verify
だけ使え -
$hashed
だけ取っておけ
まとめ
絵で〆
-
password_hash
とpassword_verify
の箱を越えるのが引数と戻り値 - ※ DB の箱は「これだけ保存する」って意味
- 別に
password_hash
とpassword_verify
は DB アクセスしない
- 別に
このスクラップは2021/06/02にクローズされました