Question
According to online articles the .NET Core PasswordHasher<>
class uses:
ASP.NET Core Identity Version 3: PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations
The source code says this:
/*
* 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-SHA512, 128-bit salt, 256-bit subkey, 100000 iterations.
* Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey }
* (All UInt32s are stored big-endian.)
*/
What's the equivalent to hashing and validating a string with PasswordHasher<>
V3 in Node?
What have I tried
Since crypto
doesn't seem to support "PBKDF2 with HMAC-SHA256" I'm currently trying pbkdf2-hmac
like this:
const generatedHash = arrayBufferToBase64(
await pbkdf2Hmac(password, salt, 100000, 32, "SHA-256")
);
The thing I'm least certain about is the fourth argument (32) as on one hand .NET says "256-bit subkey" and on the other pbkdf2Hmac
says that the fourth argument should be "intended length in octets of the derived key"; my reasoning was 256 / 8 = 32
but that's a wild guess.
In any case I've tried 32
, 64
and 256
for that value, I've also tried all combinations of SHA1, SHA256, SHA512 with 1000, 10000, 100000 iterations and nothing gives me the same base64
string as PasswordHasher<>
.
What am I missing?
Sample password
Given the password sample123 !
the hash generated by PasswordHasher<>
is:
AQAAAAEAACcQAAAAEAdmj6YuRrVMYBXbeGrV/j3VqSzvhIhE/xYWA0S1hpfxH5FuepoGJjite6n/Fh4quw==
Notice that if I read the implementation correctly then the salt is randomly generated and embedded in the hash.
CodePudding user response:
For validation, the subkey must be calculated using PBKDF2 and compared with the subkey in the hash.
All information needed to calculate the key using PBKDF2 is contained in the hash:
- the digest used (prf)
- iteration count (iter)
- salt length and salt subkey in the hash is located at the end of the hash and has a length of 256 bits = 32 bytes.
This allows the key to be derived using PBKDF2 if the password is known:
var hashB64 = "AQAAAAEAACcQAAAAEAdmj6YuRrVMYBXbeGrV/j3VqSzvhIhE/xYWA0S1hpfxH5FuepoGJjite6n/Fh4quw=="
var password = "sample123 !";
// Get digest, salt and iteration count:
// Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey }
var hash = Buffer.from(hashB64, "base64");
var marker = hash.subarray(0,1); // 0: V2, 1: V3
var prf = hash.readUInt32BE(1); // 0: HMAC/SHA1, 1: HMAC/SHA256, 2: HMAC/SHA512
var iter = hash.readUInt32BE(1 4);
var saltLen = hash.readUInt32BE(1 2*4);
var salt = hash.subarray(1 3*4, 1 3*4 saltLen);
var subkey = hash.subarray(1 3*4 saltLen);
// Calculate subkey with pbkdf2
var keySize = 32;
var subkeyPbkdf2 = crypto.pbkdf2Sync(password, salt, iter, keySize, prf == 0 ? "sha1" : (prf == 1 ? "sha256" : "sha512"));
console.log(subkey.compare(subkeyPbkdf2)); // 0 = successfully validated
The posted sample hash uses SHA-256 as digest, an iteration count of 10000, and a salt length of 16 bytes.
A new hash can be determined by generating a random 16 bytes salt, calculating the subkey from it using PBKDF2, and contacting all data according to the above specification.
Example (using the above values):
var marker = 1;
var prf = 1;
var iter = 10000;
var saltLen = 16;
var salt = "B2aPpi5GtUxgFdt4atX PQ=="; // random salt
var subkey = "1aks74SIRP8WFgNEtYaX8R RbnqaBiY4rXup/xYeKrs="; // via PBKDF2
var markerBuf = Buffer.allocUnsafe(1);
markerBuf.writeUInt8(marker);
var prfBuf = Buffer.allocUnsafe(4);
prfBuf.writeUInt32BE(prf);
var iterBuf = Buffer.allocUnsafe(4);
iterBuf.writeUInt32BE(iter);
var saltLenBuf = Buffer.allocUnsafe(4);
saltLenBuf.writeUInt32BE(saltLen);
var saltBuf = Buffer.from(salt, "base64");
var subkeyBuf = Buffer.from(subkey, "base64");
var hashPbkdf2 = Buffer.concat([markerBuf, prfBuf, iterBuf, saltLenBuf, saltBuf, subkeyBuf]);
var hashB64 = "AQAAAAEAACcQAAAAEAdmj6YuRrVMYBXbeGrV/j3VqSzvhIhE/xYWA0S1hpfxH5FuepoGJjite6n/Fh4quw==";
console.log(hashB64 === hashPbkdf2.toString("base64")); // true