Module: BetterAuth::Password

Defined in:
lib/better_auth/password.rb

Constant Summary collapse

PREFIX =
"bcrypt_sha256$"
BCRYPT_PREFIXES =
["$2a$", "$2b$", "$2x$", "$2y$"].freeze
SCRYPT =
{
  N: 16_384,
  r: 16,
  p: 1,
  length: 64
}.freeze

Class Method Summary collapse

Class Method Details

.bcrypt_hash?(digest) ⇒ Boolean

Returns:

  • (Boolean)


93
94
95
# File 'lib/better_auth/password.rb', line 93

def bcrypt_hash?(digest)
  BCRYPT_PREFIXES.any? { |prefix| digest.start_with?(prefix) }
end

.bcrypt_password_classObject



109
110
111
112
113
114
# File 'lib/better_auth/password.rb', line 109

def bcrypt_password_class
  require "bcrypt"
  BCrypt::Password
rescue LoadError
  nil
end

.call_verifier(verifier, password, digest) ⇒ Object



101
102
103
104
105
106
107
# File 'lib/better_auth/password.rb', line 101

def call_verifier(verifier, password, digest)
  if verifier.arity == 1
    verifier.call(password: password, hash: digest)
  else
    verifier.call(password, digest)
  end
end

.hash(password, hasher: nil, algorithm: :scrypt) ⇒ Object



21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/better_auth/password.rb', line 21

def hash(password, hasher: nil, algorithm: :scrypt)
  return hasher.call(password) if hasher.respond_to?(:call)

  case (hasher || algorithm || :scrypt).to_sym
  when :scrypt
    hash_scrypt(password)
  when :bcrypt
    hash_bcrypt(password)
  else
    raise Error, "Unsupported password hasher: #{hasher || algorithm}. Supported hashers are :scrypt and :bcrypt."
  end
end

.hash_bcrypt(password) ⇒ Object



81
82
83
84
# File 'lib/better_auth/password.rb', line 81

def hash_bcrypt(password)
  klass = require_bcrypt!
  "#{PREFIX}#{klass.create(password_input(password))}"
end

.hash_scrypt(password) ⇒ Object



52
53
54
55
56
# File 'lib/better_auth/password.rb', line 52

def hash_scrypt(password)
  salt = SecureRandom.random_bytes(16).unpack1("H*")
  key = scrypt_key(password, salt)
  "#{salt}:#{key.unpack1("H*")}"
end

.password_input(password) ⇒ Object



48
49
50
# File 'lib/better_auth/password.rb', line 48

def password_input(password)
  Digest::SHA256.hexdigest(password.to_s)
end

.require_bcrypt!Object



116
117
118
# File 'lib/better_auth/password.rb', line 116

def require_bcrypt!
  bcrypt_password_class || raise(Error, "The :bcrypt password hasher requires the optional bcrypt gem. Add `gem \"bcrypt\"` to your Gemfile.")
end

.scrypt_hash?(digest) ⇒ Boolean

Returns:

  • (Boolean)


97
98
99
# File 'lib/better_auth/password.rb', line 97

def scrypt_hash?(digest)
  /\A[0-9a-fA-F]{32}:[0-9a-fA-F]{128}\z/.match?(digest.to_s)
end

.scrypt_key(password, salt) ⇒ Object



70
71
72
73
74
75
76
77
78
79
# File 'lib/better_auth/password.rb', line 70

def scrypt_key(password, salt)
  OpenSSL::KDF.scrypt(
    password.to_s.unicode_normalize(:nfkc),
    salt: salt,
    N: SCRYPT.fetch(:N),
    r: SCRYPT.fetch(:r),
    p: SCRYPT.fetch(:p),
    length: SCRYPT.fetch(:length)
  )
end

.verify(password:, hash:, verifier: nil, algorithm: :scrypt) ⇒ Object



34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/better_auth/password.rb', line 34

def verify(password:, hash:, verifier: nil, algorithm: :scrypt)
  return call_verifier(verifier, password, hash) if verifier.respond_to?(:call)

  digest = hash.to_s
  if digest.start_with?(PREFIX)
    return verify_bcrypt(password_input(password), digest.delete_prefix(PREFIX))
  end

  return verify_bcrypt(password.to_s, digest) if bcrypt_hash?(digest)
  return verify_scrypt(password, digest) if scrypt_hash?(digest)

  false
end

.verify_bcrypt(password, digest) ⇒ Object



86
87
88
89
90
91
# File 'lib/better_auth/password.rb', line 86

def verify_bcrypt(password, digest)
  klass = require_bcrypt!
  klass.new(digest) == password.to_s
rescue BCrypt::Errors::InvalidHash
  false
end

.verify_scrypt(password, digest) ⇒ Object



58
59
60
61
62
63
64
65
66
67
68
# File 'lib/better_auth/password.rb', line 58

def verify_scrypt(password, digest)
  salt, key = digest.to_s.split(":", 2)
  return false unless salt && key

  expected = scrypt_key(password, salt).unpack1("H*")
  return false unless expected.bytesize == key.bytesize

  OpenSSL.fixed_length_secure_compare(expected, key.downcase)
rescue OpenSSL::KDF::KDFError, ArgumentError
  false
end