Parsing an ASP.NET Core Identity password hash with JavaScript

Understanding how ASP.NET Core Identity stores passwords internally is useful particularly if you ever need to migrate to a different identity setup.

Understanding how ASP.NET Core Identity stores passwords internally is useful particularly if you ever need to migrate to a different identity setup.

I created a user in an ASP.NET Core app with password: heygaldin!.
The PasswordHash field in the ASPNetUsers table had this value:

AQAAAAEAACcQAAAAEPOWUjkBBjnBkT/oFFjsx0EdDCjFhGopC7jS4lWP2FSYdMxbkneSGyQ/OvRHUIegxg==

Until recently, I always thought that the value simply contained the hash+salt in it. Turns out there's a bit of jugaad going on in there:

// source: https://github.com/dotnet/aspnetcore/blob/v6.0.14/src/Identity/Extensions.Core/src/PasswordHasher.cs#L19-L32
/* =======================
* HASHED PASSWORD FORMATS
* =======================
*
* Version 2:
* PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations.
* (See also: SDL crypto guidelines v5.1, Part III)
* Format: { 0x00, salt, subkey }
*
* Version 3:
* PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations.
* Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey }
* (All UInt32s are stored big-endian.)
*/

Now that we know how the passwords are stored internally, it should be possible to extract the true password hash & salt from the given hash.

Let's load the given hash into a buffer:

const hash = 'AQAAAAEAACcQAAAAEPOWUjkBBjnBkT/oFFjsx0EdDCjFhGopC7jS4lWP2FSYdMxbkneSGyQ/OvRHUIegxg=='
const buffer = Buffer.from(hash, 'base64')

Now that we have our buffer, we should be able to extract data fairly easily. From the format, we know that v2 starts with 0x00 and v3 starts with 0x01:

// reference: https://github.com/dotnet/aspnetcore/blob/v6.0.5/src/Identity/Extensions.Core/src/PasswordHasher.cs#L19-L32
function getIdentityVersion(buffer: Buffer) : "v2" | "v3" | undefined {
  const identifier = buffer[0]
  if(identifier === 0x00) return "v2"
  if(identifier === 0x01) return "v3"
  return undefined
}

All of the users I'm trying to migrate use v3, so the rest of the post is v3-only. However, adapting this to support v2 should be pretty straightforward too.

Here's how I could find the algorithm in use:

// reference: https://github.com/dotnet/aspnetcore/blob/v6.0.5/src/DataProtection/Cryptography.KeyDerivation/src/KeyDerivationPrf.cs
function getKeyDerivationPrf(buffer: Buffer) : "HMACSHA1" | "HMACSHA256" | "HMACSHA512" | undefined {
  const algorithm = buffer.readIntBE(1, 4)
  switch (algorithm) {
    case 0: return "HMACSHA1"
    case 1: return "HMACSHA256"
    case 2: return 'HMACSHA512'
    default: return undefined
  }
}

And the iteration count:

function getIterationCount(buffer: Buffer) : number | undefined {
  const identifier = getIdentityVersion(buffer)
  if(identifier !== "v3") return undefined

  return buffer.readIntBE(5, 4);
}

And the salt length:

function getSaltLength(buffer: Buffer) : number | undefined {
  const identifier = getIdentityVersion(buffer)
  if(identifier !== "v3") return undefined

  return buffer.readIntBE(9, 4);
}

And then the salt and the password hash:

function getSalt(buffer: Buffer) : string | undefined {
  const saltLength = getSaltLength(buffer)
  if(saltLength === undefined) return undefined

  return buffer.slice(13, 13 + saltLength).toString('base64')
}

function getPasswordHash(buffer: Buffer) : string | undefined {
  const saltLength = getSaltLength(buffer)
  if(saltLength === undefined) return undefined

  return buffer.slice(13 + saltLength).toString('base64')
}

And we print them together:

console.log(`   Buffer length: ${buffer.length}`)
console.log(`Identity Version: ${getIdentityVersion(buffer)}`)
console.log(`       Algorithm: ${getKeyDerivationPrf(buffer)}`)
console.log(` Iteration Count: ${getIterationCount(buffer)}`)
console.log(`     Salt Length: ${getSaltLength(buffer)}`)
console.log(`            Salt: ${getSalt(buffer)}`)
console.log(`   Password Hash: ${getPasswordHash(buffer)}`)

I got something like this:

   Buffer length: 61
Identity Version: v3
       Algorithm: HMACSHA256
 Iteration Count: 10000
     Salt Length: 16
            Salt: 85ZSOQEGOcGRP+gUWOzHQQ==
   Password Hash: HQwoxYRqKQu40uJVj9hUmHTMW5J3khskPzr0R1CHoMY=

Straightforward once you know the format!