6

Last week I upgrade Fedora to the brand new 28 release, which came with a mongodb upgrade to 3.6. See How to repair mongodb service after an upgrade to Fedora 28? for how I managed to resolve my first problem which was that mongod would no longer start. Now I'm facing an other problem on the Rails application that use this same database.

This most probably is unrelated to the mongodb upgrade, but I thought it might worth providing that context and don't miss a solution for not providing enough of it.

So since the system upgrade any login attempt on this Rails project will fail with a BCrypt::Errors::InvalidHash in Devise::SessionsController#create error, raised at bcrypt (3.1.11) lib/bcrypt/password.rb:60:ininitialize'`. Analyzing further in a Rails console of the project, it seems any call to this method will fail:

> BCrypt::Password.create('TestPassword')
BCrypt::Errors::InvalidHash: invalid hash
from /home/psychoslave/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/bcrypt-3.1.11/lib/bcrypt/password.rb:60:in `initialize'

I tried to bundle uninstall/reinstall bcrypt, and even use the github repository version of the bcrypt gem instead, but it didn't change anything.

Looking at /home/psychoslave/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/bcrypt-3.1.11/lib/bcrypt/password.rb:60:ininitialize'`, the problem seems that the hash is not valid.

# Initializes a BCrypt::Password instance with the data from a stored hash.
def initialize(raw_hash)
  if valid_hash?(raw_hash)
    self.replace(raw_hash)
    @version, @cost, @salt, @checksum = split_hash(self)
  else
    raise Errors::InvalidHash.new("invalid hash")
  end
end

And the corresponding test is as follow:

  def valid_hash?(h)
    h =~ /^\$[0-9a-z]{2}\$[0-9]{2}\$[A-Za-z0-9\.\/]{53}$/
  end

The hash itself is created through BCrypt::Engine.hash_secret(secret, BCrypt::Engine.generate_salt(cost)), which in the platform I use call __bc_crypt(secret.to_s, salt), which seems to be calling bcrypt-3.1.11/ext/mri/bcrypt_ext.c.

More importantly, adding a binding.pry in the valid_hash? method, it's possible to see what the hash value returned for a call to BCrypt::Password.create('TestPassword'), it's actually a rather long string whose start seems usual, but end up with what is most likely misgenerated sequence:

"$2a$10$Eb1f8DSkGh4G1u5GicyTYujBk6SwFXKYCH.nqxapmBlqJ0eFYdX32\x00\x00\x00\x00\xD1F\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00T\xBD\x02\x00\x00\x00\x00\x00\xF1V\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\xE2\xB0\x02\x00\x00\x00\x
00\x00AW\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00 \x04\x00\x00\x00\x00\x00\x00\x86\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xB5\xF8\x0E\x00\x00\x00\x00\x00q\xD8\x01\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00…"

I can provide a dump of a whole hash if it might be of any interest (around 32Ko!).

psychoslave
  • 2,783
  • 3
  • 27
  • 44
  • What does this line say `lib/bcrypt/password.rb:60:ininitialize'`? I mean the definition of `initialize`. – Jagdeep Singh May 07 '18 at 11:31
  • I updated the description of the problem to match your question @JagdeepSingh and provide further information. So far it seems to me that the problem is that – at least on my system – `__bc_crypt(secret.to_s, salt)` doesn't provide the expected result. – psychoslave May 07 '18 at 12:34
  • 1
    Hold on, there is an official [issue on the BCrypt gem](https://github.com/codahale/bcrypt-ruby/issues/170) as well as an [bug report on red hat tracker](https://bugzilla.redhat.com/show_bug.cgi?id=1537140). – psychoslave May 07 '18 at 15:09
  • I resolved the issue by appending a `.first(60)` to the output of`BCrypt::Engine.hash_secret(password,salt)` getting `BCrypt::Engine.hash_secret(password,salt).first(60)` at two places in my session/log-in controller – Martin T. Jun 10 '21 at 14:27

2 Answers2

13

Here is a circumvent solution which make rspec of bcrypt pass all tests successfully again.

This is really a uggly hack while waitting a proper solution, but does the job until then. Just change ~/.rbenv/versions/2.4.3/lib/ruby/gems/2.4.0/gems/bcrypt-3.1.11/lib/bcrypt/engine.rb (path to adapt, of course), line 51 from:

- __bc_crypt(secret.to_s, salt)
+ __bc_crypt(secret.to_s, salt).gsub(/(\n|\x00).*/, '')

That is, trunk the string starting at the first "\x00" or "\n" occurrence, if any.

Credit note: this version of the hack was proposed by Andrey Sitnik, and I replaced the one I proposed here independently, before discovering it.

After that, BCrypt::Password#create will function again:

> BCrypt::Password.create('TestPassword')
=> "$2a$10$YPRnQF3ZihXHpa9kSx7Mpu.j28PlbdwaNs2umSQvAGkS.JJ.syGye"
psychoslave
  • 2,783
  • 3
  • 27
  • 44
  • 1
    Been banging my head against this for a while. Just learning BDD testing so thought it was my specs! Thanks, @psychoslave. – Colin Wu May 08 '18 at 15:10
5

I had this issue with a (very) old application and BCrypt 3.1.10. Upgrading to 3.1.12 resolved the issue. :)

XtraSimplicity
  • 5,704
  • 1
  • 28
  • 28