Opal Desfire Card - Transport NSW Australia

Discuss RFID Readers using HID, Mifare, etc
Post Reply
User avatar
ZerOne
Site Admin
Posts: 108
Joined: Sun Dec 13, 2020 8:21 am

Information has been obtained from the following link
https://gist.github.com/auscompgeek/65b ... ba19262420

Opal is the public transport smartcard ticketing system in Sydney, Australia.
Opal cards are MIFARE DESFire EV1 cards, with the application ID 0x314553. All files are restricted except for file 7, which is freely readable.
The official Android app can interpret this free read data. Much of the information here was derived by reverse engineering this app.
User avatar
ZerOne
Site Admin
Posts: 108
Joined: Sun Dec 13, 2020 8:21 am

Format
The free read file is 16 octets long.

The data is essentially a 128-bit integer, stored in little-endian.
As the fields are not byte-aligned, reading the data as a bitstream is difficult (read: impossible) without reversing the order of the octets.

Offsets and lengths listed are measured in bits, and assume you have reversed the octets. Ranges listed do not include the end bit.

Start End Length Description
0 16 16 Checksum
16 20 1 Weekly paid journey count
20 21 1 Auto top-up enabled
21 25 4 Last tap usage type
25 28 3 Mode of transport
28 39 11 Last tap time: minutes since 00:00
39 54 15 Last tap date: days since epoch
54 75 21 Balance in cents (two's complement)
75 91 16 Transaction sequence number
91 92 1 Card status, 1 if blocked
92 96 4 Serial number check digit
96 128 32 Serial number
User avatar
ZerOne
Site Admin
Posts: 108
Joined: Sun Dec 13, 2020 8:21 am

Card number

All card numbers start with the Opal issuer number "308522".
This is then concatenated with the serial number zero-padded to 9 digits, followed by the check digit.
User avatar
ZerOne
Site Admin
Posts: 108
Joined: Sun Dec 13, 2020 8:21 am

Tap date/time

The epoch is 1 January 1980 (1980-01-01).
It is believed that tap dates and times are stored in local time (Australia/NSW).
User avatar
ZerOne
Site Admin
Posts: 108
Joined: Sun Dec 13, 2020 8:21 am

Modes of transport

ID Description
0 Rail
1 Ferry or Light Rail
2 Bus
3 Reserved
4 Reserved
5 Reserved
6 Reserved
7 Reserved
User avatar
ZerOne
Site Admin
Posts: 108
Joined: Sun Dec 13, 2020 8:21 am

Usage types

ID Description
0 Card has not been used
1 Tap on: new journey
2 Tap on: transfer from same mode
3 Tap on: transfer from different mode
4 Tap on: Manly-CQ ferry: new journey
5 Tap on: Manly-CQ ferry: transfer from another ferry 1
6 Tap on: Manly-CQ ferry: transfer from different mode
7 Tap off: distance based fare
8 Tap off: flat-rate fare
9 Tap off: automatically completed journey (failure to tap off)
10 Tap off: end of trip without start (failure to tap on)
11 Tap off: tap on reversal
12 Unsuccessful tap (low balance?)
13 (reserved)
14 (reserved)
15 (reserved)


Transfers between ferries can only occur at Circular Quay.
User avatar
ZerOne
Site Admin
Posts: 108
Joined: Sun Dec 13, 2020 8:21 am

Checksum

The checksum is a CRC-16-CCITT checksum of the original data (without the checksum).
It is stored in big-endian on the card, and is hence little-endian once read in reversed.
User avatar
ZerOne
Site Admin
Posts: 108
Joined: Sun Dec 13, 2020 8:21 am

Opal Decode for Python
https://gist.github.com/auscompgeek/c05 ... b84a2bd7d1

Code: Select all

#!/usr/bin/env python3

import binascii
import enum
import typing


class Mode(enum.IntEnum):
    RAIL = 0
    FERRY = 1
    BUS = 2


class Usage(enum.IntEnum):
    UNUSED = 0
    NEW_JOURNEY = 1
    TRANSFER_SAME_MODE = 2
    TRANSFER_INTERMODE = 3
    NEW_JOURNEY_MCQ = 4
    TRANSFER_SAME_MODE_MCQ = 5
    TRANSFER_INTERMODE_MCQ = 6
    DISTANCE_BASED_FARE = 7
    FLAT_FARE = 8
    AUTO_COMPLETE_JOURNEY = 9
    END_NO_START = 10
    TAP_REVERSAL = 11
    UNSUCCESSFUL = 12


class FreeReadData(typing.NamedTuple):
    serial: str
    serial_check: int
    blocked: bool
    seq_num: int
    balance: int
    last_date: int
    last_time: int
    last_mode: Mode
    last_usage: Usage
    auto_topup_enabled: bool
    journey_count: int
    crc16: int


def twos_complement(input_value: int, num_bits: int) -> int:
    """Calculate a two's complement integer from the given input value's bits."""
    mask = 1 << (num_bits - 1)
    return (input_value & ~mask) - (input_value & mask)


def split_bits(data: int, num_bits: int) -> typing.Tuple[int, int]:
    return data & ((1 << num_bits) - 1), data >> num_bits


def parse_data(b: bytes) -> FreeReadData:
    assert len(b) == 16
    data = int.from_bytes(b, "little")

    serial = "%09d" % (data & 0xFFFFFFFF)
    data >>= 32
    serial_check = data & 0xF
    data >>= 4
    blocked = bool(data & 1)
    data >>= 1
    seq_num = data & 0xFFFF
    data >>= 16
    balance_unsigned, data = split_bits(data, 21)
    last_date, data = split_bits(data, 15)
    last_time, data = split_bits(data, 11)
    last_mode = Mode(data & 0b111)
    data >>= 3
    last_usage = Usage(data & 0xF)
    data >>= 4
    auto_topup_enabled = bool(data & 1)
    data >>= 1
    journey_count = data & 0xF
    data >>= 4
    crc16 = data

    balance = twos_complement(balance_unsigned, 21)

    return FreeReadData(
        serial,
        serial_check,
        blocked,
        seq_num,
        balance,
        last_date,
        last_time,
        last_mode,
        last_usage,
        auto_topup_enabled,
        journey_count,
        crc16,
    )


if __name__ == "__main__":
    b = bytes.fromhex(input())
    data = parse_data(b)
    print(data)
    # print(binascii.crc_hqx(b[:-2], 0))
User avatar
ZerOne
Site Admin
Posts: 108
Joined: Sun Dec 13, 2020 8:21 am

Reading Card Data Using an Android Device

Because File 7 on the Opal Card is unrestricted, the file contents can be read using an Android Device that supports NFC,
using the excellent NFC Tag Info Software written by NXP.

This software is available free of charge on the Google Play Store. (See Link Below)
NFC Tag Info by NXP

The following is a screenshot of a scanned Opal Card
Image
Post Reply

Return to “RFID Reader Discussion”