Importing Password Hashes from Drupal 7

Hi, I’ve successfully imported the “antoinette@contoso.com” custom hash example from https://auth0.com/docs/users/references/bulk-import-database-schema-examples using the import extension but my real example isn’t working.

Drupal 7 uses sha512 with base64 encoding and a salt which is not stored in the database. I think the salt is random and I can look in the code to find out how long it is, etc. The reference for the hash code is https://api.drupal.org/api/drupal/includes!password.inc/7.x.

Any suggestions how I can make an importable user?

Here is a non-real example of a hash: $S$D.oOkSJYpbtcVkh1QWSm8BzRKYpsEHrrqPRiaDmiqr.GVE3QisdC

Here are some examples of repeated calls of _password_generate_salt(DRUPAL_HASH_COUNT)

“$S$Dg.CLIAbK”
“$S$DgJg7eBjV”
“$S$DCJG/FAi5”
“$S$DKhn5.nXz”
“$S$DHVVmeH8q”
“$S$DdH4fZ1nk”

Hi jcable, and welcome to the community! :partying_face:

Where is the salt stored currently if not in the DB? You need the salt to check the password hash. Your current application would have to know it to verify the hash. In order to import it into Auth0, you will need it as well.

According to this SO, the salts should be in the pass column: https://drupal.stackexchange.com/questions/176008/getting-password-hashes-and-salts-from-drupal-7

2 Likes

Thanks - sorry for the tardy response - other projects have intervened but that SO looks like the right answer. I’ll let you know how I get on.

I’ve successfully used the import/export extension to import a couple of users using this:

function convert({ mail, pass }) {
  const buf = Buffer.from(pass, 'base64');
  const settings = buf.slice(0, 4);
  const salt = buf.slice(4, 12);
  const hash = buf.slice(12);
  return {
    "email": mail,
    "email_verified": true,
    "custom_password_hash": {
        "algorithm": "sha512",
        "hash": {
            "value": hash.toString('base64'),
            "encoding": "base64"
        },
        "salt": {
            "value": salt.toString('base64'),
            "position": "prefix"
        }
    }
  };
}

When I use the test login form as the imported user it recognises my email and seems to accept my password but wants me to change my password as its the first time I’ve logged in. This of course negates the purpose. Am I missing a parameter?

Reading the docs it looks like the ‘password_set_date’ field might help but it isn’t clear.

Thanks for your help

Julian

Ah, I’m missing the hash.salt.encoding field.

But that didn’t help.

But looking at the user it could be that I have MFA enabled.

No I think I just don’t have the hash/salt right yet:

"error": {
      "message": "Password change required.",
      "reason": "Verification failed for the provided custom_password_hash: {'algorithm':'sha512','hash':{'value':'TXSm8BzRK...','encoding':'base64'},'salt':{'value':'Ili...','position':'prefix','encoding':'base64'}}"
    },
    "device_id": "v0:06e7a3d0-8c39-11ea-aaad-03e339e36dd3"
  },

Making progress but still bamboozled

  1. That SO post, when it talks about bytes in the pass it means characters in the base64 encoded password so if the content of the users.pass field is

$S$D.oOkSJYpbtcVkh1QWSm8BzRKYpsEHrrqPRiaDmiqr.GVE3QisdC

Then
settings is ‘$S$D’
salt is ‘.oOkSJYp’
hash is ‘btcVkh1QWSm8BzRKYpsEHrrqPRiaDmiqr.GVE3QisdC’

  1. The Drupal in-code comments say that a base64 encoded sha512 password is always 86 characters but all the examples generated by my Drupal 7 system are 55 characters

  2. the hash values split out of the pass field are rejected by auth0 because they are not a multiple of 4 characters. If I round trip the salt and hash through a node buffer it becomes acceptable to auth0 but still doesn’t allow the correct password to validate

  3. the Drupal _password_crypt function uses the 4th character of the settings as a log2 loop counter to hash the hash with itself. I don’t see how auth0 can be compatible with this unless there is a field to pass the loop count in. So in my example the character is D meaning 4.

  4. finally, nodejs and php seem to handle base64 differently. There are ‘.’ characters in the php base64 strings which are eliminated by round-tripping through a node Buffer.

in case it’s any use here is my current code, but it isn’t right.

function fixBase64(s) {
  return Buffer.from(s, 'base64').toString('base64');
}

function convert({ mail, pass }) {
  const setting = pass.slice(0, 12);
  const salt = setting.slice(4);
  const hash = pass.slice(12);
  return {
    "email": mail,
    "email_verified": true,
    "custom_password_hash": {
        "algorithm": 'sha512',
        "hash": {
            "value": fixBase64(hash),
            "encoding": "base64"
        },
        "salt": {
            "value": fixBase64(salt),
            "position": "prefix",
            "encoding": "base64"
        }
    }
  };
}

I’m not what is going wrong exactly but sha512 is 64 bytes (512 / 8) and base 64 encoded it is 86 or 88 bytes depending on padding. If the Drupal in code comment examples or your hashes are a different length then I think probably you’re not using the raw bytes from the hash, but instead using the hash that has already gone through some sort of encoding. Also, I think bcrypt only uses the first 55 bytes, is it possible you are loading the hash using bcrypt…? :man_shrugging:t3:

Thanks Thomas - no, its not using bcrypt but that’s leading me down a path that will help. I’m going to investigate drupal_hash_base64. This SO question is also relevant.

OK, I’ve translated Drupal 7’s password.inc file into commonJS module here.

It now correctly identifies correct passwords from the database pass field.

I’ve added a _password_base64_decode function which correctly reverses the Drupal 7 _password_base64_encode function and this round trips correctly. (This isn’t intended to be production code - its very brute force).

I’ve created auth0 user imports from this by using my _password_base64_decode function separately on the hashed password and the hashed salt fields and then encoding these to hex. These import successfully.

But, as I expected, I still get wrong password errors when I log in using the imported user and the correct password.

I think I’ve eliminated all other possible errors and the problem must now be that auth0 does not support stretching the hash.

I think to support Drupal 7 import we need to add an iterations field to the custom_password_hash object and implement the stretching functionality.

The code is:

count = 1 << count_log2;
  const pb = Buffer.from(password);
  do {
      hash = crypto.createHash(algo);
      hash.update(Buffer.concat([buffer, pb]));
      buffer = hash.digest();
  } while (--count);

as in lines 184 to 190 in my gist.

count_log2 is 15 in current Drupal 7 configurations and signalled in the settings as the ‘D’ in ‘$S$D’.

This is what my convert function looks like now with an assumed log2_iterations field:

const drupal = require('./password');

function convert({ mail, pass }) {
  const settings = pass.slice(0,4);
  const hashedSalt = pass.slice(4,12);
  const hashedPass = pass.slice(12);
  const salt = drupal._password_base64_decode(hashedSalt);
  const hash = drupal._password_base64_decode(hashedPass);
  const log2_iterations = settings.charCodeAt(3)-53;
  return {
    "email": mail,
    "email_verified": true,
    "custom_password_hash": {
        "algorithm": 'sha512',
        log2_iterations,
        "hash": {
            "value": hash.toString('hex')
        },
        "salt": {
            "value": salt.toString('hex'),
            "position": "prefix",
        }
    }
  };
}

FYI the Drupal folks pointed me at this link which describes the stretching.