-
Notifications
You must be signed in to change notification settings - Fork 282
/
password.rb
106 lines (97 loc) · 3.66 KB
/
password.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
module BCrypt
# 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$12$C5.FIvVDS9W4AYZ/Ib37YuWd/7ozp1UaMhU28UKrfSxp2oDchbi3K"
#
# # 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 = options[:cost] || BCrypt::Engine.cost
raise ArgumentError if cost > BCrypt::Engine::MAX_COST
Password.new(BCrypt::Engine.hash_secret(secret, BCrypt::Engine.generate_salt(cost)))
end
def valid_hash?(h)
/\A\$[0-9a-z]{2}\$[0-9]{2}\$[A-Za-z0-9\.\/]{53}\z/ === h
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.
#
# Comparison edge case/gotcha:
#
# secret = "my secret"
# @password = BCrypt::Password.create(secret)
#
# @password == secret # => True
# @password == @password # => False
# @password == @password.to_s # => False
# @password.to_s == @password # => True
# @password.to_s == @password.to_s # => True
#
# secret == @password # => probably False, because the secret is not a BCrypt::Password instance.
def ==(secret)
hash = BCrypt::Engine.hash_secret(secret, @salt)
return false if hash.strip.empty? || strip.empty? || hash.bytesize != bytesize
# Constant time comparison so they can't tell the length.
res = 0
bytesize.times { |i| res |= getbyte(i) ^ hash.getbyte(i) }
res == 0
end
alias_method :is_password?, :==
private
# Returns true if +h+ is a valid hash.
def valid_hash?(h)
self.class.valid_hash?(h)
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.to_str, c.to_i, h[0, 29].to_str, mash[-31, 31].to_str
end
end
end