From 8b6f21f69db10467b92bc70bfd12998ede0895b8 Mon Sep 17 00:00:00 2001 From: Michael Farrell Date: Sat, 20 May 2023 17:46:59 +1000 Subject: [PATCH 1/5] Implement support for reading Opal card (Sydney, Australia) --- .../main/nfc/scenes/nfc_scene_device_info.c | 1 + .../nfc_scene_mf_desfire_read_success.c | 59 +++--- .../main/nfc/scenes/nfc_scene_nfc_data_info.c | 2 +- .../main/nfc/scenes/nfc_scene_saved_menu.c | 1 + firmware/targets/f7/api_symbols.csv | 3 + firmware/targets/f7/furi_hal/furi_hal_rtc.c | 10 +- .../targets/furi_hal_include/furi_hal_rtc.h | 11 + lib/nfc/nfc_worker.c | 13 ++ lib/nfc/parsers/nfc_supported_card.c | 15 ++ lib/nfc/parsers/nfc_supported_card.h | 6 + lib/nfc/parsers/opal.c | 194 ++++++++++++++++++ lib/nfc/parsers/opal.h | 5 + lib/nfc/protocols/mifare_desfire.c | 24 +++ lib/nfc/protocols/mifare_desfire.h | 3 + 14 files changed, 310 insertions(+), 37 deletions(-) create mode 100644 lib/nfc/parsers/opal.c create mode 100644 lib/nfc/parsers/opal.h diff --git a/applications/main/nfc/scenes/nfc_scene_device_info.c b/applications/main/nfc/scenes/nfc_scene_device_info.c index 9780ffe41d2..5d51c0816c8 100644 --- a/applications/main/nfc/scenes/nfc_scene_device_info.c +++ b/applications/main/nfc/scenes/nfc_scene_device_info.c @@ -52,6 +52,7 @@ void nfc_scene_device_info_on_enter(void* context) { } } else if( dev_data->protocol == NfcDeviceProtocolMifareClassic || + dev_data->protocol == NfcDeviceProtocolMifareDesfire || dev_data->protocol == NfcDeviceProtocolMifareUl) { furi_string_set(temp_str, nfc->dev->dev_data.parsed_data); } diff --git a/applications/main/nfc/scenes/nfc_scene_mf_desfire_read_success.c b/applications/main/nfc/scenes/nfc_scene_mf_desfire_read_success.c index 39030397fc4..633549eb5d8 100644 --- a/applications/main/nfc/scenes/nfc_scene_mf_desfire_read_success.c +++ b/applications/main/nfc/scenes/nfc_scene_mf_desfire_read_success.c @@ -20,35 +20,40 @@ void nfc_scene_mf_desfire_read_success_on_enter(void* context) { Widget* widget = nfc->widget; // Prepare string for data display - FuriString* temp_str = furi_string_alloc_printf("\e#MIFARE DESfire\n"); - furi_string_cat_printf(temp_str, "UID:"); - for(size_t i = 0; i < nfc_data->uid_len; i++) { - furi_string_cat_printf(temp_str, " %02X", nfc_data->uid[i]); - } + FuriString* temp_str = NULL; + if(furi_string_size(nfc->dev->dev_data.parsed_data)) { + temp_str = furi_string_alloc_set(nfc->dev->dev_data.parsed_data); + } else { + temp_str = furi_string_alloc_printf("\e#MIFARE DESFire\n"); + furi_string_cat_printf(temp_str, "UID:"); + for(size_t i = 0; i < nfc_data->uid_len; i++) { + furi_string_cat_printf(temp_str, " %02X", nfc_data->uid[i]); + } - uint32_t bytes_total = 1UL << (data->version.sw_storage >> 1); - uint32_t bytes_free = data->free_memory ? data->free_memory->bytes : 0; - furi_string_cat_printf(temp_str, "\n%lu", bytes_total); - if(data->version.sw_storage & 1) { - furi_string_push_back(temp_str, '+'); - } - furi_string_cat_printf(temp_str, " bytes, %lu bytes free\n", bytes_free); - - uint16_t n_apps = 0; - uint16_t n_files = 0; - for(MifareDesfireApplication* app = data->app_head; app; app = app->next) { - n_apps++; - for(MifareDesfireFile* file = app->file_head; file; file = file->next) { - n_files++; + uint32_t bytes_total = 1UL << (data->version.sw_storage >> 1); + uint32_t bytes_free = data->free_memory ? data->free_memory->bytes : 0; + furi_string_cat_printf(temp_str, "\n%lu", bytes_total); + if(data->version.sw_storage & 1) { + furi_string_push_back(temp_str, '+'); + } + furi_string_cat_printf(temp_str, " bytes, %lu bytes free\n", bytes_free); + + uint16_t n_apps = 0; + uint16_t n_files = 0; + for(MifareDesfireApplication* app = data->app_head; app; app = app->next) { + n_apps++; + for(MifareDesfireFile* file = app->file_head; file; file = file->next) { + n_files++; + } + } + furi_string_cat_printf(temp_str, "%d Application", n_apps); + if(n_apps != 1) { + furi_string_push_back(temp_str, 's'); + } + furi_string_cat_printf(temp_str, ", %d file", n_files); + if(n_files != 1) { + furi_string_push_back(temp_str, 's'); } - } - furi_string_cat_printf(temp_str, "%d Application", n_apps); - if(n_apps != 1) { - furi_string_push_back(temp_str, 's'); - } - furi_string_cat_printf(temp_str, ", %d file", n_files); - if(n_files != 1) { - furi_string_push_back(temp_str, 's'); } notification_message_block(nfc->notifications, &sequence_set_green_255); diff --git a/applications/main/nfc/scenes/nfc_scene_nfc_data_info.c b/applications/main/nfc/scenes/nfc_scene_nfc_data_info.c index 92ad7b56ef4..b44bb5e64cd 100644 --- a/applications/main/nfc/scenes/nfc_scene_nfc_data_info.c +++ b/applications/main/nfc/scenes/nfc_scene_nfc_data_info.c @@ -40,7 +40,7 @@ void nfc_scene_nfc_data_info_on_enter(void* context) { furi_string_cat_printf( temp_str, "\e#%s\n", nfc_mf_classic_type(dev_data->mf_classic_data.type)); } else if(protocol == NfcDeviceProtocolMifareDesfire) { - furi_string_cat_printf(temp_str, "\e#MIFARE DESfire\n"); + furi_string_cat_printf(temp_str, "\e#MIFARE DESFire\n"); } else { furi_string_cat_printf(temp_str, "\e#Unknown ISO tag\n"); } diff --git a/applications/main/nfc/scenes/nfc_scene_saved_menu.c b/applications/main/nfc/scenes/nfc_scene_saved_menu.c index e45dc4eb7dd..4573cdc4528 100644 --- a/applications/main/nfc/scenes/nfc_scene_saved_menu.c +++ b/applications/main/nfc/scenes/nfc_scene_saved_menu.c @@ -148,6 +148,7 @@ bool nfc_scene_saved_menu_on_event(void* context, SceneManagerEvent event) { application_info_present = true; } else if( dev_data->protocol == NfcDeviceProtocolMifareClassic || + dev_data->protocol == NfcDeviceProtocolMifareDesfire || dev_data->protocol == NfcDeviceProtocolMifareUl) { application_info_present = nfc_supported_card_verify_and_parse(dev_data); } diff --git a/firmware/targets/f7/api_symbols.csv b/firmware/targets/f7/api_symbols.csv index ccbaa531765..fae2f10c39b 100644 --- a/firmware/targets/f7/api_symbols.csv +++ b/firmware/targets/f7/api_symbols.csv @@ -1991,6 +1991,8 @@ Function,-,mf_df_cat_key_settings,void,"MifareDesfireKeySettings*, FuriString*" Function,-,mf_df_cat_version,void,"MifareDesfireVersion*, FuriString*" Function,-,mf_df_check_card_type,_Bool,"uint8_t, uint8_t, uint8_t" Function,-,mf_df_clear,void,MifareDesfireData* +Function,-,mf_df_get_application,MifareDesfireApplication*,"MifareDesfireData*, const uint8_t[3]*" +Function,-,mf_df_get_file,MifareDesfireFile*,"MifareDesfireApplication*, uint8_t" Function,-,mf_df_parse_get_application_ids_response,_Bool,"uint8_t*, uint16_t, MifareDesfireApplication**" Function,-,mf_df_parse_get_file_ids_response,_Bool,"uint8_t*, uint16_t, MifareDesfireFile**" Function,-,mf_df_parse_get_file_settings_response,_Bool,"uint8_t*, uint16_t, MifareDesfireFile*" @@ -3083,6 +3085,7 @@ Variable,+,furi_hal_i2c_bus_external,FuriHalI2cBus, Variable,+,furi_hal_i2c_bus_power,FuriHalI2cBus, Variable,+,furi_hal_i2c_handle_external,FuriHalI2cBusHandle, Variable,+,furi_hal_i2c_handle_power,FuriHalI2cBusHandle, +Variable,+,furi_hal_rtc_days_per_month,const uint8_t[2][12], Variable,+,furi_hal_sd_spi_handle,FuriHalSpiBusHandle*, Variable,+,furi_hal_spi_bus_d,FuriHalSpiBus, Variable,+,furi_hal_spi_bus_handle_display,FuriHalSpiBusHandle, diff --git a/firmware/targets/f7/furi_hal/furi_hal_rtc.c b/firmware/targets/f7/furi_hal/furi_hal_rtc.c index 7bd45c35d71..abce7de920a 100644 --- a/firmware/targets/f7/furi_hal/furi_hal_rtc.c +++ b/firmware/targets/f7/furi_hal/furi_hal_rtc.c @@ -39,15 +39,7 @@ typedef struct { _Static_assert(sizeof(SystemReg) == 4, "SystemReg size mismatch"); -#define FURI_HAL_RTC_SECONDS_PER_MINUTE 60 -#define FURI_HAL_RTC_SECONDS_PER_HOUR (FURI_HAL_RTC_SECONDS_PER_MINUTE * 60) -#define FURI_HAL_RTC_SECONDS_PER_DAY (FURI_HAL_RTC_SECONDS_PER_HOUR * 24) -#define FURI_HAL_RTC_MONTHS_COUNT 12 -#define FURI_HAL_RTC_EPOCH_START_YEAR 1970 -#define FURI_HAL_RTC_IS_LEAP_YEAR(year) \ - ((((year) % 4 == 0) && ((year) % 100 != 0)) || ((year) % 400 == 0)) - -static const uint8_t furi_hal_rtc_days_per_month[][FURI_HAL_RTC_MONTHS_COUNT] = { +const uint8_t furi_hal_rtc_days_per_month[2][FURI_HAL_RTC_MONTHS_COUNT] = { {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}}; diff --git a/firmware/targets/furi_hal_include/furi_hal_rtc.h b/firmware/targets/furi_hal_include/furi_hal_rtc.h index e706c5c76a0..ed7aee228dc 100644 --- a/firmware/targets/furi_hal_include/furi_hal_rtc.h +++ b/firmware/targets/furi_hal_include/furi_hal_rtc.h @@ -12,6 +12,17 @@ extern "C" { #endif +#define FURI_HAL_RTC_SECONDS_PER_MINUTE 60 +#define FURI_HAL_RTC_SECONDS_PER_HOUR (FURI_HAL_RTC_SECONDS_PER_MINUTE * 60) +#define FURI_HAL_RTC_SECONDS_PER_DAY (FURI_HAL_RTC_SECONDS_PER_HOUR * 24) +#define FURI_HAL_RTC_EPOCH_START_YEAR 1970 +#define FURI_HAL_RTC_IS_LEAP_YEAR(year) \ + ((((year) % 4 == 0) && ((year) % 100 != 0)) || ((year) % 400 == 0)) + +#define FURI_HAL_RTC_MONTHS_COUNT 12 + +extern const uint8_t furi_hal_rtc_days_per_month[2][FURI_HAL_RTC_MONTHS_COUNT]; + typedef struct { // Time uint8_t hour; /**< Hour in 24H format: 0-23 */ diff --git a/lib/nfc/nfc_worker.c b/lib/nfc/nfc_worker.c index 28a1f682797..daa8fee59a3 100644 --- a/lib/nfc/nfc_worker.c +++ b/lib/nfc/nfc_worker.c @@ -219,6 +219,19 @@ static bool nfc_worker_read_mf_desfire(NfcWorker* nfc_worker, FuriHalNfcTxRxCont do { if(!furi_hal_nfc_detect(&nfc_worker->dev_data->nfc_data, 300)) break; if(!mf_df_read_card(tx_rx, data)) break; + FURI_LOG_I(TAG, "Trying to parse a supported card ..."); + + // The model for parsing DESFire is a little different to other cards; + // we don't have parsers to provide encryption keys, so we can read the + // data normally, and then pass the read data to a parser. + // + // There are fully-protected DESFire cards, but providing keys for them + // is difficult (and unnessesary for many transit cards). + for(size_t i = 0; i < NfcSupportedCardTypeEnd; i++) { + if(nfc_supported_card[i].protocol == NfcDeviceProtocolMifareDesfire) { + if(nfc_supported_card[i].parse(nfc_worker->dev_data)) break; + } + } read_success = true; } while(false); diff --git a/lib/nfc/parsers/nfc_supported_card.c b/lib/nfc/parsers/nfc_supported_card.c index fc2dc34e0ee..f389dfad37a 100644 --- a/lib/nfc/parsers/nfc_supported_card.c +++ b/lib/nfc/parsers/nfc_supported_card.c @@ -6,6 +6,7 @@ #include "troika_4k_parser.h" #include "two_cities.h" #include "all_in_one.h" +#include "opal.h" NfcSupportedCard nfc_supported_card[NfcSupportedCardTypeEnd] = { [NfcSupportedCardTypePlantain] = @@ -50,6 +51,14 @@ NfcSupportedCard nfc_supported_card[NfcSupportedCardTypeEnd] = { .read = all_in_one_parser_read, .parse = all_in_one_parser_parse, }, + [NfcSupportedCardTypeOpal] = + { + .protocol = NfcDeviceProtocolMifareDesfire, + .verify = stub_parser_verify_read, + .read = stub_parser_verify_read, + .parse = opal_parser_parse, + }, + }; bool nfc_supported_card_verify_and_parse(NfcDeviceData* dev_data) { @@ -65,3 +74,9 @@ bool nfc_supported_card_verify_and_parse(NfcDeviceData* dev_data) { return card_parsed; } + +bool stub_parser_verify_read( + NfcWorker* nfc_worker __attribute__((unused)), + FuriHalNfcTxRxContext* tx_rx __attribute__((unused))) { + return false; +} diff --git a/lib/nfc/parsers/nfc_supported_card.h b/lib/nfc/parsers/nfc_supported_card.h index 4af59aded63..877bda73745 100644 --- a/lib/nfc/parsers/nfc_supported_card.h +++ b/lib/nfc/parsers/nfc_supported_card.h @@ -11,6 +11,7 @@ typedef enum { NfcSupportedCardTypeTroika4K, NfcSupportedCardTypeTwoCities, NfcSupportedCardTypeAllInOne, + NfcSupportedCardTypeOpal, NfcSupportedCardTypeEnd, } NfcSupportedCardType; @@ -31,3 +32,8 @@ typedef struct { extern NfcSupportedCard nfc_supported_card[NfcSupportedCardTypeEnd]; bool nfc_supported_card_verify_and_parse(NfcDeviceData* dev_data); + +// stub_parser_verify_read does nothing, and always reports that it does not +// support the card. This is needed for DESFire card parsers which can't +// provide keys, and only use NfcSupportedCard->parse. +bool stub_parser_verify_read(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx); diff --git a/lib/nfc/parsers/opal.c b/lib/nfc/parsers/opal.c new file mode 100644 index 00000000000..05b67fd7ec9 --- /dev/null +++ b/lib/nfc/parsers/opal.c @@ -0,0 +1,194 @@ +/* + * opal.c - Parser for Opal card (Sydney, Australia). + * + * Copyright 2023 Michael Farrell + * + * This will only read "standard" MIFARE DESFire-based Opal cards. Free travel + * cards (including School Opal cards, veteran, vision-impaired persons and + * TfNSW employees' cards) and single-trip tickets are MIFARE Ultralight C + * cards and not supported. + * + * Reference: https://github.com/metrodroid/metrodroid/wiki/Opal + * + * Note: The card values are all little-endian (like Flipper), but the above + * reference was originally written based on Java APIs, which are big-endian. + * This implementation presumes a little-endian system. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "nfc_supported_card.h" +#include "opal.h" + +#include +#include +#include + +#include + +static const uint8_t opal_aid[3] = {0x31, 0x45, 0x53}; +static const char* opal_modes[5] = + {"Rail / Metro", "Ferry / Light Rail", "Bus", "Unknown mode", "Manly Ferry"}; +static const char* opal_usages[14] = { + "New / Unused", + "Tap on: new journey", + "Tap on: transfer from same mode", + "Tap on: transfer from other mode", + "", // Manly Ferry: new journey + "", // Manly Ferry: transfer from ferry + "", // Manly Ferry: transfer from other + "Tap off: distance fare", + "Tap off: flat fare", + "Automated tap off: failed to tap off", + "Tap off: end of trip without start", + "Tap off: reversal", + "Tap on: rejected", + "Unknown usage", +}; + +// Converts an Opal timestamp to FuriHalRtcDateTime. +// +// Opal measures days since 1980-01-01 and minutes since midnight, and presumes +// all days are 1440 minutes. +void opal_date_time_to_furi(uint16_t days, uint16_t minutes, FuriHalRtcDateTime* out) { + if(!out) return; + uint16_t diy; + out->year = 1980; + out->month = 0; + // 1980-01-01 is a Tuesday + out->weekday = ((days + 1) % 7) + 1; + out->hour = minutes / 60; + out->minute = minutes % 60; + out->second = 0; + + // What year is it? + for(;;) { + diy = (FURI_HAL_RTC_IS_LEAP_YEAR(out->year) ? 366 : 365); + if(days < diy) break; + days -= diy; + out->year++; + } + + // 1-index the day of the year + days++; + // What month is it? + uint8_t is_leap = diy - 365; + + for(;;) { + uint8_t dim = furi_hal_rtc_days_per_month[is_leap][out->month]; + if(days <= dim) break; + days -= dim; + out->month++; + } + + out->month++; + out->day = days; +} + +bool opal_parser_parse(NfcDeviceData* dev_data) { + if(dev_data->protocol != NfcDeviceProtocolMifareDesfire) { + return false; + } + + MifareDesfireApplication* app = mf_df_get_application(&dev_data->mf_df_data, &opal_aid); + if(app == NULL) { + return false; + } + MifareDesfireFile* f = mf_df_get_file(app, 0x07); + if(f == NULL || f->type != MifareDesfireFileTypeStandard || f->settings.data.size != 16 || + !f->contents) { + return false; + } + + uint32_t serial = *(uint32_t*)(f->contents); // bit 96..127 + uint8_t serial2 = serial / 10000000; + uint16_t serial3 = (serial / 1000) % 10000; + uint16_t serial4 = (serial % 1000); + + uint8_t check_digit = f->contents[4] & 0x0f; // bit 92..95 + if(check_digit > 9) { + return false; + } + bool blocked = (f->contents[4] >> 4) & 0x1; // bit 91 + uint16_t txn_number = (*(uint32_t*)(f->contents + 4)) >> 5; // bit 75..90 + + int32_t balance = ((*(uint32_t*)(f->contents + 6)) >> 5) & 0x1fffff; // bit 54..74 + char* sign = ""; + if((balance & 0x100000) > 0) { + // Negative balance. Make this a positive value again and record the + // sign separately, because then we can handle balances of -99..-1 + // cents, as the "dollars" division below would result in a positive + // zero value. + balance = abs((int32_t)(balance | 0xfff00000)); + sign = "-"; + } + uint8_t cents = balance % 100; + int32_t dollars = balance / 100; + + uint16_t days = ((*(uint32_t*)(f->contents + 9)) >> 2) & 0x7fff; // bit 39..53 + uint16_t minutes = ((*(uint32_t*)(f->contents + 11)) >> 1) & 0x7ff; // bit 28..38 + FuriHalRtcDateTime timestamp; + opal_date_time_to_furi(days, minutes, ×tamp); + + uint8_t mode = ((*(uint16_t*)(f->contents + 12)) >> 4) & 0x7; // bit 25..27 + if(mode >= 3) { + // 3..7 are "reserved", but we use 4 to indicate the Manly Ferry. + mode = 3; + } + + uint8_t usage = ((*(uint16_t*)(f->contents + 12)) >> 7) & 0xf; // bit 21..24 + if(usage >= 4 && usage <= 6) { + // Usages 4..6 associated with the Manly Ferry, which correspond to + // usages 1..3 for other modes. + usage -= 3; + mode = 4; + } + + bool auto_topup = (f->contents[13] >> 3) & 0x1; // bit 20 + uint8_t weekly_journeys = f->contents[13] >> 4; // bit 16..19 + + const char* mode_str = (mode <= 4 ? opal_modes[mode] : opal_modes[3]); + const char* usage_str = (usage <= 12 ? opal_usages[usage] : opal_usages[13]); + + furi_string_printf( + dev_data->parsed_data, + "\e#Opal: $%s%ld.%02hu\n3085 22%02hhu %04hu %03hu%01hhu\n%s, %s\n", + sign, + dollars, + cents, + serial2, + serial3, + serial4, + check_digit, + mode_str, + usage_str); + FuriString* timestamp_str = furi_string_alloc(); + locale_format_date(timestamp_str, ×tamp, locale_get_date_format(), "-"); + furi_string_cat(dev_data->parsed_data, timestamp_str); + furi_string_cat_str(dev_data->parsed_data, " at "); + + locale_format_time(timestamp_str, ×tamp, locale_get_time_format(), false); + furi_string_cat(dev_data->parsed_data, timestamp_str); + + furi_string_free(timestamp_str); + furi_string_cat_printf( + dev_data->parsed_data, "\nWeekly journeys: %hhu, Txn #%hu\n", weekly_journeys, txn_number); + + if(auto_topup) { + furi_string_cat_str(dev_data->parsed_data, "Auto-topup enabled\n"); + } + if(blocked) { + furi_string_cat_str(dev_data->parsed_data, "Card blocked\n"); + } + return true; +} diff --git a/lib/nfc/parsers/opal.h b/lib/nfc/parsers/opal.h new file mode 100644 index 00000000000..42caf9a1790 --- /dev/null +++ b/lib/nfc/parsers/opal.h @@ -0,0 +1,5 @@ +#pragma once + +#include "nfc_supported_card.h" + +bool opal_parser_parse(NfcDeviceData* dev_data); diff --git a/lib/nfc/protocols/mifare_desfire.c b/lib/nfc/protocols/mifare_desfire.c index 23308ae95e3..e0ead737f87 100644 --- a/lib/nfc/protocols/mifare_desfire.c +++ b/lib/nfc/protocols/mifare_desfire.c @@ -42,6 +42,30 @@ void mf_df_clear(MifareDesfireData* data) { data->app_head = NULL; } +MifareDesfireApplication* mf_df_get_application(MifareDesfireData* data, const uint8_t (*aid)[3]) { + if(!data) { + return NULL; + } + for(MifareDesfireApplication* app = data->app_head; app; app = app->next) { + if(memcmp(aid, app->id, 3) == 0) { + return app; + } + } + return NULL; +} + +MifareDesfireFile* mf_df_get_file(MifareDesfireApplication* app, uint8_t id) { + if(!app) { + return NULL; + } + for(MifareDesfireFile* file = app->file_head; file; file = file->next) { + if(file->id == id) { + return file; + } + } + return NULL; +} + void mf_df_cat_data(MifareDesfireData* data, FuriString* out) { mf_df_cat_card_info(data, out); for(MifareDesfireApplication* app = data->app_head; app; app = app->next) { diff --git a/lib/nfc/protocols/mifare_desfire.h b/lib/nfc/protocols/mifare_desfire.h index 963a18f585b..3dc7c4c2a7c 100644 --- a/lib/nfc/protocols/mifare_desfire.h +++ b/lib/nfc/protocols/mifare_desfire.h @@ -130,6 +130,9 @@ void mf_df_cat_file(MifareDesfireFile* file, FuriString* out); bool mf_df_check_card_type(uint8_t ATQA0, uint8_t ATQA1, uint8_t SAK); +MifareDesfireApplication* mf_df_get_application(MifareDesfireData* data, const uint8_t (*aid)[3]); +MifareDesfireFile* mf_df_get_file(MifareDesfireApplication* app, uint8_t id); + uint16_t mf_df_prepare_get_version(uint8_t* dest); bool mf_df_parse_get_version_response(uint8_t* buf, uint16_t len, MifareDesfireVersion* out); From a7e00a1801949c808635a959ce39642e29c186cd Mon Sep 17 00:00:00 2001 From: Michael Farrell Date: Mon, 29 May 2023 11:59:40 +1000 Subject: [PATCH 2/5] stub_parser_verify_read: used UNUSED macro --- lib/nfc/parsers/nfc_supported_card.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/nfc/parsers/nfc_supported_card.c b/lib/nfc/parsers/nfc_supported_card.c index f389dfad37a..153d4d3c511 100644 --- a/lib/nfc/parsers/nfc_supported_card.c +++ b/lib/nfc/parsers/nfc_supported_card.c @@ -75,8 +75,8 @@ bool nfc_supported_card_verify_and_parse(NfcDeviceData* dev_data) { return card_parsed; } -bool stub_parser_verify_read( - NfcWorker* nfc_worker __attribute__((unused)), - FuriHalNfcTxRxContext* tx_rx __attribute__((unused))) { +bool stub_parser_verify_read(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx) { + UNUSED(nfc_worker); + UNUSED(tx_rx); return false; } From 47c3097697940f485488501434d93026dfe2d111 Mon Sep 17 00:00:00 2001 From: Michael Farrell Date: Mon, 29 May 2023 12:41:17 +1000 Subject: [PATCH 3/5] furi_hal_rtc: expose calendaring as functions --- firmware/targets/f7/api_symbols.csv | 6 ++-- firmware/targets/f7/furi_hal/furi_hal_rtc.c | 28 ++++++++++++--- .../targets/furi_hal_include/furi_hal_rtc.h | 35 +++++++++++++------ lib/nfc/parsers/opal.c | 9 +++-- 4 files changed, 55 insertions(+), 23 deletions(-) diff --git a/firmware/targets/f7/api_symbols.csv b/firmware/targets/f7/api_symbols.csv index fae2f10c39b..1055cb2bfd4 100644 --- a/firmware/targets/f7/api_symbols.csv +++ b/firmware/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,27.0,, +Version,+,27.1,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/cli/cli.h,, Header,+,applications/services/cli/cli_vcp.h,, @@ -1313,6 +1313,8 @@ Function,+,furi_hal_rtc_datetime_to_timestamp,uint32_t,FuriHalRtcDateTime* Function,-,furi_hal_rtc_deinit_early,void, Function,+,furi_hal_rtc_get_boot_mode,FuriHalRtcBootMode, Function,+,furi_hal_rtc_get_datetime,void,FuriHalRtcDateTime* +Function,+,furi_hal_rtc_get_days_per_month,uint8_t,"_Bool, uint8_t" +Function,+,furi_hal_rtc_get_days_per_year,uint16_t,uint16_t Function,+,furi_hal_rtc_get_fault_data,uint32_t, Function,+,furi_hal_rtc_get_heap_track_mode,FuriHalRtcHeapTrackMode, Function,+,furi_hal_rtc_get_locale_dateformat,FuriHalRtcLocaleDateFormat, @@ -1325,6 +1327,7 @@ Function,+,furi_hal_rtc_get_timestamp,uint32_t, Function,-,furi_hal_rtc_init,void, Function,-,furi_hal_rtc_init_early,void, Function,+,furi_hal_rtc_is_flag_set,_Bool,FuriHalRtcFlag +Function,+,furi_hal_rtc_is_leap_year,_Bool,uint16_t Function,+,furi_hal_rtc_reset_flag,void,FuriHalRtcFlag Function,+,furi_hal_rtc_set_boot_mode,void,FuriHalRtcBootMode Function,+,furi_hal_rtc_set_datetime,void,FuriHalRtcDateTime* @@ -3085,7 +3088,6 @@ Variable,+,furi_hal_i2c_bus_external,FuriHalI2cBus, Variable,+,furi_hal_i2c_bus_power,FuriHalI2cBus, Variable,+,furi_hal_i2c_handle_external,FuriHalI2cBusHandle, Variable,+,furi_hal_i2c_handle_power,FuriHalI2cBusHandle, -Variable,+,furi_hal_rtc_days_per_month,const uint8_t[2][12], Variable,+,furi_hal_sd_spi_handle,FuriHalSpiBusHandle*, Variable,+,furi_hal_spi_bus_d,FuriHalSpiBus, Variable,+,furi_hal_spi_bus_handle_display,FuriHalSpiBusHandle, diff --git a/firmware/targets/f7/furi_hal/furi_hal_rtc.c b/firmware/targets/f7/furi_hal/furi_hal_rtc.c index abce7de920a..8dfe1a13ec7 100644 --- a/firmware/targets/f7/furi_hal/furi_hal_rtc.c +++ b/firmware/targets/f7/furi_hal/furi_hal_rtc.c @@ -39,7 +39,13 @@ typedef struct { _Static_assert(sizeof(SystemReg) == 4, "SystemReg size mismatch"); -const uint8_t furi_hal_rtc_days_per_month[2][FURI_HAL_RTC_MONTHS_COUNT] = { +#define FURI_HAL_RTC_SECONDS_PER_MINUTE 60 +#define FURI_HAL_RTC_SECONDS_PER_HOUR (FURI_HAL_RTC_SECONDS_PER_MINUTE * 60) +#define FURI_HAL_RTC_SECONDS_PER_DAY (FURI_HAL_RTC_SECONDS_PER_HOUR * 24) +#define FURI_HAL_RTC_MONTHS_COUNT 12 +#define FURI_HAL_RTC_EPOCH_START_YEAR 1970 + +static const uint8_t furi_hal_rtc_days_per_month[2][FURI_HAL_RTC_MONTHS_COUNT] = { {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}}; @@ -387,7 +393,7 @@ uint32_t furi_hal_rtc_datetime_to_timestamp(FuriHalRtcDateTime* datetime) { uint8_t leap_years = 0; for(uint16_t y = FURI_HAL_RTC_EPOCH_START_YEAR; y < datetime->year; y++) { - if(FURI_HAL_RTC_IS_LEAP_YEAR(y)) { + if(furi_hal_rtc_is_leap_year(y)) { leap_years++; } else { years++; @@ -398,10 +404,10 @@ uint32_t furi_hal_rtc_datetime_to_timestamp(FuriHalRtcDateTime* datetime) { ((years * furi_hal_rtc_days_per_year[0]) + (leap_years * furi_hal_rtc_days_per_year[1])) * FURI_HAL_RTC_SECONDS_PER_DAY; - uint8_t year_index = (FURI_HAL_RTC_IS_LEAP_YEAR(datetime->year)) ? 1 : 0; + bool leap_year = furi_hal_rtc_is_leap_year(datetime->year); - for(uint8_t m = 0; m < (datetime->month - 1); m++) { - timestamp += furi_hal_rtc_days_per_month[year_index][m] * FURI_HAL_RTC_SECONDS_PER_DAY; + for(uint8_t m = 1; m < datetime->month; m++) { + timestamp += furi_hal_rtc_get_days_per_month(leap_year, m) * FURI_HAL_RTC_SECONDS_PER_DAY; } timestamp += (datetime->day - 1) * FURI_HAL_RTC_SECONDS_PER_DAY; @@ -411,3 +417,15 @@ uint32_t furi_hal_rtc_datetime_to_timestamp(FuriHalRtcDateTime* datetime) { return timestamp; } + +uint16_t furi_hal_rtc_get_days_per_year(uint16_t year) { + return furi_hal_rtc_days_per_year[furi_hal_rtc_is_leap_year(year) ? 1 : 0]; +} + +bool furi_hal_rtc_is_leap_year(uint16_t year) { + return (((year) % 4 == 0) && ((year) % 100 != 0)) || ((year) % 400 == 0); +} + +uint8_t furi_hal_rtc_get_days_per_month(bool leap_year, uint8_t month) { + return furi_hal_rtc_days_per_month[leap_year ? 1 : 0][month - 1]; +} diff --git a/firmware/targets/furi_hal_include/furi_hal_rtc.h b/firmware/targets/furi_hal_include/furi_hal_rtc.h index ed7aee228dc..186d22f0798 100644 --- a/firmware/targets/furi_hal_include/furi_hal_rtc.h +++ b/firmware/targets/furi_hal_include/furi_hal_rtc.h @@ -12,17 +12,6 @@ extern "C" { #endif -#define FURI_HAL_RTC_SECONDS_PER_MINUTE 60 -#define FURI_HAL_RTC_SECONDS_PER_HOUR (FURI_HAL_RTC_SECONDS_PER_MINUTE * 60) -#define FURI_HAL_RTC_SECONDS_PER_DAY (FURI_HAL_RTC_SECONDS_PER_HOUR * 24) -#define FURI_HAL_RTC_EPOCH_START_YEAR 1970 -#define FURI_HAL_RTC_IS_LEAP_YEAR(year) \ - ((((year) % 4 == 0) && ((year) % 100 != 0)) || ((year) % 400 == 0)) - -#define FURI_HAL_RTC_MONTHS_COUNT 12 - -extern const uint8_t furi_hal_rtc_days_per_month[2][FURI_HAL_RTC_MONTHS_COUNT]; - typedef struct { // Time uint8_t hour; /**< Hour in 24H format: 0-23 */ @@ -266,6 +255,30 @@ uint32_t furi_hal_rtc_get_timestamp(); */ uint32_t furi_hal_rtc_datetime_to_timestamp(FuriHalRtcDateTime* datetime); +/** Gets the number of days in the year according to the Gregorian calendar. + * + * @param year Input year. + * + * @return number of days in `year`. + */ +uint16_t furi_hal_rtc_get_days_per_year(uint16_t year); + +/** Check if a year a leap year in the Gregorian calendar. + * + * @param year Input year. + * + * @return true if `year` is a leap year. + */ +bool furi_hal_rtc_is_leap_year(uint16_t year); + +/** Get the number of days in the month. + * + * @param leap_year true to calculate based on leap years + * @param month month to check, where 1 = January + * @return the number of days in the month + */ +uint8_t furi_hal_rtc_get_days_per_month(bool leap_year, uint8_t month); + #ifdef __cplusplus } #endif diff --git a/lib/nfc/parsers/opal.c b/lib/nfc/parsers/opal.c index 05b67fd7ec9..5a54dc82001 100644 --- a/lib/nfc/parsers/opal.c +++ b/lib/nfc/parsers/opal.c @@ -64,7 +64,7 @@ void opal_date_time_to_furi(uint16_t days, uint16_t minutes, FuriHalRtcDateTime* if(!out) return; uint16_t diy; out->year = 1980; - out->month = 0; + out->month = 1; // 1980-01-01 is a Tuesday out->weekday = ((days + 1) % 7) + 1; out->hour = minutes / 60; @@ -73,7 +73,7 @@ void opal_date_time_to_furi(uint16_t days, uint16_t minutes, FuriHalRtcDateTime* // What year is it? for(;;) { - diy = (FURI_HAL_RTC_IS_LEAP_YEAR(out->year) ? 366 : 365); + diy = furi_hal_rtc_get_days_per_year(out->year); if(days < diy) break; days -= diy; out->year++; @@ -82,16 +82,15 @@ void opal_date_time_to_furi(uint16_t days, uint16_t minutes, FuriHalRtcDateTime* // 1-index the day of the year days++; // What month is it? - uint8_t is_leap = diy - 365; + bool is_leap = furi_hal_rtc_is_leap_year(out->year); for(;;) { - uint8_t dim = furi_hal_rtc_days_per_month[is_leap][out->month]; + uint8_t dim = furi_hal_rtc_get_days_per_month(is_leap, out->month); if(days <= dim) break; days -= dim; out->month++; } - out->month++; out->day = days; } From 6aa1e61eb6d6ed5bced6a1aad2ffb55fdb0e98f3 Mon Sep 17 00:00:00 2001 From: Michael Farrell Date: Mon, 29 May 2023 13:54:40 +1000 Subject: [PATCH 4/5] opal: use bit-packed struct to parse, rather than manually shifting about --- lib/nfc/parsers/opal.c | 75 ++++++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 32 deletions(-) diff --git a/lib/nfc/parsers/opal.c b/lib/nfc/parsers/opal.c index 5a54dc82001..b5ca37eb61d 100644 --- a/lib/nfc/parsers/opal.c +++ b/lib/nfc/parsers/opal.c @@ -56,6 +56,24 @@ static const char* opal_usages[14] = { "Unknown usage", }; +// Opal file 0x7 structure. Assumes a little-endian CPU. +typedef struct __attribute__((__packed__)) { + uint32_t serial : 32; + uint8_t check_digit : 4; + bool blocked : 1; + uint16_t txn_number : 16; + int32_t balance : 21; + uint16_t days : 15; + uint16_t minutes : 11; + uint8_t mode : 3; + uint16_t usage : 4; + bool auto_topup : 1; + uint8_t weekly_journeys : 4; + uint16_t checksum : 16; +} OpalFile; + +static_assert(sizeof(OpalFile) == 16); + // Converts an Opal timestamp to FuriHalRtcDateTime. // // Opal measures days since 1980-01-01 and minutes since midnight, and presumes @@ -109,55 +127,45 @@ bool opal_parser_parse(NfcDeviceData* dev_data) { return false; } - uint32_t serial = *(uint32_t*)(f->contents); // bit 96..127 - uint8_t serial2 = serial / 10000000; - uint16_t serial3 = (serial / 1000) % 10000; - uint16_t serial4 = (serial % 1000); + OpalFile* o = (OpalFile*)f->contents; + + uint8_t serial2 = o->serial / 10000000; + uint16_t serial3 = (o->serial / 1000) % 10000; + uint16_t serial4 = (o->serial % 1000); - uint8_t check_digit = f->contents[4] & 0x0f; // bit 92..95 - if(check_digit > 9) { + if(o->check_digit > 9) { return false; } - bool blocked = (f->contents[4] >> 4) & 0x1; // bit 91 - uint16_t txn_number = (*(uint32_t*)(f->contents + 4)) >> 5; // bit 75..90 - int32_t balance = ((*(uint32_t*)(f->contents + 6)) >> 5) & 0x1fffff; // bit 54..74 char* sign = ""; - if((balance & 0x100000) > 0) { + if(o->balance < 0) { // Negative balance. Make this a positive value again and record the // sign separately, because then we can handle balances of -99..-1 // cents, as the "dollars" division below would result in a positive // zero value. - balance = abs((int32_t)(balance | 0xfff00000)); + o->balance = abs(o->balance); sign = "-"; } - uint8_t cents = balance % 100; - int32_t dollars = balance / 100; + uint8_t cents = o->balance % 100; + int32_t dollars = o->balance / 100; - uint16_t days = ((*(uint32_t*)(f->contents + 9)) >> 2) & 0x7fff; // bit 39..53 - uint16_t minutes = ((*(uint32_t*)(f->contents + 11)) >> 1) & 0x7ff; // bit 28..38 FuriHalRtcDateTime timestamp; - opal_date_time_to_furi(days, minutes, ×tamp); + opal_date_time_to_furi(o->days, o->minutes, ×tamp); - uint8_t mode = ((*(uint16_t*)(f->contents + 12)) >> 4) & 0x7; // bit 25..27 - if(mode >= 3) { + if(o->mode >= 3) { // 3..7 are "reserved", but we use 4 to indicate the Manly Ferry. - mode = 3; + o->mode = 3; } - uint8_t usage = ((*(uint16_t*)(f->contents + 12)) >> 7) & 0xf; // bit 21..24 - if(usage >= 4 && usage <= 6) { + if(o->usage >= 4 && o->usage <= 6) { // Usages 4..6 associated with the Manly Ferry, which correspond to // usages 1..3 for other modes. - usage -= 3; - mode = 4; + o->usage -= 3; + o->mode = 4; } - bool auto_topup = (f->contents[13] >> 3) & 0x1; // bit 20 - uint8_t weekly_journeys = f->contents[13] >> 4; // bit 16..19 - - const char* mode_str = (mode <= 4 ? opal_modes[mode] : opal_modes[3]); - const char* usage_str = (usage <= 12 ? opal_usages[usage] : opal_usages[13]); + const char* mode_str = (o->mode <= 4 ? opal_modes[o->mode] : opal_modes[3]); + const char* usage_str = (o->usage <= 12 ? opal_usages[o->usage] : opal_usages[13]); furi_string_printf( dev_data->parsed_data, @@ -168,7 +176,7 @@ bool opal_parser_parse(NfcDeviceData* dev_data) { serial2, serial3, serial4, - check_digit, + o->check_digit, mode_str, usage_str); FuriString* timestamp_str = furi_string_alloc(); @@ -181,12 +189,15 @@ bool opal_parser_parse(NfcDeviceData* dev_data) { furi_string_free(timestamp_str); furi_string_cat_printf( - dev_data->parsed_data, "\nWeekly journeys: %hhu, Txn #%hu\n", weekly_journeys, txn_number); + dev_data->parsed_data, + "\nWeekly journeys: %hhu, Txn #%hu\n", + o->weekly_journeys, + o->txn_number); - if(auto_topup) { + if(o->auto_topup) { furi_string_cat_str(dev_data->parsed_data, "Auto-topup enabled\n"); } - if(blocked) { + if(o->blocked) { furi_string_cat_str(dev_data->parsed_data, "Card blocked\n"); } return true; From 769ee211febf2c42527dd8fb4b16e274cbb08c06 Mon Sep 17 00:00:00 2001 From: Aleksandr Kutuzov Date: Mon, 29 May 2023 18:54:19 +0900 Subject: [PATCH 5/5] Update f18 api symbols --- firmware/targets/f18/api_symbols.csv | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/firmware/targets/f18/api_symbols.csv b/firmware/targets/f18/api_symbols.csv index 68248a6d2ba..08aa6913366 100644 --- a/firmware/targets/f18/api_symbols.csv +++ b/firmware/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,27.0,, +Version,+,27.1,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/cli/cli.h,, Header,+,applications/services/cli/cli_vcp.h,, @@ -1048,6 +1048,8 @@ Function,+,furi_hal_rtc_datetime_to_timestamp,uint32_t,FuriHalRtcDateTime* Function,-,furi_hal_rtc_deinit_early,void, Function,+,furi_hal_rtc_get_boot_mode,FuriHalRtcBootMode, Function,+,furi_hal_rtc_get_datetime,void,FuriHalRtcDateTime* +Function,+,furi_hal_rtc_get_days_per_month,uint8_t,"_Bool, uint8_t" +Function,+,furi_hal_rtc_get_days_per_year,uint16_t,uint16_t Function,+,furi_hal_rtc_get_fault_data,uint32_t, Function,+,furi_hal_rtc_get_heap_track_mode,FuriHalRtcHeapTrackMode, Function,+,furi_hal_rtc_get_locale_dateformat,FuriHalRtcLocaleDateFormat, @@ -1060,6 +1062,7 @@ Function,+,furi_hal_rtc_get_timestamp,uint32_t, Function,-,furi_hal_rtc_init,void, Function,-,furi_hal_rtc_init_early,void, Function,+,furi_hal_rtc_is_flag_set,_Bool,FuriHalRtcFlag +Function,+,furi_hal_rtc_is_leap_year,_Bool,uint16_t Function,+,furi_hal_rtc_reset_flag,void,FuriHalRtcFlag Function,+,furi_hal_rtc_set_boot_mode,void,FuriHalRtcBootMode Function,+,furi_hal_rtc_set_datetime,void,FuriHalRtcDateTime*