パスワードハッシュあれこれ

Webアプリなどでユーザーのパスワードを保存する際に、ハッシュ化
して元のパスワードをわかりにくくする、というのは周知のセキュリティ処理であると思いますが、ハッシュ化
て具体的にどういう処理をしているんだ、とかそれやっとけば本当に安全?など自分なりに気になっていたことを調べて簡潔にまとめてみました。
ハッシュ化とは
ハッシュ化とは、元のデータから一定の計算手順に従ってハッシュ値と呼ばれる規則性のない固定長の値を求め、その値によって元のデータを置き換えること。
英語で細かく刻む、ごちゃ混ぜにするという意味のHashですね。ハッシュドポテトのハッシュです。原型を留めなくする、というとややバイオレンスでしょうか。でも実際にハッシュ化された文字列データから元のデータを目測で探ることはほぼ不可能になります。
ハッシュ関数
プログラム上でハッシュ化を行うにはハッシュ関数
と呼ばれる関数に元データを渡して実行する形をとります。ハッシュ関数
とは簡単に言うとデータ置換処理手順ハッシュアルゴリズム
に沿ってハッシュ化を行う関数で、異なるアルゴリズムの規格をもったものが様々あります。
メジャーなハッシュアルゴリズム
だとSHA1
、SHA256
、MD5
といったものが挙げられます。
例としてPHPでハッシュ化を行うハッシュ関数 md5()
を実行してみます。
echo md5('hello') . "\n";
echo md5('hello') . "\n";
echo md5('bello') . "\n";
echo md5('goodmorning');
出力内容
5d41402abc4b2a76b9719d911017c592
5d41402abc4b2a76b9719d911017c592
5a4d5215fa1fb5435e5322dbeb60dd3c
55b9e608315d8a9195c174c96c100c23
各行がそれぞれハッシュ関数に渡された引数の文字列がハッシュ化された結果となります。特徴として、
- 元データが同じ値であれば、そのハッシュ値も常に同じ
- 元データが一文字違うだけで、ハッシュ値は大きく変化する
- 元データの長さに関わらず、同じハッシュ関数で生成されたハッシュ値は常に同じ長さになる
などが挙げられます。これらはMD5
に限らずハッシュアルゴリズム全体の特徴と言えます。
ハッシュ化 vs 暗号化
よく知らないうちはハッシュ化
と暗号化
を混同しがちになってしまっていましたが、ハッシュ化の性質には上記の特徴に加えて「不可逆変換
」という暗号化のそれとは異なるものがあります。要するにハッシュ値から元データを復元することはできない
ということです。
ハッシュ化
ハッシュアルゴリズム(md5
, sha1
, etc…)に従ってデータをハッシュ値に置換する。置換されたデータを元に戻すことはできない一方通行の処理。
暗号化
暗号化アルゴリズム(AES
, DES
, etc…)に従ってデータを別のバイナリデータに組み替える。正しい「鍵データ」を使用することで元データに戻す(複合
する)ことができる。
両者についてざっくり説明するとこんな感じです。したがって、
- パスワードの保管: 保存するだけなので複合する必要がない & 「鍵」があれば複合できてしまうというのはマズい →
ハッシュ化
- 機密情報を保護する: 後で複合できるようにしておきたい →
暗号化
といった具合に用途もはっきりと分けられるようです。
安全性
で、結局ハッシュ化さえすればパスワードは安全なの?と言うと、PHPドキュメントのパスワードハッシュの項などにも書かれているのですが、上記で挙げたSHA1
やMD5
などは、弱点などが既に露呈していることや、コンピューターによる「逆算」処理速度が向上したことによって、現在これらのハッシュ関数は素での使用は完全に非推奨
となっています。
さほど複雑な置換処理を行なっていないことや、同じ元データは決まって同じハッシュ値に置き換わるという仕様上、総当たり攻撃
や辞書攻撃
を用いられてパスワード を強引に割り出されることが可能となってしまっています。
実際にサービス上でパスワードのハッシュ化を行う際は、プログラム言語で用意されているネイティブのハッシュ化機能や米国国立標準技術研究所(NIST)が認可しているハッシュアルゴリズムを使用します。
加えて次に述べるパスワードの保護力を強化する追加処理も用います。
安全性を高める手法
ここではソルト
とストレッチング
について取り上げます。
ソルトとは?
元データをより推測されにくくするために、ハッシュ処理時に追加データを付与します。この追加されたデータのことをソルト
と言います。
ストレッチングとは?
ハッシュ化処理を重ね掛けすることです。繰り返した分だけ解析にかかる時間を延ばすことができるという寸法です。基本的に企業やプロジェクトで回数の条件を定め、数百回から数万回単位のストレッチング処理を施すことが多い模様です。回数が多いほど安全性も増しますが、その分サーバー負荷や処理速度への影響も大きくなることを注意する必要もあります。
PHPでの例
PHPにはバージョン5.5以上で使えるpassword_hash()
というまさにパスワードのハッシュ化のための関数が用意されています。この関数にはソルト
を自動生成する機能があります(手動で設定することもできますが、ランダム生成と比べると安全性に劣るため推奨されていません)。
echo password_hash('goodevening', PASSWORD_DEFAULT);
元データはそのままに3回ほど実行してみます。
$2y$10$gm4G0IEbeiwd.COE83vz4.h5iI.cJSjiZknPCqtgnzYiin0U0liKm
$2y$10$5CCR2RdfFPdmavkFsmsSYu3W8e1c6LM3972PlFf5rz22.joUPeBOS
$2y$10$KpkIf3dFI.JgxQkDCEreUuNPjFK0d1HF4h2PL/LpkOnmo5wsFwhrq
元データは一緒なのに実行するたびに異なるハッシュ値が生成されていますね。
これはpassword_hash()
が毎回ランダムで生成したソルト
文字列を付け加えた上でハッシュ化を行なっているからです。こうすることで単純なハッシュ化よりもはるかに攻撃難易度を高められます。
ちなみに共通している先頭の$2y$10$
の文字列ですが、これは二つの要素を表しています。
$2y$
: 使用したハッシュアルゴリズムを意味する文字列。2yはBlowfishというアルゴリズムを表します。$10$
: 処理に要したコスト。計算の反復数。
また、検証のやり方ですがpassword_hash()
で生成したハッシュ値の検証をするにはpassword_verify()
を使用します。上の3つのケースでやってみます。
var_dump(
password_verify('goodevening', '$2y$10$gm4G0IEbeiwd.COE83vz4.h5iI.cJSjiZknPCqtgnzYiin0U0liKm')
);
var_dump(
password_verify('goodevening', '$2y$10$5CCR2RdfFPdmavkFsmsSYu3W8e1c6LM3972PlFf5rz22.joUPeBOS')
);
var_dump(
password_verify('hello', '$2y$10$KpkIf3dFI.JgxQkDCEreUuNPjFK0d1HF4h2PL/LpkOnmo5wsFwhrq')
);
実行結果
bool(true)
bool(true)
bool(false)
異なるハッシュ値でも検証はきっちりされます!
まとめ
昨今サービスや企業の大小に関わらず、パスワード流出のニュースが絶えない気がする中、「パスワードの保護にどのような方法が取られているか」、「防御措置を施しているのにパスワードが割り出されてしまう原因は」などを理解しやすくなれたらいいなと思い、今回改めて調べた次第ですが、今まで様々な経緯でどのような攻撃方法が使われ、それに対しどのような対策が講じられたかの移り変わりを見ていくのは中々興味深いものがありました。
とりあえず改めてよく分かったことは、当たり前だけど同じパスワードの使い回しだけは絶対にやらない方がいいってことかな〜(汗)