metroflip_scene_charliecard.c 56 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323
  1. /*
  2. * Parser for MBTA CharlieCard (Boston, MA, USA).
  3. *
  4. * Copyright 2024 Zachary Weiss <me@zachary.ws>
  5. *
  6. * Public security research on the MBTA's fare system stretches back to 2008,
  7. * starting with Russel Ryan, Zack Anderson, and Alessandro Chiesa's
  8. * "Anatomy of a Subway Hack", for which they were famously issued a gag order.
  9. * A thorough history of research & researchers deserving of credit is
  10. * detailed by @bobbyrsec in his 2022 blog post (& presentation):
  11. * "Operation Charlie: Hacking the MBTA CharlieCard from 2008 to Present"
  12. * https://medium.com/@bobbyrsec/operation-charlie-hacking-the-mbta-charliecard-from-2008-to-present-24ea9f0aaa38
  13. *
  14. * Fare gate IDs, card types, and general assistance courtesy of the
  15. * minds behind DEFCON 31's "Boston Infinite Money Glitch" presentation:
  16. * — Matthew Harris; mattyharris.net <matty@mattyharris.net>
  17. * — Zachary Bertocchi; zackbertocchi.com <zach@zachbertocci.com>
  18. * — Scott Campbell; josephscottcampbell.com <scott@josephscottcampbell.com>
  19. * — Noah Gibson; <noahgibson06@proton.me>
  20. * Talk available at: https://www.youtube.com/watch?v=1JT_lTfK69Q
  21. *
  22. * TODOs:
  23. * — Reverse engineer passes (sectors 4 & 5?), impl.
  24. * — Infer transaction flag meanings
  25. * — Infer remaining unknown bytes in the balance sectors (2 & 3)
  26. * — Improve string output formatting, esp. of transaction log
  27. * — Mapping of buses to garages, and subsequently, route subsets via
  28. * http://roster.transithistory.org/ data
  29. * — Mapping of stations to lines
  30. * — Add'l data fields for side of station fare gates are on? Some stations
  31. * separate inbound & outbound sides, so direction could be inferred
  32. * from gates used.
  33. * — Continually gather data on fare gate ID mappings, update as collected;
  34. * check locations this might be scrapable / inferrable from:
  35. * [X] MBTA GTFS spec (https://www.mbta.com/developers/gtfs) features & IDs
  36. * seem too-coarse-grained & uncorrelated
  37. * [X] MBTA ArcGIS (https://mbta-massdot.opendata.arcgis.com/) & Tableau
  38. * (https://public.tableau.com/app/profile/mbta.office.of.performance.management.and.innovation/vizzes)
  39. * files don't seem to have anything of that resolution (only down to ridership by station)
  40. * [X] (skim of) MBTA public GitHub (https://github.com/mbta) repos make no reference to fare-gate-level data
  41. * [X] (skim of) MBTA public engineering docs (https://www.mbta.com/engineering) unfruitful;
  42. * Closest mention spotted is 2014 "Ridership and Service Statistics"
  43. * (https://cdn.mbta.com/sites/default/files/fmcb-meeting-docs/reports-policies/2014-07-mbta-bluebook-ed14.pdf)
  44. * where on pg.40, "Equipment at Stations" is enumerated, and fare gates counts are given,
  45. * listed as "AFC Gates" (presumably standing for "Automated Fare Collection")
  46. * [X] Josiah Zachery criminal trial public evidence — convicted partially on
  47. * data on his CharlieCard, appeals partially on basis of legality of this search.
  48. * Prev. court case (gag order mentioned in preamble) leaked some data in the files
  49. * entered into evidence. Seemingly did not happen here; fare gate IDs unmentioned,
  50. * only ever the nature of stored/saved data and methods of retrieval.
  51. * Appelate case dockets 2019-P-0401, SJC-12952, SJ-2017-0390
  52. * (https://www.ma-appellatecourts.org/party)
  53. * Trial court indictment 04/02/2015, Case# 1584CR10265 @Suffolk County Criminal Superior Court
  54. * (https://www.masscourts.org/eservices/home.page.16)
  55. * [ ] FOIA / public records request?
  56. * (https://massachusettsdot.mycusthelp.com/WEBAPP/_rs/(S(tbcygdlm0oojy35p1wv0y2y5))/supporthome.aspx)
  57. * [X] MBTA data blog? (https://www.massdottracker.com/datablog/)
  58. * [ ] MassDOT developers Google group? (https://groups.google.com/g/massdotdevelopers)
  59. * [X] preexisting posts
  60. * [ ] ask directly?
  61. * [ ] Other?
  62. *
  63. * This program is free software: you can redistribute it and/or modify it
  64. * under the terms of the GNU General Public License as published by
  65. * the Free Software Foundation, either version 3 of the License, or
  66. * (at your option) any later version.
  67. *
  68. * This program is distributed in the hope that it will be useful, but
  69. * WITHOUT ANY WARRANTY; without even the implied warranty of
  70. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  71. * General Public License for more details.
  72. *
  73. * You should have received a copy of the GNU General Public License
  74. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  75. */
  76. #include <flipper_application.h>
  77. #include "../metroflip_i.h"
  78. #include <nfc/protocols/mf_classic/mf_classic_poller_sync.h>
  79. #include <nfc/protocols/mf_classic/mf_classic.h>
  80. #include <nfc/protocols/mf_classic/mf_classic_poller.h>
  81. #include <dolphin/dolphin.h>
  82. #include <bit_lib.h>
  83. #include <datetime.h>
  84. #include <furi_hal.h>
  85. #include <locale/locale.h>
  86. #include <nfc/nfc.h>
  87. #include <nfc/nfc_device.h>
  88. #include <nfc/nfc_listener.h>
  89. #define TAG "Metroflip:Scene:CharlieCard"
  90. // starts Wednesday 2003/1/1 @ midnight
  91. #define CHARLIE_EPOCH \
  92. (DateTime) { \
  93. 0, 0, 0, 1, 1, 2003, 4 \
  94. }
  95. // timestep is one minute
  96. #define CHARLIE_TIME_DELTA_SECS 60
  97. #define CHARLIE_END_VALID_DELTA_SECS 60 * 8
  98. #define CHARLIE_N_TRANSACTION_HISTORY 10
  99. #define CHARLIE_N_PASSES 4
  100. typedef struct {
  101. uint64_t a;
  102. uint64_t b;
  103. } MfClassicKeyPair;
  104. // always from the same set of keys (cf. default keys dict for list w/o multiplicity)
  105. // we only care about the data in the first half of the sectors
  106. // second half sectors keys seemingly change position sometimes across cards?
  107. // no data stored there, but might want to impl some custom read function
  108. // accounting for this such that reading is faster (else it seems to fall back on dict
  109. // approach for remaining keys)...
  110. static const MfClassicKeyPair charliecard_1k_keys[] = {
  111. {.a = 0x3060206F5B0A, .b = 0xF1B9F5669CC8},
  112. {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89},
  113. {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89},
  114. {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89},
  115. {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89},
  116. {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89},
  117. {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89},
  118. {.a = 0x5EC39B022F2B, .b = 0xF662248E7E89},
  119. {.a = 0x3A09594C8587, .b = 0x62387B8D250D},
  120. {.a = 0xF238D78FF48F, .b = 0x9DC282D46217},
  121. {.a = 0xAFD0BA94D624, .b = 0x92EE4DC87191},
  122. {.a = 0xB35A0E4ACC09, .b = 0x756EF55E2507},
  123. {.a = 0x447AB7FD5A6B, .b = 0x932B9CB730EF},
  124. {.a = 0x1F1A0A111B5B, .b = 0xAD9E0A1CA2F7},
  125. {.a = 0xD58023BA2BDC, .b = 0x62CED42A6D87},
  126. {.a = 0x2548A443DF28, .b = 0x2ED3B15E7C0F},
  127. };
  128. typedef struct {
  129. uint16_t dollars;
  130. uint8_t cents;
  131. } Money;
  132. #define FARE_BUS \
  133. (Money) { \
  134. 1, 70 \
  135. }
  136. #define FARE_SUB \
  137. (Money) { \
  138. 2, 40 \
  139. }
  140. typedef struct {
  141. DateTime date;
  142. uint16_t gate;
  143. uint8_t g_flag;
  144. Money fare;
  145. uint16_t f_flag;
  146. } Transaction;
  147. typedef struct {
  148. bool valid;
  149. uint16_t pre;
  150. uint16_t post;
  151. DateTime date;
  152. } Pass;
  153. typedef struct {
  154. uint16_t n_uses;
  155. uint8_t active_balance_sector;
  156. } CounterSector;
  157. typedef struct {
  158. Money balance;
  159. uint16_t type;
  160. DateTime issued;
  161. DateTime end_validity;
  162. } BalanceSector;
  163. // IdMapping approach borrowed from Jeremy Cooper's 'clipper.c'
  164. typedef struct {
  165. uint16_t id;
  166. const char* name;
  167. } IdMapping;
  168. // this should be a complete accounting of types, (1 and 7 day pass types maybe missing?)
  169. static const IdMapping charliecard_types[] = {
  170. // Regular card types
  171. {.id = 367, .name = "Adult"},
  172. {.id = 366, .name = "SV Adult"},
  173. {.id = 418, .name = "Student"},
  174. {.id = 419, .name = "Senior"},
  175. {.id = 420, .name = "TAP"},
  176. {.id = 417, .name = "Blind"},
  177. {.id = 426, .name = "Child"},
  178. {.id = 410, .name = "Employee ID Without Passback"},
  179. {.id = 414, .name = "Employee ID With Passback"},
  180. {.id = 415, .name = "Retiree"},
  181. {.id = 416, .name = "Police/Fire"},
  182. // Passes
  183. {.id = 135, .name = "30 Day Local Bus Pass"},
  184. {.id = 136, .name = "30 Day Inner Express Bus Pass"},
  185. {.id = 137, .name = "30 Day Outer Express Bus Pass"},
  186. {.id = 138, .name = "30 Day LinkPass"},
  187. {.id = 139, .name = "30 Day Senior LinkPass"},
  188. {.id = 148, .name = "30 Day TAP LinkPass"},
  189. {.id = 150, .name = "Monthly Student LinkPass"},
  190. {.id = 424, .name = "Monthly TAP LinkPass"},
  191. {.id = 425, .name = "Monthly Senior LinkPass"},
  192. {.id = 421, .name = "Senior TAP/Permit"},
  193. {.id = 422, .name = "Senior TAP/Permit 30 Days"},
  194. // Commuter rail passes
  195. {.id = 166, .name = "30 Day Commuter Rail Zone 1A Pass"},
  196. {.id = 167, .name = "30 Day Commuter Rail Zone 1 Pass"},
  197. {.id = 168, .name = "30 Day Commuter Rail Zone 2 Pass"},
  198. {.id = 169, .name = "30 Day Commuter Rail Zone 3 Pass"},
  199. {.id = 170, .name = "30 Day Commuter Rail Zone 4 Pass"},
  200. {.id = 171, .name = "30 Day Commuter Rail Zone 5 Pass"},
  201. {.id = 172, .name = "30 Day Commuter Rail Zone 6 Pass"},
  202. {.id = 173, .name = "30 Day Commuter Rail Zone 7 Pass"},
  203. {.id = 174, .name = "30 Day Commuter Rail Zone 8 Pass"},
  204. {.id = 175, .name = "30 Day Interzone 1 Pass"},
  205. {.id = 176, .name = "30 Day Interzone 2 Pass"},
  206. {.id = 177, .name = "30 Day Interzone 3 Pass"},
  207. {.id = 178, .name = "30 Day Interzone 4 Pass"},
  208. {.id = 179, .name = "30 Day Interzone 5 Pass"},
  209. {.id = 180, .name = "30 Day Interzone 6 Pass"},
  210. {.id = 181, .name = "30 Day Interzone 7 Pass"},
  211. {.id = 182, .name = "30 Day Interzone 8 Pass"},
  212. {.id = 140, .name = "One Way Interzone Adult 1 Zone"},
  213. {.id = 141, .name = "One Way Interzone Adult 2 Zones"},
  214. {.id = 142, .name = "One Way Interzone Adult 3 Zones"},
  215. {.id = 143, .name = "One Way Interzone Adult 4 Zones"},
  216. {.id = 144, .name = "One Way Interzone Adult 5 Zones"},
  217. {.id = 145, .name = "One Way Interzone Adult 6 Zones"},
  218. {.id = 146, .name = "One Way Interzone Adult 7 Zones"},
  219. {.id = 147, .name = "One Way Interzone Adult 8 Zones"},
  220. {.id = 428, .name = "One Way Half Fare Zone 1"},
  221. {.id = 429, .name = "One Way Half Fare Zone 2"},
  222. {.id = 430, .name = "One Way Half Fare Zone 3"},
  223. {.id = 431, .name = "One Way Half Fare Zone 4"},
  224. {.id = 432, .name = "One Way Half Fare Zone 5"},
  225. {.id = 433, .name = "One Way Half Fare Zone 6"},
  226. {.id = 434, .name = "One Way Half Fare Zone 7"},
  227. {.id = 435, .name = "One Way Half Fare Zone 8"},
  228. {.id = 436, .name = "One Way Interzone Half Fare 1 Zone"},
  229. {.id = 437, .name = "One Way Interzone Half Fare 2 Zones"},
  230. {.id = 438, .name = "One Way Interzone Half Fare 3 Zones"},
  231. {.id = 439, .name = "One Way Interzone Half Fare 4 Zones"},
  232. {.id = 440, .name = "One Way Interzone Half Fare 5 Zones"},
  233. {.id = 441, .name = "One Way Interzone Half Fare 6 Zones"},
  234. {.id = 442, .name = "One Way Interzone Half Fare 7 Zones"},
  235. {.id = 443, .name = "One Way Interzone Half Fare 8 Zones"},
  236. {.id = 509, .name = "Group Interzone 1 Zones"},
  237. {.id = 510, .name = "Group Interzone 2 Zones"},
  238. {.id = 511, .name = "Group Interzone 3 Zones"},
  239. {.id = 512, .name = "Group Interzone 4 Zones"},
  240. {.id = 513, .name = "Group Interzone 5 Zones"},
  241. {.id = 514, .name = "Group Interzone 6 Zones"},
  242. {.id = 515, .name = "Group Interzone 7 Zones"},
  243. {.id = 516, .name = "Group Interzone 8 Zones"},
  244. {.id = 952, .name = "Zone 1 Student Monthly Pass"},
  245. {.id = 953, .name = "Zone 2 Student Monthly Pass"},
  246. {.id = 954, .name = "Zone 3 Student Monthly Pass"},
  247. {.id = 955, .name = "Zone 4 Student Monthly Pass"},
  248. {.id = 956, .name = "Zone 5 Student Monthly Pass"},
  249. {.id = 957, .name = "Zone 6 Student Monthly Pass"},
  250. {.id = 958, .name = "Zone 7 Student Monthly Pass"},
  251. {.id = 959, .name = "Zone 8 Student Monthly Pass"},
  252. {.id = 960, .name = "Zone 9 Student Monthly Pass"},
  253. {.id = 961, .name = "Zone 10 Student Monthly Pass"},
  254. {.id = 963, .name = "Interzone 1 Zone Student Monthly Pass"},
  255. {.id = 964, .name = "Interzone 2 Zone Student Monthly Pass"},
  256. {.id = 965, .name = "Interzone 3 Zone Student Monthly Pass"},
  257. {.id = 966, .name = "Interzone 4 Zone Student Monthly Pass"},
  258. {.id = 967, .name = "Interzone 5 Zone Student Monthly Pass"},
  259. {.id = 968, .name = "Interzone 6 Zone Student Monthly Pass"},
  260. {.id = 969, .name = "Interzone 7 Zone Student Monthly Pass"},
  261. {.id = 970, .name = "Interzone 8 Zone Student Monthly Pass"},
  262. {.id = 971, .name = "Interzone 9 Zone Student Monthly Pass"},
  263. {.id = 972, .name = "Interzone 10 Zone Student Monthly Pass"},
  264. };
  265. static const size_t kNumTypes = COUNT_OF(charliecard_types);
  266. // Incomplete, and subject to change
  267. // Only covers Orange & Blue line stations
  268. // Gathered manually, and provided courtesy of, DEFCON31 researchers
  269. // as cited above.
  270. static const IdMapping charliecard_fare_gate_ids[] = {
  271. // Davis
  272. {.id = 6766, .name = "Davis"},
  273. {.id = 6767, .name = "Davis"},
  274. {.id = 6768, .name = "Davis"},
  275. {.id = 6769, .name = "Davis"},
  276. {.id = 6770, .name = "Davis"},
  277. {.id = 6771, .name = "Davis"},
  278. {.id = 6772, .name = "Davis"},
  279. {.id = 2167, .name = "Davis"},
  280. {.id = 7020, .name = "Davis"},
  281. // Porter
  282. {.id = 6781, .name = "Porter"},
  283. {.id = 6780, .name = "Porter"},
  284. {.id = 6779, .name = "Porter"},
  285. {.id = 6778, .name = "Porter"},
  286. {.id = 6777, .name = "Porter"},
  287. {.id = 6776, .name = "Porter"},
  288. {.id = 6775, .name = "Porter"},
  289. {.id = 2168, .name = "Porter"},
  290. {.id = 7021, .name = "Porter"},
  291. {.id = 6782, .name = "Porter"},
  292. // Oak Grove
  293. {.id = 6640, .name = "Oak Grove"},
  294. {.id = 6641, .name = "Oak Grove"},
  295. {.id = 6639, .name = "Oak Grove"},
  296. {.id = 2036, .name = "Oak Grove"},
  297. {.id = 6642, .name = "Oak Grove"},
  298. {.id = 6979, .name = "Oak Grove"},
  299. // Downtown Crossing
  300. {.id = 2091, .name = "Downtown Crossing"},
  301. {.id = 6995, .name = "Downtown Crossing"},
  302. {.id = 6699, .name = "Downtown Crossing"},
  303. {.id = 6700, .name = "Downtown Crossing"},
  304. {.id = 1926, .name = "Downtown Crossing"},
  305. {.id = 2084, .name = "Downtown Crossing"},
  306. {.id = 6994, .name = "Downtown Crossing"},
  307. {.id = 6695, .name = "Downtown Crossing"},
  308. {.id = 6694, .name = "Downtown Crossing"},
  309. {.id = 6696, .name = "Downtown Crossing"},
  310. {.id = 2336, .name = "Downtown Crossing"},
  311. {.id = 1056, .name = "Downtown Crossing"},
  312. {.id = 6814, .name = "Downtown Crossing"},
  313. {.id = 6813, .name = "Downtown Crossing"},
  314. {.id = 2212, .name = "Downtown Crossing"},
  315. {.id = 7038, .name = "Downtown Crossing"},
  316. // State
  317. {.id = 7092, .name = "State"},
  318. {.id = 1844, .name = "State"},
  319. {.id = 6689, .name = "State"},
  320. {.id = 6988, .name = "State"},
  321. {.id = 6991, .name = "State"},
  322. {.id = 2083, .name = "State"},
  323. {.id = 6688, .name = "State"},
  324. {.id = 6687, .name = "State"},
  325. {.id = 6686, .name = "State"},
  326. {.id = 2078, .name = "State"},
  327. {.id = 6987, .name = "State"},
  328. {.id = 7090, .name = "State"},
  329. {.id = 1842, .name = "State"},
  330. // Haymarket
  331. {.id = 6684, .name = "Haymarket"},
  332. {.id = 6683, .name = "Haymarket"},
  333. {.id = 6682, .name = "Haymarket"},
  334. {.id = 6681, .name = "Haymarket"},
  335. {.id = 2073, .name = "Haymarket"},
  336. {.id = 7074, .name = "Haymarket"},
  337. {.id = 6883, .name = "Haymarket"},
  338. {.id = 6884, .name = "Haymarket"},
  339. {.id = 6885, .name = "Haymarket"},
  340. {.id = 6886, .name = "Haymarket"},
  341. {.id = 2303, .name = "Haymarket"},
  342. {.id = 6986, .name = "Haymarket"},
  343. // North Station
  344. {.id = 6985, .name = "North Station"},
  345. {.id = 2063, .name = "North Station"},
  346. {.id = 6671, .name = "North Station"},
  347. {.id = 6672, .name = "North Station"},
  348. {.id = 6673, .name = "North Station"},
  349. {.id = 6674, .name = "North Station"},
  350. {.id = 6675, .name = "North Station"},
  351. {.id = 6676, .name = "North Station"},
  352. {.id = 6677, .name = "North Station"},
  353. {.id = 6678, .name = "North Station"},
  354. {.id = 6984, .name = "North Station"},
  355. {.id = 2062, .name = "North Station"},
  356. {.id = 6668, .name = "North Station"},
  357. {.id = 6667, .name = "North Station"},
  358. {.id = 6666, .name = "North Station"},
  359. {.id = 6665, .name = "North Station"},
  360. {.id = 6664, .name = "North Station"},
  361. // Sullivan Square
  362. {.id = 6654, .name = "Sullivan Square"},
  363. {.id = 6655, .name = "Sullivan Square"},
  364. {.id = 6656, .name = "Sullivan Square"},
  365. {.id = 6657, .name = "Sullivan Square"},
  366. {.id = 6658, .name = "Sullivan Square"},
  367. {.id = 6659, .name = "Sullivan Square"},
  368. {.id = 2053, .name = "Sullivan Square"},
  369. {.id = 6982, .name = "Sullivan Square"},
  370. // Community College
  371. {.id = 6661, .name = "Community College"},
  372. {.id = 6662, .name = "Community College"},
  373. {.id = 2056, .name = "Community College"},
  374. {.id = 6983, .name = "Community College"},
  375. // Assembly
  376. {.id = 3876, .name = "Assembly"},
  377. {.id = 3875, .name = "Assembly"},
  378. {.id = 6957, .name = "Assembly"},
  379. {.id = 6956, .name = "Assembly"},
  380. {.id = 6955, .name = "Assembly"},
  381. {.id = 6954, .name = "Assembly"},
  382. {.id = 6953, .name = "Assembly"},
  383. {.id = 7101, .name = "Assembly"},
  384. {.id = 3873, .name = "Assembly"},
  385. {.id = 3872, .name = "Assembly"},
  386. // Wellington
  387. {.id = 6981, .name = "Wellington"},
  388. {.id = 2042, .name = "Wellington"},
  389. {.id = 6650, .name = "Wellington"},
  390. {.id = 6651, .name = "Wellington"},
  391. {.id = 6652, .name = "Wellington"},
  392. {.id = 6653, .name = "Wellington"},
  393. // Malden
  394. {.id = 6980, .name = "Malden Center"},
  395. {.id = 2037, .name = "Malden Center"},
  396. {.id = 6645, .name = "Malden Center"},
  397. {.id = 6646, .name = "Malden Center"},
  398. {.id = 6647, .name = "Malden Center"},
  399. {.id = 6648, .name = "Malden Center"},
  400. // Chinatown
  401. {.id = 6704, .name = "Chinatown"},
  402. {.id = 6705, .name = "Chinatown"},
  403. {.id = 2099, .name = "Chinatown"},
  404. {.id = 7003, .name = "Chinatown"},
  405. {.id = 7002, .name = "Chinatown"},
  406. {.id = 2096, .name = "Chinatown"},
  407. {.id = 6702, .name = "Chinatown"},
  408. {.id = 6701, .name = "Chinatown"},
  409. // Tufts Medical Center
  410. {.id = 6707, .name = "Tufts Medical Center"},
  411. {.id = 6708, .name = "Tufts Medical Center"},
  412. {.id = 6709, .name = "Tufts Medical Center"},
  413. {.id = 6710, .name = "Tufts Medical Center"},
  414. {.id = 6711, .name = "Tufts Medical Center"},
  415. {.id = 2105, .name = "Tufts Medical Center"},
  416. {.id = 7004, .name = "Tufts Medical Center"},
  417. {.id = 1941, .name = "Tufts Medical Center"},
  418. {.id = 7006, .name = "Tufts Medical Center"},
  419. // Back Bay
  420. {.id = 7007, .name = "Back Bay"},
  421. {.id = 1480, .name = "Back Bay"},
  422. {.id = 6714, .name = "Back Bay"},
  423. {.id = 6715, .name = "Back Bay"},
  424. {.id = 6716, .name = "Back Bay"},
  425. {.id = 6717, .name = "Back Bay"},
  426. {.id = 6718, .name = "Back Bay"},
  427. {.id = 6719, .name = "Back Bay"},
  428. {.id = 6720, .name = "Back Bay"},
  429. {.id = 1801, .name = "Back Bay"},
  430. {.id = 7009, .name = "Back Bay"},
  431. // Massachusetts Avenue
  432. {.id = 7010, .name = "Massachusetts Avenue"},
  433. {.id = 2118, .name = "Massachusetts Avenue"},
  434. {.id = 6724, .name = "Massachusetts Avenue"},
  435. {.id = 6723, .name = "Massachusetts Avenue"},
  436. {.id = 6722, .name = "Massachusetts Avenue"},
  437. {.id = 6721, .name = "Massachusetts Avenue"},
  438. // Ruggles
  439. {.id = 6726, .name = "Ruggles"},
  440. {.id = 6727, .name = "Ruggles"},
  441. {.id = 6728, .name = "Ruggles"},
  442. {.id = 2122, .name = "Ruggles"},
  443. {.id = 2123, .name = "Ruggles"},
  444. {.id = 2124, .name = "Ruggles"},
  445. {.id = 1804, .name = "Ruggles"},
  446. // Roxbury Crossing
  447. {.id = 6737, .name = "Roxbury Crossing"},
  448. {.id = 6736, .name = "Roxbury Crossing"},
  449. {.id = 6735, .name = "Roxbury Crossing"},
  450. {.id = 6734, .name = "Roxbury Crossing"},
  451. {.id = 6733, .name = "Roxbury Crossing"},
  452. {.id = 2125, .name = "Roxbury Crossing"},
  453. {.id = 7012, .name = "Roxbury Crossing"},
  454. // Jackson Square
  455. {.id = 6741, .name = "Jackson Square"},
  456. {.id = 6740, .name = "Jackson Square"},
  457. {.id = 6739, .name = "Jackson Square"},
  458. {.id = 2131, .name = "Jackson Square"},
  459. {.id = 7013, .name = "Jackson Square"},
  460. {.id = 7014, .name = "Jackson Square"},
  461. {.id = 2135, .name = "Jackson Square"},
  462. {.id = 6743, .name = "Jackson Square"},
  463. {.id = 6744, .name = "Jackson Square"},
  464. {.id = 6745, .name = "Jackson Square"},
  465. // Green Street
  466. {.id = 6746, .name = "Green Street"},
  467. {.id = 6747, .name = "Green Street"},
  468. {.id = 6748, .name = "Green Street"},
  469. {.id = 2142, .name = "Green Street"},
  470. {.id = 7015, .name = "Green Street"},
  471. // Forest Hills
  472. {.id = 6750, .name = "Forest Hills"},
  473. {.id = 6751, .name = "Forest Hills"},
  474. {.id = 6752, .name = "Forest Hills"},
  475. {.id = 6753, .name = "Forest Hills"},
  476. {.id = 6754, .name = "Forest Hills"},
  477. {.id = 6755, .name = "Forest Hills"},
  478. {.id = 2150, .name = "Forest Hills"},
  479. {.id = 7016, .name = "Forest Hills"},
  480. {.id = 6950, .name = "Forest Hills"},
  481. {.id = 6951, .name = "Forest Hills"},
  482. {.id = 604, .name = "Forest Hills"},
  483. {.id = 7096, .name = "Forest Hills"},
  484. // South Station
  485. {.id = 7039, .name = "South Station"},
  486. {.id = 2215, .name = "South Station"},
  487. {.id = 6816, .name = "South Station"},
  488. {.id = 6817, .name = "South Station"},
  489. {.id = 6818, .name = "South Station"},
  490. {.id = 6819, .name = "South Station"},
  491. {.id = 6820, .name = "South Station"},
  492. {.id = 6821, .name = "South Station"},
  493. {.id = 6822, .name = "South Station"},
  494. {.id = 6823, .name = "South Station"},
  495. {.id = 7040, .name = "South Station"},
  496. {.id = 2228, .name = "South Station"},
  497. {.id = 6827, .name = "South Station"},
  498. {.id = 6826, .name = "South Station"},
  499. {.id = 6825, .name = "South Station"},
  500. {.id = 6824, .name = "South Station"},
  501. // Courthouse
  502. {.id = 6929, .name = "Courthouse"},
  503. {.id = 2357, .name = "Courthouse"},
  504. {.id = 7079, .name = "Courthouse"},
  505. {.id = 6933, .name = "Courthouse"},
  506. {.id = 6932, .name = "Courthouse"},
  507. {.id = 2358, .name = "Courthouse"},
  508. {.id = 6792, .name = "Courthouse"},
  509. // Bowdoin
  510. {.id = 6937, .name = "Bowdoin"},
  511. {.id = 2367, .name = "Bowdoin"},
  512. {.id = 7085, .name = "Bowdoin"},
  513. // Government Center
  514. {.id = 6963, .name = "Government Center"},
  515. {.id = 6962, .name = "Government Center"},
  516. {.id = 6961, .name = "Government Center"},
  517. {.id = 6960, .name = "Government Center"},
  518. {.id = 6959, .name = "Government Center"},
  519. {.id = 6958, .name = "Government Center"},
  520. {.id = 5298, .name = "Government Center"},
  521. // Aquarium
  522. {.id = 6609, .name = "Aquarium"},
  523. {.id = 6608, .name = "Aquarium"},
  524. {.id = 1877, .name = "Aquarium"},
  525. {.id = 6965, .name = "Aquarium"},
  526. {.id = 6610, .name = "Aquarium"},
  527. {.id = 1880, .name = "Aquarium"},
  528. {.id = 1871, .name = "Aquarium"},
  529. {.id = 6966, .name = "Aquarium"},
  530. // Maverick
  531. {.id = 7088, .name = "Maverick"},
  532. {.id = 6944, .name = "Maverick"},
  533. {.id = 4384, .name = "Maverick"},
  534. {.id = 6946, .name = "Maverick"},
  535. {.id = 6947, .name = "Maverick"},
  536. {.id = 6948, .name = "Maverick"},
  537. {.id = 6949, .name = "Maverick"},
  538. {.id = 1840, .name = "Maverick"},
  539. {.id = 7083, .name = "Maverick"},
  540. // Airport
  541. {.id = 6613, .name = "Airport"},
  542. {.id = 6612, .name = "Airport"},
  543. {.id = 6611, .name = "Airport"},
  544. {.id = 6968, .name = "Airport"},
  545. {.id = 2009, .name = "Airport"},
  546. {.id = 6616, .name = "Airport"},
  547. {.id = 6615, .name = "Airport"},
  548. {.id = 6614, .name = "Airport"},
  549. {.id = 6970, .name = "Airport"},
  550. {.id = 1847, .name = "Airport"},
  551. // Wood Island
  552. {.id = 6618, .name = "Wood Island"},
  553. {.id = 6619, .name = "Wood Island"},
  554. {.id = 2010, .name = "Wood Island"},
  555. {.id = 6971, .name = "Wood Island"},
  556. // Orient Heights
  557. {.id = 6621, .name = "Orient Heights"},
  558. {.id = 6622, .name = "Orient Heights"},
  559. {.id = 6623, .name = "Orient Heights"},
  560. {.id = 2014, .name = "Orient Heights"},
  561. {.id = 6972, .name = "Orient Heights"},
  562. {.id = 6974, .name = "Orient Heights"},
  563. {.id = 1868, .name = "Orient Heights"},
  564. // Suffolk Downs
  565. {.id = 6625, .name = "Suffolk Downs"},
  566. {.id = 6626, .name = "Suffolk Downs"},
  567. {.id = 2017, .name = "Suffolk Downs"},
  568. {.id = 6975, .name = "Suffolk Downs"},
  569. // Beachmont
  570. {.id = 6628, .name = "Beachmont"},
  571. {.id = 6629, .name = "Beachmont"},
  572. {.id = 6630, .name = "Beachmont"},
  573. {.id = 2021, .name = "Beachmont"},
  574. {.id = 6976, .name = "Beachmont"},
  575. // Revere Beach
  576. {.id = 6632, .name = "Revere Beach"},
  577. {.id = 6633, .name = "Revere Beach"},
  578. {.id = 2024, .name = "Revere Beach"},
  579. {.id = 6977, .name = "Revere Beach"},
  580. // Wonderland
  581. {.id = 6638, .name = "Wonderland"},
  582. {.id = 6637, .name = "Wonderland"},
  583. {.id = 6636, .name = "Wonderland"},
  584. {.id = 2025, .name = "Wonderland"},
  585. {.id = 6978, .name = "Wonderland"},
  586. };
  587. static const size_t kNumFareGateIds = COUNT_OF(charliecard_fare_gate_ids);
  588. // **********************************************************
  589. // ********************* MISC HELPERS ***********************
  590. // **********************************************************
  591. static const uint8_t*
  592. pos_to_ptr(const MfClassicData* data, uint8_t sector_num, uint8_t block_num, uint8_t byte_num) {
  593. // returns pointer to specified sector/block/byte of MFClassic card data
  594. uint8_t block_offset = mf_classic_get_first_block_num_of_sector(sector_num);
  595. return &data->block[block_offset + block_num].data[byte_num];
  596. }
  597. static uint64_t pos_to_num(
  598. const MfClassicData* data,
  599. uint8_t sector_num,
  600. uint8_t block_num,
  601. uint8_t byte_num,
  602. uint8_t byte_len) {
  603. // returns numeric values at specified card location, for given byte length.
  604. // assumes big endian.
  605. return bit_lib_bytes_to_num_be(pos_to_ptr(data, sector_num, block_num, byte_num), byte_len);
  606. }
  607. static DateTime dt_delta(DateTime dt, uint64_t delta_secs) {
  608. // returns shifted DateTime, from initial DateTime and time offset in seconds
  609. DateTime dt_shifted = {0};
  610. datetime_timestamp_to_datetime(datetime_datetime_to_timestamp(&dt) + delta_secs, &dt_shifted);
  611. return dt_shifted;
  612. }
  613. static bool dt_ge(DateTime dt1, DateTime dt2) {
  614. // compares two DateTimes
  615. return datetime_datetime_to_timestamp(&dt1) >= datetime_datetime_to_timestamp(&dt2);
  616. }
  617. static bool dt_eq(DateTime dt1, DateTime dt2) {
  618. // compares two DateTimes
  619. return datetime_datetime_to_timestamp(&dt1) == datetime_datetime_to_timestamp(&dt2);
  620. }
  621. static bool get_map_item(uint16_t id, const IdMapping* map, size_t sz, const char** out) {
  622. // code borrowed from Jeremy Cooper's 'clipper.c'. Used as follows:
  623. // const char* s; if(!get_map_item(_,_,_,&s)) {s="Default str";}
  624. // TODO: change to furistring out?
  625. for(size_t i = 0; i < sz; i++) {
  626. if(map[i].id == id) {
  627. *out = map[i].name;
  628. return true;
  629. }
  630. }
  631. return false;
  632. }
  633. uint32_t time_now() {
  634. return furi_hal_rtc_get_timestamp();
  635. }
  636. static bool is_debug() {
  637. return furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug);
  638. }
  639. // **********************************************************
  640. // ******************** FIELD PARSING ***********************
  641. // **********************************************************
  642. static Money money_parse(
  643. const MfClassicData* data,
  644. uint8_t sector_num,
  645. uint8_t block_num,
  646. uint8_t byte_num) {
  647. // CharlieCards store all money values in two bytes as half-cents
  648. // bitmask removes sign/flag, bitshift converts half-cents to cents, div & mod yield dollars & cents
  649. uint16_t amt = (pos_to_num(data, sector_num, block_num, byte_num, 2) & 0x7FFF) >> 1;
  650. return (Money){amt / 100, amt % 100};
  651. }
  652. static DateTime
  653. date_parse(const MfClassicData* data, uint8_t sector_num, uint8_t block_num, uint8_t byte_num) {
  654. // Dates are 3 bytes, in minutes since 2003/1/1 ("CHARLIE_EPOCH")
  655. uint32_t ts_charlie = pos_to_num(data, sector_num, block_num, byte_num, 3);
  656. return dt_delta(CHARLIE_EPOCH, ts_charlie * CHARLIE_TIME_DELTA_SECS);
  657. }
  658. static DateTime end_validity_parse(
  659. const MfClassicData* data,
  660. uint8_t sector_num,
  661. uint8_t block_num,
  662. uint8_t byte_num) {
  663. // End validity field is weird; shares first byte with another variable (the card type field),
  664. // occupying the last 5 bits (and subsequent two bytes), hence bitmask
  665. uint32_t ts_charlie_ev = pos_to_num(data, sector_num, block_num, byte_num, 3) & 0x1FFFFF;
  666. // additionally, instead of minute deltas, is in 8 minute increments
  667. // relative to CHARLIE_EPOCH (2003/1/1), per DEFCON31 researcher's work
  668. return dt_delta(CHARLIE_EPOCH, ts_charlie_ev * CHARLIE_END_VALID_DELTA_SECS);
  669. }
  670. static Pass
  671. pass_parse(const MfClassicData* data, uint8_t sector_num, uint8_t block_num, uint8_t byte_num) {
  672. // WIP; testing only. Speculating it may be structured as follows
  673. // Sub-byte field divisions not drawn to scale, see code for exact bit offsets
  674. //
  675. // 0 1 2 3 4 5
  676. // +----.----.----.----+----.----+
  677. // | uk1 | date | uk2 |
  678. // +----.----.----.----+----.----+
  679. //
  680. // "Blank" entries are as follows:
  681. // 0 1 2 3 4 5
  682. // +----.----.----.----.----.----+
  683. // | 00 20 00 00 00 00 |
  684. // +----.----.----.----.----.----+
  685. //
  686. // even when not blank, uk1 LSB seems to always be set to 1...
  687. // the sole bit set to 1 on the blank entry seems to divide
  688. // the uk1 and date fields, and is always set to 1 regardless
  689. // same is true of type & end-validity split found in balance sector
  690. //
  691. // likely fields incl
  692. // — type #,
  693. // — a secondary date field (eg start/end, end validity or normal format)
  694. // — ID of FVM from which the pass was loaded
  695. // check for empty, if so, return struct filled w/ 0s
  696. // (incl "valid" field: hence, "valid" is false-y)
  697. if(pos_to_num(data, sector_num, block_num, byte_num, 6) == 0x002000000000) {
  698. return (Pass){0};
  699. }
  700. // const DateTime start = date_parse(data, sector_num, block_num, byte_num + 1);
  701. const uint16_t pre = pos_to_num(data, sector_num, block_num, byte_num, 2) >> 6;
  702. const uint16_t post = (pos_to_num(data, sector_num, block_num, byte_num + 4, 2) >> 2) & 0x3ff;
  703. // these values make sense for a date, but implied position of type
  704. // before end validity, as seen in balance sector, doesn't seem
  705. // to produce sensible values
  706. const DateTime date = end_validity_parse(data, sector_num, block_num, byte_num + 1);
  707. // DateTime start = date_parse(data, sector_num, block_num, byte_num);
  708. // uint16_t type = 0; // pos_to_num(data, sector_num, block_num, byte_num + 3, 2) >> 6;
  709. return (Pass){true, pre, post, date};
  710. }
  711. static Transaction
  712. transaction_parse(const MfClassicData* data, uint8_t sector, uint8_t block, uint8_t byte) {
  713. // This function parses individual transactions. Each transaction packs 7 bytes, stored as follows:
  714. //
  715. // 0 1 2 3 4 5 6
  716. // +----.----.----+----.--+-+----.----+
  717. // | date | loc |f| amt |
  718. // +----.----.----+----.--+-+----.----+
  719. //
  720. // Where date is in the typical format, loc represents the fare gate tapped, and amt is the fare amount.
  721. // Amount appears to contain some flag bits, however, it is unclear what precisely their function is.
  722. //
  723. // Gate ID ("loc") is only the first 13 bits of 0x3:0x5, the final three bits appear to be flags ("f").
  724. // Least significant flag bit seems to indicate:
  725. // — When f & 1 == 1, fare (the amount by which balance is decremented)
  726. // — When f & 1 == 0, refill (the amount by which balance is incremented)
  727. // MSB (sign bit) of amt seems to serve the same role, just inverted, ie
  728. // — When amt & 0x8000 == 0, fare
  729. // — When amt & 0x8000 == 0x8000, refill
  730. // Only contradiction between the two observed is on cards w/ passes;
  731. // MSB of amt seems to be set for every transaction when (remaining bits of) amt is 0 on a card w/ a pass
  732. // Hence, using f's LSB as method for inferring fare v. refill
  733. //
  734. // Remaining unknown bits:
  735. // — f & 0b100; seems to be set on fares where the card has a pass, and amt is 0
  736. // — f & 0b010
  737. // — amt & 1; does not seem to correspond with card type, last transaction, first transaction, refill v. fare, etc
  738. const DateTime date = date_parse(data, sector, block, byte);
  739. const uint16_t gate = pos_to_num(data, sector, block, byte + 3, 2) >> 3;
  740. const uint8_t g_flag = pos_to_num(data, sector, block, byte + 3, 2) & 0b111;
  741. const Money fare = money_parse(data, sector, block, byte + 5);
  742. const uint16_t f_flag = pos_to_num(data, sector, block, byte + 5, 2) & 0x8001;
  743. return (Transaction){date, gate, g_flag, fare, f_flag};
  744. }
  745. // **********************************************************
  746. // ******************* SECTOR PARSING ***********************
  747. // **********************************************************
  748. static uint32_t mfg_sector_parse(const MfClassicData* data) {
  749. // Manufacturer data (Sector 0)
  750. //
  751. // 0 1 2 3 4 5 6 7 8 9 A B C D E F
  752. // +----.----.----.----+----+----.----.----.----+----+----.----.----.----.----+----+
  753. // 0x000 | UID | rc | 88 04 00 C8 | uk | 00 20 00 00 00 | uk |
  754. // +----.----.----.----+----+----.----.----.----+----+----.----.----.----.----+----+
  755. // 0x010 | 4E 0F 04 10 04 10 04 10 04 10 04 10 04 10 04 10 |
  756. // +----.----.----.----.----.----.----.----.----.----.----.----.----.----.----.----+
  757. // 0x020 | ... 00 00 ... |
  758. // +----.----.----.----.----.----.----.----.----.----.----.----.----.----.----.----+
  759. //
  760. // rc := "redundancy check" (lrc / bcc)
  761. // uk := "unknown"
  762. size_t uid_len = 0;
  763. const uint8_t* uid = mf_classic_get_uid(data, &uid_len);
  764. const uint32_t card_number = bit_lib_bytes_to_num_be(uid, 4);
  765. return card_number;
  766. }
  767. static CounterSector counter_sector_parse(const MfClassicData* data) {
  768. // Trip/transaction counters (Sector 1)
  769. //
  770. // 0 1 2 3 4 5 6 7 8 9 A B C D E F
  771. // +----.----.----.----.----.----.----.----.----.----.----.----.----.----.----.----+
  772. // 0x040 | 04 10 23 45 66 77 ... 00 00 ... |
  773. // +----.----+----+----.----.----.----.----.----.----.----.----.----.----.----.----+
  774. // 0x050 | uses1 | uk | ... 00 00 ... |
  775. // +----.----+----+----.----.----.----.----.----.----.----.----.----.----.----.----+
  776. // 0x060 | uses2 | uk | ... 00 00 ... |
  777. // +----.----+----+----.----.----.----.----.----.----.----.----.----.----.----.----+
  778. //
  779. // uk := "unknown"; if nonzero, seems to only occupy the first 4 bits (ie, uk & 0xF0 == uk),
  780. // with the remaining 4 zero
  781. // Card has two sectors (2 & 3) containing balance data, with two
  782. // corresponding trip counters in 0x50:0x51 & 0x60:0x61 (sector 1, byte 0:1 of blocks 1 & 2).
  783. // The *lower* of the two values *minus one* is the true use count,
  784. // and corresponds to the active balance sector,
  785. // (0x50 counter lower -> sector 2 active, 0x60 counter lower -> 3 active)
  786. // per DEFCON31 researcher's findings
  787. const uint16_t n_uses1 = pos_to_num(data, 1, 1, 0, 2);
  788. const uint16_t n_uses2 = pos_to_num(data, 1, 2, 0, 2);
  789. const bool is_sec2_active = n_uses1 <= n_uses2;
  790. const uint8_t active_sector = is_sec2_active ? 2 : 3;
  791. const uint16_t n_uses = (is_sec2_active ? n_uses1 : n_uses2) - 1;
  792. return (CounterSector){n_uses, active_sector};
  793. }
  794. static BalanceSector balance_sector_parse(const MfClassicData* data, uint8_t active_sector) {
  795. // Balance & misc card info (Sector 2 or 3)
  796. //
  797. // 0 1 2 3 4 5 6 7 8 9 A B C D E F
  798. // +----+----.----.----+----.----+----.----.----+----.----+----.----+----+----.----+
  799. // 0x080 | 11 | date last | loc last| date issued | 65 00 | unknown | 00 | crc | 0x0C0
  800. // +----+----.----.----+----+----+----+----+----+----.----+----.----+----+----.----+
  801. // 0x090 | type |end validity| uk | balance | 00 | unknown | crc | 0x0D0
  802. // +----.----.----.----+----+----.----+----+----.----.----.----.----.----+----.----+
  803. // 0x0A0 | 20 ... 00 00 ... 04 | crc | 0x0E0
  804. // +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+
  805. //
  806. // "Active" balance sector alternates between 2 and 3
  807. // Last trip/transaction info in balance sector ("date last" & "loc last")
  808. // is also included in transaction log, hence don't bother to read here
  809. //
  810. // Inactive balance sector represent the transaction N-1 version
  811. // (where active sector represents data from transaction N).
  812. const DateTime issued = date_parse(data, active_sector, 0, 6);
  813. const DateTime end_validity = end_validity_parse(data, active_sector, 1, 1);
  814. // Card type data stored in the first 10bits of block 1
  815. // (0x90 or 0xD0 depending on active sector)
  816. // bitshift (2bytes = 16 bits) by 6bits for just first 10bits
  817. const uint16_t type = pos_to_num(data, active_sector, 1, 0, 2) >> 6;
  818. const Money bal = money_parse(data, active_sector, 1, 5);
  819. return (BalanceSector){bal, type, issued, end_validity};
  820. }
  821. static Pass* passes_parse(const MfClassicData* data) {
  822. // Passes, speculative (Sectors 4 &/or 5)
  823. //
  824. // 0 1 2 3 4 5 6 7 8 9 A B C D E F
  825. // +----.----.----.----.----.----+----+----.----.----.----.----.----+----+----.----+
  826. // 0x100 | pass0/2? | 00 | pass1/3? | 00 | crc | 0x140
  827. // +----.----.----.----.----.----+----+----.----.----.----.----.----+----+----.----+
  828. // 0x110 | ... 00 00 ... | crc | 0x150
  829. // +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+
  830. // 0x120 | ... 00 ... 05 | crc | 0x160
  831. // +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+
  832. //
  833. // WIP. Read in all speculative passes into array
  834. // 4 separate fields? active vs inactive sector for 2 passes?
  835. // something else entirely?
  836. Pass* passes = malloc(sizeof(Pass) * CHARLIE_N_PASSES);
  837. for(size_t i = 0; i < CHARLIE_N_PASSES; i++) {
  838. passes[i] = pass_parse(data, 4 + (i / 2), 0, (i % 2) * 7);
  839. }
  840. return passes;
  841. }
  842. static Transaction* transactions_parse(const MfClassicData* data) {
  843. // Transaction history (Sectors 6–7)
  844. //
  845. // 0 1 2 3 4 5 6 7 8 9 A B C D E F
  846. // +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+
  847. // 0x180 | transaction0 | transaction1 | crc |
  848. // +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+
  849. // ... ... ... ...
  850. // +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+
  851. // 0x1D0 | transaction8 | transaction9 | crc |
  852. // +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+
  853. // 0x1E0 | ... 00 00 ... | crc |
  854. // +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+
  855. //
  856. // Transactions are not sorted, rather, appear to get overwritten
  857. // sequentially. (eg, sorted modulo array rotation)
  858. Transaction* transactions = malloc(sizeof(Transaction) * CHARLIE_N_TRANSACTION_HISTORY);
  859. // Parse each transaction field using some modular math magic to get the offsets:
  860. // move from sector 6 -> 7 after the first 6 transactions
  861. // move a block within a given sector every 2 transactions, reset every 3 blocks (as sector has changed)
  862. // alternate between a start byte of 0 and 7 with every iteration
  863. for(size_t i = 0; i < CHARLIE_N_TRANSACTION_HISTORY; i++) {
  864. transactions[i] = transaction_parse(data, 6 + (i / 6), (i / 2) % 3, (i % 2) * 7);
  865. }
  866. // Iterate through the array to find the maximum (newest) date value
  867. int max_idx = 0;
  868. for(int i = 1; i < CHARLIE_N_TRANSACTION_HISTORY; i++) {
  869. if(dt_ge(transactions[i].date, transactions[max_idx].date)) {
  870. max_idx = i;
  871. }
  872. }
  873. // Sort by rotating
  874. for(int r = 0; r < (max_idx + 1); r++) {
  875. // Store the first element
  876. Transaction temp = transactions[0];
  877. // Shift elements to the left
  878. for(int i = 0; i < CHARLIE_N_TRANSACTION_HISTORY - 1; i++) {
  879. transactions[i] = transactions[i + 1];
  880. }
  881. // Move the first element to the last
  882. transactions[CHARLIE_N_TRANSACTION_HISTORY - 1] = temp;
  883. }
  884. // Reverse order, such that newest is first, oldest last
  885. for(int i = 0; i < CHARLIE_N_TRANSACTION_HISTORY / 2; i++) {
  886. // Swap elements at index i and size - i - 1
  887. Transaction temp = transactions[i];
  888. transactions[i] = transactions[CHARLIE_N_TRANSACTION_HISTORY - i - 1];
  889. transactions[CHARLIE_N_TRANSACTION_HISTORY - i - 1] = temp;
  890. }
  891. return transactions;
  892. }
  893. /*
  894. static DateTime expiry(DateTime iss) {
  895. // Per Metrodroid CharlieCard parser (https://github.com/metrodroid/metrodroid/blob/master/src/commonMain/kotlin/au/id/micolous/metrodroid/transit/charlie/CharlieCardTransitData.kt)
  896. // Expiry not explicitly stored in card data; rather, calculated from date of issue
  897. // Cards were first issued in 2006, expired in 5 years, w/ no printed expiry date
  898. // Cards issued after 2011 expire in 10 years
  899. //
  900. // Per DEFCON31 researcher's work (cited above):
  901. // Student cards last one school year and expire at the end of August the following year
  902. // Pre-2011 issued cards expire in 7 years, not 5 as claimed by Metrodroid
  903. // Post-2011 expire in 10 years, less one day
  904. // Redundant function given the existance of the end validity field?
  905. // Any important distinctions between the two?
  906. // perhaps additionally clipping to 2030-12-__ in anticipation of upcoming system migration?
  907. // need to get a new card to confirm.
  908. // TODO add card type logic for student card expiry
  909. DateTime exp;
  910. if(iss.year < 2011) {
  911. // add 7 years; assumes average year of 8766 hrs (to account for leap years)
  912. // may be off by a few hours as a result
  913. exp = dt_delta(iss, 7 * 8766 * 60 * 60);
  914. } else {
  915. // add 10 years, subtract a day. Same assumption as above
  916. exp = dt_delta(iss, ((10 * 8766) - 24) * 60 * 60);
  917. }
  918. return exp;
  919. }
  920. static bool expired(DateTime expiry, DateTime last_transaction) {
  921. // if a card has sat unused for >2 years, expired (verify this claim?)
  922. // else expired if current date > expiry date
  923. uint32_t ts_exp = datetime_datetime_to_timestamp(&expiry);
  924. uint32_t ts_last = datetime_datetime_to_timestamp(&last_transaction);
  925. uint32_t ts_now = time_now();
  926. return (ts_exp <= ts_now) | ((ts_now - ts_last) >= (2 * 365 * 24 * 60 * 60));
  927. }
  928. */
  929. // **********************************************************
  930. // ****************** STRING FORMATTING *********************
  931. // **********************************************************
  932. void locale_format_dt_cat(FuriString* out, const DateTime* dt) {
  933. // helper to print datetimes
  934. FuriString* s = furi_string_alloc();
  935. LocaleDateFormat date_format = locale_get_date_format();
  936. const char* separator = (date_format == LocaleDateFormatDMY) ? "." : "/";
  937. locale_format_date(s, dt, date_format, separator);
  938. furi_string_cat(out, s);
  939. locale_format_time(s, dt, locale_get_time_format(), false);
  940. furi_string_cat_printf(out, " ");
  941. furi_string_cat(out, s);
  942. furi_string_free(s);
  943. }
  944. void type_format_cat(FuriString* out, uint16_t type) {
  945. const char* s;
  946. if(!get_map_item(type, charliecard_types, kNumTypes, &s)) {
  947. s = "";
  948. furi_string_cat_printf(out, "Unknown-%u", type);
  949. }
  950. furi_string_cat_str(out, s);
  951. }
  952. void pass_format_cat(FuriString* out, Pass pass) {
  953. furi_string_cat_printf(out, "\n-Pre: %b", pass.pre);
  954. // type_format_cat(out, pass.type);
  955. furi_string_cat_printf(out, "\n-Post: ");
  956. type_format_cat(out, pass.post);
  957. // locale_format_dt_cat(out, &pass.start);
  958. furi_string_cat_printf(out, "\n-Date: ");
  959. locale_format_dt_cat(out, &pass.date);
  960. }
  961. void passes_format_cat(FuriString* out, Pass* passes) {
  962. // only print passes if DEBUG on
  963. if(!is_debug()) {
  964. return;
  965. }
  966. // only print if there is at least 1 valid pass to print
  967. bool any_valid = false;
  968. for(size_t i = 0; i < CHARLIE_N_PASSES; i++) {
  969. any_valid |= passes[i].valid;
  970. }
  971. if(!any_valid) {
  972. return;
  973. }
  974. furi_string_cat_printf(out, "\nPasses (DEBUG / WIP):");
  975. for(size_t i = 0; i < CHARLIE_N_PASSES; i++) {
  976. if(passes[i].valid) {
  977. furi_string_cat_printf(out, "\nPass %u", i + 1);
  978. pass_format_cat(out, passes[i]);
  979. furi_string_cat_printf(out, "\n");
  980. }
  981. }
  982. }
  983. void money_format_cat(FuriString* out, Money money) {
  984. furi_string_cat_printf(out, "$%u.%02u", money.dollars, money.cents);
  985. }
  986. void transaction_format_cat(FuriString* out, Transaction transaction) {
  987. const char* sep = " ";
  988. const char* sta;
  989. locale_format_dt_cat(out, &transaction.date);
  990. furi_string_cat_printf(out, "\n%s", !!(transaction.g_flag & 0x1) ? "-" : "+");
  991. money_format_cat(out, transaction.fare);
  992. if(!!(transaction.g_flag & 0x1) && (transaction.fare.dollars == FARE_BUS.dollars) &&
  993. (transaction.fare.cents == FARE_BUS.cents)) {
  994. // if not a refill, and the fare amount is equal to bus fare (any better approach? flag bits for modality?)
  995. // format for bus — supposedly some correlation between gate ID & bus #, haven't investigated
  996. furi_string_cat_printf(out, "%s#%u", sep, transaction.gate);
  997. } else if(get_map_item(transaction.gate, charliecard_fare_gate_ids, kNumFareGateIds, &sta)) {
  998. // station found in fare gate ID map, append station name
  999. furi_string_cat_str(out, sep);
  1000. furi_string_cat_str(out, sta);
  1001. } else {
  1002. // no found station in fare gate ID map & not a bus, just print ID w/o add'l info
  1003. furi_string_cat_printf(out, "%s#%u", sep, transaction.gate);
  1004. }
  1005. // print flags for debugging purposes
  1006. if(is_debug()) {
  1007. furi_string_cat_printf(out, "%s%x%s%x", sep, transaction.g_flag, sep, transaction.f_flag);
  1008. }
  1009. }
  1010. void transactions_format_cat(FuriString* out, Transaction* transactions) {
  1011. furi_string_cat_printf(out, "\nTransactions:");
  1012. for(size_t i = 0; i < CHARLIE_N_TRANSACTION_HISTORY; i++) {
  1013. furi_string_cat_printf(out, "\n");
  1014. transaction_format_cat(out, transactions[i]);
  1015. furi_string_cat_printf(out, "\n");
  1016. }
  1017. }
  1018. // **********************************************************
  1019. // **************** NFC PLUGIN BOILERPLATE ******************
  1020. // **********************************************************
  1021. static bool charliecard_parse(FuriString* parsed_data, const MfClassicData* data) {
  1022. bool parsed = false;
  1023. do {
  1024. // Verify key
  1025. // arbitrary sector in the main data portion
  1026. const uint8_t verify_sector = 3;
  1027. const MfClassicSectorTrailer* sec_tr =
  1028. mf_classic_get_sector_trailer_by_sector(data, verify_sector);
  1029. const uint64_t key_a =
  1030. bit_lib_bytes_to_num_be(sec_tr->key_a.data, COUNT_OF(sec_tr->key_a.data));
  1031. const uint64_t key_b =
  1032. bit_lib_bytes_to_num_be(sec_tr->key_b.data, COUNT_OF(sec_tr->key_b.data));
  1033. if(key_a != charliecard_1k_keys[verify_sector].a) break;
  1034. if(key_b != charliecard_1k_keys[verify_sector].b) break;
  1035. // parse card data
  1036. const uint32_t card_number = mfg_sector_parse(data);
  1037. const CounterSector counter_sector = counter_sector_parse(data);
  1038. const BalanceSector balance_sector =
  1039. balance_sector_parse(data, counter_sector.active_balance_sector);
  1040. Pass* passes = passes_parse(data);
  1041. Transaction* transactions = transactions_parse(data);
  1042. // print/append card data
  1043. furi_string_cat_printf(parsed_data, "\e#CharlieCard");
  1044. furi_string_cat_printf(parsed_data, "\nSerial: 5-%lu", card_number);
  1045. // Type and balance 0 on some (Perq) cards
  1046. // (ie no "main" type / balance / end validity,
  1047. // essentially only pass & trip info)
  1048. // skip/change formatting for that case?
  1049. furi_string_cat_printf(parsed_data, "\nBal: ");
  1050. money_format_cat(parsed_data, balance_sector.balance);
  1051. furi_string_cat_printf(parsed_data, "\nType: ");
  1052. type_format_cat(parsed_data, balance_sector.type);
  1053. furi_string_cat_printf(parsed_data, "\nTrip Count: %u", counter_sector.n_uses);
  1054. furi_string_cat_printf(parsed_data, "\nIssued: ");
  1055. locale_format_dt_cat(parsed_data, &balance_sector.issued);
  1056. if(!dt_eq(balance_sector.end_validity, CHARLIE_EPOCH) &
  1057. dt_ge(balance_sector.end_validity, balance_sector.issued)) {
  1058. // sometimes (seen on Perq cards) end validity field is all 0
  1059. // When this is the case, calc'd end validity is equal to CHARLIE_EPOCH).
  1060. // Only print if not 0, & end validity after issuance date
  1061. furi_string_cat_printf(parsed_data, "\nExpiry: ");
  1062. locale_format_dt_cat(parsed_data, &balance_sector.end_validity);
  1063. }
  1064. // const DateTime last = date_parse(data, active_sector, 0, 1);
  1065. // furi_string_cat_printf(parsed_data, "\nExpired: %s", expired(e_v, last) ? "Yes" : "No");
  1066. transactions_format_cat(parsed_data, transactions);
  1067. free(transactions);
  1068. passes_format_cat(parsed_data, passes);
  1069. free(passes);
  1070. parsed = true;
  1071. } while(false);
  1072. return parsed;
  1073. }
  1074. void metroflip_charliecard_widget_callback(GuiButtonType result, InputType type, void* context) {
  1075. Metroflip* app = context;
  1076. UNUSED(result);
  1077. if(type == InputTypeShort) {
  1078. scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
  1079. }
  1080. }
  1081. static NfcCommand
  1082. metroflip_scene_charlicard_poller_callback(NfcGenericEvent event, void* context) {
  1083. furi_assert(context);
  1084. furi_assert(event.event_data);
  1085. furi_assert(event.protocol == NfcProtocolMfClassic);
  1086. NfcCommand command = NfcCommandContinue;
  1087. const MfClassicPollerEvent* mfc_event = event.event_data;
  1088. Metroflip* app = context;
  1089. if(mfc_event->type == MfClassicPollerEventTypeCardDetected) {
  1090. view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventCardDetected);
  1091. command = NfcCommandContinue;
  1092. } else if(mfc_event->type == MfClassicPollerEventTypeCardLost) {
  1093. view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventCardLost);
  1094. app->sec_num = 0;
  1095. command = NfcCommandStop;
  1096. } else if(mfc_event->type == MfClassicPollerEventTypeRequestMode) {
  1097. mfc_event->data->poller_mode.mode = MfClassicPollerModeRead;
  1098. } else if(mfc_event->type == MfClassicPollerEventTypeRequestReadSector) {
  1099. MfClassicKey key = {0};
  1100. bit_lib_num_to_bytes_be(charliecard_1k_keys[app->sec_num].a, COUNT_OF(key.data), key.data);
  1101. MfClassicKeyType key_type = MfClassicKeyTypeA;
  1102. mfc_event->data->read_sector_request_data.sector_num = app->sec_num;
  1103. mfc_event->data->read_sector_request_data.key = key;
  1104. mfc_event->data->read_sector_request_data.key_type = key_type;
  1105. mfc_event->data->read_sector_request_data.key_provided = true;
  1106. if(app->sec_num == 16) {
  1107. mfc_event->data->read_sector_request_data.key_provided = false;
  1108. app->sec_num = 0;
  1109. }
  1110. app->sec_num++;
  1111. } else if(mfc_event->type == MfClassicPollerEventTypeSuccess) {
  1112. nfc_device_set_data(
  1113. app->nfc_device, NfcProtocolMfClassic, nfc_poller_get_data(app->poller));
  1114. const MfClassicData* mfc_data = nfc_device_get_data(app->nfc_device, NfcProtocolMfClassic);
  1115. FuriString* parsed_data = furi_string_alloc();
  1116. Widget* widget = app->widget;
  1117. dolphin_deed(DolphinDeedNfcReadSuccess);
  1118. furi_string_reset(app->text_box_store);
  1119. if(!charliecard_parse(parsed_data, mfc_data)) {
  1120. furi_string_reset(app->text_box_store);
  1121. FURI_LOG_I(TAG, "Unknown card type");
  1122. furi_string_printf(parsed_data, "\e#Unknown card\n");
  1123. }
  1124. widget_add_text_scroll_element(widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
  1125. widget_add_button_element(
  1126. widget, GuiButtonTypeRight, "Exit", metroflip_charliecard_widget_callback, app);
  1127. furi_string_free(parsed_data);
  1128. view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
  1129. command = NfcCommandStop;
  1130. metroflip_app_blink_stop(app);
  1131. } else if(mfc_event->type == MfClassicPollerEventTypeFail) {
  1132. FURI_LOG_I(TAG, "fail");
  1133. command = NfcCommandStop;
  1134. }
  1135. return command;
  1136. }
  1137. void metroflip_scene_charliecard_on_enter(void* context) {
  1138. Metroflip* app = context;
  1139. dolphin_deed(DolphinDeedNfcRead);
  1140. app->sec_num = 0;
  1141. // Setup view
  1142. Popup* popup = app->popup;
  1143. popup_set_header(popup, "Apply\n card to\nthe back", 68, 30, AlignLeft, AlignTop);
  1144. popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
  1145. // Start worker
  1146. view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup);
  1147. nfc_scanner_alloc(app->nfc);
  1148. app->poller = nfc_poller_alloc(app->nfc, NfcProtocolMfClassic);
  1149. nfc_poller_start(app->poller, metroflip_scene_charlicard_poller_callback, app);
  1150. metroflip_app_blink_start(app);
  1151. }
  1152. bool metroflip_scene_charliecard_on_event(void* context, SceneManagerEvent event) {
  1153. Metroflip* app = context;
  1154. bool consumed = false;
  1155. if(event.type == SceneManagerEventTypeCustom) {
  1156. if(event.event == MetroflipCustomEventCardDetected) {
  1157. Popup* popup = app->popup;
  1158. popup_set_header(popup, "DON'T\nMOVE", 68, 30, AlignLeft, AlignTop);
  1159. consumed = true;
  1160. } else if(event.event == MetroflipCustomEventCardLost) {
  1161. Popup* popup = app->popup;
  1162. popup_set_header(popup, "Card \n lost", 68, 30, AlignLeft, AlignTop);
  1163. consumed = true;
  1164. } else if(event.event == MetroflipCustomEventWrongCard) {
  1165. Popup* popup = app->popup;
  1166. popup_set_header(popup, "WRONG \n CARD", 68, 30, AlignLeft, AlignTop);
  1167. consumed = true;
  1168. } else if(event.event == MetroflipCustomEventPollerFail) {
  1169. Popup* popup = app->popup;
  1170. popup_set_header(popup, "Failed", 68, 30, AlignLeft, AlignTop);
  1171. consumed = true;
  1172. }
  1173. } else if(event.type == SceneManagerEventTypeBack) {
  1174. scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
  1175. consumed = true;
  1176. }
  1177. return consumed;
  1178. }
  1179. void metroflip_scene_charliecard_on_exit(void* context) {
  1180. Metroflip* app = context;
  1181. widget_reset(app->widget);
  1182. if(app->poller) {
  1183. nfc_poller_stop(app->poller);
  1184. nfc_poller_free(app->poller);
  1185. }
  1186. // Clear view
  1187. popup_reset(app->popup);
  1188. metroflip_app_blink_stop(app);
  1189. }