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