2012年8月14日火曜日

PBKDF2によるパスワード暗号化の手順

会員制ウェブサイトのように、ユーザからのパスワードを保存するシステムでは、必ずパスワードを暗号化して復元できないようにして保存しなければなりません。なぜなら、もし情報漏洩が起こったときにユーザのパスワードが漏洩してしまったら、他のウェブサイトなどへも侵入され、致命的な結果につながりえるからです。

もしあなたが技術者でなくても、技術者や外注先にたいしてパスワードの暗号化保存を要件に入れるべきです。そのときはこの記事をお見せください。

(もちろん全てのウェブサイトで使うパスワードを変えるのが望ましいでしょうし、もし可能ならパスワードに頼らないハードウェアトークンによる認証などの強力な仕組みを採用するのが良いのでしょうが、現状では皆がそこまでできていないのが現実かと思います。)

これまでですと、パスワード保存にはSHA1やMD5のハッシュ関数の適用などによって暗号化していることが多かったかと思います。しかし、いまどきではSHA1関数などは一瞬で計算が終わってしまいますので、総当たり攻撃をされると弱いという問題があります。

それを多少なりとも改善するのがPBKDF2によるパスワード暗号化手法です。

PBKDF2はパスワードから暗号鍵を導出するための関数ですが、SHAなどのハッシュ関数(HMAC)を繰り返し何千回もかけることにより、総当たり攻撃に時間がかかるようにしています。

PBKDF2はOpenSSL 1.0.0以降から実装されましたので、簡単に利用することができます。以下はRuby 1.8.7でのコードですが、他の言語でもOpenSSLを使えば簡単に利用できるかと思います。[1]

require 'openssl'
key = "hogehogehogehoge" # 秘密の文字列
user.salt = OpenSSL::Random.random_bytes(16).unpack('h*')[0]
user.iteration = 10000
user.hashed_password = OpenSSL::PKCS5.pbkdf2_hmac(password,user.salt+key,user.iteration,16,"sha256").unpack("h*")[0]
user.save

この10000という数字が繰り返し回数ですので、これを増やすことによって安全性を高めることができます。この数をユーザーごとにデータベースに保存しておけば、途中で繰り返し回数を高めて安全性を強化することもできますね。

saltというのは、ユーザーごとにランダムの文字列をパスワードに加えることで、ユーザーごとに暗号化結果が異なるようにし、総当たりをやりにくくするとともに、推測をしにくくします。これはデータベースに保存しておかないと認証できなくなってしまいます。

keyというのは、saltに秘密情報を付加しておくことで、データベースだけが流出した場合に、鍵を総当たりしにくくするための付加鍵です。これは私の思いつきなので、意味があるかどうかよく分かりません。

PBKDF2は、他にもAESなどの暗号鍵をパスワードから導出したりすることにも利用することができます。[2]

弱点としては、こうした計算量に依存するものは、クラウドやハードウェアの力によってかなりのスピードで破ることができてしまうことがあります。[2]

計算速度が命なので、同じような関数をRubyやJavaやPHPなどで独自実装することは避けるべきだと思います。Cで記述されたOpenSSLのような高速な実装を使わないと、すぐに破られてしまうでしょう。

また、PBKDF2関数を単に2回適用することで繰り返し回数を単純に増やすのと同じ結果を得ることはできないようですので、既存のパスワードを定期的に強化していくのは難しいのかなあと思いました。(このへん方法があれば知りたいです)

パスワードに依存しない認証方式がもっと普及すればいいんでしょうが、一向に普及する兆しもないですね。OpenIDといっても、結局は他の同種のサイトに依存してるだけですし。

参考:
  1. パスワードのハッシュに使うべきPBKDF2、Bcrypt、HMACの各言語実装一覧 - Dマイナー志向
  2. PBKDF2 - Wikipedia, the free encyclopedia

1 件のコメント:

  1. salt+key の部分ですが、これはsaltとkeyをconcatするという意味でしょうか。
    saltだけならば完全な乱数であったのに対して、keyを後ろにつなげてしまう事で、全体としてのソルトに秘密とは言え固定長の部分を作り出してしまうので、かえって脆弱になっているのではないかと思いました。
    2つのレコードがあった場合に、それぞれのソルトの一部がkeyという共通部分を持ってしまうことで、本来のアルゴリズムには存在しなかった解読情報を与えてしまっているのではないかと思うのです。

    返信削除