8
votes

I'm dealing with an old database with $2y hashes. I've dug into this a bit, also stumbled on the stack overflow on the difference between $2a and $2y.

I looked into the node module for bcrypt which seems to generate and compare only $2a hashes.

I found a website that generates $2y hashes so I can test them with bcrypt.

Here's an example of a $2y hash of the string helloworld.

helloworld:$2y$10$tRM7x9gGKhcAmpeqKEdhj.qRWCr4qoV1FU9se0Crx2hkMVNL2ktEW

Seems the module has no way of validating $2y hashes.

Here's my test.

var Promise = require('bluebird')
var bcrypt = require('bcrypt')

var string = 'helloworld'

Promise.promisifyAll(bcrypt)

// bcrypt.genSalt(10, function(err, salt) {
//   bcrypt.hash(string, salt, function(err, hash) {
//     console.log(hash)
//   })
// })

var hashesGeneratedUsingBcryptModule = [
  '$2a$10$6ppmIdlNEPwxWJskPaQ7l.d2fblh.GO6JomzrcpiD/hxGPOXA3Bsq',
  '$2a$10$YmpoYCDHzdAPMbd9B8l48.hkSnylnAPbOym367FKIEPa0ixY.o4b.',
  '$2a$10$Xfy3OPurrZEmbmmO0x1wGuFMdRTlmOgEMS0geg4wTj1vKcvXXjk06',
  '$2a$10$mYgwmdPZjiEncp7Yh5UB1uyPkoyavxrYcOIzzY4mzSniGpI9RbhL.',
  '$2a$10$dkBVTe2A2DAn24PUq1GZYe7AqL8WQqwOi8ZWBJAauOg60sk44DkOC'
]

var hashesGeneratedUsingAspirineDotOrg = [
  '$2y$10$MKgpAXLJkwx5tpijWX99Qek2gf/irwvp5iSfxuFoDswIjMIbj2.Ma',
  '$2y$10$tRM7x9gGKhcAmpeqKEdhj.qRWCr4qoV1FU9se0Crx2hkMVNL2ktEW'
]

var hashesGeneratedUsingAspirineDotOrgSwippedYForA = [
  '$2a$10$MKgpAXLJkwx5tpijWX99Qek2gf/irwvp5iSfxuFoDswIjMIbj2.Ma',
  '$2a$10$tRM7x9gGKhcAmpeqKEdhj.qRWCr4qoV1FU9se0Crx2hkMVNL2ktEW'
]

hashesGeneratedUsingBcryptModule = hashesGeneratedUsingBcryptModule.map(hash => bcrypt.compareAsync(string, hash))
hashesGeneratedUsingAspirineDotOrg = hashesGeneratedUsingAspirineDotOrg.map(hash => bcrypt.compareAsync(string, hash))
hashesGeneratedUsingAspirineDotOrgSwippedYForA = hashesGeneratedUsingAspirineDotOrgSwippedYForA.map(hash => bcrypt.compareAsync(string, hash))

Promise.all(hashesGeneratedUsingBcryptModule)
.tap(() => console.log('hashesGeneratedUsingBcryptModule'))
.then(console.log)

Promise.all(hashesGeneratedUsingAspirineDotOrg)
.tap(() => console.log('hashesGeneratedUsingAspirineDotOrg'))
.then(console.log)

Promise.all(hashesGeneratedUsingAspirineDotOrgSwippedYForA)
.tap(() => console.log('hashesGeneratedUsingAspirineDotOrgSwippedYForA'))
.then(console.log)

Here are the results:

// hashesGeneratedUsingAspirineDotOrg
// [ false, false ]
// hashesGeneratedUsingBcryptModule
// [ true, true, true, true, true ]
// hashesGeneratedUsingAspirineDotOrgSwippedYForA
// [ false, false ]

I'm stumped on how I can compare $2y hashes in node.

There's another Stack Overflow question / answer that says you can just change the $2y to $2a but that still fails for me.

Update!

I was using the generator incorrectly because it's a .htpasswd password generator you have to put in the username and password in this format.

reggi helloworld

And the output corresponds here:

reggi:$2y$10$iuC7GYH/h1Gl1aDmcpLFpeJXN9OZXZUYnaqD2NnGLQiVGQYBDtbtO

Before I as putting just

helloword

Which I'm assuming hashed a empty string.

With these changes changing the y to an a works in bcrypt. And twin-bcrypt just works.

1
I vaguely recall having better luck working the other way -- taking the $2a$ hashes generated by bcrypt in javascript, replacing 2a with 2y, and then comparing using 2y libraries in other languages (php natively and BCrypt from .net could both handle it, which struck me as very odd). I can dig up the test code I had, if that would be helpful to you. - dvlsg
@dvlsg Got it. That makes sense. So I need to compare $2y hashes in node, not $2a hashes in php, which I'm guessing works by replacing the a to y. - ThomasReggi
Yeah, I was actually storing the hashes as $2y in the database, using them as-is for both PHP and .NET, but when I used them in node I had an extra convert step which swapped y back to a before comparison. It felt wrong, but it looked like 2a and 2y used the same structure for the rest of the salt/hash. - dvlsg

1 Answers

12
votes
  • When using bcrypt change the y to an a.
  • When using twin-bcrypt the hash just works.

When using http://aspirine.org/htpasswd_en.html make sure that you provide a username and password.

reggi helloworld

Then:

reggi:$2y$10$Am0Nf/B6.S/Wkpr6IVdIZeuHWNa/fqoLyTNmlyrSg22AjRf2vS.T.

Here's a working example with both bcrypt and twin-bcrypt.

var twinBcrypt = require('twin-bcrypt')
var bcrypt = require('bcrypt')

var string = 'helloworld'

var bcryptAttempt = bcrypt.compareSync(string, "$2y$10$Am0Nf/B6.S/Wkpr6IVdIZeuHWNa/fqoLyTNmlyrSg22AjRf2vS.T.".replace(/^\$2y/, "$2a"))
console.log(bcryptAttempt)

var twinBcryptAttempt = twinBcrypt.compareSync(string, "$2y$10$Am0Nf/B6.S/Wkpr6IVdIZeuHWNa/fqoLyTNmlyrSg22AjRf2vS.T.")
console.log(twinBcryptAttempt)

Outputs:

true
true