views:

780

answers:

13

We have a system where we want to prevent the same credit card number being registered for two different accounts. As we don't store the credit card number internally - just the last four digits and expiration date - we cannot simply compare credit card numbers and expiration dates.

Our current idea is to store a hash (SHA-1) in our system of the credit card information when the card is registered, and to compare hashes to determine if a card has been used before.

Usually, a salt is used to avoid dictionary attacks. I assume we are vulnerable in this case, so we should probably store a salt along with the hash.

Do you guys see any flaws in this method? Is this a standard way of solving this problem?

A: 

Yes, comparing hashes should work fine in this case.

Ben Hoffstein
A: 

A salted hash should work just fine. Having a salt-per-user system should be plenty of security.

Chris Marasti-Georg
I agree. Keep in mind that this makes registering a credit card an O(n) operation where n is the existing number of cards registered. Plus hashing is a fairly expensive operation.
Johan
+1  A: 

Comparing hashes is a good solution. Make sure that you don't just salt all the credit card numbers with the same constant salt, though. Use a different salt (like the expiration date) on each card. This should make you fairly impervious to dictionary attacks.

From this Coding Horror article:

Add a long, unique random salt to each password you store. The point of a salt (or nonce, if you prefer) is to make each password unique and long enough that brute force attacks are a waste of time. So, the user's password, instead of being stored as the hash of "myspace1", ends up being stored as the hash of 128 characters of random unicode string + "myspace1". You're now completely immune to rainbow table attack.

amdfan
Yes, but if you use a different random salt for each hash, you need to try every salt on the new value you want to compare. Use a long random but constant string + expiration date.
MattW.
If you are using the expiration date in the salt, how is it constant?
amdfan
+2  A: 

Almost a good idea.

Storing just the hash is a good idea, it has served in the password world for decades.

Adding a salt seems like a fair idea, and indeed makes a brute force attack that much harder for the attacker. But that salt is going to cost you a lot of extra effort when you actually check to ensure that a new CC is unique: You'll have to SHA-1 your new CC number N times, where N is the number of salts you have already used for all of the CCs you are comparing it to. If indeed you choose good random salts you'll have to do the hash for every other card in your system. So now it is you doing the brute force. So I would say this is not a scalable solution.

You see, in the password world a salt adds no cost because we just want to know if the clear text + salt hashes to what we have stored for this particular user. Your requirement is actually pretty different.

You'll have to weigh the trade off yourself. Adding salt doesn't make your database secure if it does get stolen, it just makes decoding it harder. How much harder? If it changes the attack from requiring 30 seconds to requiring one day you have achieved nothing -- it will still be decoded. If it changes it from one day to 30 years you have achived someting worth considering.

Jeff
Ah - and if SHA-1 is so expensive that adding that salt will in fact cost the hacker years then it is also going to be so expensive that you'll probably be suffering to add to CC number 10,000. But you can always just test it vs your expected customer base size and see if it hurts of not.
Jeff
MiniQuark
-1 MiniQuark is right. This scheme is inferior to other proposals made here. The fact that credit cards might simply be guessable is not addressed here.
Accipitridae
A: 

SHA1 is broken. Course, there isn't much information out on what a good replacement is. SHA2?

Cory R. King
Both SHA1 and MD5 are only broken in that it's possible to generate two strings with the same hash. Neither is broken when it comes to calculating preimages for a hash, or when it comes to generating a string with the same hash as a known string.
Nick Johnson
A: 

If you combine the last 4 digits of the card number with the card holder's name (or just last name) and the expiration date you should have enough information to make the record unique. Hashing is nice for security, but wouldn't you need to store/recall the salt in order to replicate the hash for a duplicate check?

Rob Allen
yes you would - the point is that table based attacks take a long time to generate. If you have a different salt for each card number, building the table will only get you, at most, 1 card. The cost is not worth it.
Chris Marasti-Georg
I was advocating against the salted hash - I think it's overkill when the OP's purpose is to simply prevent multiple instances of data that, by themselves, are not unique enough to match against.
Rob Allen
A: 

I think a good solution as hinted to above, would be to store a hash value of say Card Number, Expiration date, and name. That way you can still do quick comparisons...

Mitchel Sellers
+1  A: 

@Cory R. King

SHA 1 isn't broken, per se. What the article shows is that it's possible to generate 2 strings which have the same hash value in less than brute force time. You still aren't able to generate a string that equates to a SPECIFIC hash in a reasonable amount of time. There is a big difference between the two.

Kibbee
A: 

Sha1 broken is not a problem here. All broken means is that it's possible to calculate collisions (2 data sets that have the same sha1) more easily than you would expect. This might be a problem for accepting arbitrary files based on their sha1 but it has no relevence for an internal hashing application.

Martin Beckett
+1  A: 

PCI DSS states that you can store PANs (credit card numbers) using a strong one-way hash. They don't even require that it be salted. That said you should salt it with a unique per card value. The expiry date is a good start but perhaps a bit too short. You could add in other pieces of information from the card, such as the issuer. You should not use the CVV/security number as you are not allowed to store it. If you do use the expiry date then when the cardholder gets issued a new card with the same number it will count as a different card. This could be a good or bad thing depending on your requirements.

