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!