12

I'm working for a company that is generating gift card codes which can be used to pay for goods on online stores.

I'm wondering what the most secure way of generating these gift card codes are. The length needs to be 16 characters (though that is negotiable) and can be alphanumeric (though numeric would be more customer friendly).

From what I can see, the most secure way to do this is generate a gift card code of a specific length with the following Java code:

static final String AB = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
static SecureRandom rnd = new SecureRandom();

String randomString( int len ){
   StringBuilder sb = new StringBuilder( len );
   for( int i = 0; i < len; i++ ) 
      sb.append( AB.charAt( rnd.nextInt(AB.length()) ) );
   return sb.toString();
}

This is taken from the Stack Overflow answer here. I removed the lowercase letters from the string to make it more user friendly. So this produces 36 ^ 16 combinations. Numeric alone would be 10 ^ 16 combinations. I believe numeric alone would be enough but it's often stressed that, given the increasing prevalence of gift card fraud, the string should be alphanumeric. So that's question one: numeric or alphanumeric?

When users use the gift cards on an online store to pay for goods, a call is made to our API which returns the balance and currency for that gift card. Given that the gift card codes are entered on 3rd party servers, these gift cards are now available to people with access to those servers. This is obviously a problem in the case where there is still a balance left after a user has partially redeemed one.

One option would be to, when the call to our API is made (with the gift card code) to get the balance, we return and save on their store a random string which can only be used by the online store when they are billing us - we will match that with the gift card code on our system. The problem with that is presumably the gift card code the user enters on checkout gets logged somewhere in their logs, and is accessible to anyone with access to those logs.

Another option is that we refresh the gift card code after it is partially redeemed. So the user essentially gets issued with a new gift card code for the balance and the previous one is cancelled. This is probably the most secure, but not that user friendly. So that's the second question: how do we secure gift card codes that are only partially redeemed and still have value left on them?

EDIT:

We also have a Check Balance page where a user enters a gift card code and the currency and balance are returned. This presumably creates some additional security concerns.

Mark
  • 181
  • 1
  • 10
  • 5
    If you use alphanumeric, please remove 0 and O from the list. having to retype a 16 character string because you couldn't differentiate this character is painful – Sefa Apr 09 '18 at 08:27
  • Same for 1 and I, unless you choose a font that for both the cards and the user interfaces that clearly distinguishes between the two. – YLearn Apr 09 '18 at 12:45
  • 1
    I must write a bot to upvote your comment sefa. – Mister Verleg Apr 12 '18 at 10:14

5 Answers5

8

To prevent fraud, you need a sufficiently low probability of the attacker guessing any valid code. For 1 million cards, a 10^16 code will be guessed on the average each 10^10 attempts. If your site is completely secure and your API is brute force resistant, numeric should suffice to keep that approach to fraud not cost-effective.

But it's very brittle security, and the hashing protection below will be easy to crack in a database leak with such a small keyspace. IOW, an attacker with access to your DB will be able to brute-force a lot of valid codes. An alphanumeric code will provide more robust security. You can also compromise using a pattern for user-friendliness.

Since in your system the code is effectively both a login and a password, to reduce their leakage you need to change the communication protocol to eliminate transmission and storage of plaintext codes.

The simplest protocol for that would be to store Salt1 and SaltHash1=hash(Code+Salt1), then transmit Salt1 to the client, create Salt2 at the client, generate hash(hash(Code+Salt1)+Salt2) client-side and check it against hash(SaltHash1+Salt2).

That remains vulnerable if your database is compromised, as the attacker only really needs a list of valid SaltHash1 to make purchases through your API. It's also slow as you have to hash all valid codes to check one.

To make it more robust, you can rather transform all the codes with a PBKDF like Argon2id, which is a slow and irreversible function, and only store the result. Using a slow function ensures an attacker with a leaked database cannot easily brute-force all 10^16 codes to find all that fit and simulate a legit user. You still have to trust the stores not to store the code and enforce HSTS (https-only) connections.

It's impossible to make this system secure against a rogue store that logs user input - if they have the complete code, they by definition own the card. The only defense is to only permit code redemption through a form loaded directly from your server (it can be embedded in a third-party page). If you do that, no one but the user and your PBKDF ever sees the code; the store just gets your validation response.

