Webシステムを構築する際、ユーザー用のログインパスワードをデータベースに保存させたい時があるかと思います。
そんな時、「パスワードを暗号化させなければならない」と言うと「暗号化とハッシュ化は違うよ」と指摘されたことはないでしょうか。
また、ハッシュ関数と一口に言ってもMD5、SHA-1、password_hashなどがあり、どれを使えばよいか悩んでいる方も多いと思います。
今回の記事では、ハッシュ関数の特徴や利用目的、現状の安全性についての説明をまとめ、PHPでパスワードをデータベースに保存する際に使用すべきハッシュ関数は何かを探っていきたいと思います。
目次
PHPで使えるハッシュ関数 基礎知識
パスワードをハッシュ化してデータベースに保存する方法を学ぶ前に、ハッシュ関数とは何なのか、基本的な知識をまとめておきたいと思います。
ハッシュ関数とは
ハッシュ関数とは「要約関数」とも呼ばれ、任意のデータを入力すると「ハッシュ値」と呼ばれる「英数字を羅列したようなデータ」を返す関数です。
ハッシュ値を生成するためのアルゴリズムはいくつかあり、それぞれの計算方法に応じて出力される内容が異なってきます。
ハッシュ値を生成する際に用いられる代表的なアルゴリズムは以下の通りです。
- md5
- SHA-1
- SHA-2
例えば「Hello」という文字列をハッシュ関数に入力し「md5」というアルゴリズムを使ってハッシュ化すると「8b1a9953c4611296a827abf8c47804d7」という32文字のハッシュ値が返されます。
一見すると元の文字列からランダムな処理をおこなっているように思えますが、入力値が同じであれば、常に同じハッシュ値が返されるため、何等かの規則に基づいて計算していると推測されます。
決してランダムな処理でハッシュ値を生成しているわけではないということは理解しておくとよいでしょう。
ハッシュ関数の特徴
ハッシュ関数を使うと、入力した値が同じ値であれば常に同じハッシュ値が返されますが、入力した値が少しでも異なると全く別の出力値が返されるという特徴があります。
元のデータが「Hello1」「Hello2」「Hello3」といった連番が付与された文字列であったとしても、それぞれ全く異なるハッシュ値が返されるため、出力された値から規則性を見つけることはできません。
また、どんな長さの値を入力しても必ず同じ長さの値で返されるといった特徴も持っています。
つまり元の入力データをハッシュ値に変換する計算過程で何らかの情報が欠落している、と考えられるため、ハッシュ値から元のデータが何だったのかを逆算して求めることはほぼ不可能です。
こういった「元のデータに戻せない特徴」のことを「不可逆性」といいますので、ハッシュ関数が持つ特徴として覚えておくとよいでしょう。
なお、必ず同じ長さの値で返されるということは、全く別の値を入力したとしても、出力されるハッシュ値が一致してしまう危険がある、ということになります。
出力されたハッシュ値が一致してしまうことを「ハッシュの衝突」と呼んでいるのですが、衝突する可能性があるかどうかが信頼できるハッシュ関数かどうかを判断する基準となりますので、ハッシュ関数が持つリスクとして覚えておくとよいでしょう。
ハッシュの利用目的
ハッシュ関数は情報セキュリティの場面でよく扱われます。
一般的に情報セキュリティが確保されている状態というと、「機密性」「完全性」「可用性」の三要素がバランス良く対応されている状態のことを指すのですが、その中でもハッシュ関数は「入力した値が少しでも異なると全く別のハッシュ値が返される」という特徴から、「そのデータが正しいのか、変更されていないのか」といった「完全性」を確保したい場合に利用されます。
例えば、誰かにメッセージを送信したい時に、メッセージとともにメッセージのハッシュ値を送信する、といった使い方です。
メッセージを受け取った側は、受けとったメッセージのハッシュ値と、送られてきたハッシュ値を照合することで、メッセージが改ざんされていないかどうかを確認することができます。
また、これからやろうとしている「ハッシュ化したパスワードをデータベースに保存する」というのもハッシュ関数の代表的な利用方法です。
「元のデータに戻せない特徴」を活かし、Webシステムにログインするために必要となるパスワードをハッシュ化してデータベースに保存しておけば、仮にデータベースの管理者権限が他人に奪われたとしても英数字の羅列であるハッシュ値ではパスワードが知られてしまう危険は低くなります。
その他には、電子署名におけるデータ送信者の証明やネットバンキングなどで使用されるワンタイムパスワードなど、身近なところでも利用されていますので、情報セキュリティを強化するために、なくてはならない技術と言えるでしょう。
暗号化とハッシュ化
ここで、「暗号化」と「ハッシュ化」の違いについて言及しておきたいと思います。
ハッシュ化については前述したとおり、不可逆性の特徴を持つハッシュ値に変換する技術のため、元のデータにもどす「復号」を前提としていません。
それに対し「暗号化」は「鍵」など、正しい手順を用いれば簡単に元のデータを得ることができるため、「復号」を前提としています。
一見すると両方とも元のデータを他者にわからないようにしているため混同されがちですが、「ハッシュ化」と「暗号化」は似て非なる技術であることは意識して覚えておくと良いでしょう。
PHPで使えるハッシュ関数 様々なハッシュ関数
それでは、PHPで使えるハッシュ関数にはどのようなものがあるのかを見ていきましょう。
ハッシュ関数のセキュリティレベルについては、強衝突耐性(特定のデータのハッシュ値と同じハッシュ値をもつ別のデータを見つけることができるかどうか)と弱衝突耐性(どんなデータからでも同じハッシュ値を持つデータを作ることができるかどうか)が基準となっていますので、それらについても簡単に紹介します。
MD5
MD5は「Message Digest Algorithm 5」の略称で、入力されたデータから128bitのハッシュ値を出力します。
近年では強衝突耐性が突破され、ハッシュが衝突するデータを10分程度の計算で見つけることができてしまうため、セキュリティ対策としてMD5を使用するのは推奨されていません。
ただし弱衝突耐性が突破されたという話は聞きませんので、データが改ざんされていないかどうかの確認などに利用するのが主な利用用途となっています。
SHA-1
SHA-1は「Secure Hash Algorytumシリーズ」のハッシュ関数で、入力されたデータから160bitのハッシュ値を出力します。
こちらも強衝突耐性が突破されていますので、セキュリティ用途でSHA-1を使用するのは推奨されていません。
SHA-2・SHA-256/SHA-512
SHA-2はSHA-1を改良したハッシュ関数で、256bitのハッシュ値を出力するSHA-256や512bitのハッシュ値を出力するSHA-512などハッシュ長の異なる様々なバリエーションが存在します。
今の所有効な攻撃手段が見つかっていないため、安全性が確保されたハッシュ関数といえます。
password_hash()
password_hash()はPHP5.5以降で使うことができるハッシュ関数で、パスワードを扱うのに特化しています。
入力されたデータに「ソルト」と呼ばれるランダムで生成した文字列を付け加えた上でハッシュ化を行うため、ハッシュ化するたびに異なるハッシュ値が出力されます。
出力されるハッシュ値が毎回異なっていても、password_verify()を使用することで元のデータが同一かどうかを判定することが可能となっています。
PHPで使えるハッシュ関数 パスワードのハッシュ化
いくつかあるハッシュ関数を紹介させていただきましたが、パスワードをハッシュ化させる場合、現状においては「password_hash()」を使用するのが最も安全である、ということがお分かりいただけたかと思います。
それでは今回学習した内容を応用し、password_hash()を使ったパスワード登録機能を実装した「ログイン認証機能」を作成してみましょう。
Login.phpの作成
まずはログイン画面を作成します。
テキストエディタで「Login.php」を作成し、以下のコードを記述してWebサーバーにアップしてください。
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 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
<?php $db['host'] = "◯◯◯◯"; // DBサーバのURL $db['user'] = "◯◯◯◯"; // ユーザー名 $db['pass'] = "◯◯◯◯"; // ユーザー名のパスワード $db['dbname'] = "◯◯◯◯"; // データベース名 // エラーメッセージの初期化 $errorMessage = ""; // ログインボタンが押された場合 if (isset($_POST["login"])) { // 1. ユーザIDの入力チェック if (empty($_POST["userid"])) { // emptyは値が空のとき $errorMessage = 'ユーザーIDが未入力です。'; } else if (empty($_POST["password"])) { $errorMessage = 'パスワードが未入力です。'; } if (!empty($_POST["userid"]) && !empty($_POST["password"])) { // 入力したユーザIDを格納 $userid = $_POST["userid"]; // 2. ユーザIDとパスワードが入力されていたら認証する $dsn = sprintf('mysql:host=%s; dbname=%s; charset=utf8', $db['host'], $db['dbname']); // 3. エラー処理 try { $pdo = new PDO($dsn, $db['user'], $db['pass'], array(PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION)); $stmt = $pdo->prepare('SELECT * FROM t_user WHERE t_user_id = ?'); $stmt->execute(array($userid)); $password = $_POST["password"]; if ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { if (password_verify($password, $row['t_user_password'])) { // 入力したIDのユーザー名を取得 $id = $row['t_user_id']; $sql = "SELECT * FROM t_user WHERE t_user_id = $id"; //入力したIDからユーザー名を取得 $stmt = $pdo->query($sql); foreach ($stmt as $row) { $row['t_user_name']; } header("Location: Main.php"); exit(); } else { $errorMessage = 'ユーザーIDあるいはパスワードに誤りがあります。'; } } else { $errorMessage = 'ユーザーIDあるいはパスワードに誤りがあります。'; } } catch (PDOException $e) { $errorMessage = 'データベースエラー'; } } } ?> <!doctype html> <html> <head> <meta charset="UTF-8"> <title>ログイン</title> </head> <body> <h1>ログイン画面</h1> <form id="loginForm" name="loginForm" action="" method="POST"> <fieldset> <legend>ログインフォーム</legend> <div><font color="#ff0000"><?php echo htmlspecialchars($errorMessage, ENT_QUOTES); ?></font></div> <label for="userid">ユーザーID</label><input type="text" id="userid" name="userid" placeholder="ユーザーIDを入力" value="<?php if (!empty($_POST["userid"])) {echo htmlspecialchars($_POST["userid"], ENT_QUOTES);} ?>"> <br> <label for="password">パスワード</label><input type="password" id="password" name="password" value="" placeholder="パスワードを入力"> <br> <input type="submit" id="login" name="login" value="ログイン"> </fieldset> </form> <br> <form action="SignUp.php"> <fieldset> <legend>ユーザー登録</legend> <input type="submit" value="新規登録"> </fieldset> </form> </body> </html> |
Main.phpの作成
続いてメイン画面を作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<?php ?> <!doctype html> <html> <head> <meta charset="UTF-8"> <title>メイン</title> </head> <body> <h1>メイン画面</h1> <p>ようこそ<u><?php echo htmlspecialchars($_SESSION["NAME"], ENT_QUOTES); ?></u>さん</p> <ul> <li><a href="Logout.php">ログアウト</a></li> </ul> </body> </html> |
SignUp.phpの作成
続いて新規ユーザーを登録する画面を作成します。
同様に「SignUp.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 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
<?php $db['host'] = "◯◯◯◯"; // DBサーバのURL $db['user'] = "◯◯◯◯"; // ユーザー名 $db['pass'] = "◯◯◯◯"; // ユーザー名のパスワード $db['dbname'] = "◯◯◯◯"; // データベース名 // エラーメッセージ、登録完了メッセージの初期化 $errorMessage = ""; $signUpMessage = ""; // ログインボタンが押された場合 if (isset($_POST["signUp"])) { // 1. ユーザIDの入力チェック if (empty($_POST["username"])) { // 値が空のとき $errorMessage = 'ユーザーIDが未入力です。'; } else if (empty($_POST["password"])) { $errorMessage = 'パスワードが未入力です。'; } else if (empty($_POST["password2"])) { $errorMessage = 'パスワードが未入力です。'; } if (!empty($_POST["username"]) && !empty($_POST["password"]) && !empty($_POST["password2"]) && $_POST["password"] === $_POST["password2"]) { // 入力したユーザIDとパスワードを格納 $username = $_POST["username"]; $password = $_POST["password"]; // 2. ユーザIDとパスワードが入力されていたら認証する $dsn = sprintf('mysql:host=%s; dbname=%s; charset=utf8', $db['host'], $db['dbname']); // 3. エラー処理 try { $pdo = new PDO($dsn, $db['user'], $db['pass'], array(PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION)); $stmt = $pdo->prepare("INSERT INTO t_user(t_user_name, t_user_password) VALUES (?, ?)"); $stmt->execute(array($username, password_hash($password, PASSWORD_DEFAULT))); $userid = $pdo->lastinsertid(); // 登録した(DB側でauto_incrementした)IDを$useridに入れる $signUpMessage = '登録が完了しました。あなたの登録IDは '. $userid. ' です。パスワードは '. $password. ' です。'; // ログイン時に使用するIDとパスワード } catch (PDOException $e) { $errorMessage = 'データベースエラー'; // $e->getMessage() でエラー内容を参照可能(デバッグ時のみ表示) // echo $e->getMessage(); } } else if($_POST["password"] != $_POST["password2"]) { $errorMessage = 'パスワードに誤りがあります。'; } } ?> <!doctype html> <html> <head> <meta charset="UTF-8"> <title>新規登録</title> </head> <body> <h1>新規登録画面</h1> <form id="loginForm" name="loginForm" action="" method="POST"> <fieldset> <legend>新規登録フォーム</legend> <div><font color="#ff0000"><?php echo htmlspecialchars($errorMessage, ENT_QUOTES); ?></font></div> <div><font color="#0000ff"><?php echo htmlspecialchars($signUpMessage, ENT_QUOTES); ?></font></div> <label for="username">ユーザー名</label><input type="text" id="username" name="username" placeholder="ユーザー名を入力" value="<?php if (!empty($_POST["username"])) {echo htmlspecialchars($_POST["username"], ENT_QUOTES);} ?>"> <br> <label for="password">パスワード</label><input type="password" id="password" name="password" value="" placeholder="パスワードを入力"> <br> <label for="password2">パスワード(確認用)</label><input type="password" id="password2" name="password2" value="" placeholder="再度パスワードを入力"> <br> <input type="submit" id="signUp" name="signUp" value="新規登録"> </fieldset> </form> <br> <form action="Login.php"> <input type="submit" value="戻る"> </form> </body> </html> |
ログイン認証
ソースコードを見ると、SignUp.phpでpassword_hash()によるハッシュ化されたパスワードがデータベースに登録される処理となっています。
そして、Login.phpのpassword_verify()で入力したパスワードと、データベースに登録されているパスワードを照合する処理を行うことで、ログイン認証の機能が実装されていることがお分かりいただけるかと思います。
実際に「SignUp.php」にアクセスしてユーザーを登録し、どのようなパスワードがデータベースに保存されているかを確認してみていただければと思います。
phpMyAdminで使用できるハッシュ関数
余談ですが、phpMyAdminでデータを1件ずつ登録する場合、password_hash関数は使えません。
password_hash関数を使う場合、PHPでパスワードをハッシュ化した後、データベースに登録する必要があります。
まとめ
いかがでしたか。
PHPでパスワードをハッシュ化するのに適したハッシュ関数は見つかりましたでしょうか。
PHPの場合、今は「password_hash」を使うのが最も便利ですので「これ以外を使ってはいけない」と豪語する方も見受けられますが、「password_hash」もいつかは破られてしまうかもしれない、と考えると、随時、最新情報を注視しておく必要があります。
また、「ハッシュ化」という用語については「暗号化」と明確に区別しておくことが重要ですが、厳密な違いについては別の機会に紹介したいと思っています。
今回は「パスワードのハッシュ化」についての説明となりますので、今後、パスワードをハッシュ化したい、といった場合には、こちらの記事を思い出していただければ幸いです。