今回はログイン機能の実装において、レインボーテーブル攻撃を回避するための手段として考えられたソルト付きのパスワードハッシュ化を実装していきたいと思います。
レインボーテーブル攻撃とは
レインボーテーブルは「あるハッシュ値に対して総当たり攻撃を行った際の計算結果を、別のハッシュ値を攻撃する際に使用する」というアイデアに端を発する。例えば、平文 Pi (i = 1, 2, …) と、それらをハッシュ化した値 Ci をテーブルに格納しておき、このテーブルを逆引きすればハッシュ値から対応する平文が得られる。
せっかくパスワードを平文で保存せずにちゃんとハッシュ化してデータベースに保存したとしても、
データベースの情報が流出したときに悪意のある人が作成したレインボーテーブルを使われたら、すぐに平文のパスワードを入手されてしまうよねという話です。
そこで対策方法として出てくるのが『ソルトをパスワードにつけて、それをハッシュ化する』というものです。
とは言っても現在のphpではpassward_hash()とpassword_verify()という関数がサポートされて、特に何もしなくても標準でパスワードにソルトを付けたものをハッシュしてくれるようになりました。
なのでパスワードを管理したいだけならそこらへんの詳しい事情や仕組みについては理解していなくても安全にログイン機能を実装できるのですが、
自分でもちゃんと実装できた方が、php以外の言語でweb開発をするときなんかに役に立つし、実際に実装した方がセキュリティあたりの理解にもなると思ったので、今回はソルト付きハッシュ化を自力で実装していきます。
ソースコード一覧
以下がログイン機能のソースコードになります。
ハッシュ化する部分に焦点を置いてそれ以外は端折っているので、あくまでテスト用のコードになります。
<index.php>
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 |
<?php /* POSTメソッドでクエリが十分な場合に通す */ if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST["userid"], $_POST["password"]) && $_POST["userid"] != "" && $_POST["password"] != "") { try { /* PDOインスタンスを生成 */ $dbh = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'root', 'root', array()); $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch (PDOException $e) { echo $e; exit; } /* ソルトを加えてハッシュ化 */ $salt = substr(base_convert(hash('sha256', uniqid()), 16, 36), 0, 20); $_POST["password"] = hash('sha256', $_POST["password"].$salt); /* DBに挿入 */ $stmt = $dbh->prepare("INSERT INTO salthashlogin (userid, salt ,hash_password) VALUES (?, ?, ?)"); $stmt->bindValue(1, $_POST["userid"]); $stmt->bindValue(2, $salt); $stmt->bindValue(3, $_POST["password"]); try { $stmt->execute(); } catch (PDOException $e) { /* 一意性制約などに引っかかった場合はエラーを表示 */ echo $e; exit; } } ?> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>会員登録</title> </head> <body> <h1>ソルト付きパスワードテスト</h1> <p>ユーザーIDとパスワードを入力してください</p> <form action="/" method="post"> <div> <span>ユーザーID: </span> <input type="text" name="userid"></input> </div> <div> <span>パスワード: </span> <input type="password" name="password"></input> </div> <div> <input type="submit" value="送信"></input> </div> </form> </body> </html> |
<logintest.php>
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 61 62 63 |
<?php if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST["userid"], $_POST["password"]) && $_POST["userid"] != "" && $_POST["password"] != "") { try { $dbh = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'root', 'root', array()); $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch (PDOException $e) { echo $e; exit; } $stmt = $dbh->prepare("SELECT * FROM salthashlogin WHERE userid=?"); $stmt->bindValue(1, $_POST["userid"]); if ($stmt->execute()) { $result = $stmt->fetchAll(); if (count($result) == 1) { /* DBからソルトとハッシュ化されたパスワードを取得 */ $salt = $result[0]["salt"]; $password = $result[0]["hash_password"]; /* 入力されたパスワードが正しいか評価 */ $is_correct = (hash('sha256', $_POST["password"].$salt) === $password) ? TRUE : FALSE; } } } ?> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>登録確認テスト</title> </head> <body> <h1>登録確認テスト</h1> <?php if (isset($is_correct)) { $message = ($is_correct) ? 'ログイン情報は正しいです' : 'ログイン情報が正しくありません'; echo('<div><span>結果: </span>'.$message.'</div>'); } ?> <p>ユーザーIDとパスワードを入力してください</p> <form action="/logintest.php" method="post"> <div> <span>ユーザーID: </span> <input type="text" name="userid"></input> </div> <div> <span>パスワード: </span> <input type="password" name="password"></input> </div> <div> <input type="submit" value="送信"></input> </div> </form> </body> </html> |
データベースはDB名を『test』にして、カラムとしてid(int), userid(varchar), salt(varchar), hash_password(varchar)を指定しておいてください。
1 2 3 4 5 6 7 8 |
CREATE TABLE `salthashlogin` ( `id` int(10) NOT NULL, `userid` varchar(256) NOT NULL, `salt` varchar(256) NOT NULL, `hash_password` varchar(256) NOT NULL ) |
ソースコードの解説
ソースコードを実行すると、以下のような結果になります。
/にアクセスすると、登録画面が出てきます。
ここにユーザー名とパスワードを入力することで新しいアカウントを取得することができます。
入力したユーザー名が既に使用されていた場合には、
1 2 3 4 5 6 7 |
} catch (PDOException $e) { /* 一意性制約などに引っかかった場合はエラーを表示 */ echo $e; exit; } |
に書いてあるように、例外処理のメッセージが表示されます。
ログインの際には、$saltに20文字のランダムな文字列を生成したものを格納して、それをパスワードに加えたのちhash()でハッシュ化しています。
1 2 3 |
hash('sha256', $_POST["password"].$salt); |
実際に登録をしてみると、データベースにレコードが1行追加されているのがわかると思います。
ソルトはユーザーごとに異なるので、これもデータベースにsaltカラムとして保存するようにしています。
/logintest.phpにアクセスすると登録確認テストができます。
これは実際に登録したユーザー名とパスワードがデータベースに存在するかを確認するためのものです。
ここに先ほど登録したユーザーIDとパスワードを入力し、それが正しいと、上の画像のように『ログイン情報は正しい』と表示されます。
一方で入力したログイン情報が正しくないと、『ログイン情報が正しくありません』と表示されることになります。
ログイン情報が正しいかどうかを確認しているコードは以下の部分です。
1 2 3 4 5 6 7 |
/* DBからソルトとハッシュ化されたパスワードを取得 */ $salt = $result[0]["salt"]; $password = $result[0]["hash_password"]; /* 入力されたパスワードが正しいか評価 */ $is_correct = (hash('sha256', $_POST["password"].$salt) === $password) ? TRUE : FALSE; |