Class: CryptCheckpass::Bcrypt
- Inherits:
-
CryptCheckpass
- Object
- CryptCheckpass
- CryptCheckpass::Bcrypt
- Defined in:
- lib/crypt_checkpass/bcrypt.rb
Overview
BCrypt is a blowfish based password hash function. BSD devs and users tend
to love this function. As of writing this is the only hash function that
OpenBSD's crypt(3) understands. Also, because ActiveModel::SecurePassword
is backended by this algorithm, Ruby on Rails users tends to use it.
Newhash:
In addition to the OpenBSD-ish usage described in README, you can
also use crypto_newhash
to create a new password hash using bcrypt:
crypt_newhash(password, id: 'bcrypt', rounds: 4, ident: '2b')
where:
password
is the raw binary password that you want to digest.id
is "bcrypt" when you want a bcrypt hash.rounds
is an integer ranging 4 to 31 inclusive, which is the number of iterations.ident
is the name of the variant. Variants of bcrypt are described below. Note however that what we don't support old variants that are known to be problematic. This parameter changes the name of the output but not the contents.
The generated password hash has following format.
Format:
A bcrypt hashed string has following structure:
%r{
(?<id> [2] [abxy]? ){0}
(?<cost> [0-9]{2} ){0}
(?<salt> [A-Za-z0-9./]{22} ){0}
(?<csum> [A-Za-z0-9./]{31} ){0}
\A [$] \g<id>
[$] \g<cost>
[$] \g<salt>
\g<csum>
\z
}x
id
is 2-something that denotes the variant of the hash. See below.cost
is a zero-padded decimal integer that specifies number of iterations in logs,salt
andcsum
are the salt and checksum strings. Both are encoded in base64-like strings that do not strictly follow RFC4648. There is no separating$
sign is between them so you have to count the characters to tell which is which. Also, because they are base64, there are "unused" bits at the end of each.
Variants:
According to Wikipedia entry, there are 5 variants of bcrypt output:
Variant
$2$
: This was the initial version. It did not take Unicodes into account. Not currently active.Variant
$2a$
: Unicode problem fixed, but suffered wraparound bug. OpenBSD people decided to abandon this to move to$2b$
. Also suffered CVE-2011-2483. The people behind that CVE requested sysadmins to replace their$2a$
with$2x
, indicating the data is broken. Not currently active.Variant
$2b$
: updated algorithm to fix wraparound bug. Now active.Variant
$2x$
: see above. No new password hash shall generate this one.Variant
$2y$
: updated algorithm to fix CVE-2011-2483. Now active.
Fun facts:
It is by spec that the algorithm ignores password longer than 72 octets.
According to Python Passlib, variant
$2b$
and$2y$
are "identical in all but name."Rails (bcrypt-ruby) reportedly uses
$2a$
even today. However they seem fixed known flaws by themselves, without changing names. So their algorithm is arguably safe. Maybe this can be seen as a synonym of$2b$
/$2y
.
Class Method Summary collapse
-
.checkpass?(pass, hash) ⇒ true, false
Checks if the given password matches the hash.
-
.new_with_openbsd_pref(pass, pref) ⇒ String
This is to implement OpenBSD-style
crypt_newhash()
function. -
.newhash(pass, id: 'bcrypt', rounds: nil, ident: '2b') ⇒ String
Generate a new password hash string.
-
.provide?(id) ⇒ true, false
Checks if the given ID can be handled by this class.
-
.understand?(str) ⇒ true, false
Checks if the given hash string can be handled by this class.
Methods inherited from CryptCheckpass
crypt_checkpass?, crypt_newhash
Class Method Details
.checkpass?(pass, hash) ⇒ true, false
Checks if the given password matches the hash.
143 144 145 146 147 148 149 150 151 |
# File 'lib/crypt_checkpass/bcrypt.rb', line 143 def self.checkpass? pass, hash require 'bcrypt' # bcrypt gem accepts `$2a$` and `$2x` only. We have to tweak. expected = hash.sub %r/\A\$2[by]\$/, "$2a$" obj = BCrypt::Password.new expected actual = BCrypt::Engine.hash_secret pass, obj.salt return consttime_memequal? expected, actual end |
.new_with_openbsd_pref(pass, pref) ⇒ String
This is to implement OpenBSD-style crypt_newhash()
function.
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 |
# File 'lib/crypt_checkpass/bcrypt.rb', line 188 def self.new_with_openbsd_pref pass, pref require 'bcrypt' func, rounds = pref.split ',', 2 unless match? func, /\A(bcrypt|blowfish)\z/ then raise NotImplementedError, <<-"end".strip, func hash algorithm %p not supported right now. end end cost = nil case rounds when NilClass then cost = BCrypt::Engine::DEFAULT_COST when "a" then cost = BCrypt::Engine::DEFAULT_COST when /\A([12][0-9]|3[01]|[4-9])\z/ then cost = rounds.to_i else raise NotImplementedError, <<-"end".strip, rounds cost function %p not supported right now. end end return __generate pass, cost, '2b' end |
.newhash(pass, id: 'bcrypt', rounds: nil, ident: '2b') ⇒ String
There is no way to specify salt. That's a bad idea.
Generate a new password hash string.
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'lib/crypt_checkpass/bcrypt.rb', line 164 def self.newhash pass, id: 'bcrypt', rounds: nil, ident: '2b' require 'bcrypt' len = pass.bytesize raise ArgumentError, <<-"end", len if len > 72 password is %d bytes, which is too long (up to 72) end rounds ||= BCrypt::Engine::DEFAULT_COST case rounds when 4..31 then return __generate pass, rounds, ident else raise ArgumentError, <<-"end", rounds integer %d out of range of (4..31) end end end |
.provide?(id) ⇒ true, false
Checks if the given ID can be handled by this class. A class is free to handle several IDs, like 'argon2i', 'argon2d', ...
154 155 156 |
# File 'lib/crypt_checkpass/bcrypt.rb', line 154 def self.provide? id return id == 'bcrypt' end |
.understand?(str) ⇒ true, false
Checks if the given hash string can be handled by this class.
130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/crypt_checkpass/bcrypt.rb', line 130 def self.understand? str return match? str, %r{ (?<id> [2] [abxy]? ){0} (?<cost> [0-9]{2} ){0} (?<remain> [A-Za-z0-9./]{53} ){0} \A [$] \g<id> [$] \g<cost> [$] \g<remain> \z }x end |