Passing data securely through QR codes? We have to implement a way to securely transfer data from a POS to a mobile app using QR codes.


The mobile app gets the payment parameters (amount, description, purchase_id, store_id) to create the payment intent by scanning a QR code generated from the POS,
the app passes the info to a trusted server through an API and
if the payment succeeds, it generates a QR code for the POS, including some parameters the store needs to match the payment back in the POS system.


We need to make sure that the system is secure so is not possible for an attacker to fake these QR codes.

Would this ensure the system security?


The trusted server generates a key pair and a shared secret
The commerce (POS) generates a key pair
The trusted server and the POS exchange their public keys and the shared secret
The QR code contains 2 parts: an encrypted body + signature
The encrypted body is a JSON string encrypted using the server’s / POS’s public key. The only way to decipher the body is using the private key so the structure of the message remains unknown.
The signature is a HMAC-SHA256(body, shared-key). So we can verify the integrity of the body once decrypted in the server or POS.


Does this make sense?
If the POS system is online, then its simple. Just have a purchase ID in the QR.
The POS will call your API, and "create" an unpaid purchase.
This purchase ID is encoded into an QR.

Then the customer scans this QR, and approves payment. You don't even need to display a returning QR, because the POS could poll the API at regular intervals and check when the ID is "paid".

If you want to be able to have an offline POS, its also pretty easy. You need a shared key, that is unique per physical store location (store_id must also be unique per store location, store_id is then used by the payment server to look up the correct HMAC key).

If a store accidentially leaks their shared key, they must replace it immediately, else they risk customers creating fake QR codes to get products for free.

The store have a QR code with:
amount + "-" + description + "-" + purchase_id + "-" + store_id + hmac_sha256(amount + "-" + description + "-" + purchase_id + "-" + store_id + "-unpaid-", SHARED_KEY)

You send this to server. Server validates the hmac, and then, upon payment, creates the following QR:
hmac_sha256(amount + "-" + description + "-" + purchase_id + "-" + store_id + "-paid-", SHARED_KEY)

Assuming a shared hmac_sha256 key of: SHAREDKESHAREDKESHAREDKESHAREDKE

First you create the purchase:
100-USB memory-0001-1212-475d1fa90f24e134e76ffcc075ad3e7ce2c76e1ae78c7c8408c7508882ee9952
(89 characters)

Then app sends this for payment.
When payment is done, you create the following QR:
0899dd28bb8e5cfe6d54deb0000df01cec88f65b85a0b4148d7b49dddd2453d9
(64 characters)

NOTE: The original purchase data is NOT needed in the "reply QR" - the cash register already have this data from the pending transaction, that it can use to check the reply QR. So the "reply QR" becomes much smaller and more easier to scan.

NOTE2: Even if someone learns the structure, its still a impossibility to create a valid reply HMAC from the request HMAC. Changing "-unpaid-" into "-paid-" would require knowledge of the shared key.

NOTE3: purchase_id must be unique among a single physical store location (store_id). If a store is generating duplicate purchase_id's, a customer can reuse a paid order to get products for free. If a store location has multiple offline POS not even connected with each other, the store, which also chooses purchase_id, must prepent the POS location to purchase_id, eg have purchase_id 10000-19999 for POS 1, 20000-29999 for POS 2, and so on. In my solution, purchase_id can be of any length required for the purchase_id not to roll over.

You really don't need to mix in RSA at all.

Actually, you don't need an HMAC for the request at all (but its ALWAYS required for the Response). Its just a customer protection so any scan errors or any sheninigans doesn't cause a customer to do a payment for a non-existing purchase.
The reason is that the request isn't security-sensitive, is because if the customer edits the purchase request, the response QR for the paid purchase will not validate, and the hacking customer will not get anything out of it.

But you must atleast have some error protection so not a damaged QR display or strong light falling on QR-code or the phone camera causes it to generate a purchase request, which when paid for, will not release any products, and thus cause a legit, fair customer to lose money. Thats extremely unlikely however, as QR code contains their own checksums, but feel free to add any checksum you like to the request. It can be HMAC, a md5 hash, a CRC32, whatever. Even a truncated hash suffices, if you must keep the request size down to not cause too large (and unscannable) QR codes.

The customer editing a purchase request to for example try to get products cheaper, and then getting a invalid response QR back, would be equvalient of a customer taking a 20 dollar bill, and trying to make a 100 dollar bill out of it, failing the fraud attempt, and thus losing the 20 dollar bill. So basically, any protection on the request is only for customer protection against themselves, which you can skip if your situation does not require it.