ActiveRecord で Salted SHA1 を使ってパスワードを保存
ActiveRecordでパスワード(Password)を上手に保存するより。
やはりパスワードをDBに生でいれるのはどうかと思うので、
before_save {|user| user.password = Digest::SHA256.digest(user.password)}こんな感じに書いてやりましたところ、新規登録時は問題ないのですが、パスワード以外のデータを更新したときにもパスワードが勝手に変わるようになってしまいました。
「パスワード(確認用)」のフィールドを新たに設けて以下のようにすることで解決。
before_save {|user| user.password = Digest::SHA256.digest(user.password) unless user.password_confirmation.blank?} validates_confirmation_of :password
もうちょっとキレイに書けるはず…と思って ActiveRecord::Base のマニュアルを読んだら、 Overwriting default accessors というのがあった。 なので、 User モデルにこう書けば OK のはず。
def password=(password) self[:password] = Digest::SHA256.hexdigest(password) end
こうすれば password 属性に値を代入するときにハッシュ化してくれるので確実。
ハッシュに salt を付ける
ハッシュ化するだけだと、たまたま2人が同じパスワードを登録した場合に、ハッシュ後の値も同じになる。
>> user1 = User.new(:password => 'kagamine') >> user2 = User.new(:password => 'kagamine') >> user1.password => "e4450a612f7223eae6db2f8bbb4d9f9e13099059c8e061c922b632b6061c8e05" >> user2.password => "e4450a612f7223eae6db2f8bbb4d9f9e13099059c8e061c922b632b6061c8e05"
MD5破りにGoogleを活用って話もあるし、これじゃちょっとなので、 パスワードの保存に SMD5 (Salted MD5) や SSHA1を使うで書いたように salt をつけて保存するようにしてみた。(users テーブルに salt という列が必要) salt は8バイトのデータをランダムに生成するようにしている。
require 'digest/sha2' class User < ActiveRecord::Base def salt self[:salt] ||= OpenSSL::Random.random_bytes(8).unpack('H*').first end def password=(password) self[:password] = Digest::SHA256.hexdigest(password + salt) end end
ユーザごとに salt が作られるので、2人が同じパスワードを使っていても、 DB には違う値が格納される。
>> user1 = User.new(:password => 'kagamine') >> user2 = User.new(:password => 'kagamine') >> user1.salt => "2403f75bfb83d74c" >> user1.password => "5f4d5a848dfa49f321d1318a1ac5b8404a8da7045e1365bf4376a0f503228c56" >> user2.salt => "5ee895b472a073a1" >> user2.password => "97cea15a4a8ca748ef5b00110fe41a91fe418518269bf5e74c747b0ac09f55f2"
パスワードの比較のために password_match? メソッドを作った。 同じ salt と password を持つ User オブジェクトを作って比較するという、ちょっと手抜きな処理。
def password_match?(password) self.password == User.new(:salt => salt, :password => password).password end
これで OK。
>> user1.password_match?('kagamine') => true >> user1.password_match?('miku') => false
補足
- 思いつきで作ったので、間違ってたらごめんなさい
- これを使うよりも login_generator を参考にしたほうがいいかも?
- ちゃんと確かめてないけど、あっちは salt が全ユーザで共通だったような…
それから、本当に使うときは使用したハッシュアルゴリズム (MD5 とか SHA1 とか) も User オブジェクトに格納していた方がいいと思う。
User.new(:password => 'kagamine', :hash_alg => 'MD5') User.new(:password => 'kagamine', :hash_alg => 'SHA1')
みたいにね。 5年も使えば、他のアルゴリズムに切り替える必要もでてくるだろうし、その時にユーザごとにアルゴリズムを切り替えられないと、データの移行ができなくなるから。 データの移行ってのは MD5 のハッシュ値を SHA1 のハッシュ値に変換するって意味じゃない(それは無理)。 MD5 でハッシュ化するユーザと SHA1 でハッシュ化するユーザが同じシステムで共存できるって意味ね。次にパスワードを変えたときに、新しいハッシュアルゴリズムを使うようにして、少しずつ移行していく。
疑問
Rails でのユーザ管理のスタンダードってあるのかな? やっぱり login_generator を使っているんだろうか。
追記
Twitter で acts_as_authenticated があるよって教えてもらった。 yugui さんのRails勉強会@東京 第22回に詳しく書かれている。 password と crypted_password の2つの属性を持つようにしておいて、 password と salt から生成した crypted_password だけを DB に格納するようになってる。 確かに、フォームでのエラー時にパスワードを自動補完しようと思ったら、生のパスワードも必要だよなぁ。