PHP7.2 から導入された パスワードハッシュ関数のハッシュアルゴリズム Argon2 を使ってみた
概要
2017/11/30にPHP7.2がリリースされた。
PHP5.5.0 から導入されたパスワードハッシュ関数を使ってみた の更新から3年。PHP7.2でハッシュアルゴリズム Argon2 が追加されたので調べてみた。
TL;DR
Argon2
Argon2は、2015年の Password Hashing Competition のWinnerになったパスワードハッシュアルゴリズムで、クリエイティブ・コモンズのCC0 と Apache 2.0 のデュアルライセンスで公開 されている。
で、論文がこれ。 Argon2: the memory-hard function for password hashing and other applications
実際には Argon2dとArgon2iという2つのアルゴリズムで構成されているが、password_* のアルゴリズムとして採用されているのはArgon2iの方。Argon2iはサイドチャネル攻撃(side-channel timing attacks)耐性があり、Argon2dはGPUを使った攻撃に対する耐性がある。
ハッシュアルゴリズムが使うメモリコスト(メモリ使用量)と時間コスト(回数)と並列度(ハッシュ化時に利用するCPUスレッドの数)をパラメータとして設定し、サーバーサイドアプリケーションの負荷に応じてハッシュ強度を設定できるアルゴリズムのようだ。そのパラメータの設定値は定数として定義されていて
# Argon2で定義されている定数を覗いてみる <?php # メモリコスト(メモリ使用量; 単位 KiB) echo PASSWORD_ARGON2_DEFAULT_MEMORY_COST . PHP_EOL; # 時間コスト(回数) echo PASSWORD_ARGON2_DEFAULT_TIME_COST . PHP_EOL; # 並列度(CPUスレッド数) echo PASSWORD_ARGON2_DEFAULT_THREADS . PHP_EOL;
# 実行結果 1024 2 2
な感じ。
php.netのマニュアル によれば、メモリコストと時間コストについて下記のように書かれているが、
memory_cost (integer) - Argon2 ハッシュの計算に用いるメモリの最大値 (バイト数) を設定します。 デフォルトは PASSWORD_ARGON2_DEFAULT_MEMORY_COSTです。
time_cost (integer) - Argon2 ハッシュの計算にかける時間の最大値を設定します。 デフォルトは PASSWORD_ARGON2_DEFAULT_TIME_COSTです。
PHP7.2が実際にrequireしているライブラリ のコードを紐解くと、メモリコストの単位はKiB(キビバイト)で、時間コストは(時間という概念ではなく)イテレーション数ということなので、マニュアルは少し誤植があるのかもしれない。
API一覧
APIについては password_* が公開されたPHP5.5から大きく変更されていない。 したがってArgon2の利用方法と注意点だけをかいつまんで説明することにする。
password_hash
まずはハッシュ生成関数。
string password_hash ( string $password , integer $algo [, array $options ] )
第1引数 $password
は生パスワード、第2引数 $algo
はハッシュアルゴリズム定数、第3引数は $options
アルゴリズムがサポートするオプションを入力する。ハッシュアルゴリズムをArgon2をするだけであれば、$algo
に PASSWORD_ARGON2I を指定するだけでよい。
<?php $raw_passwd = '3kanmarusan_Passw0rd'; $hashed_passwd = password_hash($raw_passwd, PASSWORD_ARGON2I);
# 生成されたハッシュ $argon2i$v=19$m=1024,t=2,p=2$SEJ2M2VNL3B5MXZNN0tDYg$44a3kX0OxiFzoy4bCtkHnAyy7/TVkvIVU7kdyj8ewCg
$argon2iで始まっているので、Argon2っぽい。$m=1024,t=2,p=2がハッシュ化時に使用しているパラメータ。これはPHPで定義されたデフォルト値そのものである(前述)。パラメータは連想配列で指定することができる。
<?php $hashed_passwd = password_hash($raw_passwd, PASSWORD_ARGON2I, ['memory_cost' => 4096, 'time_cost' => 8, 'threads' => 4]);
# 生成されたハッシュ $argon2i$v=19$m=4096,t=8,p=4$ejcvNmtJL3J1R2kwQWRkVA$Z1QBd6MShZmK+hIGGXfOQx1tq1djPjt8yJC1RDh//6Y
生成されたパスワードハッシュにも $m=4096,t=8,p=4 とあるので指定通りになっている。
ちなみに、ハッシュアルゴリズムのデフォルト(PASSWORD_DEFAULT)に変更があるか調べたところ、PHP7.2の段階では変更がないようだ。
# PASSWORD_DEFAULTはまだbcryptということを調べる <?php echo "[constants]" . PHP_EOL; echo " PASSWORD_BCYRPT: " . PASSWORD_BCRYPT . PHP_EOL; echo " PASSWORD_ARGON2I: " . PASSWORD_ARGON2I . PHP_EOL; echo " PASSWORD_DEFAULT: " . PASSWORD_DEFAULT . PHP_EOL;
# 実行結果 [constants] PASSWORD_BCYRPT: 1 PASSWORD_ARGON2I: 2 PASSWORD_DEFAULT: 1
PHP RFCのプロセスによれば、PHP7.4でハッシュアルゴリズムのデフォルトをArgon2に変更しようとする提案があったようが、今回のRFCでは取り下げられたようだ。
[Resolved] Inclusion on 7.4
Per discussion on the internals mailing list during an initial vote, this RFC no longer proposes changes to PASSWORD_DEFAULT in 7.4.
password_get_info
先ほどの説明の通り、password_hashは、アルゴリズムやコスト、ソルトといった情報もハッシュに含めて返す。password_get_infoは、password_hashで生成した有効なハッシュを第1引数$hash
で渡すと、ハッシュに関する情報の配列を返すメソッド。bcryptの時とインタフェースに変更はないが、戻り値の options が少し変わる。
array password_get_info ( string $hash )
先ほどArgon2で生成したハッシュ $argon2i$v=19$m=4096,t=8,p=4$ejcvNmtJL3J1R2kwQWRkVA$Z1QBd6MShZmK+hIGGXfOQx1tq1djPjt8yJC1RDh//6Y について確認してみる。
<?php # password_hash で生成した有効なハッシュ(Argon2) $hash = '$argon2i$v=19$m=4096,t=8,p=4$ejcvNmtJL3J1R2kwQWRkVA$Z1QBd6MShZmK+hIGGXfOQx1tq1djPjt8yJC1RDh//6Y'; $info = password_get_info($hash); print_r($info);
Array ( [algo] => 2 [algoName] => argon2i [options] => Array ( [memory_cost] => 4096 [time_cost] => 8 [threads] => 4 ) )
algo
の値が、PASSWORD_ARGON2I(=2) を指しており、options
にハッシュ化時に使用したパラメータが表示されていることがわかる。
password_verify
ここは変更なし
password_needs_rehash を使ってパスワードハッシュアルゴリズムをアップデートする
パスワードをハッシュで保存しているときに、パスワードハッシュの強度(アルゴリズムやコスト、ソルトやストレッチングの回数など)を変更したい場合がある。PHP7.1まではアルゴリズムが bcrypt のみだったのでコストやソルトの変更しかできなかったが、PHP7.2からはアルゴリズムが追加されたのでハッシュアルゴリズムが変更できるか試してみる。
なお、ハッシュで保存している=生パスワードを知り得ないので、ハッシュ強度の変更をオフラインで実行することはできない。したがって、ユーザーの認証が成功した時に(生のパスワードが分かっているので)オンラインでハッシュの変更(rehash)をする。
下記は、bcryptアルゴリズムでハッシュ化されて保存されているパスワードハッシュに対して、認証成功後にArgon2アルゴリズムで再ハッシュする疑似コードである。
<?php $raw_passwd = '3kanmarusan_Passw0rd'; // ユーザーの入力 $hashed_passwd = '$2y$10$fAO/bg1Ti9.yBM3wC3FyJeIfrIql9dVFx/dhTDZO.FjSSjylRRCLK'; // DBなどから引っ張ってきたもの(これはbcrypyでハッシュ化されている) // まず認証しようず! if(password_verify($raw_passwd, $hashed_passwd)) { // パスワード認証成功。 if(password_needs_rehash($hashed_passwd, PASSWORD_ARGON2I)) { // rehashが必要 $new_hashed_passwd = password_hash($raw_passwd, PASSWORD_ARGON2I); // (新しいハッシュをDBに格納) // ... } } else { // パスワード認証失敗 }