at posts/single.html

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 に格納するようになってる。 確かに、フォームでのエラー時にパスワードを自動補完しようと思ったら、生のパスワードも必要だよなぁ。

関連する日記