/
bcrypt.rb
196 lines (178 loc) · 6.45 KB
/
bcrypt.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# A wrapper for OpenBSD's bcrypt/crypt_blowfish password-hashing algorithm.
if RUBY_PLATFORM == "java"
require 'java'
else
require "openssl"
end
if defined?(RUBY_ENGINE) and RUBY_ENGINE == "maglev"
require 'bcrypt_engine'
else
require 'bcrypt_ext'
end
# A Ruby library implementing OpenBSD's bcrypt()/crypt_blowfish algorithm for
# hashing passwords.
module BCrypt
module Errors
class InvalidSalt < StandardError; end # The salt parameter provided to bcrypt() is invalid.
class InvalidHash < StandardError; end # The hash parameter provided to bcrypt() is invalid.
class InvalidCost < StandardError; end # The cost parameter provided to bcrypt() is invalid.
class InvalidSecret < StandardError; end # The secret parameter provided to bcrypt() is invalid.
end
# A Ruby wrapper for the bcrypt() C extension calls and the Java calls.
class Engine
# The default computational expense parameter.
DEFAULT_COST = 10
# The minimum cost supported by the algorithm.
MIN_COST = 4
# Maximum possible size of bcrypt() salts.
MAX_SALT_LENGTH = 16
if RUBY_PLATFORM != "java"
# C-level routines which, if they don't get the right input, will crash the
# hell out of the Ruby process.
private_class_method :__bc_salt
private_class_method :__bc_crypt
end
# Given a secret and a valid salt (see BCrypt::Engine.generate_salt) calculates
# a bcrypt() password hash.
def self.hash_secret(secret, salt, cost = nil)
if valid_secret?(secret)
if valid_salt?(salt)
if cost.nil?
cost = autodetect_cost(salt)
end
if RUBY_PLATFORM == "java"
Java.bcrypt_jruby.BCrypt.hashpw(secret.to_s, salt.to_s)
else
__bc_crypt(secret.to_s, salt)
end
else
raise Errors::InvalidSalt.new("invalid salt")
end
else
raise Errors::InvalidSecret.new("invalid secret")
end
end
# Generates a random salt with a given computational cost.
def self.generate_salt(cost = DEFAULT_COST)
cost = cost.to_i
if cost > 0
if cost < MIN_COST
cost = MIN_COST
end
if RUBY_PLATFORM == "java"
Java.bcrypt_jruby.BCrypt.gensalt(cost)
else
prefix = "$2a$05$CCCCCCCCCCCCCCCCCCCCC.E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW"
__bc_salt(prefix, cost, OpenSSL::Random.random_bytes(MAX_SALT_LENGTH))
end
else
raise Errors::InvalidCost.new("cost must be numeric and > 0")
end
end
# Returns true if +salt+ is a valid bcrypt() salt, false if not.
def self.valid_salt?(salt)
!!(salt =~ /^\$[0-9a-z]{2,}\$[0-9]{2,}\$[A-Za-z0-9\.\/]{22,}$/)
end
# Returns true if +secret+ is a valid bcrypt() secret, false if not.
def self.valid_secret?(secret)
secret.respond_to?(:to_s)
end
# Returns the cost factor which will result in computation times less than +upper_time_limit_in_ms+.
#
# Example:
#
# BCrypt.calibrate(200) #=> 10
# BCrypt.calibrate(1000) #=> 12
#
# # should take less than 200ms
# BCrypt::Password.create("woo", :cost => 10)
#
# # should take less than 1000ms
# BCrypt::Password.create("woo", :cost => 12)
def self.calibrate(upper_time_limit_in_ms)
40.times do |i|
start_time = Time.now
Password.create("testing testing", :cost => i+1)
end_time = Time.now - start_time
return i if end_time * 1_000 > upper_time_limit_in_ms
end
end
# Autodetects the cost from the salt string.
def self.autodetect_cost(salt)
salt[4..5].to_i
end
end
# A password management class which allows you to safely store users' passwords and compare them.
#
# Example usage:
#
# include BCrypt
#
# # hash a user's password
# @password = Password.create("my grand secret")
# @password #=> "$2a$10$GtKs1Kbsig8ULHZzO1h2TetZfhO4Fmlxphp8bVKnUlZCBYYClPohG"
#
# # store it safely
# @user.update_attribute(:password, @password)
#
# # read it back
# @user.reload!
# @db_password = Password.new(@user.password)
#
# # compare it after retrieval
# @db_password == "my grand secret" #=> true
# @db_password == "a paltry guess" #=> false
#
class Password < String
# The hash portion of the stored password hash.
attr_reader :checksum
# The salt of the store password hash (including version and cost).
attr_reader :salt
# The version of the bcrypt() algorithm used to create the hash.
attr_reader :version
# The cost factor used to create the hash.
attr_reader :cost
class << self
# Hashes a secret, returning a BCrypt::Password instance. Takes an optional <tt>:cost</tt> option, which is a
# logarithmic variable which determines how computational expensive the hash is to calculate (a <tt>:cost</tt> of
# 4 is twice as much work as a <tt>:cost</tt> of 3). The higher the <tt>:cost</tt> the harder it becomes for
# attackers to try to guess passwords (even if a copy of your database is stolen), but the slower it is to check
# users' passwords.
#
# Example:
#
# @password = BCrypt::Password.create("my secret", :cost => 13)
def create(secret, options = { :cost => BCrypt::Engine::DEFAULT_COST })
raise ArgumentError if options[:cost] > 31
Password.new(BCrypt::Engine.hash_secret(secret, BCrypt::Engine.generate_salt(options[:cost]), options[:cost]))
end
end
# Initializes a BCrypt::Password instance with the data from a stored hash.
def initialize(raw_hash)
if valid_hash?(raw_hash)
self.replace(raw_hash)
@version, @cost, @salt, @checksum = split_hash(self)
else
raise Errors::InvalidHash.new("invalid hash")
end
end
# Compares a potential secret against the hash. Returns true if the secret is the original secret, false otherwise.
def ==(secret)
super(BCrypt::Engine.hash_secret(secret, @salt))
end
alias_method :is_password?, :==
private
# Returns true if +h+ is a valid hash.
def valid_hash?(h)
h =~ /^\$[0-9a-z]{2}\$[0-9]{2}\$[A-Za-z0-9\.\/]{53}$/
end
# call-seq:
# split_hash(raw_hash) -> version, cost, salt, hash
#
# Splits +h+ into version, cost, salt, and hash and returns them in that order.
def split_hash(h)
_, v, c, mash = h.split('$')
return v, c.to_i, h[0, 29].to_str, mash[-31, 31].to_str
end
end
end