An approach to make your data more secure is to make each operation computationally expensive. For instance if you md5 twice it will take an attacker longer to crack the codes.

Its fairly trivial to generate valid credit card numbers and to attempt a charge through for each possible expiry date. However, it is computationally expensive. If you make it more expensive to crack your hashes then it wouldn't be worthwhile for anyone to bother; even if they had the salts, hashes and the method you used.

J.D. Fitz.Gerald
+9  A: 

People are over thinking the design of this, I think. Use a salted, highly secure (e.g. "computationally expensive") hash like sha-256, with a per-record unique salt.

You should do a low-cost, high accuracy check first, then do the high-cost definitive check only if that check hits.

Step 1:

Look for matches to the last 4 digits (and possibly also the exp. date, though there's some subtleties there that may need addressing).

Step 2:

If the simple check hits, use the salt, get the hash value, do the in depth check.

The last 4 digits of the cc# are the most unique (partly because it includes the LUHN check digit as well) so the percentage of in depth checks you will do that won't ultimately match (the false positive rate) will be very, very low (a fraction of a percent), which saves you a tremendous amount of overhead relative to the naive "do the hash check every time" design.

Wedge
That sounds like a good idea, to do the last 4 digits check before comparing hashes. Thanks!
Lars A. Brekken
Storing the last 4 digits simplifies a brute force attack even further.
Nick Johnson
@Arachnid Sadly, storing the last 4 digits is common practice for merchants. However, you could just compare against the expiration date as the first test if you absolutely did not want to keep the last 4 digits.
Wedge
+9  A: 

Let's do a little math: Credit card numbers are 16 digits long. The first seven digits are 'major industry' and issuer numbers, and the last digit is the luhn checksum. That leaves 8 digits 'free', for a total of 100,000,000 account numbers, multiplied by the number of potential issuer numbers (which is not likely to be very high). There are implementations that can do millions of hashes per second on everyday hardware, so no matter what salting you do, this is not going to be a big deal to brute force.

By sheer coincidence, when looking for something giving hash algorithm benchmarks, I found this article about storing credit card hashes, which says:

Storing credit cards using a simple single pass of a hash algorithm, even when salted, is fool-hardy. It is just too easy to brute force the credit card numbers if the hashes are compromised.

...

When hashing credit card number, the hashing must be carefully designed to protect against brute forcing by using strongest available cryptographic hash functions, large salt values, and multiple iterations.

The full article is well worth a thorough read. Unfortunately, the upshot seems to be that any circumstance that makes it 'safe' to store hashed credit card numbers will also make it prohibitively expensive to search for duplicates.

Nick Johnson
+4  A: 

Do not store a simple SHA-1 of the credit card number, it would be way to easy to crack (especially since the last 4 digits are known). We had the same problem in my company: here is how we solved it.

First solution

  1. For each credit card, we store the last 4 digits, the expiration date, a long random salt (50 bytes long), and the salted hash of the CC number. We use the bcrypt hash algorithm because it is very secure and can be tuned to be as CPU-intensive as you wish. We tuned it to be very expensive (about 1 second per hash on our server!). But I guess you could use SHA-256 instead and iterate as many times as needed.
  2. When a new CC number is entered, we start by finding all the existing CC numbers that end with the same 4 digits and have the same expiration date. Then, for each matching CC, we check whether its stored salted hash matches the salted hash calculated from its salt and the new CC number. In other words, we check whether or not hash(stored_CC1_salt+CC2)==stored_CC1_hash.

Since we have roughly 100k credit cards in our database, we need to calculate about 10 hashes, so we get the result in about 10 seconds. In our case, this is fine, but you may want to tune bcrypt down a bit. Unfortunately, if you do, this solution will be less secure. On the other hand, if you tune bcrypt to be even more CPU-intensive, it will take more time to match CC numbers.

Even though I believe that this solution is way better than simply storing an unsalted hash of the CC number, it will not prevent a very motivated pirate (who manages to get a copy of the database) to break one credit card in an average time of 2 to 5 years. So if you have 100k credit cards in your database, and if the pirate has a lot of CPU, then he can can recover a few credit card numbers every day!

This leads me to the belief that you should not calculate the hash yourself: you have to delegate that to someone else. This is the second solution (we are in the process of migrating to this second solution).

Second solution

Simply have your payment provider generate an alias for your credit card.

  1. for each credit card, you simply store whatever you want to store (for example the last 4 digits & the expiration date) plus a credit card number alias.
  2. when a new credit card number is entered, you contact your payment provider and give it the CC number (or you redirect the client to the payment provider, and he enters the CC number directly on the payment provider's web site). In return, you get the credit card alias! That's it. Of course you should make sure that your payment provider offers this option, and that the generated alias is actually secure (for example, make sure they don't simply calculate a SHA-1 on the credit card number!). Now the pirate has to break your system plus your payment provider's system if he wants to recover the credit card numbers.

It's simple, it's fast, it's secure (well, at least if your payment provider is). The only problem I see is that it ties you to your payment provider.

Hope this helps.

MiniQuark