Closed15

ハッシュ ( + PHP ) のお勉強

データベース移行時におけるハッシュ値の扱いをどうしたら良いかよくわからないので、ハッシュ ( + PHP ) のお勉強

ソルト

  • ハッシュ化前に対象文字列に付加するランダム文字列
  • 同一文字列のハッシュ衝突を防ぐために用いる
  • 複数のユーザで同一のソルトを使っていて、かつ同一のパスワードだった場合に、同じパスワードであることがわかってしまう
    • ユーザごとに異なるソルトを設定することが推奨される

ストレッチング

  • ハッシュ化を何重も繰り返すこと
  • 解析に要する時間を長期化するために用いる
  • 当然ハッシュ化自体も高負荷化する

PHP におけるパスワードハッシュ

参考

2018年のパスワードハッシュ - Qiita

ハッシュ化

password_hash

概要

  • password_hash は同じ値を渡しても結果が毎回異なる
    • あらかじめ先に対応表を作っておく攻撃を防ぐため
    • 違うユーザの同じパスワードを同じだと認識できなくするため
    • ソルトをランダムで生成しているから

アルゴリズム

  • PASSWORD_DEFAULT
    • 強力なアルゴリズムが追加されると値は追従する
    • 現状は PASSWORD_BCRYPT と同値
  • PASSWORD_BCRYPT
    • crypt 互換
  • PASSWORD_ARGON2I
    • PHP が Argon2 を有効にしてコンパイルされている場合のみ
    • 今は調査対象外
  • PASSWORD_ARGON2ID
    • 同上

crypt

crypt

  • 不可逆ハッシュ化をする
  • ハッシュ方式はソルトで決まる
    • 察するに現状は CRYPT_BLOWFISH に該当する?
    • これは SHAxxx とはまた別の何か?
    • bcrypt - Wikipedia
      • 生成結果が $2y$ で始まっているので、これかな
  • crypt のソルト引数の生成と crypt のストレッチをよしなにするラッパーが password_hash

認証

password_verify

  • 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 === abcxxx === 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_DEFAULTcryptCRYPT_BLOWFISH に相当する

hash_equals

  • stringstring が一致するか確認できる
  • タイミング攻撃に対して安全である

password_verify

  • $plain$hashed が合致するか確認できる
  • 察するに hash_equalscrypt のラッパーである
    • $hashed$salt として crypt の実行
    • その結果を hash_equals で安全に比較

結論

  • password_hashpassword_verify だけ使え
  • $hashed だけ取っておけ

まとめ

絵で〆

  • password_hashpassword_verify の箱を越えるのが引数と戻り値
  • ※ DB の箱は「これだけ保存する」って意味
    • 別に password_hashpassword_verify は DB アクセスしない

このスクラップは2021/06/02にクローズされました
作成者以外のコメントは許可されていません