How to understand bcrypt / blowfish password hashes (PHP to Node user migration example) 🔑

Migrating users to a new identity platform or new system in general can be a pain. you don't want to require having them change their passwords - so you need to re-store their hashes and replicate the hashing on the new system. Here's how you can do that from Laravel PHP to Node.js. 🔑

The structure of a bcrypt hash

A hash generated with the bcrypt algorithm consists of essentially 4 parts. For the rest of the article we'll use this password and its hash (generated by Laravel PHP):

const password = '!!!123Password'
const hash = '$2y$10$6qKNkQJQxyHJ2l/ZLFhdj.6fV7Cw810GjTRFDdKDNYKNX2k93cm3q'

The structure of the hash gives information on how it was generated, e.g.

  • $2y: Depicts the algorithm version.
  • $10: Depicts the number of salt rounds.
  • $6qKNkQJQxyHJ2l/ZLFhdj.: The 22 characters after the third dollar sign depicts the actual salt value.
  • 6fV7Cw810GjTRFDdKDNYKNX2k93cm3q: Represents the hashed password.

Different algorithm versions

You can read this wiki for in-depth info, but TL;DR:

  • You might see:
    • $2a$
    • $2x$
    • $2y$
    • $2b$

Out in the wild, and the wiki explains what they're supposed to mean. Today we're just focused on migrating them from Laravel to Node.js.

Getting started

Let's assume you've transfered your user data to a new system already (or are connecting to the db using a new system). You now have a brand new Node.js server running and it can read the password hashes and it can accept user login requests. What do we do?

Replicate the algorithm

First and foremost, for consistency you need to ensure that when users create new profiles on your new system that you are storing the passwords the same way. This requires you to replicate the bcrypt algo in Node and now that you know how to identify it - you can write it.

const bcrypt = require('bcrypt')
const database = require('controllers/database')

exports.handler = async function register(username, password) {
  const saltRounds = 10 // Second $ in the hashes.
  const hashed = bcrypt.hashSync(password, saltRounds)

  // Save the user
  return await database.insert(username, hashed)
}

Here you have ensured that you at least keep things consistent by generating hashes based on the same principle. Now when a user logs in you can verify them:

const bcrypt = require('bcrypt')
const database = require('controllers/database')

exports.handler = async function login(username, password) {
  const user = await database.getUserByUsername(username)
  const hashedPassword = user.password

  const authenticated = bcrypt.compareSync(password, hashedPassword)

  if (!authenticated) {
    return redirect('/login')
  }

  return true // some login success payload
}

OOPS... This won't work if you've got $2y algos stored in your db. And if you check the hashes that Node generates, it's $2b hashes 🤔. What now?

Well the answer is a small, but kind of weird change to the hashedPassword value:

const nodeFriendlyHash = hashedPassword.replace('$2y$', '$2a$') // huh?

Again.. what?

Kind of doesn't make sense to use 2a right? Since neither of the hashes use 2a, but this is the requirement for the compareSync function to work on 2y hashes. Your final validation would look like this:

const bcrypt = require('bcrypt')
const database = require('controllers/database')

exports.handler = async function login(username, password) {
  const user = await db.getUserByUsername(username)

  // 👇 This leaves the $2b$ passwords in tact and working as normal.
  const hashedPassword = user.password.replace('$2y$', '$2a$')
  const authenticated = bcrypt.compareSync(password, hashedPassword)

  if (!authenticated) {
    return redirect('/login')
  }

  return true // some login success payload
}

Conclusion

Understanding the structure of a bcrypt hash can help in situations like I had recently where we are migrating our users. Finding the hash function in Laravel password_hash in PHP or whatever language can be difficult if it's abstracted away. At least now you might be able to identify a bcrypt hash by first sight AND perhaps be able to reproduce it in a different language / system without much pain.

Until next time!

- Kaizen