git ssb

0+

dangerousbeans / %aPBe2k3ugtjBr4rrsU1…



Tree: 007857af56030fbe4c85f43162679572662a396d

Files: 007857af56030fbe4c85f43162679572662a396d / lib / bcrypt.rb

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

Built with git-ssb-web