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