ZOMVID-21
  • 2,450
  • 11
  • 17
  • Thank you, this is certainly thought provoking. We hadn't considered the scenario of a user getting access to the DB – Mark Apr 03 '18 at 14:19
  • 2
    It's certainly a bad scenario, but they happen. In this case not storing the actual codes (only an irreversible derivative/secret) can make the difference between closing shop and a bad quarter. Re: Check balance - since you're doing this through your server rather than a third party, why is that a particular concern? Just make sure not to send the actual code. – ZOMVID-21 Apr 03 '18 at 15:36
  • When you say not to send the actual code, you mean send from the client to our API the result of hash(hash(Code+Salt1)+Salt2) ? – Mark Apr 04 '18 at 07:22
  • Yes. Only send the hash to compare it to yours. (Although, most practically, after splitting the code into open and secret portions, so as not to hash the whole list.) – ZOMVID-21 Apr 04 '18 at 08:13
  • 1
    Noticed you've edited the question. I can't provide an authoritative answer, but to give a really brief "exec summary": – ZOMVID-21 Apr 10 '18 at 17:09
  • 1
    Your storage solution is complex and dangerous. It suffers from a [pass the hash vulnerability](https://en.wikipedia.org/wiki/Pass_the_hash). If a user gets a dump of the database, they can connect to the service, use the `salt2` they get from the service and the `hash(code+salt1)` that they stole to login without ever knowing the real password. Just bcrypt the code before storing it in the DB. Let the user pass the code to the app via TLS and you can repeat the bcrypt on the server side to do the comparison. Not fancy, but secure and well-tested. – Neil Smithline Apr 13 '18 at 18:41
  • You're right, it's vulnerable. It was an attempt to keep solving OP's perceived problem of defending against a rogue store, but that compromises security in worse ways. I'll edit to deemphasize it. – ZOMVID-21 Apr 13 '18 at 18:51
  • @NeilSmithline we email a link to the user, when they click on it they will see the gift card code. It sounds like if we use bcrypt, we'll need to regenerate a gift card code each time they click on the link, brcypt it and save in the DB, then send the gift card code to the browser. There's no way to avoid regenerating the gift card code each time? – Mark Apr 25 '18 at 13:29
1

Given that the gift card codes are entered on 3rd party servers, these gift cards are now available to people with access to those servers.

It's true that the server admins could access the gift cards, but they can also access the credit card numbers users enter in the online stores. If you can trust an online store to collect a credit card number without doing anything nefarious, why can't you also trust them to collect a gift card ID? (Or maybe you're thinking that when an admin steals CC numbers, that's not your problem, but if they steal gift card codes, it is your problem...) Maybe properly vetting your merchants needs to be part of your business model.

Your idea of changing the gift card code after each purchase should work, but how do you tell the user what their new code is? Perhaps you could email them notifying them of the transaction, include their remaining balance, and provide a link for them to login and obtain their new gift card ID. I agree though, that could be somewhat annoying to some users. It really would depend on how trustworthy the merchant appears to be, and the user might know best. Perhaps you could allow the user to decide if they want to enable the reset option, and maybe even allow them to set a threshold above which it occurs (say $50 for example).

TTT
  • 9,122
  • 4
  • 19
  • 31
  • That's true. We have a contract in place with the merchants guaranteeing the non-storing of voucher codes etc – Mark Apr 25 '18 at 10:22
1

I believe there is a foundational issue that you cannot address technically. Most established anti-fraud measures reduce the risk, but you must accept that fraud will happen.

This is fundamentally a trust issue. You are trusting the stores to debit the correct amount from the gift card for each transaction and to only post transactions authorized by the user. You are also trusting that the user will keep the card secret.

Trust issues require auditing. As with credit cards, there is a potential for misuse by 3rd parties. Many of the PCI DSS rules surrounding credit cards are designed to minimize the risk of fraud, but even with those rules the credit card industry faces a substantial risk of fraud. To address that risk, the bank monitors for signs of fraud, and statements are available to cardholders so they can also review the account activity for unauthorized transactions. You cannot eliminate the risk of fraud, so you must monitor and report on activity---and allow users to do the same.

Vigilance and responsiveness are the only long-term solutions. In spite of reasonable security measures, you cannot prevent fraud entirely. If this were possible, the credit card industry would save billions of dollars annually. They couldn't do it, and your company probably won't either. Your company and the users must watch for abuse and take corrective action as needed. If your company issues gift cards, they must commit to anti-fraud staffing for the life of the program. Even if you do not pursue a legal case, you will be expected to provide a remedy to your users. Someone must be empowered to investigate and adjudicate fraud claims.

There are established security measures that you can implement. Most gift cards start with a zero balance, and/or they are inactive until sold. The account number or CVV should be hidden until sold (either sealed in a tamper-evident package or behind a scratch-off). At the point of sale, activation is based on a serial number rather than the gift card ID---and the gift card ID is not derived from that serial number in any discernible way, or vice versa.

Secrets are shared, and that breaks most security. With gift cards and traditional credit cards, the users supply 3rd party sellers with everything necessary to impersonate them. This is a stark contrast with blockchain-based systems which allow users to keep their secrets confidential. When you are dealing with a fundamentally insecure system, you have to implement compensating controls or mitigations for those risks. Then you accept whatever risk remains and soldier on.

DoubleD
  • 3,862
  • 1
  • 6
  • 14
1

In the credit card world, each online transaction where a customer types in their card numbers into a browser, require the card number AND the CVV2 code. This is typically a 3-digit code printed on the back of the card. The merchants are advised to NEVER store this number, but can keep the card number (under very tight restrictions).

The analogy is that the card number is like a Username, while the CVV2 is like the password (sort-of, maybe!). Your issue seems to be that you're only relying on card numbers, which is like relying on usernames only for security. This is a problem, there should be someway to authenticate the card, the card number is merely an identifier.

My suggestion is to add some sort of PIN to the transaction. The customer must provide the PIN in the API call before it can be approved. That PIN must never be stored on the merchants logs. If you're worried about this, try creating a One-Time-PIN (OTP) that you sms the customer when a transaction is being made, but this adds a heap load of complexity, but also removes the risk of merchants re-using the card for another transaction--since they'll never have the OTP.

Changing the card number is an even more difficult thing to do -- and probably not the best customer journey, I wouldn't advise it.

keithRozario
  • 3,571
  • 2
  • 12
  • 24
  • Just looking at these solutions now. I think a OTP and an sms is a viable solution. Certainly at least the introduction of a PIN will be necessary – Mark Apr 25 '18 at 08:33
  • I talked with the higher-ups and an OTP is a non-runner. Having a PIN and explicitly stating in contract with merchant that it cannot be stored or logged is a good solution though - and making them liable if they do – Mark Apr 25 '18 at 14:11
  • @Mark to be fair, that’s exactly what PCI-DSS is. Although there’s way more funky crypto to protect the PIN, the systems at the merchants end know the card number etc, and are required to purge them when no longer used :). Just curious why OTP was not chosen, was it cost, complexity or user-experience? – keithRozario Apr 27 '18 at 13:26
  • Because the merchants - these are big well-known online stores - do not want the checkout process interrupted in any way, lest it brings down conversion. Ya realised that re. PCI rules recently. – Mark Apr 27 '18 at 14:11
