After driving for 20 minutes to buy a microsd card, I opened the raspberry pi box to discover that a microsd card was included in the box. Yay me.
I started out by installing Ubuntu Core on the Pi, since I thought it would be easy to create a snap from the nodejs application that would talk to the pcscd service. This proved a little more difficult, so after about half a day trying I ended up installing Ubuntu Server on the Pi. Installation of the required services and nodejs environment went without issue.
Talking to the NFC tags in nodejs was pretty easy; I used a nodejs package that already handles most of the conversation between pcscd daemon and the nodejs environment, and the repository included examples on how to passport protect NTAG213s, so that jumpstarted development.
The NTAG2x password protection only allows you to set a 4-byte password, so you can’t really call it a security measure. I decided I would use the 4-byte password to discourage users overwriting the tag, but the true authentication would lay in using an SHA-256 HMAC on the content I would write. The 32 byte signature already takes quite a big bite on the 144 byte total available memory, but given the offline requirements this seems like a fair tradeoff. The password also only enforces write protection, I decided to keep all data on the card readable; since I’m planning to open-source the whole project it doesn’t seem logical to disallow users to read their won tag.
In our current ‘CatLab Drinks’ application, an organisation can organise multiple events. Our own organisation is active in multiple locations, so it makes sense to have a separate pricelist for each location. Early on we have decided that credit bought at one event would be transferable to other events, so each nfc card is linked to the organisation, not the event. So I gave each organisation their own ‘nfc secret’ which is used to calculate both the NTAG213 password and the HMAC.
Since we’re planning to allow users to topup their credit with their cellphone, I decided to encode all data in the NDEF format. This way I was able to write one NDEF record with the user specific URL for topup, while the signed wallet data would be stored in a second NDEF record. There is a very, very small overhead, but giving users with an NFC-ready phone the ability to scan their card and topup straight from their phone, seems worth it.
It took me some sleepless nights to figure out what to actually store on the NFC tags. ‘Current balance’ is an obvious one, but the requirement that not all POSs need to be online at all times makes the whole thing a little more difficult. In the end I decided to take a practical approach that – according to our sale statistics – would work out fine:
- transaction count
- timestamp last transaction
- 5x last transaction amount
All data is stored in 32bit signed integer. I briefly thought about storing the timestamp in 64bit, but it would be silly to think this format would survive another 19 years (and also I couldn’t figure out how to write 64 bit signed integer).
There are only 2 vital elements of this content: ‘balance’ and ‘transaction count’. Balance is obviously required, since otherwise it would be impossible to check if a user has enough credits to buy a drink. Transaction count, however, is also of vital importance. For starters it avoids people rewriting old data to the card. If they would do so, the POS would notice that the last seen transaction count is lower than the new transaction count, and can then dismiss the card.
(There is still a situation where an offline bar could not be in the possibility to transmit the new ‘transaction count’ to the other bars, and a user who decides to go to an online bar would be able to double-spend, but honestly this situation seems too uncommon to take into account. Also, when the offline bar would finally come online, it would upload all its transactions and the card would be charged into negative values.)
The 5 last transactions are not really required, they are just used for online POSs that don’t know about offline transactions. An online POS will always upload the card state to the server, and the last 5 transactions could then be used to fill in the gaps between the last known transaction and the current transactions. If the amount of transactions would exceed 5, a 6th ‘unreliable’ transaction would be constructed that holds the difference between the Nth and the 5th transaction.
Finally, the timestamp is not used at all. I’m not sure if it is usefull for salting purposes, but even if it’s not it might be a nice-to-have for the future.
I took some time checking the security requirements and I talking to some people about the format; it seems a reasonably secure system. So I got started and tried to write the data to the NFC cards.
One of my main concerns was writing to the tags. Writes can be interrupted at any time, and since the data I’m writing is rather long there isn’t any tear-protection available. I briefly thought about writing the balance data twice, so that there is always one record to recover from, but the space limitations of NTAG213 finally forced me to abandon the idea.
In the end I just went for storing the latest known (valid) data in the browser localstorage of the POS terminal, and throwing a big warning message whenever a write fails. This way, a user that presents their card and interrupts the write, will be asked to scan its card again. If a user would at this point walk away, he would end up with an invalid card. It will be up to my UX design to make sure that the bartenders handle this situation correctly and ask the user to scan their card again.
I also improved the NFC nodejs service to only write data that has changed since the last write, lowering the risk of tearing. Since the first NDEF message (with the topup url) will always stay the same, there is no reason to write that on every transaction.
Topup & spending
With the write-procedure implemented in a reasonably reliable way, it was now time to implement the POS side of things: allowing our own staff to topup cards and allow bartenders to sell beers… More about that in a next blog.