When studying MIFARE® DESFire® EV1 communication, one may notice that these cards offer both secure AES authentication and the Secure Messaging feature. This means that some operations (sometimes) could be performed securely even when there are some untrusted proxies/sniffers between the communication sides (namely the card and the readers).
First, let’s talk a little bit about what are the abovementioned features and what is the profit of using them.
Note: MIFARE and DESFire are trademarks of NXP B.V. This site is not affiliated with NXP.
EV1 Authentication
The authentication between the card and the “reader” is a variant of the shared-secret challenge-response scheme. To have a successful authentication, both sides must know the proper authentication key for a DES/3DES/AES symmetric cipher. The key is not disclosed in plaintext.
Instead, the two sides of communication exchange encrypted messages. Both communication sides prove each other that they can encrypt/decrypt with the cipher (=> implies knowing the valid authentication key).
Finally, they agree on some (random) session key that could be used in some further operations that require cryptographic signing/encryption. This protocol makes it impossible to decipher the generated session key out of sniffed communication unless the attacker knows the authentication key.
Note: After the authentication process, the commands are still sent in plaintext. For ordinary operations like file read/write, it is required to know the session key only if the secure communication mode is enabled for the particular file (which is described in the next section). Also, note that the EV2 secure messaging provides much better security, but we are not going to describe it here.
EV1 Communication modes
Plain mode
The file is read or written normally, using unprotected plaintext.
MACed mode
The payload for reading/writing operations is provided in plaintext but with MAC (Message Authentication Code) appended to the end. This means that:
- when writing to a file, the reader must properly sign the command or the operation would fail;
- when reading a file, the card would append a signature which may be then verified by the reader to ensure authenticity/integrity;
The MAC is calculated using a custom CMAC algorithm which uses a session key cipher in CBC mode. What is unusual about this CMAC is that it’s stateful - the IV is retained between operations. Thus, the MAC of the operation depends on all previous operations since the last authentication.
APDU-wrapped packet structure:
90 0C 00 00 0D 01 01 00 00 00 [8-byte MAC] 00
^^ ^^ ^^ ^^^^^^^^^^^
|| || || value
|| || file number
|| data length
operation "credit file"
Encrypted mode
The value to be read/written, together with its CRC32 (and zero-padding) is CBC-mode encrypted with the session key.
APDU wrapped packet structure:
90 0C 00 00 11 01 [encrypted_packet] 00
^^ ^^ ^^
|| || file number
|| data length
operation "credit file"
where the encrypted packet
contains the encrypted credit value and CRC32 of the whole command (INS byte + file ID + credit value).
Over-the-air services
TLDR: What it is all about
The abovementioned features of MACed or encrypted messaging may be leveraged in order to implement over-the-air services. Such possibility was already docummented by NXP in Application Note 12113 “Over-the-Air top-up with MIFARE DESFire EV2 and MIFARE Plus EV1”.
AN12113 suggests that one could implement a mobile application, that would establish an (untrusted) proxy between a tapped card and the remote server. The remote server will then “talk” to the card in a secure manner in order to perform some operations, e.g. increase the stored credit value.
Important: With this approach, the smartphone application doesn’t need to know the card’s authentication keys.
Implementation recommendations
Although the AN12113 documents’s title suggests that the implementation is possible only for DESFire EV2+, in the section 3 it was stated that:
An over-the-air service, like for example a value top-up which will be the main discussion point of this document, can be realized for already existing MIFARE DESFire infrastructures and of course for new, upcoming MIFARE DESFire system installations.
also in the subsection 3.1 it’s written:
For authentication, it is recommended to use Cmd.AuthenticateAES and the EV1 secure messaging or the new Cmd.AuthenticateEV2First and the EV2 secure messaging.
Because EV1 secure messaging and Cmd.AuthenticateAES are both supported on Mifare DESFire EV1, it should be possible to implement a concept that is similar to what was described in AN12113.
Topping-up a travel card from own smartphone
The concept of over-the-air services may be used in a lot of different purposes, for instance to allow passengers to (securely) top-up their travel card from their own smartphone.
As a proof-of-concept, I have implemented an Android application together with a Python 3 server backend in order to show how over-the-air top-up could really work.
Server: transactional logic in EV1
It is extremely tricky to properly implement transactions. When designing a top-up system, one must carefully consider all the possible edge cases. As the smartfone is considered as an “untrusted proxy” in between, some commands may be arbitrarily appended, altered or completely ignored. In any case, this should not affect the system in any dangerous way.
Important: The provided PoC is equipped with some mitigations, carefully check in the source code.
Some questions to consider:
- what if the client-server connection would fail in (arbitrary moment)?
- what if the client-card connection would fail in (arbitrary) moment?
- what if multiple clients would connect, pretending they have the same card UID?
- what if the same authorization code is being used by multiple users at the same time?
- …many other…
Server: clean code with async/await
Using Python 3 AsyncIO and async/await
it is possible to write all the card-communication logic within a single, yet comprehensive function.
The vc.transceive
is a coroutine that is responsible for transmitting the command to the client (over WebSocket) and then returning the client’s response.
We await
the coroutine, so when it is waiting for client’s response, the thread is not actually blocked but the server is doing some other useful things in the meantime (e.g. serving other connections/customers).
The server is single threaded itself, but due to the concurrent implementation it is able to serve multiple connections (different cards) at once.
Example code - checking credit value stored on the card.
async def check_credit(vc: VirtualCard) -> None:
# select application 0xC0FFEE
res = await vc.transceive(b"\x90\x5A\x00\x00\x03\xEE\xFF\xC0\x00")
require(res == b"\x91\x00")
# authenticate with key #2
session_key = await authenticate_aes(vc, b"\x02", 16 * b"\x55")
cmac = MifareCMAC(session_key, AES)
# get value of file #1
cmac.digest(b"\x6C\x01")
res = await vc.transceive(b"\x90\x6C\x00\x00\x01\x01\x00")
# decrypt the returned value
res_d = cmac.decrypt(res[:-2])
value = struct.unpack('<I', res_d[:4])[0]
# validate the checksum
chk = res_d[4:8]
assert struct.pack('<I', crc32(res_d[:4] + b"\x00")) == chk
# send the message to the user
await vc.ws.send_json({"type": "modal", "message": "Current value: {}".format(value)})
Client: Some bad NFC drivers
On my old phone, Samsung Galaxy A5 2017 there was a problem that the NFC tag was being reset after any inactivity longer than (I think) 125 ms. This begins to be an issue when the user is on high latency path to the server (e.g. just using cellular network connection).
I have solved the problem using a pretty coarse solution of transceiving an empty packet in a separate thread, every 10 ms.
/**
* Periodically transfer some empty packets to the tag, in order to keep it alive.
* Some drivers reset the tag after 125 ms of inactivity.
*/
class TagKeepAliveRunnable implements Runnable {
private IsoDep tag;
private boolean stop;
TagKeepAliveRunnable(IsoDep tag) {
this.tag = tag;
this.stop = false;
}
void stop() {
this.stop = true;
}
@Override
public void run() {
while (!stop) {
byte[] cmd = {};
try {
tag.transceive(cmd);
} catch (IOException e) {
// transferring empty commands may cause failure
}
SystemClock.sleep(10);
}
}
}
So when I perform IsoDep.connect
, I just start a separate thread with TagKeepAliveRunnable
and just normally work with the tag from my main thread.
I’m really unsure how portable is this workaround.
Summary
In my opinion, it is possible to securely implement over-the-air top-up and similar applications. This however must be done with extreme caution, as there is a lot of risk factors that need to be taken into account.