From c1446116099790220053a36e5eea765569c93541 Mon Sep 17 00:00:00 2001
From: MX <10697207+xMasterX@users.noreply.github.com>
Date: Wed, 10 Apr 2024 01:44:41 +0300
Subject: [PATCH 1/5] update changelog
---
CHANGELOG.md | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0066ec7e90..70b8bdf9f1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,9 +10,16 @@
* JS & HAL: Various fixes and FURI_HAL_RANDOM_MAX define added (by @Willy-JL)
* JS: BadUSB layout support (by @Willy-JL)
* JS: Module `widget` and path globals (by @jamisonderek)
-* Apps: NFC Magic - **Gen2 writing support** (by @Astrrra)
+* Apps: NFC Magic - **Gen2 writing support, Gen4 NTAG password and PACK fixes** (by @Astrrra)
* Apps: MFKey - fixed crashes (by @noproto)
* Apps: **Check out Apps updates by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev)
+* OFW: Desktop/Loader: Unload animations before loading FAPs
+* OFW: JS Documentation
+* OFW: Update radio stack to v1.19.0
+* OFW: Move crypto1 to helpers, add it to the public API
+* OFW: Explain RNG differences, add FURI_HAL_RANDOM_MAX
+* OFW: Furi: Add "out of memory" and "malloc(0)" crash messages
+* OFW: IR: Fix crash on duty_cycle=1
* OFW: Desktop: ensure that animation is unloaded before app start (fixes some out of memory crashes)
* OFW: Hide unlock with reader for MFU-C
* OFW: fbt: fixed missing FBT_FAP_DEBUG_ELF_ROOT to dist env
From 744ef8138e88fa23defa30ee9afed23457648947 Mon Sep 17 00:00:00 2001
From: MX <10697207+xMasterX@users.noreply.github.com>
Date: Wed, 10 Apr 2024 15:26:47 +0300
Subject: [PATCH 2/5] subghz add manually fixes
---
applications/main/subghz/scenes/subghz_scene_set_type.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/applications/main/subghz/scenes/subghz_scene_set_type.c b/applications/main/subghz/scenes/subghz_scene_set_type.c
index 90b30f0768..b88e449b15 100644
--- a/applications/main/subghz/scenes/subghz_scene_set_type.c
+++ b/applications/main/subghz/scenes/subghz_scene_set_type.c
@@ -445,8 +445,8 @@ bool subghz_scene_set_type_on_event(void* context, SceneManagerEvent event) {
.type = GenKeeloq,
.mod = "FM476",
.freq = 434420000,
- .keeloq.serial = key & 0x0FFFFFFF,
- .keeloq.btn = 0x04,
+ .keeloq.serial = (key & 0x0000FFFF) | 0x07150000,
+ .keeloq.btn = 0x02,
.keeloq.cnt = 0x03,
.keeloq.manuf = "Sommer(fsk476)"};
break;
@@ -455,8 +455,8 @@ bool subghz_scene_set_type_on_event(void* context, SceneManagerEvent event) {
.type = GenKeeloq,
.mod = "FM476",
.freq = 868800000,
- .keeloq.serial = key & 0x0FFFFFFF,
- .keeloq.btn = 0x04,
+ .keeloq.serial = (key & 0x0000FFFF) | 0x07150000,
+ .keeloq.btn = 0x02,
.keeloq.cnt = 0x03,
.keeloq.manuf = "Sommer(fsk476)"};
break;
From 2443a702c97e82a381b0d6b140aba63a78f0aba4 Mon Sep 17 00:00:00 2001
From: MX <10697207+xMasterX@users.noreply.github.com>
Date: Wed, 10 Apr 2024 15:38:57 +0300
Subject: [PATCH 3/5] update nfc parser
by zacharyweiss
---
.../nfc/plugins/supported_cards/charliecard.c | 761 +++++++++++-------
1 file changed, 489 insertions(+), 272 deletions(-)
diff --git a/applications/main/nfc/plugins/supported_cards/charliecard.c b/applications/main/nfc/plugins/supported_cards/charliecard.c
index 7a405fffb2..9880ca3ad7 100644
--- a/applications/main/nfc/plugins/supported_cards/charliecard.c
+++ b/applications/main/nfc/plugins/supported_cards/charliecard.c
@@ -23,29 +23,41 @@
* — 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
+ * — 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
@@ -82,12 +94,8 @@
// timestep is one minute
#define CHARLIE_TIME_DELTA_SECS 60
#define CHARLIE_END_VALID_DELTA_SECS 60 * 8
-#define CHARLIE_N_TRIP_HISTORY 10
-
-enum CharlieActiveSector {
- CHARLIE_ACTIVE_SECTOR_2,
- CHARLIE_ACTIVE_SECTOR_3,
-};
+#define CHARLIE_N_TRANSACTION_HISTORY 10
+#define CHARLIE_N_PASSES 4
typedef struct {
uint64_t a;
@@ -138,8 +146,27 @@ typedef struct {
uint16_t gate;
uint8_t g_flag;
Money fare;
- uint8_t f_flag;
-} Trip;
+ uint16_t f_flag;
+} Transaction;
+
+typedef struct {
+ bool valid;
+ uint16_t pre;
+ uint16_t post;
+ DateTime date;
+} 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 {
@@ -147,14 +174,14 @@ 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"},
{.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"},
@@ -164,16 +191,16 @@ 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
- {.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"},
@@ -388,9 +415,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"},
@@ -471,7 +496,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"},
@@ -546,7 +571,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"},
@@ -578,8 +603,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];
}
@@ -590,16 +620,29 @@ 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 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";}
@@ -614,82 +657,17 @@ static bool get_map_item(uint16_t id, const IdMapping* map, size_t sz, const cha
return false;
}
-static bool charliecard_verify(Nfc* nfc) {
- // does this suffice? Or should I check add'l keys/data/etc?
- bool verified = false;
-
- do {
- const uint8_t verify_sector = 1;
- const uint8_t verify_block = mf_classic_get_first_block_num_of_sector(verify_sector) + 1;
- FURI_LOG_D(TAG, "Verifying sector %u", verify_sector);
-
- MfClassicKey key = {0};
- bit_lib_num_to_bytes_be(
- charliecard_1k_keys[verify_sector].a, COUNT_OF(key.data), key.data);
-
- MfClassicAuthContext auth_context;
- MfClassicError error =
- mf_classic_poller_sync_auth(nfc, verify_block, &key, MfClassicKeyTypeA, &auth_context);
- if(error != MfClassicErrorNone) {
- FURI_LOG_D(TAG, "Failed to read block %u: %d", verify_block, error);
- break;
- }
-
- verified = true;
- } while(false);
-
- return verified;
+uint32_t time_now() {
+ return furi_hal_rtc_get_timestamp();
}
-static bool charliecard_read(Nfc* nfc, NfcDevice* device) {
- furi_assert(nfc);
- furi_assert(device);
-
- bool is_read = false;
-
- MfClassicData* data = mf_classic_alloc();
- nfc_device_copy_data(device, NfcProtocolMfClassic, data);
-
- do {
- MfClassicType type = MfClassicTypeMini;
- MfClassicError error = mf_classic_poller_sync_detect_type(nfc, &type);
- if(error != MfClassicErrorNone) break;
-
- data->type = type;
- if(type != MfClassicType1k) break;
-
- MfClassicDeviceKeys keys = {
- .key_a_mask = 0,
- .key_b_mask = 0,
- };
- for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) {
- bit_lib_num_to_bytes_be(
- charliecard_1k_keys[i].a, sizeof(MfClassicKey), keys.key_a[i].data);
- FURI_BIT_SET(keys.key_a_mask, i);
- bit_lib_num_to_bytes_be(
- charliecard_1k_keys[i].b, sizeof(MfClassicKey), keys.key_b[i].data);
- FURI_BIT_SET(keys.key_b_mask, i);
- }
-
- error = mf_classic_poller_sync_read(nfc, &keys, data);
- if(error == MfClassicErrorNotPresent) {
- FURI_LOG_W(TAG, "Failed to read data");
- break;
- }
-
- nfc_device_set_data(device, NfcProtocolMfClassic, data);
-
- is_read = (error == MfClassicErrorNone);
- } while(false);
-
- mf_classic_free(data);
-
- return is_read;
+static bool is_debug() {
+ return furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug);
}
-uint32_t time_now() {
- return furi_hal_rtc_get_timestamp();
-}
+// **********************************************************
+// ******************** FIELD PARSING ***********************
+// **********************************************************
static Money money_parse(
const MfClassicData* data,
@@ -709,83 +687,249 @@ static DateTime
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;
+static DateTime end_validity_parse(
+ const MfClassicData* data,
+ uint8_t sector_num,
+ uint8_t block_num,
+ uint8_t byte_num) {
+ // 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
// 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 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
+ // +----.----.----.----+----.----+
+ // | uk1 | date | uk2 |
+ // +----.----.----.----+----.----+
+ //
+ // "Blank" entries are as follows:
+ // 0 1 2 3 4 5
+ // +----.----.----.----.----.----+
+ // | 00 20 00 00 00 00 |
+ // +----.----.----.----.----.----+
+ //
+ // 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
+ //
+ // 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) {
+ return (Pass){0};
+ }
+
+ // 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) >> 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
+ // 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, pre, post, date};
}
-static bool date_ge(DateTime dt1, DateTime dt2) {
- return datetime_datetime_to_timestamp(&dt1) >= datetime_datetime_to_timestamp(&dt2);
+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
+ // +----.----.----+----.--+-+----.----+
+ // | 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
+
+ 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 (Transaction){date, gate, g_flag, fare, f_flag};
}
-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);
+// **********************************************************
+// ******************* SECTOR PARSING ***********************
+// **********************************************************
+
+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 | 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"
- // 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)
+ 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 | uk | ... 00 00 ... |
+ // +----.----+----+----.----.----.----.----.----.----.----.----.----.----.----.----+
+ // 0x060 | uses2 | uk | ... 00 00 ... |
+ // +----.----+----+----.----.----.----.----.----.----.----.----.----.----.----.----+
+ //
+ // uk := "unknown"; if nonzero, seems to only occupy the first 4 bits (ie, uk & 0xF0 == uk),
+ // with the remaining 4 zero
+
+ // 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,
+ // 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 & 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| 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")
+ // is also included in transaction log, hence don't bother to read here
+ //
+ // 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);
+ // 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};
+}
+
+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/2? | 00 | pass1/3? | 00 | crc | 0x140
+ // +----.----.----.----.----.----+----+----.----.----.----.----.----+----+----.----+
+ // 0x110 | ... 00 00 ... | crc | 0x150
+ // +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+
+ // 0x120 | ... 00 ... 05 | crc | 0x160
+ // +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+
+ //
+ // 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);
+
+ for(size_t i = 0; i < CHARLIE_N_PASSES; i++) {
+ passes[i] = pass_parse(data, 4 + (i / 2), 0, (i % 2) * 7);
+ }
+
+ return passes;
+}
+
+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 | transaction0 | transaction1 | crc |
+ // +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+
+ // ... ... ... ...
+ // +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+
+ // 0x1D0 | transaction8 | transaction9 | crc |
+ // +----.----.----.----.----.----.----+----.----.----.----.----.----.----+----.----+
+ // 0x1E0 | ... 00 00 ... | crc |
+ // +----.----.----.----.----.----.----.----.----.----.----.----.----.----+----.----+
+ //
+ // Transactions are not sorted, rather, appear to get overwritten
+ // sequentially. (eg, sorted modulo array rotation)
+
+ Transaction* transactions = malloc(sizeof(Transaction) * CHARLIE_N_TRANSACTION_HISTORY);
+
+ // 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(date_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;
}
}
@@ -793,78 +937,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;
-}
-
-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;
+ return transactions;
}
/*
@@ -897,18 +987,23 @@ 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));
}
+*/
+
+// **********************************************************
+// ****************** STRING FORMATTING *********************
+// **********************************************************
void locale_format_dt_cat(FuriString* out, const DateTime* dt) {
// helper to print datetimes
@@ -935,36 +1030,84 @@ void type_format_cat(FuriString* out, uint16_t type) {
furi_string_cat_str(out, s);
}
+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: ");
+ 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);
+}
+
+void passes_format_cat(FuriString* out, Pass* passes) {
+ // 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");
+ }
+ }
+}
+
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 (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)) {
+ // format for bus — supposedly some correlation between gate ID & bus #, haven't investigated
+ 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(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) {
- furi_string_cat_printf(out, "%s%u%s%u", sep, trip.g_flag, sep, trip.f_flag);
+ if(is_debug()) {
+ furi_string_cat_printf(out, "%s%x%s%x", sep, transaction.g_flag, sep, transaction.f_flag);
}
}
+void transactions_format_cat(FuriString* out, Transaction* transactions) {
+ furi_string_cat_printf(out, "\nTransactions:");
+ for(size_t i = 0; i < CHARLIE_N_TRANSACTION_HISTORY; i++) {
+ furi_string_cat_printf(out, "\n");
+ transaction_format_cat(out, transactions[i]);
+ furi_string_cat_printf(out, "\n");
+ }
+}
+
+// **********************************************************
+// **************** NFC PLUGIN BOILERPLATE ******************
+// **********************************************************
+
static bool charliecard_parse(const NfcDevice* device, FuriString* parsed_data) {
furi_assert(device);
@@ -989,48 +1132,50 @@ 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?
-
- 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);
+ Transaction* transactions = transactions_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);
+ // 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, 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);
- 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);
+
+ 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 e_v = end_validity_parse(data, active_sec_enum);
- furi_string_cat_printf(parsed_data, "\nExpiry: ");
- locale_format_dt_cat(parsed_data, &e_v);
+ // const DateTime last = date_parse(data, active_sector, 0, 1);
+ // furi_string_cat_printf(parsed_data, "\nExpired: %s", expired(e_v, last) ? "Yes" : "No");
- DateTime last = date_parse(data, active_sector, 0, 1);
- furi_string_cat_printf(parsed_data, "\nExpired: %s", expired(e_v, last) ? "Yes" : "No");
+ transactions_format_cat(parsed_data, transactions);
+ free(transactions);
- 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);
+ passes_format_cat(parsed_data, passes);
+ free(passes);
parsed = true;
} while(false);
@@ -1038,6 +1183,78 @@ static bool charliecard_parse(const NfcDevice* device, FuriString* parsed_data)
return parsed;
}
+static bool charliecard_verify(Nfc* nfc) {
+ bool verified = false;
+
+ do {
+ const uint8_t verify_sector = 1;
+ const uint8_t verify_block = mf_classic_get_first_block_num_of_sector(verify_sector) + 1;
+ FURI_LOG_D(TAG, "Verifying sector %u", verify_sector);
+
+ MfClassicKey key = {0};
+ bit_lib_num_to_bytes_be(
+ charliecard_1k_keys[verify_sector].a, COUNT_OF(key.data), key.data);
+
+ MfClassicAuthContext auth_context;
+ MfClassicError error =
+ mf_classic_poller_sync_auth(nfc, verify_block, &key, MfClassicKeyTypeA, &auth_context);
+ if(error != MfClassicErrorNone) {
+ FURI_LOG_D(TAG, "Failed to read block %u: %d", verify_block, error);
+ break;
+ }
+
+ verified = true;
+ } while(false);
+
+ return verified;
+}
+
+static bool charliecard_read(Nfc* nfc, NfcDevice* device) {
+ furi_assert(nfc);
+ furi_assert(device);
+
+ bool is_read = false;
+
+ MfClassicData* data = mf_classic_alloc();
+ nfc_device_copy_data(device, NfcProtocolMfClassic, data);
+
+ do {
+ MfClassicType type = MfClassicTypeMini;
+ MfClassicError error = mf_classic_poller_sync_detect_type(nfc, &type);
+ if(error != MfClassicErrorNone) break;
+
+ data->type = type;
+ if(type != MfClassicType1k) break;
+
+ MfClassicDeviceKeys keys = {
+ .key_a_mask = 0,
+ .key_b_mask = 0,
+ };
+ for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) {
+ bit_lib_num_to_bytes_be(
+ charliecard_1k_keys[i].a, sizeof(MfClassicKey), keys.key_a[i].data);
+ FURI_BIT_SET(keys.key_a_mask, i);
+ bit_lib_num_to_bytes_be(
+ charliecard_1k_keys[i].b, sizeof(MfClassicKey), keys.key_b[i].data);
+ FURI_BIT_SET(keys.key_b_mask, i);
+ }
+
+ error = mf_classic_poller_sync_read(nfc, &keys, data);
+ if(error == MfClassicErrorNotPresent) {
+ FURI_LOG_W(TAG, "Failed to read data");
+ break;
+ }
+
+ nfc_device_set_data(device, NfcProtocolMfClassic, data);
+
+ is_read = (error == MfClassicErrorNone);
+ } while(false);
+
+ mf_classic_free(data);
+
+ return is_read;
+}
+
/* Actual implementation of app<>plugin interface */
static const NfcSupportedCardsPlugin charliecard_plugin = {
.protocol = NfcProtocolMfClassic,
From c1e8cbf3fdb1f13e27b24d25cf66a9c6de8cbc91 Mon Sep 17 00:00:00 2001
From: MX <10697207+xMasterX@users.noreply.github.com>
Date: Wed, 10 Apr 2024 15:46:20 +0300
Subject: [PATCH 4/5] update changelog
---
CHANGELOG.md | 22 ++++++++++++----------
1 file changed, 12 insertions(+), 10 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 70b8bdf9f1..9206f1b13d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,26 +1,28 @@
## New changes
* NFC: CharlieCard parser (by @zacharyweiss)
+* SubGHz: Add Manually - Sommer FM fixes
* SubGHz: Enabled tx-rx state on unused gpio pin by default (**external amp option was removed and is enabled by default now**)
-* SubGHz: Status output !TX/RX on the GDO2 CC1101 pin (by @quen0n | PR #742)
+* SubGHz: **Status output !TX/RX on the GDO2 CC1101 pin** (by @quen0n | PR #742)
* SubGHz: Reworked saved settings (by @xMasterX and @Willy-JL)
* Desktop: Fixes for animation unload (by @Willy-JL)
* Misc: Added `void` due to `-Wstrict-prototypes`
* Misc: Some code cleanup and proper log levels in nfc parsers
* Infrared: Allow external apps to use infrared settings (by @Willy-JL)
* JS & HAL: Various fixes and FURI_HAL_RANDOM_MAX define added (by @Willy-JL)
-* JS: BadUSB layout support (by @Willy-JL)
+* JS: **BadUSB layout support** (by @Willy-JL)
* JS: Module `widget` and path globals (by @jamisonderek)
* Apps: NFC Magic - **Gen2 writing support, Gen4 NTAG password and PACK fixes** (by @Astrrra)
-* Apps: MFKey - fixed crashes (by @noproto)
+* Apps: MFKey - **fixed crashes** (by @noproto)
* Apps: **Check out Apps updates by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev)
+* OFW: **Felica poller** (NFC-F)
* OFW: Desktop/Loader: Unload animations before loading FAPs
* OFW: JS Documentation
-* OFW: Update radio stack to v1.19.0
-* OFW: Move crypto1 to helpers, add it to the public API
+* OFW: **Update radio stack to v1.19.0**
+* OFW: **Move crypto1 to helpers, add it to the public API**
* OFW: Explain RNG differences, add FURI_HAL_RANDOM_MAX
* OFW: Furi: Add "out of memory" and "malloc(0)" crash messages
* OFW: IR: Fix crash on duty_cycle=1
-* OFW: Desktop: ensure that animation is unloaded before app start (fixes some out of memory crashes)
+* OFW: **Desktop: ensure that animation is unloaded before app start (fixes some out of memory crashes)**
* OFW: Hide unlock with reader for MFU-C
* OFW: fbt: fixed missing FBT_FAP_DEBUG_ELF_ROOT to dist env
* OFW: fbt: added -Wstrict-prototypes for main firmware
@@ -30,19 +32,19 @@
* OFW: L1_Mods animation update : adding VGM visual
* OFW: RFID Improvements
* OFW: Fixed plugins and UI
-* OFW: NFC: Fix mf desfire detect
+* OFW: **NFC: Fix mf desfire detect**
* OFW: infrared_transmit.h was missing `#pragma once`
* OFW: Show the wrong PIN Attempt count on the login screen
* OFW: SavedStruct: Introduce saved_struct_get_metadata
* OFW: JS CLI command
* OFW: Add ChromeOS Bad USB demo
-* OFW: Configurable Infrared TX output (previous UL version is replaced with OFW version, new features added "AutoDetect" and saving settings)
+* OFW: **Configurable Infrared TX output** (previous UL version is replaced with OFW version, new features added "AutoDetect" and saving settings)
* OFW: BadUSB: BLE, media keys, Fn/Globe key commands
-* OFW: NFC: Slix privacy password reveal and Desfire detect fix
+* OFW: NFC: Slix privacy password reveal ->(was included in previous UL release) and **Desfire detect fix**
* OFW: github: additional pre-upload checks for doxygen workflow
* OFW: NFC UI fixes
* OFW: Gui: unicode support, new canvas API
-* OFW: Api Symbols: replace asserts with checks
+* OFW: **Api Symbols: replace asserts with checks**
#### Known NFC post-refactor regressions list:
- Mifare Mini clones reading is broken (original mini working fine) (OFW)
From 6bb605f8ced38c254d38c63c522a46e22d8d5377 Mon Sep 17 00:00:00 2001
From: MX <10697207+xMasterX@users.noreply.github.com>
Date: Thu, 11 Apr 2024 01:20:39 +0300
Subject: [PATCH 5/5] Allow setting view dispatcher callbacks to NULL
---
applications/services/gui/view_dispatcher.c | 3 ---
1 file changed, 3 deletions(-)
diff --git a/applications/services/gui/view_dispatcher.c b/applications/services/gui/view_dispatcher.c
index 222ba817fd..d4c2f61e79 100644
--- a/applications/services/gui/view_dispatcher.c
+++ b/applications/services/gui/view_dispatcher.c
@@ -51,7 +51,6 @@ void view_dispatcher_set_navigation_event_callback(
ViewDispatcher* view_dispatcher,
ViewDispatcherNavigationEventCallback callback) {
furi_check(view_dispatcher);
- furi_check(callback);
view_dispatcher->navigation_event_callback = callback;
}
@@ -59,7 +58,6 @@ void view_dispatcher_set_custom_event_callback(
ViewDispatcher* view_dispatcher,
ViewDispatcherCustomEventCallback callback) {
furi_check(view_dispatcher);
- furi_check(callback);
view_dispatcher->custom_event_callback = callback;
}
@@ -68,7 +66,6 @@ void view_dispatcher_set_tick_event_callback(
ViewDispatcherTickEventCallback callback,
uint32_t tick_period) {
furi_check(view_dispatcher);
- furi_check(callback);
view_dispatcher->tick_event_callback = callback;
view_dispatcher->tick_period = tick_period;
}