Project

General

Profile

Download (2.93 KB) Statistics
| Branch: | Tag: | Revision:
require 'active_support/core_ext/module/delegation'

#
# Generic-purpose password hasher with two implementations: BCrypt and SHA1.
#
# generate_salt(cost) - generates random salt of optional cost (1-30)
# calculate_salt(object, cost) - calculates hash from given object (useful for tokens)
# hash_secret(password, salt) - returns hash from secret and salt
#
module Foreman
class PasswordHash
class PBKDF2Implementation
DEFAULT_SALT_LENGTH = 39

def generate_salt(length = DEFAULT_SALT_LENGTH, cost = Setting[:pbkdf2_cost])
"$pbkdf2sha1$#{cost}$#{SecureRandom.base64(length)}"
end

def calculate_salt(object, cost = Setting[:pbkdf2_cost])
"$pbkdf2sha1$#{cost}$#{Digest::SHA1.hexdigest(object.to_s)}"
end

def hash_secret(password, salt)
raise(Foreman::Exception.new(N_("Salt not in format of $pbkdf2sha1$#{Setting[:pbkdf2_cost]}$SALT: %s"), salt)) unless salt.start_with?('$pbkdf2sha1')
_p1, _p2, iters, clean_salt = salt.split('$', 4)
hash = OpenSSL::PKCS5.pbkdf2_hmac_sha1(password, clean_salt, iters.to_i, DEFAULT_SALT_LENGTH)
"#{salt}$#{Base64.strict_encode64(hash)}"
end
end

class BCryptImplementation
def generate_salt(cost = Setting[:bcrypt_cost])
BCrypt::Engine.generate_salt(cost)
end

def calculate_salt(object, cost = Setting[:bcrypt_cost])
"$2a$#{cost.to_s.rjust(2, '0')}$#{Digest::SHA1.hexdigest(object.to_s)}"
end

def hash_secret(password, salt)
BCrypt::Engine.hash_secret(password, salt)
rescue BCrypt::Errors::InvalidSalt
# bcrypt expects '$2a$nn$minimum22characters'
raise(Foreman::Exception.new(N_("BCrypt salt '%s' is invalid"), salt))
end
end

class SHA1Implementation
def generate_salt(_cost = nil)
Digest::SHA1.hexdigest([Time.now.utc, rand].join)
end

def calculate_salt(object, _cost = nil)
Digest::SHA1.hexdigest(object.to_s)
end

def hash_secret(password, salt)
Digest::SHA1.hexdigest([password, salt].join)
end
end

def initialize(implementation = Setting[:password_hash])
case implementation&.to_sym
when :pbkdf2sha1
@implementation = PBKDF2Implementation.new
when :bcrypt
@implementation = BCryptImplementation.new
when :sha1
@implementation = SHA1Implementation.new
when nil
raise(Foreman::Exception.new(N_("Password hash setting was not yet set")))
else
raise(Foreman::Exception.new(N_("Unknown password hash method: %s"), implementation))
end
end

def self.detect_implementation(password_string)
if password_string.start_with?('$pbkdf2sha1')
:pbkdf2sha1
elsif password_string.start_with?('$2')
:bcrypt
else
:sha1
end
end

delegate :generate_salt, :calculate_salt, :hash_secret, to: :implementation

private

attr_reader :implementation
end
end
(14-14/20)