Parsing function readability refactor

This commit is contained in:
Zachary Weiss
2024-04-07 22:10:46 -04:00
parent 3af66c7815
commit 3ea6d46c79

View File

@@ -23,8 +23,6 @@
* — Reverse engineer passes (sectors 4 & 5?), impl. * — Reverse engineer passes (sectors 4 & 5?), impl.
* — Infer transaction flag meanings * — Infer transaction flag meanings
* — Infer remaining unknown bytes in the balance sectors (2 & 3) * — Infer remaining unknown bytes in the balance sectors (2 & 3)
* ASCII art &/or unified read function for the balance sectors,
* to improve readability / interpretability by others?
* — Improve string output formatting, esp. of transaction log * — Improve string output formatting, esp. of transaction log
* — Mapping of buses to garages, and subsequently, route subsets via * — Mapping of buses to garages, and subsequently, route subsets via
* http://roster.transithistory.org/ data * http://roster.transithistory.org/ data
@@ -99,11 +97,6 @@
#define CHARLIE_N_TRIP_HISTORY 10 #define CHARLIE_N_TRIP_HISTORY 10
#define CHARLIE_N_PASSES 4 #define CHARLIE_N_PASSES 4
enum CharlieActiveSector {
CHARLIE_ACTIVE_SECTOR_2,
CHARLIE_ACTIVE_SECTOR_3,
};
typedef struct { typedef struct {
uint64_t a; uint64_t a;
uint64_t b; uint64_t b;
@@ -163,6 +156,18 @@ typedef struct {
DateTime end; DateTime end;
} Pass; } Pass;
typedef struct {
uint16_t n_uses;
uint8_t active_balance_sector;
} CounterSector;
typedef struct {
Money balance;
uint16_t type;
DateTime issued;
DateTime end_validity;
} BalanceSector;
// IdMapping approach borrowed from Jeremy Cooper's 'clipper.c' // IdMapping approach borrowed from Jeremy Cooper's 'clipper.c'
typedef struct { typedef struct {
uint16_t id; uint16_t id;
@@ -656,7 +661,7 @@ static bool is_debug() {
} }
// ********************************************************** // **********************************************************
// ********************* DATA PARSING *********************** // ******************** FIELD PARSING ***********************
// ********************************************************** // **********************************************************
static Money money_parse( static Money money_parse(
@@ -677,46 +682,6 @@ static DateTime
return dt_delta(CHARLIE_EPOCH, ts_charlie * CHARLIE_TIME_DELTA_SECS); return dt_delta(CHARLIE_EPOCH, ts_charlie * CHARLIE_TIME_DELTA_SECS);
} }
static uint16_t n_uses(const MfClassicData* data, const enum CharlieActiveSector active_sector) {
/* First two bytes of applicable block (sector 1, block 1 or 2 depending on active_sector)
The *lower* of the two values *minus one* is the true use count,
per DEFCON31 researcher's findings
*/
return pos_to_num(data, 1, 1 + active_sector, 0, 2) - 1;
}
static enum CharlieActiveSector get_active_sector(const MfClassicData* data) {
/* Card has two transaction sectors (2 & 3) containing balance data, with two
corresponding trip counters in 0x50:0x51 & 0x60:0x61 (sector 1, byte 0:1 of blocks 1 & 2).
The *lower* count variable corresponds to the active sector
(0x5_ lower -> 2 active, 0x6_ lower -> 3 active)
Sectors 2 & 3 are (largely) identical, save for trip data.
Card seems to alternate between the two, with active sector storing
the current balance & recent trip/transaction, & the inactive sector storing
the N-1 trip/transaction version of the same data.
Here I check both the trip count and the stored transaction date,
for my own sanity, to confirm the active sector.
*/
// active sector based on trip counters
const bool active_trip = n_uses(data, CHARLIE_ACTIVE_SECTOR_2) <=
n_uses(data, CHARLIE_ACTIVE_SECTOR_3);
// active sector based on transaction date
DateTime ds2 = date_parse(data, 2, 0, 1);
DateTime ds3 = date_parse(data, 3, 0, 1);
const bool active_date = datetime_datetime_to_timestamp(&ds2) >=
datetime_datetime_to_timestamp(&ds3);
// with all tested cards so far, this has been true
furi_assert(active_trip == active_date);
return active_trip ? CHARLIE_ACTIVE_SECTOR_2 : CHARLIE_ACTIVE_SECTOR_3;
}
static uint16_t type_parse(const MfClassicData* data) { static uint16_t type_parse(const MfClassicData* data) {
/* Card type data stored in the first 10bits of block 1 of sectors 2 & 3 (Block 9 & Block 13, from card start) /* Card type data stored in the first 10bits of block 1 of sectors 2 & 3 (Block 9 & Block 13, from card start)
To my knowledge, card type should never change, so we can check either To my knowledge, card type should never change, so we can check either
@@ -745,12 +710,6 @@ static DateTime end_validity_parse(
return dt_delta(CHARLIE_EPOCH, ts_charlie_ev * CHARLIE_END_VALID_DELTA_SECS); return dt_delta(CHARLIE_EPOCH, ts_charlie_ev * CHARLIE_END_VALID_DELTA_SECS);
} }
static DateTime
main_end_validity_parse(const MfClassicData* data, enum CharlieActiveSector active_sec) {
// primary card type end validity; checked in active sector (probably the same across both 2 & 3)
return end_validity_parse(data, (active_sec == CHARLIE_ACTIVE_SECTOR_2) ? 2 : 3, 1, 1);
}
static Pass static Pass
pass_parse(const MfClassicData* data, uint8_t sector_num, uint8_t block_num, uint8_t byte_num) { pass_parse(const MfClassicData* data, uint8_t sector_num, uint8_t block_num, uint8_t byte_num) {
// WIP; testing only. Speculating it may be structured as follows // WIP; testing only. Speculating it may be structured as follows
@@ -814,53 +773,100 @@ static Trip
return (Trip){date, gate, g_flag, fare, f_flag}; return (Trip){date, gate, g_flag, fare, f_flag};
} }
// Manufacturer data (Sector 0) // **********************************************************
// // ******************* SECTOR PARSING ***********************
// 0 1 2 3 4 5 6 7 8 9 A B C D E F // **********************************************************
// +----.----.----.----+----+----.----.----.----+----+----.----.----.----.----+----+
// 0x000 | UID |LRC | 88 04 00 C8 |unkn| 00 20 00 00 00 |unkn|
// +----.----.----.----+----+----.----.----.----+----+----.----.----.----.----+----+
// 0x010 | 4E 0F 04 10 04 10 04 10 04 10 04 10 04 10 04 10 |
// +----.----.----.----.----.----.----.----.----.----.----.----.----.----.----.----+
// 0x020 | ... 00 00 ... |
// +----.----.----.----.----.----.----.----.----.----.----.----.----.----.----.----+
// Trip/transaction counters (Sector 1) static uint32_t mfg_sector_parse(const MfClassicData* data) {
// // Manufacturer data (Sector 0)
// 0 1 2 3 4 5 6 7 8 9 A B C D E F //
// +----.----.----.----.----.----.----.----.----.----.----.----.----.----.----+----+ // 0 1 2 3 4 5 6 7 8 9 A B C D E F
// 0x040 | 04 10 23 45 66 77 ... 00 00 ... | // +----.----.----.----+----+----.----.----.----+----+----.----.----.----.----+----+
// +----.----+----+----.----.----.----.----.----.----.----.----.----.----.----+----+ // 0x000 | UID |LRC | 88 04 00 C8 |unkn| 00 20 00 00 00 |unkn|
// 0x050 | uses1 |unkn| ... 00 00 ... | // +----.----.----.----+----+----.----.----.----+----+----.----.----.----.----+----+
// +----.----+----+----.----.----.----.----.----.----.----.----.----.----.----.----+ // 0x010 | 4E 0F 04 10 04 10 04 10 04 10 04 10 04 10 04 10 |
// 0x060 | uses2 |unkn| ... 00 00 ... | // +----.----.----.----.----.----.----.----.----.----.----.----.----.----.----.----+
// +----.----+----+----.----.----.----.----.----.----.----.----.----.----.----.----+ // 0x020 | ... 00 00 ... |
// +----.----.----.----.----.----.----.----.----.----.----.----.----.----.----.----+
// Balance / type / last transaction (Sector 2 & 3) size_t uid_len = 0;
// const uint8_t* uid = mf_classic_get_uid(data, &uid_len);
// 0 1 2 3 4 5 6 7 8 9 A B C D E F const uint32_t card_number = bit_lib_bytes_to_num_be(uid, 4);
// +----+----.----.----+----.----+----.----.----+----.----+----.----+----+----.----+
// 0x080 | 11 | date last | loc last| date issued | 65 00 | unknown | 00 | crc | return card_number;
// +----+----.----.----+----+----+----+----+----+----.----+----.----+----+----.----+ }
// 0x090 | type |end validity|unkn| balance | 00 | unknown | crc |
// +----.----.----.----+----+----.----+----+----.----.----.----.----.----+----.----+ static CounterSector counter_sector_parse(const MfClassicData* data) {
// 0x0A0 | 20 ... 00 00 ... 04 | crc | // Trip/transaction counters (Sector 1)
// +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+ //
// 0 1 2 3 4 5 6 7 8 9 A B C D E F
// +----.----.----.----.----.----.----.----.----.----.----.----.----.----.----+----+
// 0x040 | 04 10 23 45 66 77 ... 00 00 ... |
// +----.----+----+----.----.----.----.----.----.----.----.----.----.----.----+----+
// 0x050 | uses1 |unkn| ... 00 00 ... |
// +----.----+----+----.----.----.----.----.----.----.----.----.----.----.----.----+
// 0x060 | uses2 |unkn| ... 00 00 ... |
// +----.----+----+----.----.----.----.----.----.----.----.----.----.----.----.----+
// Card has two transaction sectors (2 & 3) containing balance data, with two
// corresponding trip counters in 0x50:0x51 & 0x60:0x61 (sector 1, byte 0:1 of blocks 1 & 2).
// The *lower* of the two values *minus one* is the true use count,
// and corresponds to the active balance sector,
// (0x50 counter lower -> sector 2 active, 0x60 counter lower -> 3 active)
// per DEFCON31 researcher's findings
const uint16_t n_uses1 = pos_to_num(data, 1, 1, 0, 2);
const uint16_t n_uses2 = pos_to_num(data, 1, 2, 0, 2);
const bool is_sec2_active = n_uses1 <= n_uses2;
const uint8_t active_sector = is_sec2_active ? 2 : 3;
const uint16_t n_uses = (is_sec2_active ? n_uses1 : n_uses2) - 1;
return (CounterSector){n_uses, active_sector};
}
static BalanceSector balance_sector_parse(const MfClassicData* data, uint8_t active_sector) {
// Balance / type / last transaction (Sector 2 or 3)
//
// 0 1 2 3 4 5 6 7 8 9 A B C D E F
// +----+----.----.----+----.----+----.----.----+----.----+----.----+----+----.----+
// 0x080 | 11 | date last | loc last| date issued | 65 00 | unknown | 00 | crc | 0x0C0
// +----+----.----.----+----+----+----+----+----+----.----+----.----+----+----.----+
// 0x090 | type |end validity|unkn| balance | 00 | unknown | crc | 0x0D0
// +----.----.----.----+----+----.----+----+----.----.----.----.----.----+----.----+
// 0x0A0 | 20 ... 00 00 ... 04 | crc | 0x0E0
// +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+
//
// "Active" balance sector alternates between 2 and 3
// Last trip/transaction info in balance sector (date last & loc last)
// also included in transaction log, hence don't bother to read here
//
// Inactive balance sector represent the transaction N-1 balance data
// (where active sector represents data from transaction N).
const DateTime issued = date_parse(data, active_sector, 0, 6);
const DateTime end_validity = end_validity_parse(data, active_sector, 1, 1);
const uint16_t type = type_parse(data);
const Money bal = money_parse(data, active_sector, 1, 5);
return (BalanceSector){bal, type, issued, end_validity};
}
static Pass* passes_parse(const MfClassicData* data) { static Pass* passes_parse(const MfClassicData* data) {
// WIP. Read in all speculative passes into array // Passes, speculative (Sectors 4 &/or 5)
// Sectors 4 & 5 speculated to contain pass data,
// 4 separate fields? active vs inactive sector for 2 passes?
// Sector 4 visualized below; sector 5 layout the same.
// //
// 0 1 2 3 4 5 6 7 8 9 A B C D E F // 0 1 2 3 4 5 6 7 8 9 A B C D E F
// +----.----.----.----.----.-+--.----+----.----.----.----.----.-+--.----+----.----+ // +----.----.----.----.----.-+--.----+----.----.----.----.----.-+--.----+----.----+
// 0x100 | pass0? |0 00 | pass1? |0 00 | crc | // 0x100 | pass0? |0 00 | pass1? |0 00 | crc | 0x140
// +----.----.----.----.----.-+--.----+----.----.----.----.----.-+--.----+----.----+ // +----.----.----.----.----.-+--.----+----.----.----.----.----.-+--.----+----.----+
// 0x110 | ... 00 00 ... | crc | // 0x110 | ... 00 00 ... | crc | 0x150
// +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+ // +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+
// 0x120 | ... 00 ... 05 | crc | // 0x120 | ... 00 ... 05 | crc | 0x160
// +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+ // +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+
//
// WIP. Read in all speculative passes into array
// 4 separate fields? active vs inactive sector for 2 passes?
Pass* passes = malloc(sizeof(Pass) * CHARLIE_N_PASSES); Pass* passes = malloc(sizeof(Pass) * CHARLIE_N_PASSES);
@@ -872,7 +878,7 @@ static Pass* passes_parse(const MfClassicData* data) {
} }
static Trip* trips_parse(const MfClassicData* data) { static Trip* trips_parse(const MfClassicData* data) {
// Sectors 6 & 7 store the last 10 trips. Overall layout as follows: // Transaction history (Sectors 67)
// //
// 0 1 2 3 4 5 6 7 8 9 A B C D E F // 0 1 2 3 4 5 6 7 8 9 A B C D E F
// +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+ // +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+
@@ -885,7 +891,8 @@ static Trip* trips_parse(const MfClassicData* data) {
// 0x1E0 | ... 00 00 ... | crc | // 0x1E0 | ... 00 00 ... | crc |
// +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+ // +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+
// //
//Trips are not sorted, rather, appear to get overwritten sequentially. (eg, sorted modulo array rotation) // Transactions are not sorted, rather, appear to get overwritten
// sequentially. (eg, sorted modulo array rotation)
Trip* trips = malloc(sizeof(Trip) * CHARLIE_N_TRIP_HISTORY); Trip* trips = malloc(sizeof(Trip) * CHARLIE_N_TRIP_HISTORY);
@@ -958,7 +965,7 @@ static DateTime expiry(DateTime iss) {
} }
return exp; return exp;
}*/ }
static bool expired(DateTime expiry, DateTime last_trip) { static bool expired(DateTime expiry, DateTime last_trip) {
// if a card has sat unused for >2 years, expired (verify this claim?) // if a card has sat unused for >2 years, expired (verify this claim?)
@@ -970,6 +977,7 @@ static bool expired(DateTime expiry, DateTime last_trip) {
return (ts_exp <= ts_now) | ((ts_now - ts_last) >= (2 * 365 * 24 * 60 * 60)); return (ts_exp <= ts_now) | ((ts_now - ts_last) >= (2 * 365 * 24 * 60 * 60));
} }
*/
// ********************************************************** // **********************************************************
// ****************** STRING FORMATTING ********************* // ****************** STRING FORMATTING *********************
@@ -1011,7 +1019,7 @@ void pass_format_cat(FuriString* out, Pass pass) {
void passes_format_cat(FuriString* out, Pass* passes) { void passes_format_cat(FuriString* out, Pass* passes) {
if(is_debug()) { if(is_debug()) {
furi_string_cat_printf(out, "\nPasses (DEBUG & WIP):"); furi_string_cat_printf(out, "\nPasses (DEBUG / WIP):");
for(size_t i = 0; i < CHARLIE_N_PASSES; i++) { for(size_t i = 0; i < CHARLIE_N_PASSES; i++) {
if(passes[i].valid) { if(passes[i].valid) {
furi_string_cat_printf(out, "\nPass %u", i + 1); furi_string_cat_printf(out, "\nPass %u", i + 1);
@@ -1091,43 +1099,38 @@ static bool charliecard_parse(const NfcDevice* device, FuriString* parsed_data)
// TODO: Verify add'l? // TODO: Verify add'l?
const enum CharlieActiveSector active_sec_enum = get_active_sector(data); // parse card data
const uint8_t active_sector = (active_sec_enum == CHARLIE_ACTIVE_SECTOR_2) ? 2 : 3; const uint32_t card_number = mfg_sector_parse(data);
const CounterSector counter_sector = counter_sector_parse(data);
const BalanceSector balance_sector =
balance_sector_parse(data, counter_sector.active_balance_sector);
Pass* passes = passes_parse(data);
Trip* trips = trips_parse(data);
// print/append card data
furi_string_cat_printf(parsed_data, "\e#CharlieCard"); furi_string_cat_printf(parsed_data, "\e#CharlieCard");
size_t uid_len = 0;
const uint8_t* uid = mf_classic_get_uid(data, &uid_len);
uint32_t card_number = bit_lib_bytes_to_num_be(uid, 4);
furi_string_cat_printf(parsed_data, "\nSerial: 5-%lu", card_number); furi_string_cat_printf(parsed_data, "\nSerial: 5-%lu", card_number);
Money bal = money_parse(data, active_sector, 1, 5);
furi_string_cat_printf(parsed_data, "\nBal: "); furi_string_cat_printf(parsed_data, "\nBal: ");
money_format_cat(parsed_data, bal); money_format_cat(parsed_data, balance_sector.balance);
const uint16_t type = type_parse(data);
furi_string_cat_printf(parsed_data, "\nType: "); furi_string_cat_printf(parsed_data, "\nType: ");
type_format_cat(parsed_data, type); type_format_cat(parsed_data, balance_sector.type);
Pass* passes = passes_parse(data);
passes_format_cat(parsed_data, passes); passes_format_cat(parsed_data, passes);
free(passes); free(passes);
const uint16_t n_trips = n_uses(data, active_sec_enum); furi_string_cat_printf(parsed_data, "\nTrip Count: %u", counter_sector.n_uses);
furi_string_cat_printf(parsed_data, "\nTrip Count: %u", n_trips);
const DateTime iss = date_parse(data, active_sector, 0, 6);
furi_string_cat_printf(parsed_data, "\nIssued: "); furi_string_cat_printf(parsed_data, "\nIssued: ");
locale_format_dt_cat(parsed_data, &iss); locale_format_dt_cat(parsed_data, &balance_sector.issued);
const DateTime e_v = main_end_validity_parse(data, active_sec_enum);
furi_string_cat_printf(parsed_data, "\nExpiry: "); furi_string_cat_printf(parsed_data, "\nExpiry: ");
locale_format_dt_cat(parsed_data, &e_v); locale_format_dt_cat(parsed_data, &balance_sector.end_validity);
DateTime last = date_parse(data, active_sector, 0, 1); // const DateTime last = date_parse(data, active_sector, 0, 1);
furi_string_cat_printf(parsed_data, "\nExpired: %s", expired(e_v, last) ? "Yes" : "No"); // furi_string_cat_printf(parsed_data, "\nExpired: %s", expired(e_v, last) ? "Yes" : "No");
Trip* trips = trips_parse(data);
trips_format_cat(parsed_data, trips); trips_format_cat(parsed_data, trips);
free(trips); free(trips);