Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NFC MBTA CharlieCard parsing plugin #62

Merged
merged 5 commits into from
Mar 31, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 36 additions & 11 deletions applications/main/nfc/plugins/supported_cards/charliecard.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,35 @@
* — Scott Campbell; josephscottcampbell.com <[email protected]>
* — Noah Gibson; <[email protected]>
* Talk available at: https://www.youtube.com/watch?v=1JT_lTfK69Q
*
* TODOs:
* — 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
* — 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)
* 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)
* 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")
* [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/)
* [ ] Other?
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
Expand Down Expand Up @@ -809,16 +838,17 @@ static enum CharlieActiveSector get_active_sector(const MfClassicData* data) {
*/

// active sector based on trip counters
const bool active_trip = n_uses(data, CHARLIE_ACTIVE_SECTOR_2) <
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) >
const bool active_date = datetime_datetime_to_timestamp(&ds2) >=
datetime_datetime_to_timestamp(&ds3);

// with all tested cards so far, this has been true
// cf. type_parse() assertion comments
furi_assert(active_trip == active_date);

return active_trip ? CHARLIE_ACTIVE_SECTOR_2 : CHARLIE_ACTIVE_SECTOR_3;
Expand All @@ -833,6 +863,9 @@ static uint16_t type_parse(const MfClassicData* data) {
// 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;
// might be wise to remove the assertion; then again, it's an effective way
// to crowdsource research, as hopefully if this isn't universally true,
// someone will come running when their app crashes — probably not a best practice though haha.
furi_assert(type1 == type2);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

furi_assert() is compiled only when DEBUG=1 is enabled, which it is not by default, nor for production builds in Momentum, you'd need to compile with ./fbt <command> DEBUG=1, so assert should be fine here! if you want to enforce it in all builds, regardless of DEBUG flag, you can use furi_check()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to know! For my own education: does that mean compiling without DEBUG strips that, and is independent of the debug setting on the flipper? Or is the check kept in the compiled fap regardless, and the DEBUG compile arg simply sets the debug mode flag (locally or globally) on the flipper after flashing/launching?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DEBUG=1 refers to the compiler options, with DEBUG=0 (default) furi_assert() will be stripped completely. the Debug option in flipper settings allows to attacha live debugger, and to access some extra hidden functionality (raw reads, debug info in some parsers...), but will not bring back the stripped furi_assert() calls


return type1;
Expand Down Expand Up @@ -871,10 +904,9 @@ static DateTime expiry(DateTime iss) {
}*/

static bool expired(DateTime expiry, DateTime last_trip) {
// if a card has sat unused for >2 years, expired
// if a card has sat unused for >2 years, expired (verify this claim?)
// else expired if current date > expiry date

// TODO: end validity field?
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();
Expand Down Expand Up @@ -930,7 +962,6 @@ void trip_format_cat(FuriString* out, Trip trip) {
furi_string_cat_printf(out, "%s%u", sep, trip.gate);
}
// print flags for debugging purposes
// TODO: set up as debug-mode-only?
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);
}
Expand Down Expand Up @@ -968,7 +999,6 @@ static bool charliecard_parse(const NfcDevice* device, FuriString* parsed_data)
uint32_t card_number = bit_lib_bytes_to_num_be(uid, 4);
furi_string_cat_printf(parsed_data, "\nSerial: 5-%lu", card_number);

// Money fare = money_parse(data, active_sector, 0, 4);
Money bal = money_parse(data, active_sector, 1, 5);
furi_string_cat_printf(parsed_data, "\nBal: ");
money_format_cat(parsed_data, bal);
Expand All @@ -988,11 +1018,6 @@ static bool charliecard_parse(const NfcDevice* device, FuriString* parsed_data)
furi_string_cat_printf(parsed_data, "\nExpiry: ");
locale_format_dt_cat(parsed_data, &e_v);

/*
const DateTime exp = expiry(iss);
furi_string_cat_printf(parsed_data, "\nExp: ");
locale_format_dt_cat(parsed_data, &exp);*/

DateTime last = date_parse(data, active_sector, 0, 1);
furi_string_cat_printf(parsed_data, "\nExpired: %s", expired(e_v, last) ? "Yes" : "No");

Expand Down