PHPプログラミング

最終更新日:
公開日:

レシピ

セキュリティ

パスワードの暗号化

パスワードを暗号化するべき理由と、基本的な実装方法をバージョン別に解説します。

この記事のポイント

  • パスワードを暗号化する理由が分かる
  • PHPで暗号化に適した関数を知る
  • PHPのバージョン別に暗号化する方法を解説

目次

パスワードを暗号化する理由

アプリケーションやWebサイトにおいて、会員専用のコンテンツを用意する機会は多いと思います。
その時はFacebookなどのSNSログインも一般的にはなっていますが、今でもユーザー名(またはメールアドレス)とパスワードの組み合わせによる認証は多く使われています。
そのため、その認証システムに必要不可欠な会員の登録・更新・削除・読み込みの実装する機会は少なくありません。

これらの会員情報はいわゆる「個人情報」となり、慎重に扱うべきデータです。
その中でもパスワードは情報にアクセスするためのキーになるため、最も重要なデータと言えます。
なので、システムを構築する上でパスワードを扱う際には、文字列の暗号化をせずにそのままの状態でデータベースに格納するべきではありません。

理由は大きく2つ。
第三者による漏洩のリスクと、管理内部の不正アクセスリスクに備えるためです。

第三者による漏洩のリスク

1つ目の理由は割とポピュラーな理由ですが、悪意ある人がネットワークを介して不正にデータベースへアクセスし、個人情報データをダウンロードするという可能性です。
このときパスワードの文字列が暗号化していれば、会員がそれぞれ設定したパスワードは暗号化されているため、システムに不正アクセスをすることは防ぐことができます。

Note

もっとも、パスワードと同じテーブルに氏名やメールアドレスなどの個人情報が入っている場合は、これらの情報が漏洩してしまうことになります。
ここも含めてセキュリティをより強化したい場合、パスワードと個人情報のテーブルは別に用意するべきです。

管理内部の不正アクセスリスク

2つ目の理由は運営内部に関するリスクです。
多くの場合、データベースを運用するメンバーは複数人であったり、別の会社へ委託して運用することがあり、データベースは複数の人から参照されることが考えられます。

例えば国内では2014年7月、ベネッセの保管していた個人情報が派遣社員によって外部へ大量流出した事件が起こり話題になりました。
このように、データベースの顧客情報が内部の関係者によって不正にダウンロードされ、悪用されてしまう可能性はゼロではありません。

こちらもパスワードの暗号化以前に個人情報の取り扱いに関する問題がありますが、実際に流出してしまった場合にもパスワードが暗号化されていれば、システムへの不正アクセスは防ぐことができます。
これらの流出では最悪、パスワードも流出し、システムの不正利用による顧客への経済的、社会的ダメージが起こることです。

これら2つが暗号化をするべき大きな理由ですが、もう1つ細かい理由があります。

例えばパスワードの長さ。
パスワードの文字列の長さが分かるだけでも攻撃対象となり得ます。
例えば、「password」と8文字だった場合、8桁の文字列の組み合わせを全て計算することでパスワードを取得できることになります。
現在一般的に普及しているPCであれば、この程度の計算は非常に容易です。

以上の理由から、パスワードを保存する際は基本的に暗号化する必要があります。
暗号化の方法は様々ですが、今回はオーソドックスなハッシュ関数を用いた暗号化の実装方法を解説します。

md5関数とsha1関数

文字列のハッシュ値を生成する関数ではmd5関数sha1関数が有名ですが、「phpマニュアル」に記載のある理由から暗号化には適しません。

MD5SHA1そしてSHA256といったハッシュアルゴリズムは、高速かつ効率的なハッシュ処理のために設計されたものです。
最近のテクノロジーやハードウェア性能をもってすれば、これらのアルゴリズムの出力をブルートフォースで(力ずくで)調べて元の入力を得るのはたやすいことです。

php.net 「よく使われるハッシュ関数である md5() や sha1() は、なぜパスワードのハッシュに適していないのですか?」より引用

