Files
Momentum-Firmware/applications/main/nfc/plugins/supported_cards/smartrider.c
jay candel 65df2e4d1f NFC: SmartRider Parser (#203)
* adding smartrider_parser

* adding SmartRider parser

new parser for SmartRider cards, a public transport smart card system used in Western Australia.
extracts and interprets key information from the card, including:
-Current balance
-Card serial number
-Concession type
-Purchase cost
-Details of the last two trips (including tag on/off status, cost, route, transaction number, and journey number)

* optimising

- removed all logging to simplify output.
- used early returns for clearer error handling.
- optimized setups outside loops to improve memory use.
- simplified flows by removing unnecessary loops.
- placed variables closer to their use for better readability.
- cached data blocks to streamline data handling.
- added a helper function for parsing trips, reducing redundancy.
- corrected loop counter types to avoid compile-time errors.

* cleaning displayed data

- removed transaction (txn) and journey (jrn) numbers to declutter the trip details.
- shortened "previous trip" to "prev trip" to optimize screen space usage.

* added and refined displayed data

added auto load field "threshold amount / reload amount"
changed serial to display first two digits as SR0 for consistency with physical card.

* Format

* Improved Verification Process

- Added definitions for STANDARD_KEY_2 and STANDARD_KEY_3
- Enhanced smartrider_verify function to check for all three specific keys:
  - STANDARD_KEY_1 in Sector 0 as Key A
  - STANDARD_KEY_2 in Sector 6 as Key A
  - STANDARD_KEY_3 in Sector 6 as Key B
- Implemented read operations to verify actual key values stored on the card
- Added comparisons between read key data and expected key values
- Improved debug logging for each step of the verification process

* Integrated Verification into Parse Function

- Added key verification for sectors 0 and 6
- Implemented do-while loop structure for early exit on verification failure
- Moved block readability checks inside verification process
- Added parsed flag to indicate successful parsing
- Updated return value to reflect parsing success
- Maintained existing parsing logic and output format

* fixed false positives

recieved some cuid cards today so was able to test for myself can confirm works... finally
changes made: 
-updated key assignment in smartrider_read
-simplified key verification in smartrider_parse
-improved error handling and logging
-streamlined data parsing process
-corrected key checking logic
-added checks for required block readability
-improved flow control with strategic breaks
-adjusted block data access method

* small optimizations

- refactored `smartrider_verify` and `smartrider_read` by abstracting repeated key operations into `authenticate_and_read` function for improved code maintainability.
- optimized `smartrider_read` by introducing a loop for key setup, reducing redundancy and improving efficiency.
- streamlined error handling in `smartrider_read` by replacing do-while loop with conditional checks.
- changed standard key references to use `standard_keys` array indices

* formatting

* Delete duplicate smartrider.c

* updated smartrider.c

* found 'fbt format'

* transaction parsing updates

-removed last trip/prev trip wording and replaced with "Trip History" header
-added date in front of each transaction 
-only shows transaction cost if it's higher than 0
-changed tag on/tag off to +/- to save room
-added 8 more transactions to Trip History
-verified still working and formatted with fbt

* fixed reboot with partially unlocked card

-added bounds checking for all block accesses to prevent out-of-range memory access
-implemented improved error handling with an error_occurred flag
-introduced a maximum iteration count for date calculation to prevent infinite loops
-used snprintf with size limits for all string operations to avoid buffer overflows
-added validation for trip count to ensure it doesn't exceed the maximum allowed
-implemented checks to skip unread or out-of-range blocks during trip parsing
-added safeguards against corrupted or invalid timestamp data

* optimized SmartRider card parsing and verification

- replaced do-while loop with direct error checks in smartrider_parse
- optimized key verification using direct memcmp in smartrider_verify
- introduced inline functions for common operations (e.g., set_key, read_le16)
- replaced bubble sort with insertion sort for trip data
- simplified date calculation using a lookup table for days in month
- used uint_fast8_t for loop counters to allow compiler optimization
- added __attribute__((hot)) to key functions for aggressive optimization
- removed redundant variable declarations and function calls
- optimized memory usage with static const arrays for required blocks
- simplified error handling in smartrider_read and authenticate_and_read
- used __builtin_memcpy and __builtin_memcmp for potential compiler optimizations
- tested and formatted with fbt

* small fixes

-renamed "Trip History" to "Tag On/Off History"
-fixed date calculation to account for leap years
-misc changes

* Update changelog

---------

Co-authored-by: Willy-JL <49810075+Willy-JL@users.noreply.github.com>
2024-09-18 03:00:49 +02:00

335 lines
11 KiB
C

#include "nfc_supported_card_plugin.h"
#include <bit_lib.h>
#include <flipper_application.h>
#include <furi.h>
#include <nfc/protocols/mf_classic/mf_classic_poller_sync.h>
#include <string.h>
#define MAX_TRIPS 10
#define TAG "SmartRider"
#define MAX_BLOCKS 64
#define MAX_DATE_ITERATIONS 366
static const uint8_t STANDARD_KEYS[3][6] = {
{0x20, 0x31, 0xD1, 0xE5, 0x7A, 0x3B},
{0x4C, 0xA6, 0x02, 0x9F, 0x94, 0x73},
{0x19, 0x19, 0x53, 0x98, 0xE3, 0x2F}};
typedef struct {
uint32_t timestamp;
uint16_t cost;
uint16_t transaction_number;
uint16_t journey_number;
char route[5];
uint8_t tap_on : 1;
uint8_t block;
} __attribute__((packed)) TripData;
typedef struct {
uint32_t balance;
uint16_t issued_days;
uint16_t expiry_days;
uint16_t purchase_cost;
uint16_t auto_load_threshold;
uint16_t auto_load_value;
char card_serial_number[11];
uint8_t token;
TripData trips[MAX_TRIPS];
uint8_t trip_count;
} __attribute__((packed)) SmartRiderData;
static const char* const CONCESSION_TYPES[] = {
"Pre-issue",
"Standard Fare",
"Student",
NULL,
"Tertiary",
NULL,
"Seniors",
"Health Care",
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
"PTA Staff",
"Pensioner",
"Free Travel"};
static inline const char* get_concession_type(uint8_t token) {
return (token <= 0x10) ? CONCESSION_TYPES[token] : "Unknown";
}
static bool authenticate_and_read(
Nfc* nfc,
uint8_t sector,
const uint8_t* key,
MfClassicKeyType key_type,
MfClassicBlock* block_data) {
MfClassicKey mf_key;
memcpy(mf_key.data, key, 6);
uint8_t block = mf_classic_get_first_block_num_of_sector(sector);
if(mf_classic_poller_sync_auth(nfc, block, &mf_key, key_type, NULL) != MfClassicErrorNone) {
FURI_LOG_D(TAG, "Authentication failed for sector %d key type %d", sector, key_type);
return false;
}
if(mf_classic_poller_sync_read_block(nfc, block, &mf_key, key_type, block_data) !=
MfClassicErrorNone) {
FURI_LOG_D(TAG, "Read failed for sector %d", sector);
return false;
}
return true;
}
static bool smartrider_verify(Nfc* nfc) {
furi_assert(nfc);
MfClassicBlock block_data;
for(int i = 0; i < 3; i++) {
if(!authenticate_and_read(
nfc,
i * 6,
STANDARD_KEYS[i],
i % 2 == 0 ? MfClassicKeyTypeA : MfClassicKeyTypeB,
&block_data) ||
memcmp(block_data.data, STANDARD_KEYS[i], 6) != 0) {
FURI_LOG_D(TAG, "Authentication or key mismatch for key %d", i);
return false;
}
}
FURI_LOG_I(TAG, "SmartRider card verified");
return true;
}
static inline bool
parse_trip_data(const MfClassicBlock* block_data, TripData* trip, uint8_t block_number) {
trip->timestamp = bit_lib_bytes_to_num_le(block_data->data + 3, 4);
trip->tap_on = (block_data->data[7] & 0x10) == 0x10;
memcpy(trip->route, block_data->data + 8, 4);
trip->route[4] = '\0';
trip->cost = bit_lib_bytes_to_num_le(block_data->data + 13, 2);
trip->transaction_number = bit_lib_bytes_to_num_le(block_data->data, 2);
trip->journey_number = bit_lib_bytes_to_num_le(block_data->data + 2, 2);
trip->block = block_number;
return true;
}
static bool smartrider_read(Nfc* nfc, NfcDevice* device) {
furi_assert(nfc);
furi_assert(device);
MfClassicData* data = mf_classic_alloc();
nfc_device_copy_data(device, NfcProtocolMfClassic, data);
MfClassicType type;
if(mf_classic_poller_sync_detect_type(nfc, &type) != MfClassicErrorNone ||
type != MfClassicType1k) {
mf_classic_free(data);
return false;
}
data->type = type;
MfClassicDeviceKeys keys = {.key_a_mask = 0, .key_b_mask = 0};
for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) {
memcpy(keys.key_a[i].data, STANDARD_KEYS[i == 0 ? 0 : 1], sizeof(STANDARD_KEYS[0]));
if(i > 0) {
memcpy(keys.key_b[i].data, STANDARD_KEYS[2], sizeof(STANDARD_KEYS[0]));
FURI_BIT_SET(keys.key_b_mask, i);
}
FURI_BIT_SET(keys.key_a_mask, i);
}
MfClassicError error = mf_classic_poller_sync_read(nfc, &keys, data);
if(error != MfClassicErrorNone) {
FURI_LOG_W(TAG, "Failed to read data");
mf_classic_free(data);
return false;
}
nfc_device_set_data(device, NfcProtocolMfClassic, data);
mf_classic_free(data);
return true;
}
static bool is_leap_year(uint16_t year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
static void calculate_date(uint32_t timestamp, char* date_str, size_t date_str_size) {
uint32_t seconds_since_2000 = timestamp;
uint32_t days_since_2000 = seconds_since_2000 / 86400;
uint16_t year = 2000;
uint8_t month = 1;
uint16_t day = 1;
static const uint16_t days_in_month[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
while(days_since_2000 >= (is_leap_year(year) ? 366 : 365)) {
days_since_2000 -= (is_leap_year(year) ? 366 : 365);
year++;
}
for(month = 0; month < 12; month++) {
uint16_t dim = days_in_month[month];
if(month == 1 && is_leap_year(year)) {
dim++;
}
if(days_since_2000 < dim) {
break;
}
days_since_2000 -= dim;
}
day = days_since_2000 + 1;
month++; // Adjust month to 1-based
if(date_str_size > 0) {
size_t written = 0;
written += snprintf(date_str + written, date_str_size - written, "%02u", day);
if(written < date_str_size - 1) {
written += snprintf(date_str + written, date_str_size - written, "/");
}
if(written < date_str_size - 1) {
written += snprintf(date_str + written, date_str_size - written, "%02u", month);
}
if(written < date_str_size - 1) {
written += snprintf(date_str + written, date_str_size - written, "/");
}
if(written < date_str_size - 1) {
snprintf(date_str + written, date_str_size - written, "%02u", year % 100);
}
} else {
// If the buffer size is 0, do nothing
}
}
static bool smartrider_parse(const NfcDevice* device, FuriString* parsed_data) {
furi_assert(device);
furi_assert(parsed_data);
const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic);
SmartRiderData sr_data = {0};
if(data->type != MfClassicType1k) {
FURI_LOG_E(TAG, "Invalid card type");
return false;
}
const MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(data, 0);
if(!sec_tr || memcmp(sec_tr->key_a.data, STANDARD_KEYS[0], 6) != 0) {
FURI_LOG_E(TAG, "Key verification failed for sector 0");
return false;
}
static const uint8_t required_blocks[] = {14, 4, 5, 1, 52, 50, 0};
for(size_t i = 0; i < COUNT_OF(required_blocks); i++) {
if(required_blocks[i] >= MAX_BLOCKS ||
!mf_classic_is_block_read(data, required_blocks[i])) {
FURI_LOG_E(TAG, "Required block %d is not read or out of range", required_blocks[i]);
return false;
}
}
sr_data.balance = bit_lib_bytes_to_num_le(data->block[14].data + 7, 2);
sr_data.issued_days = bit_lib_bytes_to_num_le(data->block[4].data + 16, 2);
sr_data.expiry_days = bit_lib_bytes_to_num_le(data->block[4].data + 18, 2);
sr_data.auto_load_threshold = bit_lib_bytes_to_num_le(data->block[4].data + 20, 2);
sr_data.auto_load_value = bit_lib_bytes_to_num_le(data->block[4].data + 22, 2);
sr_data.token = data->block[5].data[8];
sr_data.purchase_cost = bit_lib_bytes_to_num_le(data->block[0].data + 14, 2);
snprintf(
sr_data.card_serial_number,
sizeof(sr_data.card_serial_number),
"%02X%02X%02X%02X%02X",
data->block[1].data[6],
data->block[1].data[7],
data->block[1].data[8],
data->block[1].data[9],
data->block[1].data[10]);
for(uint8_t block_number = 40; block_number <= 52 && sr_data.trip_count < MAX_TRIPS;
block_number++) {
if((block_number != 43 && block_number != 47 && block_number != 51) &&
mf_classic_is_block_read(data, block_number) &&
parse_trip_data(
&data->block[block_number], &sr_data.trips[sr_data.trip_count], block_number)) {
sr_data.trip_count++;
}
}
// Sort trips by timestamp (descending order)
for(uint8_t i = 0; i < sr_data.trip_count - 1; i++) {
for(uint8_t j = 0; j < sr_data.trip_count - i - 1; j++) {
if(sr_data.trips[j].timestamp < sr_data.trips[j + 1].timestamp) {
TripData temp = sr_data.trips[j];
sr_data.trips[j] = sr_data.trips[j + 1];
sr_data.trips[j + 1] = temp;
}
}
}
furi_string_printf(
parsed_data,
"\e#SmartRider\nBalance: $%lu.%02lu\nConcession: %s\nSerial: %s%s\n"
"Total Cost: $%u.%02u\nAuto-Load: $%u.%02u/$%u.%02u\n\e#Tag On/Off History\n",
sr_data.balance / 100,
sr_data.balance % 100,
get_concession_type(sr_data.token),
memcmp(sr_data.card_serial_number, "00", 2) == 0 ? "SR0" : "",
memcmp(sr_data.card_serial_number, "00", 2) == 0 ? sr_data.card_serial_number + 2 :
sr_data.card_serial_number,
sr_data.purchase_cost / 100,
sr_data.purchase_cost % 100,
sr_data.auto_load_threshold / 100,
sr_data.auto_load_threshold % 100,
sr_data.auto_load_value / 100,
sr_data.auto_load_value % 100);
for(uint8_t i = 0; i < sr_data.trip_count; i++) {
char date_str[9];
calculate_date(sr_data.trips[i].timestamp, date_str, sizeof(date_str));
uint32_t cost = sr_data.trips[i].cost;
if(cost > 0) {
furi_string_cat_printf(
parsed_data,
"%s %c $%lu.%02lu %s\n",
date_str,
sr_data.trips[i].tap_on ? '+' : '-',
cost / 100,
cost % 100,
sr_data.trips[i].route);
} else {
furi_string_cat_printf(
parsed_data,
"%s %c %s\n",
date_str,
sr_data.trips[i].tap_on ? '+' : '-',
sr_data.trips[i].route);
}
}
return true;
}
static const NfcSupportedCardsPlugin smartrider_plugin = {
.protocol = NfcProtocolMfClassic,
.verify = smartrider_verify,
.read = smartrider_read,
.parse = smartrider_parse,
};
__attribute__((used)) const FlipperAppPluginDescriptor* smartrider_plugin_ep() {
static const FlipperAppPluginDescriptor plugin_descriptor = {
.appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID,
.ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION,
.entry_point = &smartrider_plugin,
};
return &plugin_descriptor;
}
// made with love by jay candel <3