水無月ばけらのえび日記

bakera.jp > 水無月ばけらのえび日記 > 徳丸本のあれこれを実践してみて気付いたこと

徳丸本のあれこれを実践してみて気付いたこと

2011年6月16日(木曜日)

徳丸本のあれこれを実践してみて気付いたこと

更新: 2011年7月9日23時0分頃

とあるシステムで徳丸本のストレッチングを採用することにしたという話がありましたが、その実装が佳境に入ってきました。私は指示だけ出して、実装はお任せ……と思っていたのですが、基本的な部分を作ってもらったところでバトンタッチされ、私が引き継ぐ形で実際にコードを書くことになりました。

基本的には徳丸本 (www.amazon.co.jp)のオススメどおりの実装にするという方針なのですが、実際にコードを書いてみると、いろいろと気になったり迷ったりした事も出てきました。そのあたりを簡単にメモしておきます。

※ちなみに、このシステムはRuby1.9.2 + Ruby on Rails3での実装なので、PHPのコードサンプルをそのまま使っているわけではありません。

ストレッチ回数をどう決めるのか

徳丸本327ページにあるコード例を参考にして実装。アプリケーションごとの固有の値とユーザーIDを連結してソルト値とし、パスワードと連結してハッシュ値を生成。ストレッチングは、回数を設定ファイルに記述できるようにして、1000回に設定。ハッシュ値にソルトを連結してハッシュ、という動作を1000回繰り返します。

処理の負荷を心配していたのですがが、実際に動作させてみると一瞬で終了。むしろ回数はもっと増やしても良いのかもしれません。

回数の設定は設定ファイルに出したので、変えるのは簡単です。しかし、この数字を変えると、既存のユーザーがログインできなくなるという問題があります。運用開始前にしっかり検討して決めておく必要があるのですが、実際に運用してみないと負荷が分からないというパラドックスがあります。

この回数をどう決めるのかは、かなり難しい問題かもしれません。

ユーザーIDを変更するとログインできない問題

いろいろテストしていたら、唐突にログインできなくなるという不具合が発覚。調べてみたところ、ユーザーIDを変更するとログインができなくなることが判明。……って、言われてみればあたりまえの話でした。ユーザーIDをソルトに使っているわけですから、ユーザーIDが変化すると、ハッシュ値も異なるものになります。

対応方法はいくつか考えられます。

  • ユーザーIDを変更したときにハッシュ値を作り直す …… ユーザーIDを変更してユーザー情報を保存するときに、ハッシュ値を作り直すという方法。ハッシュ値を作り直すためには元のパスワードが必要なので、ユーザーID変更時に元のパスワードを入力してもらう必要があります。ユーザーが自ら変更する場合には問題ないのですが、管理者がユーザーIDを変更する機能がある場合は採用できません (管理者はユーザーのパスワードを知らないため)。
  • ソルトにユーザーIDを使用するのをやめ、DBレコードのIDを使用する …… ユーザーIDではなく、DBのレコードが持っているIDをソルトに使う方法。これは一見良さそうですが、ユーザーを新規作成するときの処理が面倒です。user = User.newとしてUserモデルのインスタンスを作成し、ハッシュ値をセットしてuser.save……としたいのですが、IDが決まるのはuser.saveが完了したときなのです。そのため、save前にはハッシュを作れません。User.new→user.save→ハッシュ値をセット→user.saveという具合にsaveを2回呼ぶ必要があります。これはこれで動くのですが、何らかの理由で2回目のsaveだけが失敗すると、絶対にログインできないユーザーがDBに残ってしまいます。
  • ソルトにユーザーIDを使用するのをやめ、専用のソルト値をDBに保存する …… 乱数で専用のソルト値を生成して、DBに保存しておく方法。徳丸本326ページにある「乱数をソルトとして使う」方法です。DBのカラムがひとつ増えますが、特に問題はないと思います。

というわけで最後の方法を採用しようかとも思ったのですが、よく考えると、そもそも、ユーザーIDを変更する機能自体が不要でした。単にユーザー情報変更のフォームがscaffold (Railsによって自動生成されたモノ) のままで、不要な機能が残っていただけだったという。

ユーザーIDが変更できなければ問題ないので、これはこれで解決。ユーザーIDを変更する機能を持つ余地がある場合には、ソルト値をDBに保存するようにするのが良いと思います。

アカウントロックの実装

徳丸本315ページでアカウントロックが推奨されているので、これも実装。

最終ログイン失敗時刻を覚えておき、ログイン失敗をカウントして、10回になったら30分ロックするだけの簡単なお仕事……と思いきや、意外と考えなければならないことが多いです。

