Many aspects of this make no sense to me... so IF this is actually what they are doing...
hash(user, password) {
salt = CS-PRNG(160bit)
s = scrypt(salt, password)
z1 = s[0:32]
z2 = s[32:64]
R = CS-PRNG(256bit)
d = HSM(R) XOR (pad_right(z1, 0x00, 32 bytes))
cek = SHA256(z2 || d)
hash = SHA256(cek)
save_record(user, d, salt, hash)
}
First and foremost, if the HSM operation is to have any meaning, it needs to be in the critical path for encrypting / decrypting data and possibly also calculating the password hash. If you don't need to go through the HSM to perform a decrypt or a hash validation, then obviously the HSM isn't actually securing anything! All it becomes is a source of entropy into the key derivation, but that's more likely to harm than help.From their own specification;
"It is important to note that the HSM factor strengthens the model in a way different than the other two factors, which rely on keeping them secret. Because the HSM is tied to a physical object, brute force attacks on our database would need to happen in proximity to the HSM, i.e., within our AWS environment, which greatly reduces the attack surface. A bad actor with a copy of the database cannot apply their own computing power to brute force cracking of passwords."
But to be clear, if you have 'd', 'salt', and 'hash', you can brute force attack passwords as;
s = scrypt(salt, password)
h = SHA256(SHA256(s[32:64] || d))
h =? hash
Now, if they were not storing 'd' in the user record, you might think to store what they call 'R' in the user record. Then you would have to go through the HSM as part of each login to derive the correct 'd' and 'cek' which is how it's supposed to work.But even that design is still not good enough. You don't want to allow an attacker to pull 'R' from your database, send it through the HSM just once, and then be able to start brute forcing the password forever from there on out. If you're going to pay for an HSM, and your going to call it for each password verification, then you better make sure an attacker is also required to call the HSM for each attempt at verifying a password. Which means you send your password hash (or something derived from it) through the HSM, not just a random 'R'!
Next, besides all of that, the design is horrific for end users. PII is encrypted with 'cek' above. That means if you lose your password, you lose your PII. Which means starting basically a new account from scratch! I can't endorse the idea that a password reset from an end user will effectively wipe out their account information and make them start over.
The vast majority of users will be logging into this service infrequently. Combine this with a typically user hostile password policy, and the result is that a large percentage of users will be resetting their password every time they need to login. With this design, that means they have to start over with validation through a third party... and guess who that third party is? Companies like Equifax now responsible for the single largest PII breach in American history.
Now they've also added something like a recovery key which the user is supposed to print out and save at the moment they create their account, and that key is used to separately encrypt all the PII. So if the user forgets their password but can find this magic piece of paper, they can enter this key (that'll be fun) as part of the password reset process and reset their password without effectively wiping their account. To think "Average Users" will succeed in doing this belies reality.
But wait... where is this magic key being saved on login.gov servers? If it was a private key they give to the user and they just keep the public key on their side that could work, but it's actually an uppercase 16 character alphanumeric token... 5.17 * 16 = 82 bits. Let's hope that's not literally the encryption key, but the alternative -- it's a token used to lookup the key in their database -- would completely defeat the purpose of encrypting the PII with a PBK in the first place?! And, even if it was the literal encryption key, when you change or update your PII, if they don't also keep it on their side, how would they update their secondary encrypted PII record?