class AspnetPasswordHasher::PasswordHasher

Public Class Methods

new(options = {}) click to toggle source
# File lib/aspnet_password_hasher/password_hasher.rb, line 9
def initialize(options = {})
  @mode = options[:mode] || :v3
  @rng = options[:random_number_generator] || SecureRandom

  case @mode
  when :v2
    @iter_count = 0
  when :v3
    @iter_count = options[:iter_count] || 10000
    if @iter_count < 1
      raise ArgumentError, "Invalid password hasher iteration count"
    end
  else
    raise ArgumentError, "Invalid password hasher compatibility mode"
  end
end

Public Instance Methods

hash_password(password) click to toggle source
# File lib/aspnet_password_hasher/password_hasher.rb, line 26
def hash_password(password)
  bytes = case @mode
          when :v2
            hash_password_v2(password)
          when :v3
            hash_password_v3(password)
          end
  Base64.strict_encode64(bytes)
end
verify_hashed_password(hashed_password, provided_password) click to toggle source
# File lib/aspnet_password_hasher/password_hasher.rb, line 36
def verify_hashed_password(hashed_password, provided_password)
  decoded_hashed_password = Base64.strict_decode64(hashed_password)
  case decoded_hashed_password[0]
  when "\x00"
    # v2
    if verify_hashed_password_v2(decoded_hashed_password, provided_password)
      @mode == :v3 ? :success_rehash_needed : :success
    else
      :failed
    end
  when "\x01"
    # v3
    result, embed_iter_count = verify_hashed_password_v3(decoded_hashed_password, provided_password)
    if result
      embed_iter_count < @iter_count ? :success_rehash_needed : :success
    else
      :failed
    end
  else
    :failed
  end
end

Private Instance Methods

hash_password_v2(password) click to toggle source
# File lib/aspnet_password_hasher/password_hasher.rb, line 61
def hash_password_v2(password)
  iter_count = 1000 # default for Rfc2898DeriveBytes
  subkey_len = 256 / 8 # 256 bits
  salt_size = 128 / 8 # 128 bits

  salt = @rng.bytes(salt_size)
  digest = OpenSSL::Digest::SHA1.new
  subkey = OpenSSL::PKCS5.pbkdf2_hmac(password, salt, iter_count, subkey_len, digest)

  output_bytes = String.new
  output_bytes << "\x00" # format marker
  output_bytes << salt
  output_bytes << subkey
  output_bytes
end
hash_password_v3(password) click to toggle source
# File lib/aspnet_password_hasher/password_hasher.rb, line 77
def hash_password_v3(password)
  prf = 1 # HMACSHA256
  salt_size = 128 / 8
  num_bytes_requested = 256 / 8

  salt = @rng.bytes(salt_size)
  digest = OpenSSL::Digest::SHA256.new
  subkey = OpenSSL::PKCS5.pbkdf2_hmac(password, salt, @iter_count, num_bytes_requested, digest)

  output_bytes = String.new
  output_bytes << "\x01" # format marker
  [prf].pack('N', buffer: output_bytes)
  [@iter_count].pack('N', buffer: output_bytes)
  [salt_size].pack('N', buffer: output_bytes)
  output_bytes << salt
  output_bytes << subkey
  output_bytes
end
verify_hashed_password_v2(hashed_password, password) click to toggle source
# File lib/aspnet_password_hasher/password_hasher.rb, line 96
def verify_hashed_password_v2(hashed_password, password)
  iter_count = 1000 # default for Rfc2898DeriveBytes
  subkey_len = 256 / 8 # 256 bits
  salt_size = 128 / 8 # 128 bits

  if hashed_password.length != 1 + subkey_len + salt_size
    return false # bad size
  end

  salt = hashed_password[1..salt_size]
  expected_subkey = hashed_password[(salt_size + 1)...hashed_password.length]

  digest = OpenSSL::Digest::SHA1.new
  actual_subkey = OpenSSL::PKCS5.pbkdf2_hmac(password, salt, iter_count, subkey_len, digest)
  expected_subkey == actual_subkey
end
verify_hashed_password_v3(hashed_password, password) click to toggle source
# File lib/aspnet_password_hasher/password_hasher.rb, line 113
def verify_hashed_password_v3(hashed_password, password)
  prf = hashed_password[1..4].unpack('N')[0]
  iter_count = hashed_password[5..8].unpack('N')[0]
  salt_len = hashed_password[9..12].unpack('N')[0]
  # salt must be >= 128 bits
  if salt_len < 128 / 8
    return [false, nil]
  end

  salt = hashed_password[13...(13 + salt_len)]
  subkey_len = hashed_password.length - 13 - salt_len
  # subkey must by >= 128 bits
  if subkey_len < 128 / 8
    return [false, nil]
  end

  expected_subkey = hashed_password[(13 + salt_len)...hashed_password.length]

  digest = case prf
           when 0
             OpenSSL::Digest::SHA1.new
           when 1
             OpenSSL::Digest::SHA256.new
           when 2
             OpenSSL::Digest::SHA512.new
           end
  actual_subkey = OpenSSL::PKCS5.pbkdf2_hmac(password, salt, iter_count, subkey_len, digest)

  [expected_subkey == actual_subkey, iter_count]
rescue StandardError
  [false, nil]
end