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?

  • 0
  • 1
  • 136
  • 4
  • 2

Answers

It's documented here:

https://cedocs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?K...

"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."

Which have been broken already:

https://shattered.io/

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
}

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"

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. 

Good idea in such cases -- enable Audit and audit events LoginFailure and Protect. Reproduce the problem and then check Audit log.