単にログイン失敗をカウントすると、アカウントロック後に正しいパスワードでログインしようとした場合にも失敗とカウントしてしまい、正しいパスワードを入れ続けているのにログインできないという問題が起きます (パスワード間違いの場合だけをカウントする必要があります)。また、ログインに成功したら、ログイン失敗カウントをクリアする必要があります。これを忘れると次回のロックが異常に素早くなります。まあ、このようなバグはテストすればすぐに発覚するので、大きな問題になることはないでしょう。

アカウントロックを実装すると、DBへのアクセスの仕方も変わってきます。徳丸本308ページには、以下のようにあります。

ログイン機能は、通常、以下のようなSQL文を用いて、IDとパスワードの両方が一致するユーザを検索し、ユーザが存在すればログインできたと見なします。

SELECT * FROM usermaster WHERE id=? AND password=?

ユーザーIDとパスワード (のハッシュ値) の両方をキーにしてユーザーを取得しているわけですが、何も考えずにアカウントロックの処理を追加すると、こうなってしまいます。

  • ユーザーIDとパスワードからハッシュ値を算出
  • ユーザーIDとハッシュ値をキーにしてユーザーを取得
  • ユーザーが取得できなかった場合、ユーザーIDだけをキーにしてあらためてユーザーを取得
  • ユーザーが取得できたらログイン失敗情報を書き込み

この場合、ログインに失敗すると2回のDBアクセスが発生することになります。これは1回にまとめることができます。

  • ユーザーIDだけをキーにしてユーザーを取得 (ユーザーが取得できなかったらログイン失敗、ここで終了)
  • ユーザーIDとパスワードからハッシュ値を算出
  • ハッシュ値が一致しなければログイン失敗、失敗情報を書き込み

ここで気になるのが、ハッシュ値を計算するタイミングです。ストレッチング回数の設定によっては、この計算には時間がかかります。一般的に、ログイン失敗時にユーザーが存在するのかどうかが分かるのは良くないとされています (徳丸本338~339ページ)。最後のような実装を採用した場合、ユーザーが存在した場合だけハッシュ値を計算することになるので、ユーザーの有無で処理にかかる時間に差が出てしまいます。

そこで、処理の順番を入れ替えて、以下のようにしました。

  • ユーザーIDとパスワードからハッシュ値を算出
  • ユーザーIDだけをキーにしてユーザーを取得 (ユーザーが取得できなかったらログイン失敗、ここで終了)
  • ハッシュ値が一致しなければログイン失敗、失敗情報を書き込み

この場合、ユーザーIDが間違っているときにもハッシュ値を計算します。これは無駄ではあるのですが、そうしないとユーザーの存在を推測されてしまう可能性があります。

ちなみに、ユーザーIDが入力されていない場合には、何も処理しないで「ユーザーIDを入力してください」というエラーを出すようにしています。ユーザーIDが空の場合にユーザーが存在しないことは明らかなので、これを隠す必要はありません。

CookieStoreによるセッション管理

ここからは余談になります。

徳丸本46ページ、「クッキーによるセッション管理」の項目には、以下のような記述があります。

クッキーは少量のデータをブラウザ側で覚えておけるものですが、アプリケーションデータを保持する目的でクッキーそのものに値を入れることはあまり行われません。その理由は以下の通りです。

  • クッキーが保持できる値の個数や文字列長には制限がある
  • クッキーの値は利用者本人が参照・変更できるので、秘密情報の格納には向かない

さらに、206~207ページにもCookieに不用意に情報を格納することの危険性について書かれています。

しかしRails2以降では、"CookieStore" というセッション管理方法がデフォルトになっています。これはCookieにセッションデータを丸ごと格納してしまうという方法で、まさに徳丸本で駄目出しされている方法そのものに見えます。……が、実はHMAC (鍵付きハッシュ) がつけられていて、利用者本人による値の改竄ができないようになっています。

ちなみに、データ本体はMarshal.dumpしたものをBase64エンコードしているだけで、特に暗号化はされていません。セッションデータの内容を閲覧することは可能なので、そこは注意が必要です。もっとも、ユーザー本人に閲覧されて困るものをセッションに格納する機会はほとんどないと思いますが。

※「ログイン済み」という情報をセッションに書き込むと確実にCookieの値が変化するため、セッション固定攻撃ができないという変な副次的作用もあったりして、なかなか面白いようです。

※2011-07-09追記: その後、ストレッチ処理を少し変更してみました:「続・徳丸本のあれこれ ストレッチング処理の変更

関連する話題: セキュリティ / プログラミング / Ruby / 徳丸本 / Nightmare

最近の日記

関わった本など