Class: CryptCheckpass::Bcrypt

Inherits:
CryptCheckpass show all
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 and csum 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.

Examples:

crypt_newhash 'password', id: 'bcrypt'
# => "$2b$10$JlxIYWbT2EUDNvIwrIYcxuKf8pzf58IV4xVWk9yPy5J/ni0LCmz7G"
crypt_checkpass? 'password', '$2b$10$JlxIYWbT2EUDNvIwrIYcxuKf8pzf58IV4xVWk9yPy5J/ni0LCmz7G'
# => true

See Also:

Class Method Summary collapse

Methods inherited from CryptCheckpass

crypt_checkpass?, crypt_newhash

Class Method Details

.checkpass?(pass, hash) ⇒ true, false

Checks if the given password matches the hash.

Parameters:

  • pass (String)

    a password to test.

  • hash (String)

    a good hash digest string.

Returns:

  • (true)

    they are identical.

  • (false)

    they are distinct.

Raises:

  • (NotImplementedError)

    don't know how to parse 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.

Parameters:

  • pass (String)

    bare, unhashed binary password.

  • pref (String)

    algorithm preference specifier.

Returns:

  • (String)

    hashed digest string of password.

Raises:

  • (NotImplementedError)

    pref not understandable.

See Also:



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

Note:

There is no way to specify salt. That's a bad idea.

Generate a new password hash string.

Parameters:

  • pass (String)

    raw binary password string.

  • id (String) (defaults to: 'bcrypt')

    name of the algorithm (ignored)

  • rounds (Integer) (defaults to: nil)

    4 to 31, inclusive.

  • ident (String) (defaults to: '2b')

    "2b" or "2y" or something like that.

Returns:

  • (String)

    hashed digest string of password.

Raises:

  • (ArgumentError)


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', ...

Parameters:

  • id (String)

    hash function ID.

Returns:

  • (true)

    it does.

  • (false)

    it desn't.



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.

Parameters:

  • str (String)

    a good hashed string.

Returns:

  • (true)

    it does.

  • (false)

    it desn't.



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