From d008c95aa0363d21761f8278d6dc72b83b40fdb6 Mon Sep 17 00:00:00 2001 From: Zachary Weiss Date: Sun, 31 Mar 2024 13:51:53 -0400 Subject: [PATCH 1/9] Notes / comments --- .../nfc/plugins/supported_cards/charliecard.c | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/applications/main/nfc/plugins/supported_cards/charliecard.c b/applications/main/nfc/plugins/supported_cards/charliecard.c index 7a405fffb..1b34f369c 100644 --- a/applications/main/nfc/plugins/supported_cards/charliecard.c +++ b/applications/main/nfc/plugins/supported_cards/charliecard.c @@ -26,26 +26,40 @@ * – ASCII art &/or unified read function for the balance sectors, * to improve readability / interpretability by others? * — Improve string output formatting, esp. of transaction log + * — Mapping of buses to garages, and subsequently, route subsets via + * http://roster.transithistory.org/ data + * — Mapping of stations to lines + * — Add'l data fields for side of station fare gates are on? Some stations + * separate inbound & outbound sides, so direction could be inferred + * from gates used. * — Continually gather data on fare gate ID mappings, update as collected; * check locations this might be scrapable / inferrable from: * [X] MBTA GTFS spec (https://www.mbta.com/developers/gtfs) features & IDs * seem too-coarse-grained & uncorrelated - * [X] MBTA ArcGIS (https://mbta-massdot.opendata.arcgis.com/) & Tableau (https://public.tableau.com/app/profile/mbta.office.of.performance.management.and.innovation/vizzes) + * [X] MBTA ArcGIS (https://mbta-massdot.opendata.arcgis.com/) & Tableau + * (https://public.tableau.com/app/profile/mbta.office.of.performance.management.and.innovation/vizzes) * files don't seem to have anything of that resolution (only down to ridership by station) * [X] (skim of) MBTA public GitHub (https://github.com/mbta) repos make no reference to fare-gate-level data * [X] (skim of) MBTA public engineering docs (https://www.mbta.com/engineering) unfruitful; - * Closest mention spotted is 2014 "Ridership and Service Statistics" (https://cdn.mbta.com/sites/default/files/fmcb-meeting-docs/reports-policies/2014-07-mbta-bluebook-ed14.pdf) + * Closest mention spotted is 2014 "Ridership and Service Statistics" + * (https://cdn.mbta.com/sites/default/files/fmcb-meeting-docs/reports-policies/2014-07-mbta-bluebook-ed14.pdf) * where on pg.40, "Equipment at Stations" is enumerated, and fare gates counts are given, - * listed as "AFC Gates" (presumably standing for "Automated Fare Control") + * listed as "AFC Gates" (presumably standing for "Automated Fare Collection") * [X] Josiah Zachery criminal trial public evidence — convicted partially on * data on his CharlieCard, appeals partially on basis of legality of this search. * Prev. court case (gag order mentioned in preamble) leaked some data in the files * entered into evidence. Seemingly did not happen here; fare gate IDs unmentioned, * only ever the nature of stored/saved data and methods of retrieval. - * Appelate case dockets 2019-P-0401, SJC-12952, SJ-2017-0390 (https://www.ma-appellatecourts.org/party) - * Trial court case 04/02/2015 #1584CR10265 @Suffolk County Criminal Superior Court (https://www.masscourts.org/eservices/home.page.16) - * [ ] FOIA / public records request? (https://massachusettsdot.mycusthelp.com/WEBAPP/_rs/(S(tbcygdlm0oojy35p1wv0y2y5))/supporthome.aspx) - * [ ] MBTA data blog? (https://www.massdottracker.com/datablog/) + * Appelate case dockets 2019-P-0401, SJC-12952, SJ-2017-0390 + * (https://www.ma-appellatecourts.org/party) + * Trial court indictment 04/02/2015, Case# 1584CR10265 @Suffolk County Criminal Superior Court + * (https://www.masscourts.org/eservices/home.page.16) + * [ ] FOIA / public records request? + * (https://massachusettsdot.mycusthelp.com/WEBAPP/_rs/(S(tbcygdlm0oojy35p1wv0y2y5))/supporthome.aspx) + * [X] MBTA data blog? (https://www.massdottracker.com/datablog/) + * [ ] MassDOT developers Google group? (https://groups.google.com/g/massdotdevelopers) + * [X] preexisting posts + * [ ] ask directly? * [ ] Other? * * This program is free software: you can redistribute it and/or modify it @@ -164,10 +178,10 @@ static const IdMapping charliecard_types[] = { // Passes {.id = 135, .name = "30 Day Local Bus Pass"}, - {.id = 136, .name = "30 Day Inner Express Bus Pass"}, // - {.id = 137, .name = "30 Day Outer Express Bus Pass"}, // - {.id = 138, .name = "30 Day LinkPass"}, // - {.id = 139, .name = "30 Day Senior LinkPass"}, // + {.id = 136, .name = "30 Day Inner Express Bus Pass"}, + {.id = 137, .name = "30 Day Outer Express Bus Pass"}, + {.id = 138, .name = "30 Day LinkPass"}, + {.id = 139, .name = "30 Day Senior LinkPass"}, {.id = 148, .name = "30 Day TAP LinkPass"}, {.id = 150, .name = "Monthly Student LinkPass"}, {.id = 424, .name = "Monthly TAP LinkPass"}, // 0b0110101000 @@ -713,7 +727,7 @@ static DateTime end_validity_parse(const MfClassicData* data, enum CharlieActiveSector active_sec) { // End validity field is a bit odd; shares byte 1 with another variable (the card type field), // occupying only the last 3 bits (and subsequent two bytes), hence bitmask - // TODO; what are the add'l 3 bits between type & end validity fields? + // TODO: what are the add'l 3 bits between type & end validity fields? uint32_t ts_charlie_ev = pos_to_num(data, (active_sec == CHARLIE_ACTIVE_SECTOR_2) ? 2 : 3, 1, 1, 3); ts_charlie_ev = ts_charlie_ev & 0x1FFFFF; From 23c8c23051ff34812cfbad53499d104d25504a0a Mon Sep 17 00:00:00 2001 From: Zachary Weiss Date: Sun, 31 Mar 2024 17:31:00 -0400 Subject: [PATCH 2/9] Minor gate ID mapping corrections --- .../main/nfc/plugins/supported_cards/charliecard.c | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/applications/main/nfc/plugins/supported_cards/charliecard.c b/applications/main/nfc/plugins/supported_cards/charliecard.c index 1b34f369c..f10c6dcdb 100644 --- a/applications/main/nfc/plugins/supported_cards/charliecard.c +++ b/applications/main/nfc/plugins/supported_cards/charliecard.c @@ -161,7 +161,7 @@ typedef struct { const char* name; } IdMapping; -// this should be a complete accounting of types, +// this should be a complete accounting of types, (1 and 7 day pass types maybe missing?) static const IdMapping charliecard_types[] = { // Regular card types {.id = 367, .name = "Adult"}, @@ -402,9 +402,7 @@ static const IdMapping charliecard_fare_gate_ids[] = { {.id = 6647, .name = "Malden Center"}, {.id = 6648, .name = "Malden Center"}, // Chinatown - {.id = 6704, - .name = - "Malden Center"}, // Entry error? Placed after "Chinatown" divider, but with name Malden Center + {.id = 6704, .name = "Chinatown"}, {.id = 6705, .name = "Chinatown"}, {.id = 2099, .name = "Chinatown"}, {.id = 7003, .name = "Chinatown"}, @@ -485,7 +483,7 @@ static const IdMapping charliecard_fare_gate_ids[] = { {.id = 7016, .name = "Forest Hills"}, {.id = 6950, .name = "Forest Hills"}, {.id = 6951, .name = "Forest Hills"}, - {.id = 604, .name = "Forest Hills"}, // Entry error? + {.id = 604, .name = "Forest Hills"}, {.id = 7096, .name = "Forest Hills"}, // South Station {.id = 7039, .name = "South Station"}, From 07f63f951232e59e570fb31c01415aaf775145c2 Mon Sep 17 00:00:00 2001 From: Zachary Weiss Date: Sun, 7 Apr 2024 16:20:08 -0400 Subject: [PATCH 3/9] Misc cleanup/refactor + passes testing --- .../nfc/plugins/supported_cards/charliecard.c | 816 ++++++++++-------- 1 file changed, 458 insertions(+), 358 deletions(-) diff --git a/applications/main/nfc/plugins/supported_cards/charliecard.c b/applications/main/nfc/plugins/supported_cards/charliecard.c index f10c6dcdb..7acbfebfe 100644 --- a/applications/main/nfc/plugins/supported_cards/charliecard.c +++ b/applications/main/nfc/plugins/supported_cards/charliecard.c @@ -97,6 +97,7 @@ #define CHARLIE_TIME_DELTA_SECS 60 #define CHARLIE_END_VALID_DELTA_SECS 60 * 8 #define CHARLIE_N_TRIP_HISTORY 10 +#define CHARLIE_N_PASSES 4 enum CharlieActiveSector { CHARLIE_ACTIVE_SECTOR_2, @@ -155,6 +156,12 @@ typedef struct { uint8_t f_flag; } Trip; +typedef struct { + DateTime start_valid; + uint16_t type; + DateTime end_valid; +} Pass; + // IdMapping approach borrowed from Jeremy Cooper's 'clipper.c' typedef struct { uint16_t id; @@ -168,7 +175,7 @@ static const IdMapping charliecard_types[] = { {.id = 366, .name = "SV Adult"}, {.id = 418, .name = "Student"}, {.id = 419, .name = "Senior"}, - {.id = 420, .name = "Tap"}, + {.id = 420, .name = "TAP"}, {.id = 417, .name = "Blind"}, {.id = 426, .name = "Child"}, {.id = 410, .name = "Employee ID Without Passback"}, @@ -184,10 +191,10 @@ static const IdMapping charliecard_types[] = { {.id = 139, .name = "30 Day Senior LinkPass"}, {.id = 148, .name = "30 Day TAP LinkPass"}, {.id = 150, .name = "Monthly Student LinkPass"}, - {.id = 424, .name = "Monthly TAP LinkPass"}, // 0b0110101000 - {.id = 425, .name = "Monthly Senior LinkPass"}, // 0b0110101001 - {.id = 421, .name = "Senior TAP/Permit"}, // 0b0110100101 - {.id = 422, .name = "Senior TAP/Permit 30 Days"}, // 0b0110100110 + {.id = 424, .name = "Monthly TAP LinkPass"}, + {.id = 425, .name = "Monthly Senior LinkPass"}, + {.id = 421, .name = "Senior TAP/Permit"}, + {.id = 422, .name = "Senior TAP/Permit 30 Days"}, // Commuter rail passes {.id = 166, .name = "30 Day Commuter Rail Zone 1A Pass"}, @@ -558,7 +565,7 @@ static const IdMapping charliecard_fare_gate_ids[] = { {.id = 2010, .name = "Wood Island"}, {.id = 6971, .name = "Wood Island"}, // Orient Heights - {.id = 6621, .name = "Orient Heights"}, // marked as needs checking + {.id = 6621, .name = "Orient Heights"}, {.id = 6622, .name = "Orient Heights"}, {.id = 6623, .name = "Orient Heights"}, {.id = 2014, .name = "Orient Heights"}, @@ -590,8 +597,13 @@ static const IdMapping charliecard_fare_gate_ids[] = { }; static const size_t kNumFareGateIds = COUNT_OF(charliecard_fare_gate_ids); +// ********************************************************** +// ********************* MISC HELPERS *********************** +// ********************************************************** + static const uint8_t* pos_to_ptr(const MfClassicData* data, uint8_t sector_num, uint8_t block_num, uint8_t byte_num) { + // returns pointer to specified sector/block/byte of MFClassic card data uint8_t block_offset = mf_classic_get_first_block_num_of_sector(sector_num); return &data->block[block_offset + block_num].data[byte_num]; } @@ -602,16 +614,24 @@ static uint64_t pos_to_num( uint8_t block_num, uint8_t byte_num, uint8_t byte_len) { + // returns numeric values at specified card location, for given byte length. + // assumes big endian. return bit_lib_bytes_to_num_be(pos_to_ptr(data, sector_num, block_num, byte_num), byte_len); } static DateTime dt_delta(DateTime dt, uint64_t delta_secs) { + // returns shifted DateTime, from initial DateTime and time offset in seconds DateTime dt_shifted = {0}; datetime_timestamp_to_datetime(datetime_datetime_to_timestamp(&dt) + delta_secs, &dt_shifted); return dt_shifted; } +static bool dt_ge(DateTime dt1, DateTime dt2) { + // compares two DateTimes + return datetime_datetime_to_timestamp(&dt1) >= datetime_datetime_to_timestamp(&dt2); +} + static bool get_map_item(uint16_t id, const IdMapping* map, size_t sz, const char** out) { // code borrowed from Jeremy Cooper's 'clipper.c'. Used as follows: // const char* s; if(!get_map_item(_,_,_,&s)) {s="Default str";} @@ -626,8 +646,439 @@ static bool get_map_item(uint16_t id, const IdMapping* map, size_t sz, const cha return false; } +uint32_t time_now() { + return furi_hal_rtc_get_timestamp(); +} + +// ********************************************************** +// ********************* DATA PARSING *********************** +// ********************************************************** + +static Money money_parse( + const MfClassicData* data, + uint8_t sector_num, + uint8_t block_num, + uint8_t byte_num) { + // CharlieCards store all money values in two bytes as half-cents + // bitmask removes sign/flag, bitshift converts half-cents to cents, div & mod yield dollars & cents + uint16_t amt = (pos_to_num(data, sector_num, block_num, byte_num, 2) & 0x7FFF) >> 1; + return (Money){amt / 100, amt % 100}; +} + +static DateTime + date_parse(const MfClassicData* data, uint8_t sector_num, uint8_t block_num, uint8_t byte_num) { + // Dates are 3 bytes, in minutes since 2003/1/1 ("CHARLIE_EPOCH") + uint32_t ts_charlie = pos_to_num(data, sector_num, block_num, byte_num, 3); + 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) { + /* 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 + without caring which is active. For my sanity, I check both, and assert equal. + */ + + // bitshift (2bytes = 16 bits) by 6bits for just first 10bits + const uint16_t type1 = pos_to_num(data, 2, 1, 0, 2) >> 6; + const uint16_t type2 = pos_to_num(data, 3, 1, 0, 2) >> 6; + furi_assert(type1 == type2); + + return type1; +} + +static DateTime end_validity_parse( + const MfClassicData* data, + uint8_t sector_num, + uint8_t block_num, + uint8_t byte_num) { + // End validity field is a bit odd; shares first byte with another variable (the card type field), + // occupying only the last 3 bits (and subsequent two bytes), hence bitmask + // TODO: what are the add'l 3 bits between type & end validity fields? + uint32_t ts_charlie_ev = pos_to_num(data, sector_num, block_num, byte_num, 3) & 0x1FFFFF; + + // additionally, instead of minute deltas, is in 8 minute increments + // relative to CHARLIE_EPOCH (2003/1/1), per DEFCON31 researcher's work + 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 + 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 + // Sub-byte field divisions not drawn to scale, see code for exact bit offsets + // + // 0 1 2 3 4 5 6 + // +----.----.----+----.-+--.----.----+ + // | start | type | end | + // +----.----.----+----.-+--.----.----+ + + DateTime start = date_parse(data, sector_num, block_num, byte_num); + uint16_t type = pos_to_num(data, sector_num, block_num, byte_num + 3, 2) >> 6; + DateTime end = end_validity_parse(data, sector_num, block_num, byte_num + 4); + + return (Pass){start, type, end}; +} + +static Trip + trip_parse(const MfClassicData* data, uint8_t sector_num, uint8_t block_num, uint8_t byte_num) { + /* This function parses individual trips. Each trip packs 7 bytes, stored as follows: + + 0 1 2 3 4 5 6 + +----.----.----+----.--+-+----.----+ + | date | loc |f| amt | + +----.----.----+----.--+-+----.----+ + + Where date is in the typical format, loc represents the fare gate tapped, and amt is the fare amount. + Amount appears to contain some flag bits, however, it is unclear what precisely their function is. + + Gate ID ("loc") is only the first 13 bits of 0x3:0x5, the final three bits appear to be flags ("f"). + Least significant flag bit (ie "loc & 0x1") seems to indicate: + — When 0, fare (the amount by which balance is decremented) + — When 1, refill (the amount by which balance is incremented) + + On monthly pass cards, MSB of amt will be set: 0x8000 (negative zero) + Seemingly randomly (irrespective of card type, last trip, etc) 0x0001 will be set on amt in addition to + whatever the regular fare is (a half cent more). I am uncertain what this flag indicates. + */ + const DateTime date = date_parse(data, sector_num, block_num, byte_num); + const uint16_t gate = pos_to_num(data, sector_num, block_num, byte_num + 3, 2) >> 3; + const uint8_t g_flag = pos_to_num(data, sector_num, block_num, byte_num + 3, 2) & 0b111; + const Money fare = money_parse(data, sector_num, block_num, byte_num + 5); + const uint8_t f_flag = pos_to_num(data, sector_num, block_num, byte_num + 5, 2) & 0x8001; + return (Trip){date, gate, g_flag, fare, f_flag}; +} + +static Pass* passes_parse(const MfClassicData* data) { + // WIP. Read in all speculative passes into array + // 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 + // +----.----.----.----.----.-+--.----+----.----.----.----.----.-+--.----+----.----+ + // 0x100 | pass1? |0 00 | pass2? |0 00 | crc1 | + // +----.----.----.----.----.-+--.----+----.----.----.----.----.-+--.----+----.----+ + // 0x110 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | crc2 | + // +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+ + // 0x120 | 00 00 00 00 00 00 00 05 00 00 00 00 00 00 | crc3 | + // +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+ + + Pass* passes = malloc(sizeof(Pass) * CHARLIE_N_PASSES); + + for(size_t i = 0; i < CHARLIE_N_PASSES; i++) { + passes[i] = pass_parse(data, 4 + (i / 2), 1, (i % 2) * 7); + } + + return passes; +} + +static Trip* trips_parse(const MfClassicData* data) { + // Sectors 6 & 7 store the last 10 trips. Overall layout as follows: + // + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + // +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+ + // 0x180 | trip0 | trip1 | crc1 | + // +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+ + // ... ... ... ... + // +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+ + // 0x1D0 | trip8 | trip9 | crc5 | + // +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+ + // 0x1E0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | crc6 | + // +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+ + // + //Trips are not sorted, rather, appear to get overwritten sequentially. (eg, sorted modulo array rotation) + + Trip* trips = malloc(sizeof(Trip) * CHARLIE_N_TRIP_HISTORY); + + // Parse each trip field using some modular math magic to get the offsets: + // move from sector 6 -> 7 after the first 6 trips + // move a block within a given sector every 2 trips, reset every 3 blocks (as sector has changed) + // alternate between a start byte of 0 and 7 with every iteration + for(size_t i = 0; i < CHARLIE_N_TRIP_HISTORY; i++) { + trips[i] = trip_parse(data, 6 + (i / 6), (i / 2) % 3, (i % 2) * 7); + } + + // Iterate through the array to find the maximum (newest) date value + int max_idx = 0; + for(int i = 1; i < CHARLIE_N_TRIP_HISTORY; i++) { + if(dt_ge(trips[i].date, trips[max_idx].date)) { + max_idx = i; + } + } + + // Sort by rotating + for(int r = 0; r < (max_idx + 1); r++) { + // Store the first element + Trip temp = trips[0]; + // Shift elements to the left + for(int i = 0; i < CHARLIE_N_TRIP_HISTORY - 1; i++) { + trips[i] = trips[i + 1]; + } + // Move the first element to the last + trips[CHARLIE_N_TRIP_HISTORY - 1] = temp; + } + + // Reverse order, such that newest is first, oldest last + for(int i = 0; i < CHARLIE_N_TRIP_HISTORY / 2; i++) { + // Swap elements at index i and size - i - 1 + Trip temp = trips[i]; + trips[i] = trips[CHARLIE_N_TRIP_HISTORY - i - 1]; + trips[CHARLIE_N_TRIP_HISTORY - i - 1] = temp; + } + + return trips; +} + +/* +static DateTime expiry(DateTime iss) { + // Per Metrodroid CharlieCard parser (https://github.com/metrodroid/metrodroid/blob/master/src/commonMain/kotlin/au/id/micolous/metrodroid/transit/charlie/CharlieCardTransitData.kt) + // Expiry not explicitly stored in card data; rather, calculated from date of issue + // Cards were first issued in 2006, expired in 5 years, w/ no printed expiry date + // Cards issued after 2011 expire in 10 years + // + // Per DEFCON31 researcher's work (cited above): + // Student cards last one school year and expire at the end of August the following year + // Pre-2011 issued cards expire in 7 years, not 5 as claimed by Metrodroid + // Post-2011 expire in 10 years, less one day + // Redundant function given the existance of the end validity field? + // Any important distinctions between the two? + + + // perhaps additionally clipping to 2030-12-__ in anticipation of upcoming system migration? + // need to get a new card to confirm. + + // TODO add card type logic for student card expiry + DateTime exp; + if(iss.year < 2011) { + // add 7 years; assumes average year of 8766 hrs (to account for leap years) + // may be off by a few hours as a result + exp = dt_delta(iss, 7 * 8766 * 60 * 60); + } else { + // add 10 years, subtract a day. Same assumption as above + exp = dt_delta(iss, ((10 * 8766) - 24) * 60 * 60); + } + + return exp; +}*/ + +static bool expired(DateTime expiry, DateTime last_trip) { + // if a card has sat unused for >2 years, expired (verify this claim?) + // else expired if current date > expiry date + + uint32_t ts_exp = datetime_datetime_to_timestamp(&expiry); + uint32_t ts_last = datetime_datetime_to_timestamp(&last_trip); + uint32_t ts_now = time_now(); + + return (ts_exp <= ts_now) | ((ts_now - ts_last) >= (2 * 365 * 24 * 60 * 60)); +} + +// ********************************************************** +// ****************** STRING FORMATTING ********************* +// ********************************************************** + +void locale_format_dt_cat(FuriString* out, const DateTime* dt) { + // helper to print datetimes + FuriString* s = furi_string_alloc(); + + LocaleDateFormat date_format = locale_get_date_format(); + const char* separator = (date_format == LocaleDateFormatDMY) ? "." : "/"; + locale_format_date(s, dt, date_format, separator); + furi_string_cat(out, s); + locale_format_time(s, dt, locale_get_time_format(), false); + furi_string_cat_printf(out, " "); + furi_string_cat(out, s); + + furi_string_free(s); +} + +void type_format_cat(FuriString* out, uint16_t type) { + const char* s; + if(!get_map_item(type, charliecard_types, kNumTypes, &s)) { + s = ""; + furi_string_cat_printf(out, "Unknown-%u", type); + } + + furi_string_cat_str(out, s); +} + +void pass_format_cat(FuriString* out, Pass pass) { + furi_string_cat_printf(out, "\nPass type: "); + type_format_cat(out, pass.type); + furi_string_cat_printf(out, "\nPass start: "); + locale_format_dt_cat(out, &pass.start_valid); + furi_string_cat_printf(out, "\nPass end: "); + locale_format_dt_cat(out, &pass.end_valid); +} + +void passes_format_cat(FuriString* out, Pass* passes) { + furi_string_cat_printf(out, "\nPasses:"); + for(size_t i = 0; i < CHARLIE_N_PASSES; i++) { + pass_format_cat(out, passes[i]); + furi_string_cat_printf(out, "\n"); + } +} + +void money_format_cat(FuriString* out, Money money) { + furi_string_cat_printf(out, "$%u.%02u", money.dollars, money.cents); +} + +void trip_format_cat(FuriString* out, Trip trip) { + const char* sep = " "; + const char* sta; + + locale_format_dt_cat(out, &trip.date); + furi_string_cat_printf(out, "\n%s", !!(trip.g_flag & 0x1) ? "-" : "+"); + money_format_cat(out, trip.fare); + if(!!(trip.g_flag & 0x1) && (trip.fare.dollars == FARE_BUS.dollars) && + (trip.fare.cents == FARE_BUS.cents)) { + // if not a refill, and the fare amount is equal to bus fare (any better approach? flag bits for modality?) + // format for bus (gate ID on busses = posted bus #) + furi_string_cat_printf(out, "%sBus#%u", sep, trip.gate); + } else if(get_map_item(trip.gate, charliecard_fare_gate_ids, kNumFareGateIds, &sta)) { + // station found in fare gate ID map, append station name + furi_string_cat_str(out, sep); + furi_string_cat_str(out, sta); + } else { + // no found station in fare gate ID map & not a bus, just print ID w/o add'l info + furi_string_cat_printf(out, "%s%u", sep, trip.gate); + } + // print flags for debugging purposes + if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { + furi_string_cat_printf(out, "%s%u%s%u", sep, trip.g_flag, sep, trip.f_flag); + } +} + +void trips_format_cat(FuriString* out, Trip* trips) { + furi_string_cat_printf(out, "\nTransactions:"); + for(size_t i = 0; i < CHARLIE_N_TRIP_HISTORY; i++) { + furi_string_cat_printf(out, "\n"); + trip_format_cat(out, trips[i]); + furi_string_cat_printf(out, "\n"); + } +} + +// ********************************************************** +// **************** NFC PLUGIN BOILERPLATE ****************** +// ********************************************************** + +static bool charliecard_parse(const NfcDevice* device, FuriString* parsed_data) { + furi_assert(device); + + const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic); + + bool parsed = false; + + do { + // Verify card type + if(data->type != MfClassicType1k) break; + + // Verify key + // arbitrary sector in the main data portion + const uint8_t verify_sector = 3; + const MfClassicSectorTrailer* sec_tr = + mf_classic_get_sector_trailer_by_sector(data, verify_sector); + + const uint64_t key_a = + bit_lib_bytes_to_num_be(sec_tr->key_a.data, COUNT_OF(sec_tr->key_a.data)); + const uint64_t key_b = + bit_lib_bytes_to_num_be(sec_tr->key_b.data, COUNT_OF(sec_tr->key_b.data)); + if(key_a != charliecard_1k_keys[verify_sector].a) break; + if(key_b != charliecard_1k_keys[verify_sector].b) break; + + // TODO: Verify add'l? + + const enum CharlieActiveSector active_sec_enum = get_active_sector(data); + const uint8_t active_sector = (active_sec_enum == CHARLIE_ACTIVE_SECTOR_2) ? 2 : 3; + + 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); + + Money bal = money_parse(data, active_sector, 1, 5); + furi_string_cat_printf(parsed_data, "\nBal: "); + money_format_cat(parsed_data, bal); + + const uint16_t type = type_parse(data); + furi_string_cat_printf(parsed_data, "\nType: "); + type_format_cat(parsed_data, type); + + Pass* passes = passes_parse(data); + passes_format_cat(parsed_data, passes); + free(passes); + + const uint16_t n_trips = n_uses(data, active_sec_enum); + 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: "); + locale_format_dt_cat(parsed_data, &iss); + + const DateTime e_v = main_end_validity_parse(data, active_sec_enum); + furi_string_cat_printf(parsed_data, "\nExpiry: "); + locale_format_dt_cat(parsed_data, &e_v); + + DateTime last = date_parse(data, active_sector, 0, 1); + 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); + free(trips); + + parsed = true; + } while(false); + + return parsed; +} + static bool charliecard_verify(Nfc* nfc) { - // does this suffice? Or should I check add'l keys/data/etc? bool verified = false; do { @@ -699,357 +1150,6 @@ static bool charliecard_read(Nfc* nfc, NfcDevice* device) { return is_read; } -uint32_t time_now() { - return furi_hal_rtc_get_timestamp(); -} - -static Money money_parse( - const MfClassicData* data, - uint8_t sector_num, - uint8_t block_num, - uint8_t byte_num) { - // CharlieCards store all money values in two bytes as half-cents - // bitmask removes sign/flag, bitshift converts half-cents to cents, div & mod yield dollars & cents - uint16_t amt = (pos_to_num(data, sector_num, block_num, byte_num, 2) & 0x7FFF) >> 1; - return (Money){amt / 100, amt % 100}; -} - -static DateTime - date_parse(const MfClassicData* data, uint8_t sector_num, uint8_t block_num, uint8_t byte_num) { - // Dates are 3 bytes, in minutes since 2003/1/1 ("CHARLIE_EPOCH") - uint32_t ts_charlie = pos_to_num(data, sector_num, block_num, byte_num, 3); - return dt_delta(CHARLIE_EPOCH, ts_charlie * CHARLIE_TIME_DELTA_SECS); -} - -static DateTime - end_validity_parse(const MfClassicData* data, enum CharlieActiveSector active_sec) { - // End validity field is a bit odd; shares byte 1 with another variable (the card type field), - // occupying only the last 3 bits (and subsequent two bytes), hence bitmask - // TODO: what are the add'l 3 bits between type & end validity fields? - uint32_t ts_charlie_ev = - pos_to_num(data, (active_sec == CHARLIE_ACTIVE_SECTOR_2) ? 2 : 3, 1, 1, 3); - ts_charlie_ev = ts_charlie_ev & 0x1FFFFF; - - // additionally, instead of minute deltas, is in 8 minute increments - // relative to CHARLIE_EPOCH (2003/1/1), per DEFCON31 researcher's work - return dt_delta(CHARLIE_EPOCH, ts_charlie_ev * CHARLIE_END_VALID_DELTA_SECS); -} - -static Trip - trip_parse(const MfClassicData* data, uint8_t sector_num, uint8_t block_num, uint8_t byte_num) { - /* This function parses individual trips. Each trip packs 7 bytes, stored as follows: - - 0 1 2 3 4 5 6 - +----.----.----+----.--+-+----.----+ - | date | loc |f| amt | - +----.----.----+----.--+-+----.----+ - - Where date is in the typical format, loc represents the fare gate tapped, and amt is the fare amount. - Amount appears to contain some flag bits, however, it is unclear what precisely their function is. - - Gate ID ("loc") is only the first 13 bits of 0x3:0x5, the final three bits appear to be flags ("f"). - Least significant flag bit (ie "loc & 0x1") seems to indicate: - — When 0, fare (the amount by which balance is decremented) - — When 1, refill (the amount by which balance is incremented) - - On monthly pass cards, MSB of amt will be set: 0x8000 (negative zero) - Seemingly randomly (irrespective of card type, last trip, etc) 0x0001 will be set on amt in addition to - whatever the regular fare is (a half cent more). I am uncertain what this flag indicates. - */ - const DateTime date = date_parse(data, sector_num, block_num, byte_num); - const uint16_t gate = pos_to_num(data, sector_num, block_num, byte_num + 3, 2) >> 3; - const uint8_t g_flag = pos_to_num(data, sector_num, block_num, byte_num + 3, 2) & 0b111; - const Money fare = money_parse(data, sector_num, block_num, byte_num + 5); - const uint8_t f_flag = pos_to_num(data, sector_num, block_num, byte_num + 5, 2) & 0x8001; - return (Trip){date, gate, g_flag, fare, f_flag}; -} - -static bool date_ge(DateTime dt1, DateTime dt2) { - return datetime_datetime_to_timestamp(&dt1) >= datetime_datetime_to_timestamp(&dt2); -} - -static Trip* trips_parse(const MfClassicData* data) { - /* Sectors 6 & 7 store the last 10 trips. Overall layout as follows: - - 0 1 2 3 4 5 6 7 8 9 A B C D E F - +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+ - 0x180 | trip0 | trip1 | crc1 | - +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+ - ... ... ... ... - +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+ - 0x1D0 | trip8 | trip9 | crc5 | - +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+ - 0x1E0 | empty | crc6 | - +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+ - - "empty" is all 0s. Trips are not sorted, rather, appear to get overwritten sequentially. (eg, sorted modulo array rotation) - */ - Trip* trips = malloc(sizeof(Trip) * CHARLIE_N_TRIP_HISTORY); - - // Parse each trip field using some modular math magic to get the offsets: - // move from sector 6 -> 7 after the first 6 trips - // move a block within a given sector every 2 trips, reset every 3 blocks (as sector has changed) - // alternate between a start byte of 0 and 7 with every iteration - for(size_t i = 0; i < CHARLIE_N_TRIP_HISTORY; i++) { - trips[i] = trip_parse(data, 6 + (i / 6), (i / 2) % 3, (i % 2) * 7); - } - - // Iterate through the array to find the maximum (newest) date value - int max_idx = 0; - for(int i = 1; i < CHARLIE_N_TRIP_HISTORY; i++) { - if(date_ge(trips[i].date, trips[max_idx].date)) { - max_idx = i; - } - } - - // Sort by rotating - for(int r = 0; r < (max_idx + 1); r++) { - // Store the first element - Trip temp = trips[0]; - // Shift elements to the left - for(int i = 0; i < CHARLIE_N_TRIP_HISTORY - 1; i++) { - trips[i] = trips[i + 1]; - } - // Move the first element to the last - trips[CHARLIE_N_TRIP_HISTORY - 1] = temp; - } - - // Reverse order, such that newest is first, oldest last - for(int i = 0; i < CHARLIE_N_TRIP_HISTORY / 2; i++) { - // Swap elements at index i and size - i - 1 - Trip temp = trips[i]; - trips[i] = trips[CHARLIE_N_TRIP_HISTORY - i - 1]; - trips[CHARLIE_N_TRIP_HISTORY - i - 1] = temp; - } - - return trips; -} - -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) { - /* 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 - without caring which is active. For my sanity, I check both, and assert equal. - */ - - // bitshift (2bytes = 16 bits) by 6bits for just first 10bits - const uint16_t type1 = pos_to_num(data, 2, 1, 0, 2) >> 6; - const uint16_t type2 = pos_to_num(data, 3, 1, 0, 2) >> 6; - furi_assert(type1 == type2); - - return type1; -} - -/* -static DateTime expiry(DateTime iss) { - // Per Metrodroid CharlieCard parser (https://github.com/metrodroid/metrodroid/blob/master/src/commonMain/kotlin/au/id/micolous/metrodroid/transit/charlie/CharlieCardTransitData.kt) - // Expiry not explicitly stored in card data; rather, calculated from date of issue - // Cards were first issued in 2006, expired in 5 years, w/ no printed expiry date - // Cards issued after 2011 expire in 10 years - // - // Per DEFCON31 researcher's work (cited above): - // Student cards last one school year and expire at the end of August the following year - // Pre-2011 issued cards expire in 7 years, not 5 as claimed by Metrodroid - // Post-2011 expire in 10 years, less one day - // Redundant function given the existance of the end validity field? - // Any important distinctions between the two? - - - // perhaps additionally clipping to 2030-12-__ in anticipation of upcoming system migration? - // need to get a new card to confirm. - - // TODO add card type logic for student card expiry - DateTime exp; - if(iss.year < 2011) { - // add 7 years; assumes average year of 8766 hrs (to account for leap years) - // may be off by a few hours as a result - exp = dt_delta(iss, 7 * 8766 * 60 * 60); - } else { - // add 10 years, subtract a day. Same assumption as above - exp = dt_delta(iss, ((10 * 8766) - 24) * 60 * 60); - } - - return exp; -}*/ - -static bool expired(DateTime expiry, DateTime last_trip) { - // if a card has sat unused for >2 years, expired (verify this claim?) - // else expired if current date > expiry date - - uint32_t ts_exp = datetime_datetime_to_timestamp(&expiry); - uint32_t ts_last = datetime_datetime_to_timestamp(&last_trip); - uint32_t ts_now = time_now(); - - return (ts_exp <= ts_now) | ((ts_now - ts_last) >= (2 * 365 * 24 * 60 * 60)); -} - -void locale_format_dt_cat(FuriString* out, const DateTime* dt) { - // helper to print datetimes - FuriString* s = furi_string_alloc(); - - LocaleDateFormat date_format = locale_get_date_format(); - const char* separator = (date_format == LocaleDateFormatDMY) ? "." : "/"; - locale_format_date(s, dt, date_format, separator); - furi_string_cat(out, s); - locale_format_time(s, dt, locale_get_time_format(), false); - furi_string_cat_printf(out, " "); - furi_string_cat(out, s); - - furi_string_free(s); -} - -void type_format_cat(FuriString* out, uint16_t type) { - const char* s; - if(!get_map_item(type, charliecard_types, kNumTypes, &s)) { - s = ""; - furi_string_cat_printf(out, "Unknown-%u", type); - } - - furi_string_cat_str(out, s); -} - -void money_format_cat(FuriString* out, Money money) { - furi_string_cat_printf(out, "$%u.%02u", money.dollars, money.cents); -} - -void trip_format_cat(FuriString* out, Trip trip) { - const char* sep = " "; - const char* sta; - - locale_format_dt_cat(out, &trip.date); - furi_string_cat_printf(out, "\n%s", !!(trip.g_flag & 0x1) ? "-" : "+"); - money_format_cat(out, trip.fare); - if(!!(trip.g_flag & 0x1) && (trip.fare.dollars == FARE_BUS.dollars) && - (trip.fare.cents == FARE_BUS.cents)) { - // if not a refill, and the fare amount is equal to bus fare (any better approach? flag bits for modality?) - // format for bus (gate ID on busses = posted bus #) - furi_string_cat_printf(out, "%sBus#%u", sep, trip.gate); - } else if(get_map_item(trip.gate, charliecard_fare_gate_ids, kNumFareGateIds, &sta)) { - // station found in fare gate ID map, append station name - furi_string_cat_str(out, sep); - furi_string_cat_str(out, sta); - } else { - // no found station in fare gate ID map & not a bus, just print ID w/o add'l info - furi_string_cat_printf(out, "%s%u", sep, trip.gate); - } - // print flags for debugging purposes - if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { - furi_string_cat_printf(out, "%s%u%s%u", sep, trip.g_flag, sep, trip.f_flag); - } -} - -static bool charliecard_parse(const NfcDevice* device, FuriString* parsed_data) { - furi_assert(device); - - const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic); - - bool parsed = false; - - do { - // Verify card type - if(data->type != MfClassicType1k) break; - - // Verify key - // arbitrary sector in the main data portion - const uint8_t verify_sector = 3; - const MfClassicSectorTrailer* sec_tr = - mf_classic_get_sector_trailer_by_sector(data, verify_sector); - - const uint64_t key_a = - bit_lib_bytes_to_num_be(sec_tr->key_a.data, COUNT_OF(sec_tr->key_a.data)); - const uint64_t key_b = - bit_lib_bytes_to_num_be(sec_tr->key_b.data, COUNT_OF(sec_tr->key_b.data)); - if(key_a != charliecard_1k_keys[verify_sector].a) break; - if(key_b != charliecard_1k_keys[verify_sector].b) break; - - // TODO: Verify add'l? - - const enum CharlieActiveSector active_sec_enum = get_active_sector(data); - const uint8_t active_sector = (active_sec_enum == CHARLIE_ACTIVE_SECTOR_2) ? 2 : 3; - - 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); - - Money bal = money_parse(data, active_sector, 1, 5); - furi_string_cat_printf(parsed_data, "\nBal: "); - money_format_cat(parsed_data, bal); - - const uint16_t type = type_parse(data); - furi_string_cat_printf(parsed_data, "\nType: "); - type_format_cat(parsed_data, type); - - const uint16_t n_trips = n_uses(data, active_sec_enum); - 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: "); - locale_format_dt_cat(parsed_data, &iss); - - const DateTime e_v = end_validity_parse(data, active_sec_enum); - furi_string_cat_printf(parsed_data, "\nExpiry: "); - locale_format_dt_cat(parsed_data, &e_v); - - DateTime last = date_parse(data, active_sector, 0, 1); - furi_string_cat_printf(parsed_data, "\nExpired: %s", expired(e_v, last) ? "Yes" : "No"); - - Trip* trips = trips_parse(data); - furi_string_cat_printf(parsed_data, "\nTransactions:"); - for(size_t i = 0; i < CHARLIE_N_TRIP_HISTORY; i++) { - furi_string_cat_printf(parsed_data, "\n"); - trip_format_cat(parsed_data, trips[i]); - furi_string_cat_printf(parsed_data, "\n"); - } - free(trips); - - parsed = true; - } while(false); - - return parsed; -} - /* Actual implementation of app<>plugin interface */ static const NfcSupportedCardsPlugin charliecard_plugin = { .protocol = NfcProtocolMfClassic, From 3af66c78153161ce8ae61189e19d815813cb9211 Mon Sep 17 00:00:00 2001 From: Zachary Weiss Date: Sun, 7 Apr 2024 20:49:06 -0400 Subject: [PATCH 4/9] ASCII of known layouts, pass testing --- .../nfc/plugins/supported_cards/charliecard.c | 119 +++++++++++++----- 1 file changed, 89 insertions(+), 30 deletions(-) diff --git a/applications/main/nfc/plugins/supported_cards/charliecard.c b/applications/main/nfc/plugins/supported_cards/charliecard.c index 7acbfebfe..963776047 100644 --- a/applications/main/nfc/plugins/supported_cards/charliecard.c +++ b/applications/main/nfc/plugins/supported_cards/charliecard.c @@ -157,9 +157,10 @@ typedef struct { } Trip; typedef struct { - DateTime start_valid; + bool valid; uint16_t type; - DateTime end_valid; + DateTime start; + DateTime end; } Pass; // IdMapping approach borrowed from Jeremy Cooper's 'clipper.c' @@ -650,6 +651,10 @@ uint32_t time_now() { return furi_hal_rtc_get_timestamp(); } +static bool is_debug() { + return furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug); +} + // ********************************************************** // ********************* DATA PARSING *********************** // ********************************************************** @@ -731,9 +736,8 @@ static DateTime end_validity_parse( uint8_t sector_num, uint8_t block_num, uint8_t byte_num) { - // End validity field is a bit odd; shares first byte with another variable (the card type field), - // occupying only the last 3 bits (and subsequent two bytes), hence bitmask - // TODO: what are the add'l 3 bits between type & end validity fields? + // End validity field is weird; shares first byte with another variable (the card type field), + // occupying the last 5 bits (and subsequent two bytes), hence bitmask uint32_t ts_charlie_ev = pos_to_num(data, sector_num, block_num, byte_num, 3) & 0x1FFFFF; // additionally, instead of minute deltas, is in 8 minute increments @@ -752,16 +756,33 @@ static Pass // WIP; testing only. Speculating it may be structured as follows // Sub-byte field divisions not drawn to scale, see code for exact bit offsets // - // 0 1 2 3 4 5 6 - // +----.----.----+----.-+--.----.----+ - // | start | type | end | - // +----.----.----+----.-+--.----.----+ + // 0 1 2 3 4 5 + // +----.----.----+----.----.----+ + // | type | end | start | + // +----.----.----+----.----.----+ + // + // "Blank" entries are as follows: + // 0 1 2 3 4 5 + // +----.----.----.----.----.--+-. + // | 00 20 00 00 00 0| + // +----.----.----.----.----.--+-. - DateTime start = date_parse(data, sector_num, block_num, byte_num); - uint16_t type = pos_to_num(data, sector_num, block_num, byte_num + 3, 2) >> 6; - DateTime end = end_validity_parse(data, sector_num, block_num, byte_num + 4); + // check for empty, if so, return struct filled w/ 0s + // (incl "valid" field: hence, "valid" is false-y) + if(pos_to_num(data, sector_num, block_num, byte_num, 6) == 0x002000000000) { + return (Pass){0}; + } - return (Pass){start, type, end}; + DateTime start = date_parse(data, sector_num, block_num, byte_num + 1); + uint16_t type = pos_to_num(data, sector_num, block_num, byte_num, 2) >> 6; + + // these values make sense for end + DateTime end = end_validity_parse(data, sector_num, block_num, byte_num + 1); + + // DateTime start = date_parse(data, sector_num, block_num, byte_num); + // uint16_t type = 0; // pos_to_num(data, sector_num, block_num, byte_num + 3, 2) >> 6; + + return (Pass){true, type, start, end}; } static Trip @@ -793,6 +814,39 @@ static Trip return (Trip){date, gate, g_flag, fare, f_flag}; } +// Manufacturer data (Sector 0) +// +// 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) +// +// 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 ... | +// +----.----+----+----.----.----.----.----.----.----.----.----.----.----.----.----+ + +// Balance / type / last transaction (Sector 2 & 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 | +// +----+----.----.----+----+----+----+----+----+----.----+----.----+----+----.----+ +// 0x090 | type |end validity|unkn| balance | 00 | unknown | crc | +// +----.----.----.----+----+----.----+----+----.----.----.----.----.----+----.----+ +// 0x0A0 | 20 ... 00 00 ... 04 | crc | +// +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+ + static Pass* passes_parse(const MfClassicData* data) { // WIP. Read in all speculative passes into array // Sectors 4 & 5 speculated to contain pass data, @@ -801,17 +855,17 @@ static Pass* passes_parse(const MfClassicData* data) { // // 0 1 2 3 4 5 6 7 8 9 A B C D E F // +----.----.----.----.----.-+--.----+----.----.----.----.----.-+--.----+----.----+ - // 0x100 | pass1? |0 00 | pass2? |0 00 | crc1 | + // 0x100 | pass0? |0 00 | pass1? |0 00 | crc | // +----.----.----.----.----.-+--.----+----.----.----.----.----.-+--.----+----.----+ - // 0x110 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | crc2 | + // 0x110 | ... 00 00 ... | crc | // +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+ - // 0x120 | 00 00 00 00 00 00 00 05 00 00 00 00 00 00 | crc3 | + // 0x120 | ... 00 ... 05 | crc | // +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+ Pass* passes = malloc(sizeof(Pass) * CHARLIE_N_PASSES); for(size_t i = 0; i < CHARLIE_N_PASSES; i++) { - passes[i] = pass_parse(data, 4 + (i / 2), 1, (i % 2) * 7); + passes[i] = pass_parse(data, 4 + (i / 2), 0, (i % 2) * 7); } return passes; @@ -822,13 +876,13 @@ static Trip* trips_parse(const MfClassicData* data) { // // 0 1 2 3 4 5 6 7 8 9 A B C D E F // +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+ - // 0x180 | trip0 | trip1 | crc1 | + // 0x180 | trip0 | trip1 | crc | // +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+ // ... ... ... ... // +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+ - // 0x1D0 | trip8 | trip9 | crc5 | + // 0x1D0 | trip8 | trip9 | crc | // +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+ - // 0x1E0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | crc6 | + // 0x1E0 | ... 00 00 ... | crc | // +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+ // //Trips are not sorted, rather, appear to get overwritten sequentially. (eg, sorted modulo array rotation) @@ -947,19 +1001,24 @@ void type_format_cat(FuriString* out, uint16_t type) { } void pass_format_cat(FuriString* out, Pass pass) { - furi_string_cat_printf(out, "\nPass type: "); + furi_string_cat_printf(out, "\n-Type: "); type_format_cat(out, pass.type); - furi_string_cat_printf(out, "\nPass start: "); - locale_format_dt_cat(out, &pass.start_valid); - furi_string_cat_printf(out, "\nPass end: "); - locale_format_dt_cat(out, &pass.end_valid); + furi_string_cat_printf(out, "\n-Start: "); + locale_format_dt_cat(out, &pass.start); + furi_string_cat_printf(out, "\n-End: "); + locale_format_dt_cat(out, &pass.end); } void passes_format_cat(FuriString* out, Pass* passes) { - furi_string_cat_printf(out, "\nPasses:"); - for(size_t i = 0; i < CHARLIE_N_PASSES; i++) { - pass_format_cat(out, passes[i]); - furi_string_cat_printf(out, "\n"); + if(is_debug()) { + furi_string_cat_printf(out, "\nPasses (DEBUG & WIP):"); + for(size_t i = 0; i < CHARLIE_N_PASSES; i++) { + if(passes[i].valid) { + furi_string_cat_printf(out, "\nPass %u", i + 1); + pass_format_cat(out, passes[i]); + furi_string_cat_printf(out, "\n"); + } + } } } @@ -988,7 +1047,7 @@ void trip_format_cat(FuriString* out, Trip trip) { furi_string_cat_printf(out, "%s%u", sep, trip.gate); } // print flags for debugging purposes - if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { + if(is_debug()) { furi_string_cat_printf(out, "%s%u%s%u", sep, trip.g_flag, sep, trip.f_flag); } } From 3ea6d46c798ee07e4a34783ece781de33c9cb19e Mon Sep 17 00:00:00 2001 From: Zachary Weiss Date: Sun, 7 Apr 2024 22:10:46 -0400 Subject: [PATCH 5/9] Parsing function readability refactor --- .../nfc/plugins/supported_cards/charliecard.c | 233 +++++++++--------- 1 file changed, 118 insertions(+), 115 deletions(-) diff --git a/applications/main/nfc/plugins/supported_cards/charliecard.c b/applications/main/nfc/plugins/supported_cards/charliecard.c index 963776047..7313b49ee 100644 --- a/applications/main/nfc/plugins/supported_cards/charliecard.c +++ b/applications/main/nfc/plugins/supported_cards/charliecard.c @@ -23,8 +23,6 @@ * — Reverse engineer passes (sectors 4 & 5?), impl. * — Infer transaction flag meanings * — 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 * — Mapping of buses to garages, and subsequently, route subsets via * http://roster.transithistory.org/ data @@ -99,11 +97,6 @@ #define CHARLIE_N_TRIP_HISTORY 10 #define CHARLIE_N_PASSES 4 -enum CharlieActiveSector { - CHARLIE_ACTIVE_SECTOR_2, - CHARLIE_ACTIVE_SECTOR_3, -}; - typedef struct { uint64_t a; uint64_t b; @@ -163,6 +156,18 @@ typedef struct { DateTime end; } 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' typedef struct { uint16_t id; @@ -656,7 +661,7 @@ static bool is_debug() { } // ********************************************************** -// ********************* DATA PARSING *********************** +// ******************** FIELD PARSING *********************** // ********************************************************** static Money money_parse( @@ -677,46 +682,6 @@ static DateTime 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) { /* 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 @@ -745,12 +710,6 @@ static DateTime end_validity_parse( 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 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 @@ -814,53 +773,100 @@ static Trip return (Trip){date, gate, g_flag, fare, f_flag}; } -// Manufacturer data (Sector 0) -// -// 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 ... | -// +----.----.----.----.----.----.----.----.----.----.----.----.----.----.----.----+ +// ********************************************************** +// ******************* SECTOR PARSING *********************** +// ********************************************************** -// 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 ... | -// +----.----+----+----.----.----.----.----.----.----.----.----.----.----.----.----+ +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 + // +----.----.----.----+----+----.----.----.----+----+----.----.----.----.----+----+ + // 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 ... | + // +----.----.----.----.----.----.----.----.----.----.----.----.----.----.----.----+ -// Balance / type / last transaction (Sector 2 & 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 | -// +----+----.----.----+----+----+----+----+----+----.----+----.----+----+----.----+ -// 0x090 | type |end validity|unkn| balance | 00 | unknown | crc | -// +----.----.----.----+----+----.----+----+----.----.----.----.----.----+----.----+ -// 0x0A0 | 20 ... 00 00 ... 04 | crc | -// +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+ + size_t uid_len = 0; + const uint8_t* uid = mf_classic_get_uid(data, &uid_len); + const uint32_t card_number = bit_lib_bytes_to_num_be(uid, 4); + + return card_number; +} + +static CounterSector counter_sector_parse(const MfClassicData* data) { + // 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) { - // WIP. Read in all speculative passes into array - // 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. + // Passes, speculative (Sectors 4 &/or 5) // // 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); @@ -872,7 +878,7 @@ static Pass* passes_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 6–7) // // 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 | // +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+ // - //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); @@ -958,7 +965,7 @@ static DateTime expiry(DateTime iss) { } return exp; -}*/ +} static bool expired(DateTime expiry, DateTime last_trip) { // 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)); } +*/ // ********************************************************** // ****************** STRING FORMATTING ********************* @@ -1011,7 +1019,7 @@ void pass_format_cat(FuriString* out, Pass pass) { void passes_format_cat(FuriString* out, Pass* passes) { 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++) { if(passes[i].valid) { 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? - const enum CharlieActiveSector active_sec_enum = get_active_sector(data); - const uint8_t active_sector = (active_sec_enum == CHARLIE_ACTIVE_SECTOR_2) ? 2 : 3; + // parse card data + 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"); - - 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); - Money bal = money_parse(data, active_sector, 1, 5); 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: "); - 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); free(passes); - const uint16_t n_trips = n_uses(data, active_sec_enum); - furi_string_cat_printf(parsed_data, "\nTrip Count: %u", n_trips); + furi_string_cat_printf(parsed_data, "\nTrip Count: %u", counter_sector.n_uses); - const DateTime iss = date_parse(data, active_sector, 0, 6); 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: "); - 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); - furi_string_cat_printf(parsed_data, "\nExpired: %s", expired(e_v, last) ? "Yes" : "No"); + // const DateTime last = date_parse(data, active_sector, 0, 1); + // 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); free(trips); From 33cfd6340a109a4f4892bba5491547a2eeb6f005 Mon Sep 17 00:00:00 2001 From: Zachary Weiss Date: Mon, 8 Apr 2024 22:14:46 -0400 Subject: [PATCH 6/9] More cleanup, pass testing --- .../nfc/plugins/supported_cards/charliecard.c | 177 ++++++++++-------- 1 file changed, 99 insertions(+), 78 deletions(-) diff --git a/applications/main/nfc/plugins/supported_cards/charliecard.c b/applications/main/nfc/plugins/supported_cards/charliecard.c index 7313b49ee..81b9bb897 100644 --- a/applications/main/nfc/plugins/supported_cards/charliecard.c +++ b/applications/main/nfc/plugins/supported_cards/charliecard.c @@ -146,14 +146,14 @@ typedef struct { uint16_t gate; uint8_t g_flag; Money fare; - uint8_t f_flag; + uint16_t f_flag; } Trip; typedef struct { bool valid; - uint16_t type; - DateTime start; - DateTime end; + uint16_t pre; + uint16_t post; + DateTime date; } Pass; typedef struct { @@ -682,20 +682,6 @@ static DateTime return dt_delta(CHARLIE_EPOCH, ts_charlie * CHARLIE_TIME_DELTA_SECS); } -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) - To my knowledge, card type should never change, so we can check either - without caring which is active. For my sanity, I check both, and assert equal. - */ - - // bitshift (2bytes = 16 bits) by 6bits for just first 10bits - const uint16_t type1 = pos_to_num(data, 2, 1, 0, 2) >> 6; - const uint16_t type2 = pos_to_num(data, 3, 1, 0, 2) >> 6; - furi_assert(type1 == type2); - - return type1; -} - static DateTime end_validity_parse( const MfClassicData* data, uint8_t sector_num, @@ -716,36 +702,45 @@ static Pass // Sub-byte field divisions not drawn to scale, see code for exact bit offsets // // 0 1 2 3 4 5 - // +----.----.----+----.----.----+ - // | type | end | start | - // +----.----.----+----.----.----+ + // +----.----.----.----+----.----+ + // | uk1 | date | uk2 | + // +----.----.----.----+----.----+ // // "Blank" entries are as follows: // 0 1 2 3 4 5 - // +----.----.----.----.----.--+-. - // | 00 20 00 00 00 0| - // +----.----.----.----.----.--+-. - + // +----.----.----.----.----.----+ + // | 00 20 00 00 00 00 | + // +----.----.----.----.----.----+ + // + // if not blank, uk1 MSB seems to always be set to 1... + // the sole bit set to 1 on the blank entry seems to divide + // the uk1 and date fields, and is always set to 1 regardless + // same is true of type & end-validity split found in balance sector + // + // // check for empty, if so, return struct filled w/ 0s // (incl "valid" field: hence, "valid" is false-y) if(pos_to_num(data, sector_num, block_num, byte_num, 6) == 0x002000000000) { return (Pass){0}; } - DateTime start = date_parse(data, sector_num, block_num, byte_num + 1); - uint16_t type = pos_to_num(data, sector_num, block_num, byte_num, 2) >> 6; + // const DateTime start = date_parse(data, sector_num, block_num, byte_num + 1); - // these values make sense for end - DateTime end = end_validity_parse(data, sector_num, block_num, byte_num + 1); + const uint16_t pre = pos_to_num(data, sector_num, block_num, byte_num, 2) >> 6; + const uint16_t post = pos_to_num(data, sector_num, block_num, byte_num + 4, 2); + + // these values make sense for a date, but implied position of type + // before end validity, as seen in balance sector, doesn't seem + // to produce sensible values + const DateTime date = end_validity_parse(data, sector_num, block_num, byte_num + 1); // DateTime start = date_parse(data, sector_num, block_num, byte_num); // uint16_t type = 0; // pos_to_num(data, sector_num, block_num, byte_num + 3, 2) >> 6; - return (Pass){true, type, start, end}; + return (Pass){true, pre, post, date}; } -static Trip - trip_parse(const MfClassicData* data, uint8_t sector_num, uint8_t block_num, uint8_t byte_num) { +static Trip trip_parse(const MfClassicData* data, uint8_t sector, uint8_t block, uint8_t byte) { /* This function parses individual trips. Each trip packs 7 bytes, stored as follows: 0 1 2 3 4 5 6 @@ -757,19 +752,23 @@ static Trip Amount appears to contain some flag bits, however, it is unclear what precisely their function is. Gate ID ("loc") is only the first 13 bits of 0x3:0x5, the final three bits appear to be flags ("f"). - Least significant flag bit (ie "loc & 0x1") seems to indicate: - — When 0, fare (the amount by which balance is decremented) - — When 1, refill (the amount by which balance is incremented) + Least significant flag bit seems to indicate: + — When f & 1 == 0, fare (the amount by which balance is decremented) + — When f & 1 == 1, refill (the amount by which balance is incremented) + MSB (sign bit) of amt seems to serve the same role, just inverted, ie + — When amt & 0x8000 == 0x8000, fare + — When amt & 0x8000 == 0, refill - On monthly pass cards, MSB of amt will be set: 0x8000 (negative zero) - Seemingly randomly (irrespective of card type, last trip, etc) 0x0001 will be set on amt in addition to - whatever the regular fare is (a half cent more). I am uncertain what this flag indicates. + Remaining unknown bits: + — f & 0b100 + — f & 0b010 + — amt & 1; does not seem to correspond with card type, last trip, first trip, refill v. fare, etc */ - const DateTime date = date_parse(data, sector_num, block_num, byte_num); - const uint16_t gate = pos_to_num(data, sector_num, block_num, byte_num + 3, 2) >> 3; - const uint8_t g_flag = pos_to_num(data, sector_num, block_num, byte_num + 3, 2) & 0b111; - const Money fare = money_parse(data, sector_num, block_num, byte_num + 5); - const uint8_t f_flag = pos_to_num(data, sector_num, block_num, byte_num + 5, 2) & 0x8001; + const DateTime date = date_parse(data, sector, block, byte); + const uint16_t gate = pos_to_num(data, sector, block, byte + 3, 2) >> 3; + const uint8_t g_flag = pos_to_num(data, sector, block, byte + 3, 2) & 0b111; + const Money fare = money_parse(data, sector, block, byte + 5); + const uint16_t f_flag = pos_to_num(data, sector, block, byte + 5, 2) & 0x8001; return (Trip){date, gate, g_flag, fare, f_flag}; } @@ -782,12 +781,15 @@ static uint32_t mfg_sector_parse(const MfClassicData* data) { // // 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| + // 0x000 | UID | rc | 88 04 00 C8 | uk | 00 20 00 00 00 | uk | // +----.----.----.----+----+----.----.----.----+----+----.----.----.----.----+----+ // 0x010 | 4E 0F 04 10 04 10 04 10 04 10 04 10 04 10 04 10 | // +----.----.----.----.----.----.----.----.----.----.----.----.----.----.----.----+ // 0x020 | ... 00 00 ... | // +----.----.----.----.----.----.----.----.----.----.----.----.----.----.----.----+ + // + // rc := "redundancy check" (lrc / bcc) + // uk := "unknown" size_t uid_len = 0; const uint8_t* uid = mf_classic_get_uid(data, &uid_len); @@ -800,13 +802,16 @@ static CounterSector counter_sector_parse(const MfClassicData* data) { // 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 ... | + // 0x050 | uses1 | uk | ... 00 00 ... | // +----.----+----+----.----.----.----.----.----.----.----.----.----.----.----.----+ + // 0x060 | uses2 | uk | ... 00 00 ... | + // +----.----+----+----.----.----.----.----.----.----.----.----.----.----.----.----+ + // + // uk := "unknown"; if nonzero, seems to only occupy the first 4 bits (ie, mask w/ 0xF0), + // with the remaining 4 zero // 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). @@ -827,27 +832,30 @@ static CounterSector counter_sector_parse(const MfClassicData* data) { } static BalanceSector balance_sector_parse(const MfClassicData* data, uint8_t active_sector) { - // Balance / type / last transaction (Sector 2 or 3) + // Balance & misc card info (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 + // 0x090 | type |end validity| uk | 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 + // Last trip/transaction info in balance sector ("date last" & "loc last") + // is also included in transaction log, hence don't bother to read here // - // Inactive balance sector represent the transaction N-1 balance data + // Inactive balance sector represent the transaction N-1 version // (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); + // Card type data stored in the first 10bits of block 1 + // (0x90 or 0xD0 depending on active sector) + // bitshift (2bytes = 16 bits) by 6bits for just first 10bits + const uint16_t type = pos_to_num(data, active_sector, 1, 0, 2) >> 6; const Money bal = money_parse(data, active_sector, 1, 5); return (BalanceSector){bal, type, issued, end_validity}; @@ -857,9 +865,9 @@ static Pass* passes_parse(const MfClassicData* data) { // Passes, speculative (Sectors 4 &/or 5) // // 0 1 2 3 4 5 6 7 8 9 A B C D E F - // +----.----.----.----.----.-+--.----+----.----.----.----.----.-+--.----+----.----+ - // 0x100 | pass0? |0 00 | pass1? |0 00 | crc | 0x140 - // +----.----.----.----.----.-+--.----+----.----.----.----.----.-+--.----+----.----+ + // +----.----.----.----.----.----+----+----.----.----.----.----.----+----+----.----+ + // 0x100 | pass0? | 00 | pass1? | 00 | crc | 0x140 + // +----.----.----.----.----.----+----+----.----.----.----.----.----+----+----.----+ // 0x110 | ... 00 00 ... | crc | 0x150 // +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+ // 0x120 | ... 00 ... 05 | crc | 0x160 @@ -867,6 +875,7 @@ static Pass* passes_parse(const MfClassicData* data) { // // WIP. Read in all speculative passes into array // 4 separate fields? active vs inactive sector for 2 passes? + // something else entirely? Pass* passes = malloc(sizeof(Pass) * CHARLIE_N_PASSES); @@ -1009,23 +1018,35 @@ void type_format_cat(FuriString* out, uint16_t type) { } void pass_format_cat(FuriString* out, Pass pass) { - furi_string_cat_printf(out, "\n-Type: "); - type_format_cat(out, pass.type); - furi_string_cat_printf(out, "\n-Start: "); - locale_format_dt_cat(out, &pass.start); - furi_string_cat_printf(out, "\n-End: "); - locale_format_dt_cat(out, &pass.end); + furi_string_cat_printf(out, "\n-Pre: %b", pass.pre); + // type_format_cat(out, pass.type); + furi_string_cat_printf(out, "\n-Post: %b", pass.post); + // locale_format_dt_cat(out, &pass.start); + furi_string_cat_printf(out, "\n-Date: "); + locale_format_dt_cat(out, &pass.date); } void passes_format_cat(FuriString* out, Pass* passes) { - if(is_debug()) { - furi_string_cat_printf(out, "\nPasses (DEBUG / WIP):"); - for(size_t i = 0; i < CHARLIE_N_PASSES; i++) { - if(passes[i].valid) { - furi_string_cat_printf(out, "\nPass %u", i + 1); - pass_format_cat(out, passes[i]); - furi_string_cat_printf(out, "\n"); - } + // only print passes if DEBUG on + if(!is_debug()) { + return; + } + + // only print if there is at least 1 valid pass to print + bool any_valid = false; + for(size_t i = 0; i < CHARLIE_N_PASSES; i++) { + any_valid |= passes[i].valid; + } + if(!any_valid) { + return; + } + + furi_string_cat_printf(out, "\nPasses (DEBUG / WIP):"); + for(size_t i = 0; i < CHARLIE_N_PASSES; i++) { + if(passes[i].valid) { + furi_string_cat_printf(out, "\nPass %u", i + 1); + pass_format_cat(out, passes[i]); + furi_string_cat_printf(out, "\n"); } } } @@ -1044,19 +1065,19 @@ void trip_format_cat(FuriString* out, Trip trip) { if(!!(trip.g_flag & 0x1) && (trip.fare.dollars == FARE_BUS.dollars) && (trip.fare.cents == FARE_BUS.cents)) { // if not a refill, and the fare amount is equal to bus fare (any better approach? flag bits for modality?) - // format for bus (gate ID on busses = posted bus #) - furi_string_cat_printf(out, "%sBus#%u", sep, trip.gate); + // format for bus — supposedly some correlation between gate ID & bus #, haven't investigated + furi_string_cat_printf(out, "%s#%u", sep, trip.gate); } else if(get_map_item(trip.gate, charliecard_fare_gate_ids, kNumFareGateIds, &sta)) { // station found in fare gate ID map, append station name furi_string_cat_str(out, sep); furi_string_cat_str(out, sta); } else { // no found station in fare gate ID map & not a bus, just print ID w/o add'l info - furi_string_cat_printf(out, "%s%u", sep, trip.gate); + furi_string_cat_printf(out, "%s#%u", sep, trip.gate); } // print flags for debugging purposes if(is_debug()) { - furi_string_cat_printf(out, "%s%u%s%u", sep, trip.g_flag, sep, trip.f_flag); + furi_string_cat_printf(out, "%s%x%s%x", sep, trip.g_flag, sep, trip.f_flag); } } @@ -1117,9 +1138,6 @@ static bool charliecard_parse(const NfcDevice* device, FuriString* parsed_data) furi_string_cat_printf(parsed_data, "\nType: "); type_format_cat(parsed_data, balance_sector.type); - passes_format_cat(parsed_data, passes); - free(passes); - furi_string_cat_printf(parsed_data, "\nTrip Count: %u", counter_sector.n_uses); furi_string_cat_printf(parsed_data, "\nIssued: "); @@ -1131,6 +1149,9 @@ static bool charliecard_parse(const NfcDevice* device, FuriString* parsed_data) // const DateTime last = date_parse(data, active_sector, 0, 1); // furi_string_cat_printf(parsed_data, "\nExpired: %s", expired(e_v, last) ? "Yes" : "No"); + passes_format_cat(parsed_data, passes); + free(passes); + trips_format_cat(parsed_data, trips); free(trips); From 2150f18496a3d8f683ba4b6a54dd213a0009e69d Mon Sep 17 00:00:00 2001 From: Zachary Weiss Date: Tue, 9 Apr 2024 20:19:27 -0400 Subject: [PATCH 7/9] Struct&var refactor for clarity --- .../nfc/plugins/supported_cards/charliecard.c | 116 +++++++++--------- 1 file changed, 61 insertions(+), 55 deletions(-) diff --git a/applications/main/nfc/plugins/supported_cards/charliecard.c b/applications/main/nfc/plugins/supported_cards/charliecard.c index 81b9bb897..136cfeb27 100644 --- a/applications/main/nfc/plugins/supported_cards/charliecard.c +++ b/applications/main/nfc/plugins/supported_cards/charliecard.c @@ -94,7 +94,7 @@ // timestep is one minute #define CHARLIE_TIME_DELTA_SECS 60 #define CHARLIE_END_VALID_DELTA_SECS 60 * 8 -#define CHARLIE_N_TRIP_HISTORY 10 +#define CHARLIE_N_TRANSACTION_HISTORY 10 #define CHARLIE_N_PASSES 4 typedef struct { @@ -147,7 +147,7 @@ typedef struct { uint8_t g_flag; Money fare; uint16_t f_flag; -} Trip; +} Transaction; typedef struct { bool valid; @@ -206,7 +206,7 @@ static const IdMapping charliecard_types[] = { {.id = 166, .name = "30 Day Commuter Rail Zone 1A Pass"}, {.id = 167, .name = "30 Day Commuter Rail Zone 1 Pass"}, {.id = 168, .name = "30 Day Commuter Rail Zone 2 Pass"}, - {.id = 169, .name = "30 Day Commuter Rail Zone 3 Pass"}, + {.id = 169, .name = "30 Day ComZmuter Rail Zone 3 Pass"}, {.id = 170, .name = "30 Day Commuter Rail Zone 4 Pass"}, {.id = 171, .name = "30 Day Commuter Rail Zone 5 Pass"}, {.id = 172, .name = "30 Day Commuter Rail Zone 6 Pass"}, @@ -717,7 +717,11 @@ static Pass // the uk1 and date fields, and is always set to 1 regardless // same is true of type & end-validity split found in balance sector // - // + // likely fields incl + // — type #, + // — a secondary date field (eg start/end, end validity or normal format) + // — ID of FVM from which the pass was loaded + // check for empty, if so, return struct filled w/ 0s // (incl "valid" field: hence, "valid" is false-y) if(pos_to_num(data, sector_num, block_num, byte_num, 6) == 0x002000000000) { @@ -727,7 +731,7 @@ static Pass // const DateTime start = date_parse(data, sector_num, block_num, byte_num + 1); const uint16_t pre = pos_to_num(data, sector_num, block_num, byte_num, 2) >> 6; - const uint16_t post = pos_to_num(data, sector_num, block_num, byte_num + 4, 2); + const uint16_t post = (pos_to_num(data, sector_num, block_num, byte_num + 4, 2) >> 2) & 0x3ff; // these values make sense for a date, but implied position of type // before end validity, as seen in balance sector, doesn't seem @@ -740,8 +744,9 @@ static Pass return (Pass){true, pre, post, date}; } -static Trip trip_parse(const MfClassicData* data, uint8_t sector, uint8_t block, uint8_t byte) { - /* This function parses individual trips. Each trip packs 7 bytes, stored as follows: +static Transaction + transaction_parse(const MfClassicData* data, uint8_t sector, uint8_t block, uint8_t byte) { + /* This function parses individual transactions. Each transaction packs 7 bytes, stored as follows: 0 1 2 3 4 5 6 +----.----.----+----.--+-+----.----+ @@ -753,23 +758,23 @@ static Trip trip_parse(const MfClassicData* data, uint8_t sector, uint8_t block, Gate ID ("loc") is only the first 13 bits of 0x3:0x5, the final three bits appear to be flags ("f"). Least significant flag bit seems to indicate: - — When f & 1 == 0, fare (the amount by which balance is decremented) - — When f & 1 == 1, refill (the amount by which balance is incremented) + — When f & 1 == 1, fare (the amount by which balance is decremented) + — When f & 1 == 0, refill (the amount by which balance is incremented) MSB (sign bit) of amt seems to serve the same role, just inverted, ie - — When amt & 0x8000 == 0x8000, fare - — When amt & 0x8000 == 0, refill + — When amt & 0x8000 == 0, fare + — When amt & 0x8000 == 0x8000, refill Remaining unknown bits: — f & 0b100 — f & 0b010 - — amt & 1; does not seem to correspond with card type, last trip, first trip, refill v. fare, etc + — amt & 1; does not seem to correspond with card type, last transaction, first transaction, refill v. fare, etc */ const DateTime date = date_parse(data, sector, block, byte); const uint16_t gate = pos_to_num(data, sector, block, byte + 3, 2) >> 3; const uint8_t g_flag = pos_to_num(data, sector, block, byte + 3, 2) & 0b111; const Money fare = money_parse(data, sector, block, byte + 5); const uint16_t f_flag = pos_to_num(data, sector, block, byte + 5, 2) & 0x8001; - return (Trip){date, gate, g_flag, fare, f_flag}; + return (Transaction){date, gate, g_flag, fare, f_flag}; } // ********************************************************** @@ -810,10 +815,10 @@ static CounterSector counter_sector_parse(const MfClassicData* data) { // 0x060 | uses2 | uk | ... 00 00 ... | // +----.----+----+----.----.----.----.----.----.----.----.----.----.----.----.----+ // - // uk := "unknown"; if nonzero, seems to only occupy the first 4 bits (ie, mask w/ 0xF0), + // uk := "unknown"; if nonzero, seems to only occupy the first 4 bits (ie, uk & 0xF0 == uk), // with the remaining 4 zero - // Card has two transaction sectors (2 & 3) containing balance data, with two + // Card has two 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, @@ -866,7 +871,7 @@ static Pass* passes_parse(const MfClassicData* data) { // // 0 1 2 3 4 5 6 7 8 9 A B C D E F // +----.----.----.----.----.----+----+----.----.----.----.----.----+----+----.----+ - // 0x100 | pass0? | 00 | pass1? | 00 | crc | 0x140 + // 0x100 | pass0/2? | 00 | pass1/3? | 00 | crc | 0x140 // +----.----.----.----.----.----+----+----.----.----.----.----.----+----+----.----+ // 0x110 | ... 00 00 ... | crc | 0x150 // +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+ @@ -886,16 +891,16 @@ static Pass* passes_parse(const MfClassicData* data) { return passes; } -static Trip* trips_parse(const MfClassicData* data) { +static Transaction* transactions_parse(const MfClassicData* data) { // Transaction history (Sectors 6–7) // // 0 1 2 3 4 5 6 7 8 9 A B C D E F // +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+ - // 0x180 | trip0 | trip1 | crc | + // 0x180 | transaction0 | transaction1 | crc | // +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+ // ... ... ... ... // +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+ - // 0x1D0 | trip8 | trip9 | crc | + // 0x1D0 | transaction8 | transaction9 | crc | // +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+ // 0x1E0 | ... 00 00 ... | crc | // +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+ @@ -903,20 +908,20 @@ static Trip* trips_parse(const MfClassicData* data) { // Transactions are not sorted, rather, appear to get overwritten // sequentially. (eg, sorted modulo array rotation) - Trip* trips = malloc(sizeof(Trip) * CHARLIE_N_TRIP_HISTORY); + Transaction* transactions = malloc(sizeof(Transaction) * CHARLIE_N_TRANSACTION_HISTORY); - // Parse each trip field using some modular math magic to get the offsets: - // move from sector 6 -> 7 after the first 6 trips - // move a block within a given sector every 2 trips, reset every 3 blocks (as sector has changed) + // Parse each transaction field using some modular math magic to get the offsets: + // move from sector 6 -> 7 after the first 6 transactions + // move a block within a given sector every 2 transactions, reset every 3 blocks (as sector has changed) // alternate between a start byte of 0 and 7 with every iteration - for(size_t i = 0; i < CHARLIE_N_TRIP_HISTORY; i++) { - trips[i] = trip_parse(data, 6 + (i / 6), (i / 2) % 3, (i % 2) * 7); + for(size_t i = 0; i < CHARLIE_N_TRANSACTION_HISTORY; i++) { + transactions[i] = transaction_parse(data, 6 + (i / 6), (i / 2) % 3, (i % 2) * 7); } // Iterate through the array to find the maximum (newest) date value int max_idx = 0; - for(int i = 1; i < CHARLIE_N_TRIP_HISTORY; i++) { - if(dt_ge(trips[i].date, trips[max_idx].date)) { + for(int i = 1; i < CHARLIE_N_TRANSACTION_HISTORY; i++) { + if(dt_ge(transactions[i].date, transactions[max_idx].date)) { max_idx = i; } } @@ -924,24 +929,24 @@ static Trip* trips_parse(const MfClassicData* data) { // Sort by rotating for(int r = 0; r < (max_idx + 1); r++) { // Store the first element - Trip temp = trips[0]; + Transaction temp = transactions[0]; // Shift elements to the left - for(int i = 0; i < CHARLIE_N_TRIP_HISTORY - 1; i++) { - trips[i] = trips[i + 1]; + for(int i = 0; i < CHARLIE_N_TRANSACTION_HISTORY - 1; i++) { + transactions[i] = transactions[i + 1]; } // Move the first element to the last - trips[CHARLIE_N_TRIP_HISTORY - 1] = temp; + transactions[CHARLIE_N_TRANSACTION_HISTORY - 1] = temp; } // Reverse order, such that newest is first, oldest last - for(int i = 0; i < CHARLIE_N_TRIP_HISTORY / 2; i++) { + for(int i = 0; i < CHARLIE_N_TRANSACTION_HISTORY / 2; i++) { // Swap elements at index i and size - i - 1 - Trip temp = trips[i]; - trips[i] = trips[CHARLIE_N_TRIP_HISTORY - i - 1]; - trips[CHARLIE_N_TRIP_HISTORY - i - 1] = temp; + Transaction temp = transactions[i]; + transactions[i] = transactions[CHARLIE_N_TRANSACTION_HISTORY - i - 1]; + transactions[CHARLIE_N_TRANSACTION_HISTORY - i - 1] = temp; } - return trips; + return transactions; } /* @@ -976,12 +981,12 @@ static DateTime expiry(DateTime iss) { return exp; } -static bool expired(DateTime expiry, DateTime last_trip) { +static bool expired(DateTime expiry, DateTime last_transaction) { // if a card has sat unused for >2 years, expired (verify this claim?) // else expired if current date > expiry date uint32_t ts_exp = datetime_datetime_to_timestamp(&expiry); - uint32_t ts_last = datetime_datetime_to_timestamp(&last_trip); + uint32_t ts_last = datetime_datetime_to_timestamp(&last_transaction); uint32_t ts_now = time_now(); return (ts_exp <= ts_now) | ((ts_now - ts_last) >= (2 * 365 * 24 * 60 * 60)); @@ -1020,7 +1025,8 @@ void type_format_cat(FuriString* out, uint16_t type) { void pass_format_cat(FuriString* out, Pass pass) { furi_string_cat_printf(out, "\n-Pre: %b", pass.pre); // type_format_cat(out, pass.type); - furi_string_cat_printf(out, "\n-Post: %b", pass.post); + furi_string_cat_printf(out, "\n-Post: "); + type_format_cat(out, pass.post); // locale_format_dt_cat(out, &pass.start); furi_string_cat_printf(out, "\n-Date: "); locale_format_dt_cat(out, &pass.date); @@ -1055,37 +1061,37 @@ void money_format_cat(FuriString* out, Money money) { furi_string_cat_printf(out, "$%u.%02u", money.dollars, money.cents); } -void trip_format_cat(FuriString* out, Trip trip) { +void transaction_format_cat(FuriString* out, Transaction transaction) { const char* sep = " "; const char* sta; - locale_format_dt_cat(out, &trip.date); - furi_string_cat_printf(out, "\n%s", !!(trip.g_flag & 0x1) ? "-" : "+"); - money_format_cat(out, trip.fare); - if(!!(trip.g_flag & 0x1) && (trip.fare.dollars == FARE_BUS.dollars) && - (trip.fare.cents == FARE_BUS.cents)) { + locale_format_dt_cat(out, &transaction.date); + furi_string_cat_printf(out, "\n%s", !!(transaction.g_flag & 0x1) ? "-" : "+"); + money_format_cat(out, transaction.fare); + if(!!(transaction.g_flag & 0x1) && (transaction.fare.dollars == FARE_BUS.dollars) && + (transaction.fare.cents == FARE_BUS.cents)) { // if not a refill, and the fare amount is equal to bus fare (any better approach? flag bits for modality?) // format for bus — supposedly some correlation between gate ID & bus #, haven't investigated - furi_string_cat_printf(out, "%s#%u", sep, trip.gate); - } else if(get_map_item(trip.gate, charliecard_fare_gate_ids, kNumFareGateIds, &sta)) { + furi_string_cat_printf(out, "%s#%u", sep, transaction.gate); + } else if(get_map_item(transaction.gate, charliecard_fare_gate_ids, kNumFareGateIds, &sta)) { // station found in fare gate ID map, append station name furi_string_cat_str(out, sep); furi_string_cat_str(out, sta); } else { // no found station in fare gate ID map & not a bus, just print ID w/o add'l info - furi_string_cat_printf(out, "%s#%u", sep, trip.gate); + furi_string_cat_printf(out, "%s#%u", sep, transaction.gate); } // print flags for debugging purposes if(is_debug()) { - furi_string_cat_printf(out, "%s%x%s%x", sep, trip.g_flag, sep, trip.f_flag); + furi_string_cat_printf(out, "%s%x%s%x", sep, transaction.g_flag, sep, transaction.f_flag); } } -void trips_format_cat(FuriString* out, Trip* trips) { +void transactions_format_cat(FuriString* out, Transaction* transactions) { furi_string_cat_printf(out, "\nTransactions:"); - for(size_t i = 0; i < CHARLIE_N_TRIP_HISTORY; i++) { + for(size_t i = 0; i < CHARLIE_N_TRANSACTION_HISTORY; i++) { furi_string_cat_printf(out, "\n"); - trip_format_cat(out, trips[i]); + transaction_format_cat(out, transactions[i]); furi_string_cat_printf(out, "\n"); } } @@ -1126,7 +1132,7 @@ static bool charliecard_parse(const NfcDevice* device, FuriString* parsed_data) const BalanceSector balance_sector = balance_sector_parse(data, counter_sector.active_balance_sector); Pass* passes = passes_parse(data); - Trip* trips = trips_parse(data); + Transaction* transactions = transactions_parse(data); // print/append card data furi_string_cat_printf(parsed_data, "\e#CharlieCard"); @@ -1152,8 +1158,8 @@ static bool charliecard_parse(const NfcDevice* device, FuriString* parsed_data) passes_format_cat(parsed_data, passes); free(passes); - trips_format_cat(parsed_data, trips); - free(trips); + transactions_format_cat(parsed_data, transactions); + free(transactions); parsed = true; } while(false); From 905c33a08b028ded4a246d4acb8ff21ba9a2a34f Mon Sep 17 00:00:00 2001 From: Zachary Weiss Date: Tue, 9 Apr 2024 21:12:46 -0400 Subject: [PATCH 8/9] Only print end validity when present Same might be worth doing for type & balance, keeping those as is for now; expiry/EV specifically given the switching logic as it has most potential for misinterpretation of null/0 value --- .../nfc/plugins/supported_cards/charliecard.c | 76 +++++++++++-------- 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/applications/main/nfc/plugins/supported_cards/charliecard.c b/applications/main/nfc/plugins/supported_cards/charliecard.c index 136cfeb27..3582eca63 100644 --- a/applications/main/nfc/plugins/supported_cards/charliecard.c +++ b/applications/main/nfc/plugins/supported_cards/charliecard.c @@ -638,6 +638,11 @@ static bool dt_ge(DateTime dt1, DateTime dt2) { return datetime_datetime_to_timestamp(&dt1) >= datetime_datetime_to_timestamp(&dt2); } +static bool dt_eq(DateTime dt1, DateTime dt2) { + // compares two DateTimes + return datetime_datetime_to_timestamp(&dt1) == datetime_datetime_to_timestamp(&dt2); +} + static bool get_map_item(uint16_t id, const IdMapping* map, size_t sz, const char** out) { // code borrowed from Jeremy Cooper's 'clipper.c'. Used as follows: // const char* s; if(!get_map_item(_,_,_,&s)) {s="Default str";} @@ -712,7 +717,7 @@ static Pass // | 00 20 00 00 00 00 | // +----.----.----.----.----.----+ // - // if not blank, uk1 MSB seems to always be set to 1... + // even when not blank, uk1 LSB seems to always be set to 1... // the sole bit set to 1 on the blank entry seems to divide // the uk1 and date fields, and is always set to 1 regardless // same is true of type & end-validity split found in balance sector @@ -746,29 +751,32 @@ static Pass static Transaction transaction_parse(const MfClassicData* data, uint8_t sector, uint8_t block, uint8_t byte) { - /* This function parses individual transactions. Each transaction packs 7 bytes, stored as follows: + // This function parses individual transactions. Each transaction packs 7 bytes, stored as follows: + // + // 0 1 2 3 4 5 6 + // +----.----.----+----.--+-+----.----+ + // | date | loc |f| amt | + // +----.----.----+----.--+-+----.----+ + // + // Where date is in the typical format, loc represents the fare gate tapped, and amt is the fare amount. + // Amount appears to contain some flag bits, however, it is unclear what precisely their function is. + // + // Gate ID ("loc") is only the first 13 bits of 0x3:0x5, the final three bits appear to be flags ("f"). + // Least significant flag bit seems to indicate: + // — When f & 1 == 1, fare (the amount by which balance is decremented) + // — When f & 1 == 0, refill (the amount by which balance is incremented) + // MSB (sign bit) of amt seems to serve the same role, just inverted, ie + // — When amt & 0x8000 == 0, fare + // — When amt & 0x8000 == 0x8000, refill + // Only contradiction between the two observed is on cards w/ passes; + // MSB of amt seems to be set for every transaction when (remaining bits of) amt is 0 on a card w/ a pass + // Hence, using f's LSB as method for inferring fare v. refill + // + // Remaining unknown bits: + // — f & 0b100; seems to be set on fares where the card has a pass, and amt is 0 + // — f & 0b010 + // — amt & 1; does not seem to correspond with card type, last transaction, first transaction, refill v. fare, etc - 0 1 2 3 4 5 6 - +----.----.----+----.--+-+----.----+ - | date | loc |f| amt | - +----.----.----+----.--+-+----.----+ - - Where date is in the typical format, loc represents the fare gate tapped, and amt is the fare amount. - Amount appears to contain some flag bits, however, it is unclear what precisely their function is. - - Gate ID ("loc") is only the first 13 bits of 0x3:0x5, the final three bits appear to be flags ("f"). - Least significant flag bit seems to indicate: - — When f & 1 == 1, fare (the amount by which balance is decremented) - — When f & 1 == 0, refill (the amount by which balance is incremented) - MSB (sign bit) of amt seems to serve the same role, just inverted, ie - — When amt & 0x8000 == 0, fare - — When amt & 0x8000 == 0x8000, refill - - Remaining unknown bits: - — f & 0b100 - — f & 0b010 - — amt & 1; does not seem to correspond with card type, last transaction, first transaction, refill v. fare, etc - */ const DateTime date = date_parse(data, sector, block, byte); const uint16_t gate = pos_to_num(data, sector, block, byte + 3, 2) >> 3; const uint8_t g_flag = pos_to_num(data, sector, block, byte + 3, 2) & 0b111; @@ -1124,8 +1132,6 @@ static bool charliecard_parse(const NfcDevice* device, FuriString* parsed_data) if(key_a != charliecard_1k_keys[verify_sector].a) break; if(key_b != charliecard_1k_keys[verify_sector].b) break; - // TODO: Verify add'l? - // parse card data const uint32_t card_number = mfg_sector_parse(data); const CounterSector counter_sector = counter_sector_parse(data); @@ -1138,6 +1144,10 @@ static bool charliecard_parse(const NfcDevice* device, FuriString* parsed_data) furi_string_cat_printf(parsed_data, "\e#CharlieCard"); furi_string_cat_printf(parsed_data, "\nSerial: 5-%lu", card_number); + // Type and balance 0 on some (Perq) cards + // (ie no "main" type / balance / end validity, + // essentially only pass & trip info) + // skip/change formatting for that case? furi_string_cat_printf(parsed_data, "\nBal: "); money_format_cat(parsed_data, balance_sector.balance); @@ -1149,18 +1159,24 @@ static bool charliecard_parse(const NfcDevice* device, FuriString* parsed_data) furi_string_cat_printf(parsed_data, "\nIssued: "); locale_format_dt_cat(parsed_data, &balance_sector.issued); - furi_string_cat_printf(parsed_data, "\nExpiry: "); - locale_format_dt_cat(parsed_data, &balance_sector.end_validity); + if(!dt_eq(balance_sector.end_validity, CHARLIE_EPOCH) & + dt_ge(balance_sector.end_validity, balance_sector.issued)) { + // sometimes (seen on Perq cards) end validity field is all 0 + // When this is the case, calc'd end validity is equal to CHARLIE_EPOCH). + // Only print if not 0, & end validity after issuance date + furi_string_cat_printf(parsed_data, "\nExpiry: "); + locale_format_dt_cat(parsed_data, &balance_sector.end_validity); + } // const DateTime last = date_parse(data, active_sector, 0, 1); // furi_string_cat_printf(parsed_data, "\nExpired: %s", expired(e_v, last) ? "Yes" : "No"); - passes_format_cat(parsed_data, passes); - free(passes); - transactions_format_cat(parsed_data, transactions); free(transactions); + passes_format_cat(parsed_data, passes); + free(passes); + parsed = true; } while(false); From 66f1558b070d51eda674b4751a29516b4ab97bdb Mon Sep 17 00:00:00 2001 From: Zachary Weiss Date: Tue, 9 Apr 2024 21:14:57 -0400 Subject: [PATCH 9/9] Typo --- applications/main/nfc/plugins/supported_cards/charliecard.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/main/nfc/plugins/supported_cards/charliecard.c b/applications/main/nfc/plugins/supported_cards/charliecard.c index 3582eca63..9880ca3ad 100644 --- a/applications/main/nfc/plugins/supported_cards/charliecard.c +++ b/applications/main/nfc/plugins/supported_cards/charliecard.c @@ -206,7 +206,7 @@ static const IdMapping charliecard_types[] = { {.id = 166, .name = "30 Day Commuter Rail Zone 1A Pass"}, {.id = 167, .name = "30 Day Commuter Rail Zone 1 Pass"}, {.id = 168, .name = "30 Day Commuter Rail Zone 2 Pass"}, - {.id = 169, .name = "30 Day ComZmuter Rail Zone 3 Pass"}, + {.id = 169, .name = "30 Day Commuter Rail Zone 3 Pass"}, {.id = 170, .name = "30 Day Commuter Rail Zone 4 Pass"}, {.id = 171, .name = "30 Day Commuter Rail Zone 5 Pass"}, {.id = 172, .name = "30 Day Commuter Rail Zone 6 Pass"},