Going by the login spec here: https://github.com/18F/identity-idp/blob/master/docs/encryption-and-key-rotation.md
And the code here: https://github.com/18F/identity-idp/blob/master/app/services/user_access_key.rb
Many aspects of this make no sense to me... IF this is actually what you 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 the 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 you were not storing 'd' in the user record, you might think to store what you 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 if 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 they make at cracking a password. Which means you send your password hash (or something derived from it) through the HSM, not just a random 'R'!