こんにちは、バックエンドエンジニアの Kaz です。
今回はアプリケーション開発における「ユーザーパスワードの保存」に着目し、いかにして安全にユーザーのパスワードを保存するかを考えていきます。
目次
パスワード保存における安全性の確保
ユーザーパスワードの実情
まずはじめに、そもそもなぜパスワードを安全に保管しておく必要があるのでしょうか。
ユーザーのパスワードが漏れると、攻撃者がユーザーになりすましてログインできるようになるほか、ユーザーは銀行からショッピングサイトまで、さまざまなサービスで パスワードを使いまわしている のです。
たとえば Keeper 社が実施したアンケート調査 では、18 〜 30 歳の 87% がパスワードを再利用している という結果がでています。
これが意味するところは、万が一あなたのサービスからパスワードが漏れてしまうと、その影響はあなたのサービスに留まらず、そのユーザーが利用する あらゆるサービスに攻撃者が不正アクセスできるようになる のです。そのためサービス開発においてパスワードの保管には細心の注意を払う必要があります。
パスワードの暗号化
万が一攻撃者に侵入され情報が漏れても大丈夫なように、データベース状などにパスワードを保管する際はハッシュ関数を用いて「一方向の暗号化」のようなものを施すことがよく行われます。これは、ざっくり言えば
- 暗号化はできるけど元には戻せない特殊な変換
- 元には戻せないけど、同じ値をこの関数に通すと同じ暗号化の結果(ハッシュ値)が得られる
- 照合するときはこの結果の値を比較する
という性質のものです。
このハッシュ関数を使って、元のパスワードに戻すことのできないハッシュ値のみをデータベースに格納しておけば、攻撃者がハッシュ値を盗んでも 元のパスワードを推測しづらくするように守ることができます。
saltによる強化
しかし「推測しづらい」とは言いつつも、これでもまだ破る方法はあるのです。ハッシュ関数は「同じ値をこの関数に通すと同じハッシュ値を得られる」という性質なのです。つまり、ハッシュ値に対してパスワードの総当たり攻撃をかけると元のパスワードを見つけ出すことができてしまう のです。それどころか、ある程度の桁数のパスワードは事前に総当りで計算済みの値が ネット上に公開されている ため、多少手間をかければ元のパスワードはカンタンに解かれてしまいます。
もちろん事前計算や総当たり攻撃は、パスワードが十分な長さを持っていれば算出に膨大な時間を要するため、耐えることは可能です。
しかしながら、すべてのユーザーに 20 文字も 30 文字もあるパスワードを要求することは現実的ではありません。そこでユーザーごとに salt と呼ばれるランダムな文字列を付与します。パスワードとくっつけて暗号化することで、実質的なパスワード強度を長くし事前計算に耐えることができるようになります。この際使ったランダムな文字列はあくまで事前計算の対策のためなので、ハッシュ化したパスワードと一緒にデータベースに保存してしまって構いません。
ストレッチングによる計算負荷の増大化
salt を使えば事前計算には耐えることができますが、ユーザー情報ひとつひとつに対して辞書攻撃や総当たり攻撃を行われた場合は効果を持ちません。そこで、次に取れる対策として「ハッシュ値の計算を複雑にして、ひとつひとつ総当りで計算する時間を増やす」という、もはや嫌がらせのような方法が取られます。この方法は「ストレッチング」といい、総当たり攻撃や辞書攻撃に効果的な対策手法になります。
ストレッチングの具体的な方法は至極シンプルで、「ハッシュ関数を数千回通す」といったものになります。これによりひとつひとつのパスワードごとに数千倍の処理時間がかかるようになるので、その分解読までの時間を遅らせることができるようになります。
bcrypt
これらを踏まえ、対策方法として「ハッシュ化」「 salt の付与」「ストレッチング」を兼ね備えたアルゴリズムがいくつか登場しています。今回はそのひとつ「 bcrypt 」をご紹介します。
bcrypt は「 blowfish 」という暗号アルゴリズムを用いたパスワードハッシュ関数で、出力結果としてハッシュ値と salt とストレッチング回数、ハッシュ関数のバージョンがセットになった値( Modular Crypt Format )を返してくれる関数です。
実際に 吾輩は猫である
という文字列を bcrypt でハッシュ化してみた結果を以下に示します。
$2a$10$4bZqo1S5J5yQk23.n/Vl4OATcXA/e7DnhRaTR7Ico4H5fXPKBcadm
│ │ │ │
│ │ │ └ ハッシュ値 (184bit; Radix-64 31桁)
│ │ └ salt値 (128bit; Radix-64 22桁)
│ └ コスト (ストレッチング回数 = 2^n; この場合2^10 = 1024回)
└ ハッシュ関数のバージョン (2 = bcrypt; a = 改訂版)
bcrypt はパスワードハッシュ関数としてのその強度はもちろん、必要なパラメーターがすべてセットになったひとつの文字列を返してくれます。なので、データベース上にはハッシュ値を保存する文字列型のカラムひとつを用意するだけで対応できるという、大きなメリットを持っています。
加えて上記例で日本語の文字列をハッシュ化したように、bcrypt は UTF-8 形式のあらゆる文字の入力を仕様でサポートしているため、英数字などに文字を制約することなくパスワードをハッシュ化・保存できるという特徴もあります。
PHPでの実装例
PHP では 5.5.0 以降から、bcrypt をサポートするパスワード用のハッシュ関数 password_hash
および検証用関数 password_verify
が追加されました。これらの関数を使うことでカンタンに bcrypt を用いたハッシュ化と検証を行うことができます。
// パスワードのハッシュ化
$hashed = password_hash($password, PASSWORD_BCRYPT);
// パスワードの検証 (ハッシュ値との比較)
$isValid = password_verify($password, $hash);
注意点
PHP を含むほとんどの bcrypt 実装では、アルゴリズム上の制約により入力値(パスワード)が 72 バイトに切り詰められます(マルチバイト文字は UTF-8 扱い)。
このため、ASCII 文字のパスワード(半角英数字・記号)の強度は最大 72 文字となります。
bcryptで安全なユーザーパスワードの保管を
ユーザーのパスワードを保管する上では、bcrypt の採用がオススメです。ぜひ bcrypt を使ってユーザーパスワードを安全に保存しましょう!
LIGはWebサイト制作を支援しています。ご興味のある方は事業ぺージをぜひご覧ください。