1
$\begingroup$

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.

The problem

Our requirement is to provide the merchants with a card fingerprint which uniquely identifies a card number. By using this fingerprint the merchant could check, for instance, if two of her clients are using the same card, or, more interesting, if someone that is not the known owner of the card is about to use it.

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

EDIT:

As pointed out by @mentallurg, fingerprint computation should be as quick as possible and contain no secret components. The problem in this scenario is it should also be opaque and secure since we can count only with the card number to compute it (not like with devices where you have several parameters like os, version, brand, mac address, etc.). Card holder name is not always provided and card expiration date could change if the physical card is replaced so we can't use them. So computing a quick fingerprint solely with the card number and providing that fingerprint to the client is not an option.

The remaining option then is to use cryptographic hash functions. By securely hashing the card number we can be certain that two card numbers that generate the same hash are actually the same cards, thus satisfying our requirements. Providing the merchant with the hash itself is still not safe, that's why we are generating a randomly secure string and associating it to the actual hash, that way, when a new card comes in, we hash it and if it matches an already existing one we can return the random fingerprint (from which no card data can be retrieved) to the merchant, without exposing the actual hash. If there are no matches then we generate a new random fingerprint, make sure it doesn't collide with an existing one and return it to the merchant.

Next question that arises is what cryptographic hash function/strategy to use. Salting the card numbers is mandatory to prevent rainbow table attacks, the problem with salts here is that they will cause two instances of the same card number to generate different hashes (this is actually a good thing when you're dealing with passwords since you don't want two equal passwords to generate the same hash) which makes it of no use for our requirements.

The best option that we can think of so far is to use a strong cryptographic hash function (tuned for a proper tradeoff between security and performance) and salt it with a unique HSM-backed secret key (or a per-merchant secret key if we want per-merchant hashes).

We are aware of all the PCI implications of saving cards numbers. So far we’ve considered the following alternatives:

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

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

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

  4. 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? Can you see some other way for securely providing a card fingerprint?

$\endgroup$
2
$\begingroup$

Questions of such kind have opinion based answers. Here is my opinion.

First review your requirements and check what do you actually want to achieve, what are the costs if you want to use key derivation (how much hardware you need for particular hashing), what are the risks if you don't use hashing.

You say you want to use card number as fingerprints. A fingerprint does not have to be resistant to computation. It is actually vise versa: calculation of fingerprint should be as quick as possible. Also it should have low collision rate. That's why often SHA256 and similar fast algorithms are used.

The algorithms like PBKDF2 and Argon2id have completely different purpose. Namely, to provide a (possibly) unique representation of a secret information and prevent retrieving this information by brute forcing.

To me this is a contradiction: You want to use key derivation algorithms for fingerprinting.

Then you are talking about merchant secret key. This again contradicts with the term fingerprint. A fingerprint should not contains any secret components. It should be possible that everyone can calculate and check it at any time.

If you mean actually key derivation, not fingerprints, and want to save card numbers without disclosing them, then I'd suggest to check if it makes sense in your case. Because brute forcing card numbers is not so hard. The first up to 4 digits identify the card system and are not random. One digit is a check sum and is also not random. The remaining 11 digits can depending on the bank be also not random. But let us assume they are random.

How secure is a 11-ditig number? 11 digits mean 37 bits. Guessing such a number is approximately like guessing a 6-character password consisting of low and upper case letters and digits. As secure are considered passwords that have about 90 bits entropy.

I assume your application is running on a hardware with usual CPUs. I assume your customers will accept delays around 1 sec, not 1 minute and not 1 hour. Suppose an attacker uses GPU that 100 faster than your CPU. There are 86 400 seconds in a day. So an attacker can test 8 640 000 numbers per day per GPU. To check all 11-digit numbers he will need $10^{11} / (8 640 000) = 11574$ days. For a small home farm with 100 GPUs it will take 115 days. For a bigger farm with 10 000 GPUs this will take just around a single day. This is true if there are sufficient RAM.

Suppose you want to use more RAM for each hash / key derivation. How much can you use? Think of how many requests you want process simultaneously and how much RAM you have. If you want to support 1000 simultaneous requests with card hash calculation and you have 128 GB RAM, then obviously you can use not more than 128 MB per hash. If your application needs the most of the RAM for other purposes, then for hashing you will have even less RAM. Which means an attacker needs less RAM, too.

That's why again I'd suggest you to review your requirements and to decide what is actually your goal.

To other questions: Comparison of PBKDF2 and Argon2 was already discussed here on StackExchange. Widely used is not always a good argument, think of MD5 for instance. In many cases MD5 can sill be used. But if consequences of hash collision have a high price, one should use other algorithms.

For tuning of parameters consider following constrains:

  • What maximal response time can your users accept?
  • How many total requests you need to support simultaneously?
  • How many hashing requests you need to support simultaneously?
  • How many CPU and RAM you need for this?
  • What is the budget available for hardware/cloud, for employees that will maintain it?

If you calculate that and discuss with your merchants or product owners, may be they will decide that costs are much higher than possible benefits of having the function they want (checking if credit card was used in other cases).

| improve this answer | |
$\endgroup$
  • $\begingroup$ The term "fingerprint" is indeed confusing in this context. I presume the OP might be talking about what is more commonly known as "Tokenization", See Tokenization_(data_security). However, I'm not 100% sure and I would wait for their clarifications. $\endgroup$ – tum_ Apr 1 at 9:03
  • $\begingroup$ Thanks for your answer and comment. I will edit my question to clarify your doubts since I have not enough space in comments. $\endgroup$ – KurtWegner Apr 1 at 17:24
0
$\begingroup$

All the PCI Compliance side is known and covered, our current concern now is with the hashing part.

Given that tokenization is part of PCI, I don't think you do have "all the PCI compliance side known and covered". PCI offers guidelines on tokenization. You should read them.

Probably the sanest option is to use HKDF with an HSM-backed secret key. Per-merchant unique tokens can be handled with by using a per-merchant salt parameter. Please do the sane thing and do this all on an HSM. If the key is known to an attacker, it's trivial to brute-force the entire search space.

| improve this answer | |
$\endgroup$
  • $\begingroup$ it's trivial to brute-force the entire search space - exactly. $\endgroup$ – mentallurg Apr 2 at 18:16
  • $\begingroup$ Thanks for your input and sharing the PCI guideline. You're totally right about handling the secret keys on an HSM. I'm curious, why choosing HDKF instead of other key derivation functions? What would be the advantages over using Argon2id for instance? $\endgroup$ – KurtWegner Apr 2 at 21:18
  • $\begingroup$ First, I'm not aware of any HSM that supports Argon2. Second, tokenization doesn't need to be slow. Attackers can't enumerate the search space if it's HKDF being done on an HSM. $\endgroup$ – Stephen Touset Apr 5 at 1:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.