レインボーテーブル対策について

ユーザパスワードの管理について記載します。
利用者(ユーザ)のパスワードを管理する場合、漏洩対策は絶対に行わなければなりません。

最低限のパスワード漏洩対策

今時パスワードを平文で管理をしているサーバは無いと思いますが、個人サーバだと絶対にないとは言い切れません。

自分のパスワードだけなら自己責任ですが、他人のパスワードを保存するなら、最低でもパスワードはhash値で保存するようにしましょう。
例えば、パスワードが「123」の場合、次のハッシュ化関数でハッシュ値をハッシュ化することができます。
 DB上にはハッシュ値のみを保存するようにし、平文のパスワードはDB上で管理してはいけません。

//パスワードの保存時
$hashed_password = hash('sha256', $user_password); //hash値の生成
$stmt = $conn->prepare("insert into user_table(username,password) values(?,?)");
$stmt->bind_param("is",$user_name,$hashed_password);
if ($stmt->execute()) {…

ログイン処理では、ユーザが入力した平文のパスワードを同じ方法でhash化し、比較すればいいのです。

//ログイン時等
$stmt = $conn->prepare("SELECT password FROM user_tableWHERE username=?");
$stmt->bind_param("s", $user_name);
$stmt->execute();
$result = $stmt->get_result();
$row = $result->fetch_assoc();
if($row["password"] === $input_password ){
 //ログイン成功
} else {
  //ログイン失敗
}

ペッパー(pepper)を使ったパスワード管理対策

 管理者がいくら対策を施しても、利用者は覚えやすい簡単なパスワードを設定することが多いのが現状です。
 ポリシーがなければ「1234」「abcd」等の脆弱なパスワードを設定するユーザも少なくありません。(ポリシーで規制をかけても、結局は覚えやすいパスワードを使用します。[abcd0123][Abcdefgh][Asdfghjk]…)
 いくらhash値を保存しているとは言え、よく使われているパスワードは、ハッシュ値自体が出回っており、ハッシュ値からパスワードを推測されてしまうようになりました。
 このよく使用されるパスワードのハッシュ値一覧は「レインボーテーブル」と呼ばれています。

悪意あるユーザ
悪意あるユーザ

パスワードのハッシュ値は「a665a45920…」か、レインボーテーブルを調べると、パスワードは「123」だな。

↑というユーザ対策になります。

そこで、パスワードをハッシュ化する前に、pepperと呼ばれる短いデータを付加してからハッシュ化することで、サイト独自のハッシュ値を生成し、レインボーテーブルが使えないようにします。
pepperを使ったパスワード生成プログラム例

$pepper="_pepper_";//サイトごとに変えて使用する
$hashed_password = hash('sha256', $user_password.$pepper); //hash値の生成
$stmt = $conn->prepare("insert into user_table(username,password) values(?,?)");
$stmt->bind_param("is",$user_name,$hashed_password);
if ($stmt->execute()) {…

これだけで、サイトごとに生成されるhash値が全く違う値になるので既存のハッシュテーブルが利用できなくなります。
ログイン等のパスワード確認でも同じようにpepperを付加してからhash値を生成して比較するだけです。

ソルト(salt)を用いたパスワード管理

pepperを使用すると、一般的なレインボーテーブルによる攻撃を防ぐことはできますが、pepperが漏洩した場合には、そのサイト専用のレインボーテーブルを作成することでパスワードを入手できてしまいます。
 また、hashデータが複数漏洩した場合には、hash値自体を比較することでユーザAとユーザBのパスワードが同じかどうか調べることができるようになってしまいます。

悪意あるユーザ
悪意あるユーザ

パスワード「1234」で登録したらハッシュ値に「9a929c…」が保存されているな。同じハッシュ値のユーザを見つければ、同じパスワードでログインできるはずだ。

 ↑というユーザ対策になります。

 ですから、pepper値自体をユーザ毎に変える仕組みがsaltです。
 一般的にはrandom関数でsalt値を取得するようですが、ユーザ毎に値が変わればいいのではないかと思っています。
 例えば次のプログラムを使えば、ユーザ毎にsaltを指定することができ、ユーザAとユーザBが同じパスワードを使っていても保存されるhash値は全く違う値になります。

$hashed_password = hash('sha256', $user_name.$user_password); //hash値の生成
$stmt = $conn->prepare("insert into user_table(username,password) values(?,?)");
$stmt->bind_param("is",$user_name,$hashed_password);
if ($stmt->execute()) {…

この場合は、ユーザ名が変わるたびにパスワードも生成する必要がある点に注意してください。
ユーザIDが変わらないのならユーザIDをsaltとして使用することもできると思います。

ストレッチングを使った理論演算対策

パソコンの性能が格段に向上したことで、ハッシュ値から元の値を計算することも理論上は可能になってきているようです。
 演算によるパスワード漏洩対策としては、ストレッチングという方法があります。
 これは、パスワードのハッシュ値を更にハッシュ化し、その値を更にハッシュ化し…
 というものです。
 一回ハッシュ化しただけで復元には膨大な量の計算が必要になるので、個人的には2回ハッシュ化すれば充分だとは思っているのですが、どこのサイトでも2回で終わらせるようなプログラムは作っていませんでした。
 今時は1000回もハッシュ化する必要があるのですか…
参考にストレッチングを使ったプログラムを載せておきます。

$iterations=1000;//ハッシュ化を繰り返す回数
$hashed_password = hash_pbkdf2("sha256", $password, $user_id, $iterations, 出力する文字列のバイト数);
$stmt = $conn->prepare("insert into user_table(username,password) values(?,?)");
$stmt->bind_param("is",$user_name,$hashed_password);
if ($stmt->execute()) {…

hash_pbkdf2命令は、ストレッチング専用の命令です。
hash_pbkdf2(アルゴリズム名, $password, salt値, 繰り返す回数 , 返り値のバイト数 [,binary 結果をバイナリで返すか 省略時はfalse]);
 当然ですが繰り返す回数が多いほどサーバへの負担が大きくなります。

結局パスワードはどうやって管理するのが正解なのか

 パスワードの漏洩リスクを考えるなら、最低でもパスワードの
   Hash値化+pepper 又は Hash値化+salt
は必要だと思います。(サーバへの負担も少ないですし、可能なら両方するべきだと思います)
 ストレッチングについては、パスワードチェックの頻度とサーバの性能、利用者の範囲や規模によって検討すべきではないでしょうか。
 個人サーバで、ページを更新するたびにユーザ情報の整合性を確認するのならストレッチングまでは不要な気がしますし、企業が外部に公開しているようなシステムにおいて一回のログインでsessionにユーザ情報を登録するようなシステムなら、ログインで多少の負荷をかけてでもストレッチングをするべき場面もあると思います。
 また、AIも進歩しているので、今後思いもよらない対策が必要になる日も来るかもしれません。

あぁ怖い