パスワードの暗号化についてまとめてみた

最近情報漏えいに関する事件が立て続けに起きてますね。





Webサイトを作るうえでセキュリティは必ず意識すべき事項です。
企業イントラで使う業務Webアプリだとある程度の適当さも許されたりしますが…
インターネットに公開するWebサイト・アプリはいつでも攻撃の危険にさらされています。




中でもとりわけパスワードの平文保存に関するニュースが目立ちます。
今回は、パスワードの平文保存のセキュリティリスクとそれを防ぐための暗号化について調べてみました。

平文保存はなぜ危険か?

パスワードの平文保存は重大なセキュリティリスクです。
平文とは暗号文の対義語でまったく無加工のデータです。
「p@ssword」というパスワードを「p@ssword」のままシステムに保存するとパスワードを平文で管理していることになります。

ユーザーIDパスワード
hoge@example.comp@ssword

これだと、保存されているパスワードとIDを見つけることさえできれば誰でもそのユーザーになりすますことができます。

いくら表のWebページが堅牢に作ってあってもシステムにはサーバーやデータベースに侵入されるリスクがありますし、システムの運用担当者が悪用したり漏えいさせるリスクもあります。

利用者のパスワードはシステムからも、その管理者でもそのまま見れてはいかんのですな。

パスワードの暗号化

パスワードを暗号化することで、人が見てもわからない暗号文にすることができます。

ハッシュ関数

パスワードの暗号化にはハッシュ関数というものがよく使われます。

ハッシュ(hash)には細切れとかごちゃまぜという意味があります。
ハッシュドポテトのハッシュですね。

ハッシュ関数を使うとハッシュアルゴリズムによって、平文から別の固定長のデータを作成することができます。これをハッシュ値といいます。

ハッシュ関数によって作り出したハッシュ値はもとの平文に戻すことが非常に困難でこれを「不可逆変換」と呼びます。

例えば「p@ssword」はsha1というハッシュアルゴリズムを通すと「36e618512a68721f032470bb0891adef3362cfa9」というハッシュ値になります。

先ほどの例でいうとユーザーがパスワードを登録したときにハッシュ関数を通してパスワードを保存することで暗号化を実現します。

PHPだとsha1関数でsha1ハッシュを生成できます。

echo sha1('passw0rd'); // 7c6a61c68ef8b9b6b061b28c348bc1ed7921cb53
ユーザーIDパスワード
hoge@example.com36e618512a68721f032470bb0891adef3362cfa9

ユーザーがログインするときはユーザーが入力したパスワードを、保存したときと同じハッシュアルゴリズムでハッシュ値を取り出し、保存していたハッシュ値と照合してパスワードを検証します。

保存されたパスワードを見ただけではパスワードが何かは判断できませんし、平文のパスワードはシステムのどこにも保存されません。

これで安全なパスワード管理が実現します!

…と、思ったら大間違い。

ソルト

この状態だと何が危険か?

レインボーテーブルという攻撃があります。
難しいので(ちゃんと理解できなかったので)ざっくり説明すると、

予めパスワードとそこから生成されるハッシュ値の対応表を作っておいて、流出したハッシュ値からパスワードを逆引きする攻撃です。

ハッシュパスワード
36e618512a68721f032470bb0891adef3362cfa9p@ssword
7c4a8d09ca3762af61e59520943dc26494f8941b123456
7c6a61c68ef8b9b6b061b28c348bc1ed7921cb53passw0rd

これに対抗するために考えられたのがソルトです。
これは、パスワードをハッシュ値にする前に、プログラムで作ったデータをプラスしてからハッシュ値にする方法です。
暗号化する前にsalt(塩)を振りかけるイメージかな?

ユーザーIDソルトハッシュ化される前ハッシュ化後
hoge@example.comsalt1p@sswordsalt1be7a10703a6708a2d807d5b46d4ae27b5a28b006
fuga@example.comsalt2p@sswordsalt2892668adf902ecf6dc6610a5330bd6ee7dd53ea0

ソルトはユーザーごとにランダムで、長い値の方が効果があります。
そうすることで、異なるユーザーが同じパスワードを登録しても別のハッシュ値が出来上がります。

ユーザーの登録日時をシードに作った乱数とかがいいかもですね。

PHPにはpassword_hashという関数があり、これを使うと呼び出すたびにランダムなソルトを生成してパスワードをハッシュ化してくれます。
// ユーザー1のパスワードを暗号化
$user1_pass = password_hash("p@ssword", PASSWORD_DEFAULT);
echo $user1_pass . PHP_EOL; // $2y$10$sei3twKCjyal.xexCjJywORLVzq8G5pRAefH1E07hcgH09oA3Nmnu

// ユーザー2のパスワードを暗号化
$user2_pass = password_hash("p@ssword", PASSWORD_DEFAULT);
echo $user2_pass . PHP_EOL; // $2y$10$bzwzvgkmUuE26a.nADWgrOpPEfhnYLtRWtkViDxSslrmkK/kYtLtW

//パスワードの照合
echo password_verify('p@ssword', $user1_pass) . PHP_EOL; // true
echo password_verify('p@ssword', $user2_pass) . PHP_EOL; // true

ストレッチング

ソルトだけでも十分ではありません。
ソルトが漏えいすれば平文を推測することが可能だからです。

そこでストレッチング(stretching)を行います。
これはハッシュ関数を通して得たハッシュ値をさらにハッシュ関数に入れるというのをn回繰り返し元の平文を推測するのをさらに困難にしようというものです。

多要素認証(MFA)

暗号化とは別の話ですが、ここまでやっても別の攻撃手段や人的ミスで万一パスワードが漏えいしたときの対策として有効なのが、多要素認証(MFA:Multi-Factor Authentication)です。

これは、パスワード認証とは別に本人しか知りえない情報を組み合わせて認証を行う手法です。

例えば、本人の携帯にSMSでワンタイムパスワードを送るとか、スマホアプリをインストールさせてそこに表示されるワンタイムパスワードを入力させる。
トークンと呼ばれる専用の端末に表示させるパスワードを入力させるといった具合です。

MFAを導入することで、万一のパスワードが漏えいしても、本人の持つ端末を盗まれなければ最悪の自体は回避できます。

大手のWebサービスAmazon、Google、Appleなどはもちろん対応しているので、設定していない方は必ず設定することをおススメします。

Webシステム、特にサーバーサイドに関わるものとして、セキュリティに関する知識は必須です。

私も実装するときはこれってセキュリティ的に問題ないだろうかっていうのを常に意識して実装するように心がけています。

工程の一部に脆弱性試験を設けるところも増えてきているので、不具合を出さなくてもセキュアじゃないシステムをくみ上げてしまうと、大きな手戻りを食らうかもしれません。

今後もセキュアプログラミングについては調べていこうと思います。

でわ。