2

I am in the process of developing an iOS app where a customer can earn rewards (money) to spend back at the business. When the customer wants to spend the money they have earned, the cashier will use the employee app to scan the customer's QR code on the customer app. To clarify, yes, there are 2 apps. One for the customer (where their identifying QR code is) and one for the employee to scan the QR codes and process the payment. My question is, how can I make this transaction secure (or as secure as possible)?

Here is what I am thinking as a potential plan, please correct me where I am wrong:

  1. When the customer logs in, their credentials are stored in Keychain (of course password is encrypted): customer_id, customer_email, customer_password.

  2. Along with these 3 separate identifying pieces of information, I will also send an encrypted string consisting of those 3 pieces of information. This encrypted string will end up being the QR code value. For example, the encrypted version of "customer_id|customer_email|customer_passord" or "19302|example@gmail.com|password123". The | are used to separate the data so it can be processed later.

  3. I will encrypt this 3-piece identifier server side since doing any encryption on the app side (client) is probably risky. So the following 4 pieces of data come to the user's app: customer_id, customer_email, customer_password (encrypted), 3_piece_identifer (encrypted).

  4. Now the user has the encrypted 3_piece_identifer in their Keychain in the app. This 3_piece_identifer will be the QR code that will be scanned by the employee. Once again, 3_piece_identifer is "19302|example@gmail.com|password123" encrypted just as an example.

  5. So now the employee uses the employee app to scan the customer's QR code (which resides on the customer app). The employee just received the encrypted 3_piece_identifer.

  6. Still on the employee app side, the 3_piece_identifer is now decrypted and processed server-side then check in the database for this customer's additional information like current balance.

Am I doing it completely wrong?

  • NOT encrypting app-side is risky (because you are sending sensitive data to the server unencrypted)! Why don't you trust your own app (with a proper crypto library I hope) to do the encryption? – NH. Oct 12 '17 at 20:55

2 Answers2

7

I think you are protecting against your threat the wrong way, and I think you're also missing out on protecting against some additional threats.

Hashing vs encryption

First and most important: the end user's password should never be encrypted. It should only ever be hashed. Encryption is reversible but hashes aren't. In the event of system breaches, you don't want someone to be able to decrypt an encrypted user password. That could just be a simple matter of accidentally using the wrong terminology: its easy to confuse the two.

The goal

Before jumping in, it helps to reiterate the big picture: what are you trying to do here? You are trying to make it easy for a kiosk to identify a user securely so that they can use a pre-paid account balance.

Breaking it down

The simplest solution would be to simply use the user's unique id to build the QR code. This would get the job done very easily. The problem with that (which your solution is trying to solve), is that the user id is probably guessable, so someone might easily figure out how to build your QR codes and start generating them for random user ids until they find someone with lots of money on their account.

Your use of the three piece identifier is designed to circumvent this problem. The goal is to make it so that only the actual user is able to generate the QR code and use their money. The issue I see is that you have used the user's password in the QR code generation process. This has the benefit of making it hard for anyone other than the user to generate the QR code, but it has a big disadvantage of increasing the "surface area" for the user's password to be stolen. Inherently, the more you use the user's password, and the more it transfers back from client to server (in any form), the more opportunities there are for a malicious user to steal it and use it to break into the actual account. Granted, doing so would require understanding the form of your 3_piece_identifier and then doing a brute-force on the password, but such things are not outside of the realm of possibility, even if it isn't the highest on the list of concerns.

An important attack vector

However, your scenario doesn't protect against a simpler and much easier attack vector: simple replay attacks. Nothing in your scenario ever causes the QR code to change, so if anyone could simply get a picture of the QR code created by the authorized user, that is all that would be necessary to pretend to be them in the future. It could be as simple as literally taking a picture of someone else's phone while they have the QR open (maybe the person standing in front of you in line?) and then opening up that picture from your phone's gallery, and scanning the picture of the QR code when you checkout. Unless the people running the cash register are paying attention (hint: they usually don't), you get to use someone else's balance with little effort.

One possible way to solve all of this

As a result, what you have isn't going to work (I don't think). I think you should not bother using the password as part of the verification, and you need something to protect against replay attacks. It can actually be very simple: when the user loads up the QR code on their phone they can hit up an API server-side to request a one-use key. This will be generated server side, and it can even just be a completely random string: it doesn't have to include the user's email/id/password, but instead is simply associated with that user's account. Make sure you and use a cryptographically secure random number generator, of course. Make it nice and long (as long as it fits well in a QR code), make sure it is unique, store it server side associated with the customer along with an expiration time (no more than a couple minutes even), and then send it down to the client. The client application turns it into a QR code, the employee app scans it, sends it back up, the server verifies that it is valid, that it hasn't expired, and can now bill the client's account properly. As long as the API endpoint that generates the one-use-key requires a logged in user to work, everything should be nice and secure (or at least, as secure as the user's account is). To recap:

  1. User logs in
  2. User requests to pay with the account balance
  3. Client sends up request of single-use-key
  4. Server generates (via CSPRNG) a unique single-use-key and stores it with the user's account, along with an expiration time
  5. Server sends single-use-key back down to client
  6. Client turns single-use-key into QR code
  7. Employee app scans QR code and converts back to single-use-key
  8. Employee app sends up single-use-key to server
  9. Server verifies key's validity and can now charge the proper account

