Same question asked here in 2014 but since a lot has changed since then, I would like to get feedback from experts on the latest best practices.
We need to hash credit card numbers to use as fingerprints. We’re aiming for the same strategy Braintree uses according to John Downey answer (the accepted answer) here, I quote:
“In the Braintree API, we offer a unique number identifier for a credit card. This identifier is a random opaque token that will always be the same for card number stored in our system. The seed for this number is different per merchant in our system so you cannot compare them across merchants.
When a new card comes in, we look it up by comparing it to a hashed + salted column. If it matches that existing column we know we can return the same unique number identifier. If it doesn't match any existing record, we use a cryptographically secure pseudo-random number generator to create a new unique number identifier and ensure it doesn't conflict with an existing one.
This way the hashed + salted value never leaves our backend but we can still provide a way for a merchant to uniquely identify stored credit cards.”
All the PCI Compliance side is known and covered, our current concern now is with the hashing part. Let’s clarify that we need the hashes to be merchant specific so “salting” with a merchant specific secret key seems like a good option (we’re also aware of the problem of safely managing merchant secret keys), this will allow the merchant to check whether two customers are using the same card number. So far we’ve considered the following alternatives:
Use HMAC using the merchant specific key as the secret cryptographic key. For the cryptographic hash function we must use a cryptographically secure one so we went for SHA3, but then we realized that SHA3 can act as an HMAC by design so, as explained here there’s no need for the HMAC nested construction.
Hash the card number salted with the merchant specific key using SHA3. This seemed like a good option, as long as one uses the strongest version of SHA3 and a long enough key. But then our concern went against brute force attacks in the event of the database being stolen. After some research we found that re-hashing the resulting hash n times would help make the attackers task slow and in the best case scenario even infeasible as long as it doesn't slow down our service. So we went for it and re-hashed our hash n times. But then we were curious about how exactly re-hashing increases security and found out that, even though re-hashing itself is a good idea, implementing your own non-standard hash schema, without understanding what features such a scheme needs in order to be secure, is not. A better option is to go for a hashing algorithm that does re-hashing by design like PBKDF2, Scrypt or Argon2id.
Use PBKDF2 to derive a key from the card number salted with the merchant specific key, setting a large number of iterations and choosing SHA3 as the hash function.
Use Argon2id, the current winner and recommendation of the password-hashing competition, with proper parameters for time, memory, and threads but we're not sure yet about how to properly tune those parameters. It would be really helpful to read your comments on how to do it.
We are debating between options 3 and 4, on the one hand PBKDF2 seems more widely used but on the other there are several resources claiming that it is no longer secure for today's hardware capacity and strongly recommending the usage of Argon2id.
So, what strategy would you use for this scenario? What flaws do you see in the previous options? Would you go for PBKDF2 or Argon2id?