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