Wednesday, March 30, 2011

Reimplement ASP.NET Membership and User Password Hashing in Ruby

I have a large database of users (~200,000) that I'm transferring from a ASP.NET application to a Ruby on Rails application. I don't really want to ask every user to reset their password and so I'm trying to re-implement the C# password hashing function in Ruby.

The old function is this:

public string EncodePassword(string pass, string saltBase64)
 {
     byte[] bytes = Encoding.Unicode.GetBytes(pass);
     byte[] src = Convert.FromBase64String(saltBase64);
     byte[] dst = new byte[src.Length + bytes.Length];
     Buffer.BlockCopy(src, 0, dst, 0, src.Length);
     Buffer.BlockCopy(bytes, 0, dst, src.Length, bytes.Length);
     HashAlgorithm algorithm = HashAlgorithm.Create("SHA1");
     byte[] inArray = algorithm.ComputeHash(dst);
     return Convert.ToBase64String(inArray);
 }

An example hashed password and salt is (and the password used was "password"):

Hashed password: "weEWx4rhyPtd3kec7usysxf7kpk=" Salt: "1ptFxHq7ALe7yXIQDdzQ9Q==" Password: "password"

Now with the following Ruby code:

require "base64"
require "digest/sha1"


password = "password"
salt = "1ptFxHq7ALe7yXIQDdzQ9Q=="

concat = salt+password

sha1 = Digest::SHA1.digest(concat)

encoded = Base64.encode64(sha1)

puts encoded

I'm not getting the correct password hash (I'm getting "+BsdIOBN/Vh2U7qWG4e+O13h3iQ=" instead of "weEWx4rhyPtd3kec7usysxf7kpk="). Can anyone see what the problem might be?

Many thanks

Arfon

From stackoverflow
  • You need to unencode the salt to convert it back to it's byte representation and then concatenate that with the password to get the hashed password value. You're using the encoding salt string directly (which is a different salt) and thus it is hashing to something different.

    require "base64"
    require "digest/sha1"
    password = "password"
    salt = Base64.decode64("1ptFxHq7ALe7yXIQDdzQ9Q==")
    concat = salt+password
    sha1 = Digest::SHA1.digest(concat)
    encoded = Base64.encode64(sha1)
    puts encoded
    
    Adam Lassek : You're skipping an important step: converting password into bytes.
    arfon : OK, so looks like I'll have to drop to the byte level to do this then?
    tvanfosson : @Adam -- not sure. In .NET it's putting them in a byte array to hand off to the hashing algorithm. Ruby may actually do the same thing using a simple string. If they are stored as null-terminated it is essentially the same as the buffer in the .NET example. I'd give it whirl and see if it works.
    arfon : One potential problem here I guess is that the Ruby Digest library can't hash a byte array, only strings.
  • You are pretty close. Unfortunately Ruby has no built-in unicode support at the moment, and your hashing function relies on it. There are workarounds. Look around the site on how to do unicode in Ruby. BTW, I think you forgot to base64 decode the salt, it looks like the ASP.net function does that.

  • I also tried this: (looking for hash of "weEWx4rhyPtd3kec7usysxf7kpk=")

    require "base64"
    require "digest/sha1"
    salt = "1ptFxHq7ALe7yXIQDdzQ9Q=="
    password = "password"
    
    salt_byte_array = []
    
    salt.each_byte do |b|
      salt_byte_array << b
    end
    
    password_byte_array = []
    
    password.each_byte do |b|
      password_byte_array << b
    end
    
    full = password + salt
    
    hash = Digest::SHA1.digest("#{full}")
    encode = Base64.encode64(hash)
    puts "Digest: #{encode}"
    

    But to no avail...

  • Just a quick update, a colleague of mine has solved this:

    require "base64"
    require "digest"
    require "jcode"
    
    
    def encode_password(password, salt)
     bytes = ""
     password.each_char { |c| bytes += c + "\x00" }
     salty = Base64.decode64(salt)
     concat = salty+bytes
     sha1 = Digest::SHA1.digest(concat)
     encoded = Base64.encode64(sha1).strip()
     puts encoded
    end
    

0 comments:

Post a Comment