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!