I would also be curious to see what suggestions other's may have.

Conor Mancone
  • 29,899
  • 13
  • 91
  • 96
  • I left it out of my answer, but make sure you aren't falling victim to the X-Y problem. You've limited your question to securing QR codes, but there are lots of ways to bill an account balance without having to resort to QR codes. There are probably completely different solutions that can be equally easier for the user and cashier while also being more secure. – Conor Mancone Sep 08 '17 at 14:44
  • Thank you for your very detailed post. I know you said the expiration of the one-use key should be around a few minutes, but what about the users that aren't able to connect to the internet to make the request for payment key (they don't have data and business might not have wifi)? Do you think it is a stretch to push the expiration to maybe an hour? Or even 30 minutes? That way the customer can request the key and get the QR code at home and then just come in and show it without having to connect to the internet at the business. Thanks again. – user3451821 Sep 09 '17 at 04:13
  • Virtually all security measures are a balancing act between security and usability. As a result, there isn't really one right answer to your question. Instead it is best to think of it from the perspective of "will the benefit this feature (in this case, a longer expiration window) has for my users be worth the increased risk?". To answer that you need to understand what the risks really are. In this particular case the risks created by a longer expiration are probably low, but only you can decide what level of risk you are willing to accept. It might not hurt to read up on risk assessments. – Conor Mancone Sep 09 '17 at 10:59
  • Thank you very much for your time Conor. Would upvote your answer if I had the reputation to do so, so for now I can only accept it. – user3451821 Sep 09 '17 at 12:17
1

Conor's solution is pretty great, but have draw backs. For example, the question that @user3451821 asked in the comment section: What should I do if the customer don't have access to the Internet. Conor's solution can't solve this. Another concern is, the server is required to do too much stuff. From generating one-time password, store it in SQL server, push it to customer device, then pull it out to verify, finally delete it and deduct the balance. This takes up a lot of compute and network resources, especially you have large number of customers using your service at the same time.

So I bring a little improvement but require a bit more experience in cryptography and tweak the processing flow but worth for decreasing the server workload, and eliminates the use of the Internet (at the client side, not during registration or log-in phase).

Registration

  1. select "create new account"
  2. fill the required data
  3. User's devices generate a Ed25519 key pair
  4. upload key pair's public key to server
  5. server save the public key along with other user details

when user want to use the balance/credit

  1. user open the app
  2. Select "generate my QR code"
  3. sign the user id, nonce and time stamp with the key pair private key, like so: signature=SIGN("user_id|time_stamp", priv_key)
  4. put user_id-timestamp-signature or any structure into a QR code and display it. Note, you must use the same timestamp that used in the signature process, and must pass all 3 arguments(user id, timestamp and the signature) to the server for verification purpose.
  5. employee scan the QR code, sent it to the server for verification
  6. server pull the public key from SQL server, and verify the signature
  7. if all good, deduct the balance in the user's account, and return a success value to employee's app

In this solution, the server is only responsible for store & read the user's public key, and preform a signature verification. Nothing more.

Hartman
  • 426
  • 2
  • 11
  • That's an interesting approach that would not have occurred to me: thanks! A couple thoughts off the top of my head: what purpose does the nonce serve? Since it is generated client-side I don't see any circumstances in which it would increase security. Seems like it is just a filler. Also to clarify, you have to send the timestamp, nonce, and signature up to the server, right? There are plenty of ways to encode all of that in a QR code, but it is worth pointing out that the signature alone is not enough. – Conor Mancone Sep 11 '17 at 19:19
  • My only actual complain (which is very fixable) is the permanent nature of the private key, although I'm not as familiar with mobile applications: they might be considered secure enough that this doesn't matter. However, if the private key were stolen (normally for web apps I'd wave my hand here and say "XSS"), you need a way to de-authorize it. Otherwise, it is effectively a permanent password, which makes it extra dangerous. Resetting your password won't stop an attacker from using a stolen private key. – Conor Mancone Sep 11 '17 at 19:22
  • Oh yeah, the nonce is for other use in my original design, but now is useless. And you're right about the arguments pass to the server not only the signature but also the original user_id and timestamp. – Hartman Sep 11 '17 at 19:25
  • With the dangerous of the private key, it must handle with care, add few layers of protection on it. If it was compromised, it can be regenerate at any time or required to regenerate when changing the password. It can be very flexible and up to the developer. – Hartman Sep 11 '17 at 19:35
  • Or we can take another approach like TOTP, it is easier to manage and require even less compute power compare to async cryptography. – Hartman Sep 11 '17 at 19:39
  • I like the idea of forcing a private key regeneration on password change. That is definitely one of those tricky parts: on the one hand you have to be able to regenerate it when necessary, but on the other hand your key-regeneration process will be a prime target for attack. What it really comes down to though is that when you are dealing with people's money, it is very important to be careful. – Conor Mancone Sep 11 '17 at 19:43
  • Let us [continue this discussion in chat](http://chat.stackexchange.com/rooms/65444/discussion-between-2awm366-and-conor-mancone). – Hartman Sep 11 '17 at 19:53