アレがアレでアレ

(できれば)プログラミング関係のことを書きたい

パスワードの保存方法について

Webアプリケーションのパスワード保存方法について、どのように保存すれば安全性が高まるのかのメモです。

パスワードをそのまま保存すると?

パスワードを平文のままデータベースに保存し、なんらかの原因により、利用者のパスワードが外部に漏洩した場合、 悪用されて利用者が被害にあうことがあります。

  • 利用者の情報漏洩
  • パスワードリスト攻撃への悪用
  • 漏洩した利用者の権限でアプリの機能を悪用する

従って、万が一パスワードが漏洩しても悪用されないように保護して保存する必要があります。

どうやって保護するか

結論から言うと、パスワードをメッセージダイジェストにしてからデータベースに保存する方法が安全性が高いです。

任意の長さのデータ(ビット列)を固定長のデータに圧縮する関数をハッシュ関数と言います。 このハッシュ関数から生成される固定長の値をメッセージダイジェスト(またはハッシュ値)と言います。

メッセージダイジェストによる保護が安全と言われる理由は、ハッシュ関数が以下の特性を持つためです。

  • ハッシュ値から元データを得ることが困難(一方向性)
  • 異なる文字列から得られるハッシュ値が同じになる確率が非常に低い(衝突耐性)

実際にMD5というハッシュ関数ハッシュ値を出してみるとこんな文字列が出力されます。
このような値にしてからデータベースに保存するわけです。

$ echo -n 'hogehoge' | md5
329435e5e66be809a656af105f42401e

ほんとに安全?

ハッシュ値から元データを得ることが困難」と書きましたが、パスワードの場合は一般的に文字数が短く、文字種も限られるため、総当たりによるオフラインブルートフォース攻撃や、レインボーテーブルといった手法で解析できてしまうようです。 そのため、単純なMD5SHA-1によるハッシュ値でパスワードを保存しても危険です。

ではどうすればいいでしょうか。現状は以下のように対策する必要があります。

  • より強力な暗号学的ハッシュ関数を使う
  • ソルトと呼ばれる文字列を元データに追加することで、見かけのパスワードをある程度長くして、同じパスワードでも異なるハッシュ値が生成されるようにする
  • ハッシュ計算を繰り返し行うストレッチングをして、攻撃にかかる計算時間を長くさせる

これらに対応した、BcryptPBKDF2といったアルゴリズムを使用することで、ハッシュ値の解析を困難にさせ安全性が高まります。

C#でBcryptを使ってみる

Bcryptアルゴリズムを使って、ハッシュ値を計算してみます。
C#ではBCrypt.Net-Nextというライブラリが有名みたいです。

github.com

コードが以下になります。
ハッシュ値計算、パスワードとハッシュ値の検証はどちらも1行で簡単に書けました。

// ハッシュ値を計算
string passwordHash1 = BCrypt.Net.BCrypt.HashPassword("hogehoge");
string passwordHash2 = BCrypt.Net.BCrypt.HashPassword("hogehoge");
Console.WriteLine(passwordHash1);
Console.WriteLine(passwordHash2);

// $2a$11$JMKdIywj9/dHESrcd3581eRx9arbApUVDLPrzkFhlGPmkaPzAVxQ2
// $2a$11$QwNFkmlQRYlegHbdVIfFkOyHP6hSa8TIa7R65Ss94Dzf9DQalqV2i

// パスワードとハッシュ値の検証
bool verified1 = BCrypt.Net.BCrypt.Verify("hogehoge", passwordHash);
bool verified2 = BCrypt.Net.BCrypt.Verify("hogehoge1", passwordHash);
Console.WriteLine(verified1); // true
Console.WriteLine(verified2); // false

HashPasswordにパスワードを渡すと、ハッシュ値が得られます。
ソルトを与えて計算しているので、同じhogehogeでも異なるハッシュ値になることが確認できます。
HashPasswordは独自のソルトを渡して計算できるようですが、自分でソルトを作るのは危険なのでライブラリに任せます。

先頭4文字目からの$11がストレッチング回数を表しています。11回ではなく、2^n乗なので、2^11 2048回です。

// ハッシュ値を計算
string passwordHash1 = BCrypt.Net.BCrypt.HashPassword("hogehoge");
string passwordHash2 = BCrypt.Net.BCrypt.HashPassword("hogehoge");
Console.WriteLine(passwordHash1);
Console.WriteLine(passwordHash2);

// $2a$11$JMKdIywj9/dHESrcd3581eRx9arbApUVDLPrzkFhlGPmkaPzAVxQ2
// $2a$11$QwNFkmlQRYlegHbdVIfFkOyHP6hSa8TIa7R65Ss94Dzf9DQalqV2i

Verifyにパスワードとハッシュ値を渡すと、一致しているか検証します。
hogehoge1はパスワードが異なるので当然falseが返ります。

// パスワードとハッシュ値の検証
bool verified1 = BCrypt.Net.BCrypt.Verify("hogehoge", passwordHash);
bool verified2 = BCrypt.Net.BCrypt.Verify("hogehoge1", passwordHash);
Console.WriteLine(verified1); // true
Console.WriteLine(verified2); // false

まとめ

  • パスワードは平文のまま保存しない
  • メッセージダイジェストにしてから保存する
  • ソルトとストレッチングで対策しているBcryptやPBKDF2を使ってメッセージダイジェストを生成する

参考: 体系的に学ぶ 安全なWebアプリケーションの作り方 第2版 脆弱性が生まれる原理と対策の実践 | 徳丸 浩 |本 | 通販 | Amazon