それでは、phpのバージョンごとに使用できるハッシュ生成関数を用いて、実際にパスワードの暗号化を行って行きます。

php バージョン5.5以降の場合

phpがバージョン5.5以降の場合、password_hash関数を使用することができます。

コード例

// 変数の初期化
$res = null;
$options = array();

// 暗号化したいテキストを設定。今回は仮に「test」とします。
$password_text = 'test';

$options = array(
	'salt' => mcrypt_create_iv(22, MCRYPT_DEV_RANDOM),
	'cost' => 12
);
$res = password_hash( $password_text, PASSWORD_DEFAULT, $options);
var_dump($res);

実行結果

解説

ハッシュ生成に使用するアルゴリズムの指定は、次のいずれかの定数で行います。

  • PASSWORD_DEFAULT – bcryptアルゴリズムを使います。生成するハッシュの長さは60文字。今後、より強力なアルゴリズムが使えるようになったら、このアルゴリズムも変更されます。
  • PASSWORD_BCRYPT – CRYPT_BLOWFISHアルゴリズムを使用します。生成するハッシュの長さは60文字。生成に失敗した場合はfalseを返します。

Note

将来phpのバージョンが更新される際に、標準のアルゴリズムもより強力なものへ変更される可能性があります。
そのため、データベースのパスワード文字列の長さを「CRYPT_BLOWFISH」の生成結果にあわせて60文字にすると、文字列の長さが足りなくなる可能性があるので、255文字など余裕を持った長さに設定することが推奨されています。

オプションの指定

3つ目のパラメータであるオプションは、「ソルト」と「コスト」を指定することができます。
この2つを連想配列形式で渡します。

  • ソルト – パスワードの解読を困難にするために、元のパスワードテキストに結合する文字列を指定します。任意の文字列を指定できますが、php7.0以降では値の指定が非推奨となり、password_hash関数が自動的に生成するソルトを使用することが推奨されています。
  • コスト – 暗号化にかけるコスト(計算回数)を指定します。初期設定は10で、431の間で指定することが可能です。サーバーのハードウェアが許す範囲で、10以上の値を指定することが推奨されています。許容できる範囲で最も大きい値を指定してください。

password_hash関数では自動にソルトを生成してくれるため、特に理由がなければ指定する必要はありません。
もしソルトを任意に設定したい場合は、その都度ランダムに生成した文字列とすることが望ましいです。
今回はmcrypt_create_iv関数でランダムな22文字を作成しています。

以上のパラメータを指定して実行すると、渡したテキストが暗号化され、文字列のハッシュ値が返ってきます。
このハッシュ値をデータベースなどに保存すればOKです。

パスワードの照合

ここまではパスワードの暗号化についてみてきました。
次にパスワードの照合方法を解説します。

password_hash関数の暗号化は一方向のため、一度実行すると通常のテキストへ戻すことはできません。
しかし、登録されたパスワードと入力されたパスワードが一致しているかを調べる必要があります。

そこで、パスワードの一致を照合をするpassword_verify関数を使用します。

コード例

// 変数の初期化
$res = null;
$options = array();

// 暗号化したいテキストを設定。今回は仮に「test」とします。
$password_text = 'test';

$options = array(
	'salt' => mcrypt_create_iv(22, MCRYPT_DEV_RANDOM),
	'cost' => 12
);
$hash = password_hash( $password_text, PASSWORD_DEFAULT, $options);

$true_password = 'test';
$false_password = 'testtest';

// パスワードを照合
var_dump( password_verify( $true_password, $hash));
var_dump( password_verify( $false_password, $hash));

実行結果

解説

最後の2行でpassword_verify関数を実行し、パスワードの照合を行なっています。
1つ目のパラメータには入力された照合したいパスワードを、2つ目のパラメータにはすでにpassword_hash関数で暗号化されたハッシュ値を渡します。

