π | .gitignore |
π | CHANGELOG |
π | COPYING |
π | README |
π | Rakefile |
π | ext |
π | lib |
π | spec |
README
1 | = bcrypt-ruby |
2 | |
3 | An easy way to keep your users' passwords secure. |
4 | |
5 | == Why you should use bcrypt |
6 | |
7 | If you store user passwords in the clear, then an attacker who steals a copy of your database has a giant list of emails |
8 | and passwords. Some of your users will only have one password -- for their email account, for their banking account, for |
9 | your application. A simple hack could escalate into massive identity theft. |
10 | |
11 | It's your responsibility as a web developer to make your web application secure -- blaming your users for not being |
12 | security experts is not a professional response to risk. |
13 | |
14 | bcrypt allows you to easily harden your application against these kinds of attacks. |
15 | |
16 | == How to install bcrypt |
17 | |
18 | sudo gem install bcrypt-ruby |
19 | |
20 | You'll need a working compiler. (Win32 folks should use Cygwin or um, something else.) |
21 | |
22 | == How to use bcrypt in your Rails application |
23 | |
24 | === The +User+ model |
25 | |
26 | require 'bcrypt' |
27 | |
28 | class User < ActiveRecord::Base |
29 | # users.password_hash in the database is a :string |
30 | include BCrypt |
31 | |
32 | def password |
33 | @password ||= Password.new(password_hash) |
34 | end |
35 | |
36 | def password=(new_password) |
37 | @password = Password.create(new_password) |
38 | self.password_hash = @password |
39 | end |
40 | |
41 | end |
42 | |
43 | === Creating an account |
44 | |
45 | def create |
46 | @user = User.new(params[:user]) |
47 | @user.password = params[:password] |
48 | @user.save! |
49 | end |
50 | |
51 | === Authenticating a user |
52 | |
53 | def login |
54 | @user = User.find_by_email(params[:email]) |
55 | if @user.password == params[:password] |
56 | give_token |
57 | else |
58 | redirect_to home_url |
59 | end |
60 | end |
61 | |
62 | === If a user forgets their password? |
63 | |
64 | # assign them a random one and mail it to them, asking them to change it |
65 | def forgot_password |
66 | @user = User.find_by_email(params[:email]) |
67 | random_password = Array.new(10).map { (65 + rand(58)).chr }.join |
68 | @user.password = random_password |
69 | @user.save! |
70 | Mailer.create_and_deliver_password_change(@user, random_password) |
71 | end |
72 | |
73 | == How to use bcrypt-ruby in general |
74 | |
75 | require 'bcrypt' |
76 | |
77 | my_password = BCrypt::Password.create("my password") #=> "$2a$10$vI8aWBnW3fID.ZQ4/zo1G.q1lRps.9cGLcZEiGDMVr5yUP1KUOYTa" |
78 | |
79 | my_password.version #=> "2a" |
80 | my_password.cost #=> 10 |
81 | my_password == "my password" #=> true |
82 | my_password == "not my password" #=> false |
83 | |
84 | my_password = BCrypt::Password.new("$2a$10$vI8aWBnW3fID.ZQ4/zo1G.q1lRps.9cGLcZEiGDMVr5yUP1KUOYTa") |
85 | my_password == "my password" #=> true |
86 | my_password == "not my password" #=> false |
87 | |
88 | Check the rdocs for more details -- BCrypt, BCrypt::Password. |
89 | |
90 | == How bcrypt() works |
91 | |
92 | bcrypt() is a hashing algorithm designed by Niels Provos and David Mazières of the OpenBSD Project. |
93 | |
94 | === Background |
95 | |
96 | Hash algorithms take a chunk of data (e.g., your user's password) and create a "digital fingerprint," or hash, of it. |
97 | Because this process is not reversible, there's no way to go from the hash back to the password. |
98 | |
99 | In other words: |
100 | |
101 | hash(p) #=> <unique gibberish> |
102 | |
103 | You can store the hash and check it against a hash made of a potentially valid password: |
104 | |
105 | <unique gibberish> =? hash(just_entered_password) |
106 | |
107 | === Rainbow Tables |
108 | |
109 | But even this has weaknesses -- attackers can just run lists of possible passwords through the same algorithm, store the |
110 | results in a big database, and then look up the passwords by their hash: |
111 | |
112 | PrecomputedPassword.find_by_hash(<unique gibberish>).password #=> "secret1" |
113 | |
114 | === Salts |
115 | |
116 | The solution to this is to add a small chunk of random data -- called a salt -- to the password before it's hashed: |
117 | |
118 | hash(salt + p) #=> <really unique gibberish> |
119 | |
120 | The salt is then stored along with the hash in the database, and used to check potentially valid passwords: |
121 | |
122 | <really unique gibberish> =? hash(salt + just_entered_password) |
123 | |
124 | bcrypt-ruby automatically handles the storage and generation of these salts for you. |
125 | |
126 | Adding a salt means that an attacker has to have a gigantic database for each unique salt -- for a salt made of 4 |
127 | letters, that's 456,976 different databases. Pretty much no one has that much storage space, so attackers try a |
128 | different, slower method -- throw a list of potential passwords at each individual password: |
129 | |
130 | hash(salt + "aadvark") =? <really unique gibberish> |
131 | hash(salt + "abacus") =? <really unique gibberish> |
132 | etc. |
133 | |
134 | This is much slower than the big database approach, but most hash algorithms are pretty quick -- and therein lies the |
135 | problem. Hash algorithms aren't usually designed to be slow, they're designed to turn gigabytes of data into secure |
136 | fingerprints as quickly as possible. bcrypt(), though, is designed to be computationally expensive: |
137 | |
138 | Ten thousand iterations: |
139 | user system total real |
140 | md5 0.070000 0.000000 0.070000 ( 0.070415) |
141 | bcrypt 22.230000 0.080000 22.310000 ( 22.493822) |
142 | |
143 | If an attacker was using Ruby to check each password, they could check ~140,000 passwords a second with MD5 but only |
144 | ~450 passwords a second with bcrypt(). |
145 | |
146 | === Cost Factors |
147 | |
148 | In addition, bcrypt() allows you to increase the amount of work required to hash a password as computers get faster. Old |
149 | passwords will still work fine, but new passwords can keep up with the times. |
150 | |
151 | The default cost factor used by bcrypt-ruby is 10, which is fine for session-based authentication. If you are using a |
152 | stateless authentication architecture (e.g., HTTP Basic Auth), you will want to lower the cost factor to reduce your |
153 | server load and keep your request times down. This will lower the security provided you, but there are few alternatives. |
154 | |
155 | == More Information |
156 | |
157 | bcrypt() is currently used as the default password storage hash in OpenBSD, widely regarded as the most secure operating |
158 | system available. |
159 | |
160 | |
161 | For a more technical explanation of the algorithm and its design criteria, please read Niels Provos and David Mazières' |
162 | Usenix99 paper: |
163 | http://www.usenix.org/events/usenix99/provos.html |
164 | |
165 | If you'd like more down-to-earth advice regarding cryptography, I suggest reading <i>Practical Cryptography</i> by Niels |
166 | Ferguson and Bruce Schneier: |
167 | http://www.schneier.com/book-practical.html |
168 | |
169 | = Etc |
170 | |
171 | Author :: Coda Hale <coda.hale@gmail.com> |
172 | Website :: http://blog.codahale.com |
173 |
Built with git-ssb-web