0

I went through all the replies and came up with a solution, taking into account the suggestions:

  • We generate a link to send to the user. The key sent in the link is a random alphanumeric string and it is hashed (MD5 or something similar) so cannot be reveversed before being saved in the DB.

  • When user clicks on link, they are redirected to our landing page, we use the key to get the order, check the status of the order and whether there is credit on it, and if it's ok we generate a 16 character length alphanumeric code and send to the UI. The 16 character code is hashed (again MD5) and saved in our database. Each time a user clicks on the link, they see a new gift card code as it's generated on the fly each time.

  • In the contract with the online store, we specify they cannot log or save the gift card code anywhere (our 2 clients are large well known online retailers)

  • On our client's online store checkout page, to pay with our gift card code, the user provides the 16 character gift card code. It is sent to our servers and the balance and a random payment I.D. is returned to the online store. This payment I.D is saved on the online store as part of the order. On order complete, the online store sends an API request (with the payment I.D.) to our servers to redeem the amount from the gift card (this functionality has been built by us and is provided to the online store via a plugin they install).

  • communication between the online store and our API is authenticated using OAuth 2.0

  • If there is a balance left on the gift card, a new gift card code is generated (user is sent a new link to get their new gift card code for the balance)

  • When the online store is billing us, they provide us with a list of payment I.D.'s, which we then match to gift card codes in our backend (then matched to our issuer).

Protects:

  • The gift card code isn't sent to an email - just the link (in our backend we can do some checks - like see of the order has expired, has the credit already been used up, before displaying the gift card code)

  • Someone with access to the online store's DB will not see our gift card codes

  • Someone who has hacked our database cannot see gift card codes (as they are hashed), nor generate the link to see the gift card codes (as the key for the link is hashed)

Let me know if any comments.

AndrolGenhald
  • 15,436
  • 5
  • 45
  • 50
Mark
  • 181
  • 1
  • 10