Strengthen password hashing algorithm
authorTom Hughes <tom@compton.nu>
Tue, 13 Aug 2013 22:07:41 +0000 (23:07 +0100)
committerTom Hughes <tom@compton.nu>
Tue, 13 Aug 2013 23:23:03 +0000 (00:23 +0100)
app/models/user.rb
lib/osm.rb
lib/password_hash.rb [new file with mode: 0644]
test/unit/password_hash_test.rb [new file with mode: 0644]

index 6677d3b..4c51089 100644 (file)
@@ -70,7 +70,7 @@ class User < ActiveRecord::Base
         end
       end
 
-      user = nil if user and user.pass_crypt != OSM::encrypt_password(options[:password], user.pass_salt)
+      user = nil if user and not PasswordHash.check(user.pass_crypt, user.pass_salt, options[:password])
     elsif options[:token]
       token = UserToken.find_by_token(options[:token])
       user = token.user if token
@@ -240,8 +240,7 @@ private
 
   def encrypt_password
     if pass_crypt_confirmation
-      self.pass_salt = OSM::make_token(8)
-      self.pass_crypt = OSM::encrypt_password(pass_crypt, pass_salt)
+      self.pass_crypt, self.pass_salt = PasswordHash.create(pass_crypt)
       self.pass_crypt_confirmation = nil
     end
   end
index 4a6237b..2ed98b9 100644 (file)
@@ -5,7 +5,6 @@ module OSM
   require 'rexml/parsers/sax2parser'
   require 'rexml/text'
   require 'xml/libxml'
-  require 'digest/md5'
 
   if defined?(SystemTimer)
     Timer = SystemTimer
@@ -569,12 +568,6 @@ module OSM
     return token
   end
 
-  # Return an encrypted version of a password
-  def self.encrypt_password(password, salt)
-    return Digest::MD5.hexdigest(password) if salt.nil?
-    return Digest::MD5.hexdigest(salt + password)
-  end
-
   # Return an SQL fragment to select a given area of the globe
   def self.sql_for_area(bbox, prefix = nil)
     tilesql = QuadTile.sql_for_area(bbox, prefix)
diff --git a/lib/password_hash.rb b/lib/password_hash.rb
new file mode 100644 (file)
index 0000000..1bd8029
--- /dev/null
@@ -0,0 +1,39 @@
+require "securerandom"
+require "openssl"
+require "base64"
+require "digest/md5"
+
+module PasswordHash
+  SALT_BYTE_SIZE = 32
+  HASH_BYTE_SIZE = 32
+  PBKDF2_ITERATIONS = 1000
+  DIGEST_ALGORITHM = "sha512"
+
+  def self.create(password)
+    salt = SecureRandom.base64(SALT_BYTE_SIZE)
+    hash = self.hash(password, salt, PBKDF2_ITERATIONS, HASH_BYTE_SIZE, DIGEST_ALGORITHM)
+    return hash, [DIGEST_ALGORITHM, PBKDF2_ITERATIONS, salt].join("!")
+  end
+
+  def self.check(hash, salt, candidate)
+    if salt.nil?
+      candidate = Digest::MD5.hexdigest(candidate)
+    elsif salt =~ /!/
+      algorithm, iterations, salt = salt.split("!")
+      size = Base64.strict_decode64(hash).length
+      candidate = self.hash(candidate, salt, iterations.to_i, size, algorithm)
+    else
+      candidate = Digest::MD5.hexdigest(salt + candidate)
+    end
+
+    return hash == candidate
+  end
+
+private
+
+  def self.hash(password, salt, iterations, size, algorithm)
+    digest = OpenSSL::Digest.new(algorithm)
+    pbkdf2 = OpenSSL::PKCS5::pbkdf2_hmac(password, salt, iterations, size, digest)
+    Base64.strict_encode64(pbkdf2)
+  end
+end
diff --git a/test/unit/password_hash_test.rb b/test/unit/password_hash_test.rb
new file mode 100644 (file)
index 0000000..61d3d49
--- /dev/null
@@ -0,0 +1,25 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class PasswordHashTest < ActiveSupport::TestCase
+  def test_md5_without_salt
+    assert_equal true, PasswordHash.check("5f4dcc3b5aa765d61d8327deb882cf99", nil, "password")
+    assert_equal false, PasswordHash.check("5f4dcc3b5aa765d61d8327deb882cf99", nil, "wrong")
+  end
+
+  def test_md5_with_salt
+    assert_equal true, PasswordHash.check("67a1e09bb1f83f5007dc119c14d663aa", "salt", "password")
+    assert_equal false, PasswordHash.check("67a1e09bb1f83f5007dc119c14d663aa", "salt", "wrong")
+    assert_equal false, PasswordHash.check("67a1e09bb1f83f5007dc119c14d663aa", "wrong", "password")
+  end
+
+  def test_default
+    hash1, salt1 = PasswordHash.create("password")
+    hash2, salt2 = PasswordHash.create("password")
+    assert_not_equal hash1, hash2
+    assert_not_equal salt1, salt2
+    assert_equal true, PasswordHash.check(hash1, salt1, "password")
+    assert_equal false, PasswordHash.check(hash1, salt1, "wrong")
+    assert_equal true, PasswordHash.check(hash2, salt2, "password")
+    assert_equal false, PasswordHash.check(hash2, salt2, "wrong")
+  end
+end