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.

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:

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

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:

1const hash = 'AQAAAAEAACcQAAAAEPOWUjkBBjnBkT/oFFjsx0EdDCjFhGopC7jS4lWP2FSYdMxbkneSGyQ/OvRHUIegxg=='
2const 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:

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

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:

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

And the iteration count:

1function getIterationCount(buffer: Buffer) : number | undefined {
2  const identifier = getIdentityVersion(buffer)
3  if(identifier !== "v3") return undefined
4
5  return buffer.readIntBE(5, 4);
6}

And the salt length:

1function getSaltLength(buffer: Buffer) : number | undefined {
2  const identifier = getIdentityVersion(buffer)
3  if(identifier !== "v3") return undefined
4
5  return buffer.readIntBE(9, 4);
6}

And then the salt and the password hash:

 1function getSalt(buffer: Buffer) : string | undefined {
 2  const saltLength = getSaltLength(buffer)
 3  if(saltLength === undefined) return undefined
 4
 5  return buffer.slice(13, 13 + saltLength).toString('base64')
 6}
 7
 8function getPasswordHash(buffer: Buffer) : string | undefined {
 9  const saltLength = getSaltLength(buffer)
10  if(saltLength === undefined) return undefined
11
12  return buffer.slice(13 + saltLength).toString('base64')
13}

And we print them together:

1console.log(`   Buffer length: ${buffer.length}`)
2console.log(`Identity Version: ${getIdentityVersion(buffer)}`)
3console.log(`       Algorithm: ${getKeyDerivationPrf(buffer)}`)
4console.log(` Iteration Count: ${getIterationCount(buffer)}`)
5console.log(`     Salt Length: ${getSaltLength(buffer)}`)
6console.log(`            Salt: ${getSalt(buffer)}`)
7console.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!

<< Previous Post

|

Next Post >>

#ASP.NET Core #C# #JavaScript