パスワードが一致すればtrue、一致しなければfalseが返ってきます。
この返り値でパスワードが一致していたかを判定することができます。

php バージョン5.4以前の場合

バージョン5.4以前ではpassword_hash関数password_verify関数に対応していません。
その代わりにcrypt関数で、先ほどと同様の暗号化を行うことができます。

コード例

// 変数の初期化
$res = null;
$password_text = 'test';
$salt = 'abcde12345abcde12345zz';
$cost = 12;

$hash = crypt( $password_text, '$2y$' . $cost . '$' . $salt . '$');

// 生成したハッシュを出力
var_dump($hash);

$true_password = 'test';
$false_password = 'testtest';

$conf1 = crypt( $true_password, '$2y$' . $cost . '$' . $salt . '$');
$conf2 = crypt( $false_password, '$2y$' . $cost . '$' . $salt . '$');

// パスワードを照合
var_dump( $hash === $conf1 );
var_dump( $hash === $conf2 );

実行結果

解説

crypt関数で、2つ目に指定しているパラメータについて解説します。

'$2y$' . $cost . '$' . $salt . '$'

$」で区切って見ていく必要があります。
まず1つ目の「2y」は暗号化に使うアルゴリズムの指定です。
今回は「Blowfishハッシュ」を使うので先述の値となります。
他にも「SHA-256ハッシュ」を使う「5」、「SHA-512ハッシュ」を使う「6」などがあります。

「Blowfishハッシュ」を使いたい場合、php バージョン5.3.7以降は「2y」を指定してください。
それ以前のバージョンでは「2y」に対応していないため、「2a」を指定してください。


次の変数「$cost」はコストを指定しています。
値は「12」が格納されているので、それが計算コストとなります。
値は「4」〜「31」の間で指定する必要があります。

最後の「$salt」はソルトです。
22文字の長さで指定してください。

なお、crypt関数password_hash関数password_verify関数と互換性があるため、対応していたらこれらの関数での検証も可能です。
しかし、バージョン5.4以前のバージョンでは対応していません。

そのため、ログインなどで入力されたデータを検証する際は、入力されたパスワードに対して同じようにcrypt関数でハッシュ値を生成し、ハッシュ値同士が等しいかで判定を行います。

php 5.2以前の場合

php 5.3以降は、システム側で「CRYPT_BLOWFISH」をサポートしていなくても、php自身の実装に含まれているため使用することができます。
しかし、それより前のバージョンではシステム側でサポートしている必要があります。

サポートしているかを調べるには、定数CRYPT_BLOWFISHの値が「1」であるかで確認することができます。

コード例

if ( CRYPT_BLOWFISH === 1 ) {
	// 上記のcrypt()を使った処理を記述
} else {
	// 下記のような代わりのハッシュ生成処理を記述
}

サポートしていない場合

もし「CRYPT_BLOWFISH」アルゴリズムをサポートしていない場合は、sha1関数を使った「ソルト&ペッパー」という手法を使う手もあります。
この手法では、上記で使っていた「ソルト」に加え、「ペッパー」という、もう1つのランダムに生成した文字列を用います。

ハッシュ生成の流れは、
1. sha1関数でパスワードからハッシュを生成
2. そのハッシュにソルトとペッパーを結合
3. 再度sha1関数でハッシュを生成、となります。

コード例

// 仮のパスワード「password」
$password = "password";

// まず$passwordからハッシュを生成
$password = sha1($password);

// ソルト&ペッパー
$salt = "abcdef123456abcdef1234";
$pepper = "pepperpepperpepperpepp";

// さきほど生成したハッシュとソルト&ペッパーを結合し、再度ハッシュを作成
$hash = sha1( $salt . $password . $pepper );

ログインなどで入力されたパスワードが正しいか判定を行う場合も、同じ手順でハッシュを生成する必要があります。
そのため、「ソルト」と「ペッパー」の同一の値を使う必要があるため、データベースへ保管しておく必要がある点に注意してください。