ASP .NET Core Identity (v3) uses PBKDF2 algorithm with HMAC-SHA256, 128-bit salt, 256-bit subkey, and 10,000 iterations. What does Caché use?
The .NET Core Identity model has an IPasswordHasher<> interface for for
- Hashing a password so that it can be stored in a database
- Verifying a provided plain-text password matches a previously stored hash.
I am getting invalid password errors during the login process when the .NET Core Identity model computes a hash from a plain text input and compares it to a password hash value I've returned from Caché. The default hashing algorithm is PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, and 10,000 iterations (detailed article on .NET Core Identity PasswordHasher). The algorithm Caché uses is probably different which may be why I am getting errors.
Consider the following in Caché for username "test1"
Do ##class(Security.Users).Exists("test1",.user,.status1) The "user" object has properties for Password and Salt.
The class documentation says it uses PBKDF2 and that the Salt value is generated from $System.Encryption.GenCryptRand but doesn't elaborate on the other properties of PBKDF2 such as key length and number of iterations.
I encode the value of user.Password as UTF8 then Base64 before passing it to the web app to compute and verify it's own hash based on plain-text input.
$system.Encryption.Base64Encode($ZCONVERT(user.Password),"O","UTF8")
This produces something like w67CisO6IGnDk1fDl8OzC8OjYk0bblknA8KYw4g= as the PasswordHash stored in the database. The .NET Core Identity hashing technique must be producing something different which is why I am getting errors.
Any thoughts on how I might solve this problem?
Comments
What do you want to achieve?
I'm simply creating an interface between Caché and a web app that is using the .NET Core Identity Model. The PasswordSignInAsync() method returns a false sign-in result and I'm trying to determine why the password isn't being accepted.
Which have been broken already:
Also. NIST considers SHA-1 as obsolete since 2011. SHA-256 is somewhat safer for now.
We have some customer projects that uses a separate user table, in these cases we usually use a stronger hash implementation:
Method SetPassword(value As %String) As %Status
{
set i%password = ..HashPassword(value)
return $$$OK
}
ClassMethod HashPassword(
value As %String,
salt As %String = "") As %String
{
if salt = "" {
set salt = $$$lcase(##class(%xsd.hexBinary).LogicalToXSD($System.Encryption.GenCryptRand(32)))
}
set hash = $$$lcase(##class(%xsd.hexBinary).LogicalToXSD($System.Encryption.PBKDF2(value, 15000, salt,256,256)))
return $$$FormatText(hash_":"_salt)
}
Method IsPasswordMatch(plainTextPassword As %String) As %Boolean
{
set salt = $piece(..password, ":", 2)
return $$ConstantTimeCompare(..HashPassword(plainTextPassword, salt), ..password)
ConstantTimeCompare(a, b)
if $length(a) '= $length(b) return 0
for i=1:1:$length(a) {
// Convert char to ASCII code and then to bitstring.
set aChar = $factor($ascii($extract(a, i)))
set bChar = $factor($ascii($extract(b, i)))
set match = $bitlogic(aChar ^ bChar)
set all = $bitlogic(all | match)
}
// 00000000 = valid
return $bitfind(all, 1) = 0
}
Good idea in such cases -- enable Audit and audit events LoginFailure and Protect. Reproduce the problem and then check Audit log.
That looks a little complex. In contrast to the above example, the default $System.Encryption.GenCryptRand() size appears to be 8 as $L(user.Salt) resolves to 8. After a bit of experimentation, I found I didn't need to encode anything at all. In this example, I'm using user test1 with password P@ssw0rd on a non-unicode 8-bit Cache installation. Use $SYSTEM.Version.IsUnicode() to check your installation
Do ##class(Security.Users).Exists("test1",.user,.status1)
set storedHash = user.Password
set computed=$System.Encryption.PBKDF2("P@ssw0rd", 1024, user.Salt,20,160)Produces the following 20 byte hash using PBKDF2 with 1024 iterations, 64 bits of salt and SHA1 (160)
%SYS>zw storedHash storedHash="n"_$c(138)_"z iSWWs"_$c(11)_"cbM"_$c(27)_"nY'"_$c(3,152)_"H" %SYS>zw computed computed="n"_$c(138)_"z iSWWs"_$c(11)_"cbM"_$c(27)_"nY'"_$c(3,152)_"H"
It's documented here:
"The hashes are calculated using the PBKDF2 algorithm with the HMAC-SHA-1 pseudorandom function, as defined in Public Key Cryptography Standard #5 v2.1: 'Password-Based Cryptography Standard.' The current implementation uses 1024 iterations, 64 bits of salt, and generates 20 byte hash values."