Implement support for reading Opal card (Sydney, Australia) (#2683)

* Implement support for reading Opal card (Sydney, Australia)
* stub_parser_verify_read: used UNUSED macro
* furi_hal_rtc: expose calendaring as functions
* opal: use bit-packed struct to parse, rather than manually shifting about
* Update f18 api symbols

Co-authored-by: あく <alleteam@gmail.com>
This commit is contained in:
micolous 2023-05-29 21:55:55 +10:00 committed by GitHub
parent 66961dab06
commit 363f555ed7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 357 additions and 38 deletions

View File

@ -52,6 +52,7 @@ void nfc_scene_device_info_on_enter(void* context) {
} }
} else if( } else if(
dev_data->protocol == NfcDeviceProtocolMifareClassic || dev_data->protocol == NfcDeviceProtocolMifareClassic ||
dev_data->protocol == NfcDeviceProtocolMifareDesfire ||
dev_data->protocol == NfcDeviceProtocolMifareUl) { dev_data->protocol == NfcDeviceProtocolMifareUl) {
furi_string_set(temp_str, nfc->dev->dev_data.parsed_data); furi_string_set(temp_str, nfc->dev->dev_data.parsed_data);
} }

View File

@ -20,35 +20,40 @@ void nfc_scene_mf_desfire_read_success_on_enter(void* context) {
Widget* widget = nfc->widget; Widget* widget = nfc->widget;
// Prepare string for data display // Prepare string for data display
FuriString* temp_str = furi_string_alloc_printf("\e#MIFARE DESfire\n"); FuriString* temp_str = NULL;
furi_string_cat_printf(temp_str, "UID:"); if(furi_string_size(nfc->dev->dev_data.parsed_data)) {
for(size_t i = 0; i < nfc_data->uid_len; i++) { temp_str = furi_string_alloc_set(nfc->dev->dev_data.parsed_data);
furi_string_cat_printf(temp_str, " %02X", nfc_data->uid[i]); } else {
} temp_str = furi_string_alloc_printf("\e#MIFARE DESFire\n");
furi_string_cat_printf(temp_str, "UID:");
uint32_t bytes_total = 1UL << (data->version.sw_storage >> 1); for(size_t i = 0; i < nfc_data->uid_len; i++) {
uint32_t bytes_free = data->free_memory ? data->free_memory->bytes : 0; furi_string_cat_printf(temp_str, " %02X", nfc_data->uid[i]);
furi_string_cat_printf(temp_str, "\n%lu", bytes_total); }
if(data->version.sw_storage & 1) {
furi_string_push_back(temp_str, '+'); 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, " bytes, %lu bytes free\n", bytes_free); furi_string_cat_printf(temp_str, "\n%lu", bytes_total);
if(data->version.sw_storage & 1) {
uint16_t n_apps = 0; furi_string_push_back(temp_str, '+');
uint16_t n_files = 0; }
for(MifareDesfireApplication* app = data->app_head; app; app = app->next) { furi_string_cat_printf(temp_str, " bytes, %lu bytes free\n", bytes_free);
n_apps++;
for(MifareDesfireFile* file = app->file_head; file; file = file->next) { uint16_t n_apps = 0;
n_files++; 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); notification_message_block(nfc->notifications, &sequence_set_green_255);

View File

@ -40,7 +40,7 @@ void nfc_scene_nfc_data_info_on_enter(void* context) {
furi_string_cat_printf( furi_string_cat_printf(
temp_str, "\e#%s\n", nfc_mf_classic_type(dev_data->mf_classic_data.type)); temp_str, "\e#%s\n", nfc_mf_classic_type(dev_data->mf_classic_data.type));
} else if(protocol == NfcDeviceProtocolMifareDesfire) { } 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 { } else {
furi_string_cat_printf(temp_str, "\e#Unknown ISO tag\n"); furi_string_cat_printf(temp_str, "\e#Unknown ISO tag\n");
} }

View File

@ -148,6 +148,7 @@ bool nfc_scene_saved_menu_on_event(void* context, SceneManagerEvent event) {
application_info_present = true; application_info_present = true;
} else if( } else if(
dev_data->protocol == NfcDeviceProtocolMifareClassic || dev_data->protocol == NfcDeviceProtocolMifareClassic ||
dev_data->protocol == NfcDeviceProtocolMifareDesfire ||
dev_data->protocol == NfcDeviceProtocolMifareUl) { dev_data->protocol == NfcDeviceProtocolMifareUl) {
application_info_present = nfc_supported_card_verify_and_parse(dev_data); application_info_present = nfc_supported_card_verify_and_parse(dev_data);
} }

View File

@ -1,5 +1,5 @@
entry,status,name,type,params entry,status,name,type,params
Version,+,27.0,, Version,+,27.1,,
Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt.h,,
Header,+,applications/services/cli/cli.h,, Header,+,applications/services/cli/cli.h,,
Header,+,applications/services/cli/cli_vcp.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_deinit_early,void,
Function,+,furi_hal_rtc_get_boot_mode,FuriHalRtcBootMode, Function,+,furi_hal_rtc_get_boot_mode,FuriHalRtcBootMode,
Function,+,furi_hal_rtc_get_datetime,void,FuriHalRtcDateTime* 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_fault_data,uint32_t,
Function,+,furi_hal_rtc_get_heap_track_mode,FuriHalRtcHeapTrackMode, Function,+,furi_hal_rtc_get_heap_track_mode,FuriHalRtcHeapTrackMode,
Function,+,furi_hal_rtc_get_locale_dateformat,FuriHalRtcLocaleDateFormat, 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,void,
Function,-,furi_hal_rtc_init_early,void, Function,-,furi_hal_rtc_init_early,void,
Function,+,furi_hal_rtc_is_flag_set,_Bool,FuriHalRtcFlag 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_reset_flag,void,FuriHalRtcFlag
Function,+,furi_hal_rtc_set_boot_mode,void,FuriHalRtcBootMode Function,+,furi_hal_rtc_set_boot_mode,void,FuriHalRtcBootMode
Function,+,furi_hal_rtc_set_datetime,void,FuriHalRtcDateTime* Function,+,furi_hal_rtc_set_datetime,void,FuriHalRtcDateTime*

1 entry status name type params
2 Version + 27.0 27.1
3 Header + applications/services/bt/bt_service/bt.h
4 Header + applications/services/cli/cli.h
5 Header + applications/services/cli/cli_vcp.h
1048 Function - furi_hal_rtc_deinit_early void
1049 Function + furi_hal_rtc_get_boot_mode FuriHalRtcBootMode
1050 Function + furi_hal_rtc_get_datetime void FuriHalRtcDateTime*
1051 Function + furi_hal_rtc_get_days_per_month uint8_t _Bool, uint8_t
1052 Function + furi_hal_rtc_get_days_per_year uint16_t uint16_t
1053 Function + furi_hal_rtc_get_fault_data uint32_t
1054 Function + furi_hal_rtc_get_heap_track_mode FuriHalRtcHeapTrackMode
1055 Function + furi_hal_rtc_get_locale_dateformat FuriHalRtcLocaleDateFormat
1062 Function - furi_hal_rtc_init void
1063 Function - furi_hal_rtc_init_early void
1064 Function + furi_hal_rtc_is_flag_set _Bool FuriHalRtcFlag
1065 Function + furi_hal_rtc_is_leap_year _Bool uint16_t
1066 Function + furi_hal_rtc_reset_flag void FuriHalRtcFlag
1067 Function + furi_hal_rtc_set_boot_mode void FuriHalRtcBootMode
1068 Function + furi_hal_rtc_set_datetime void FuriHalRtcDateTime*

View File

@ -1,5 +1,5 @@
entry,status,name,type,params entry,status,name,type,params
Version,+,27.0,, Version,+,27.1,,
Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt.h,,
Header,+,applications/services/cli/cli.h,, Header,+,applications/services/cli/cli.h,,
Header,+,applications/services/cli/cli_vcp.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_deinit_early,void,
Function,+,furi_hal_rtc_get_boot_mode,FuriHalRtcBootMode, Function,+,furi_hal_rtc_get_boot_mode,FuriHalRtcBootMode,
Function,+,furi_hal_rtc_get_datetime,void,FuriHalRtcDateTime* 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_fault_data,uint32_t,
Function,+,furi_hal_rtc_get_heap_track_mode,FuriHalRtcHeapTrackMode, Function,+,furi_hal_rtc_get_heap_track_mode,FuriHalRtcHeapTrackMode,
Function,+,furi_hal_rtc_get_locale_dateformat,FuriHalRtcLocaleDateFormat, 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,void,
Function,-,furi_hal_rtc_init_early,void, Function,-,furi_hal_rtc_init_early,void,
Function,+,furi_hal_rtc_is_flag_set,_Bool,FuriHalRtcFlag 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_reset_flag,void,FuriHalRtcFlag
Function,+,furi_hal_rtc_set_boot_mode,void,FuriHalRtcBootMode Function,+,furi_hal_rtc_set_boot_mode,void,FuriHalRtcBootMode
Function,+,furi_hal_rtc_set_datetime,void,FuriHalRtcDateTime* Function,+,furi_hal_rtc_set_datetime,void,FuriHalRtcDateTime*
@ -1991,6 +1994,8 @@ Function,-,mf_df_cat_key_settings,void,"MifareDesfireKeySettings*, FuriString*"
Function,-,mf_df_cat_version,void,"MifareDesfireVersion*, 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_check_card_type,_Bool,"uint8_t, uint8_t, uint8_t"
Function,-,mf_df_clear,void,MifareDesfireData* 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_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_ids_response,_Bool,"uint8_t*, uint16_t, MifareDesfireFile**"
Function,-,mf_df_parse_get_file_settings_response,_Bool,"uint8_t*, uint16_t, MifareDesfireFile*" Function,-,mf_df_parse_get_file_settings_response,_Bool,"uint8_t*, uint16_t, MifareDesfireFile*"

1 entry status name type params
2 Version + 27.0 27.1
3 Header + applications/services/bt/bt_service/bt.h
4 Header + applications/services/cli/cli.h
5 Header + applications/services/cli/cli_vcp.h
1313 Function - furi_hal_rtc_deinit_early void
1314 Function + furi_hal_rtc_get_boot_mode FuriHalRtcBootMode
1315 Function + furi_hal_rtc_get_datetime void FuriHalRtcDateTime*
1316 Function + furi_hal_rtc_get_days_per_month uint8_t _Bool, uint8_t
1317 Function + furi_hal_rtc_get_days_per_year uint16_t uint16_t
1318 Function + furi_hal_rtc_get_fault_data uint32_t
1319 Function + furi_hal_rtc_get_heap_track_mode FuriHalRtcHeapTrackMode
1320 Function + furi_hal_rtc_get_locale_dateformat FuriHalRtcLocaleDateFormat
1327 Function - furi_hal_rtc_init void
1328 Function - furi_hal_rtc_init_early void
1329 Function + furi_hal_rtc_is_flag_set _Bool FuriHalRtcFlag
1330 Function + furi_hal_rtc_is_leap_year _Bool uint16_t
1331 Function + furi_hal_rtc_reset_flag void FuriHalRtcFlag
1332 Function + furi_hal_rtc_set_boot_mode void FuriHalRtcBootMode
1333 Function + furi_hal_rtc_set_datetime void FuriHalRtcDateTime*
1994 Function - mf_df_cat_version void MifareDesfireVersion*, FuriString*
1995 Function - mf_df_check_card_type _Bool uint8_t, uint8_t, uint8_t
1996 Function - mf_df_clear void MifareDesfireData*
1997 Function - mf_df_get_application MifareDesfireApplication* MifareDesfireData*, const uint8_t[3]*
1998 Function - mf_df_get_file MifareDesfireFile* MifareDesfireApplication*, uint8_t
1999 Function - mf_df_parse_get_application_ids_response _Bool uint8_t*, uint16_t, MifareDesfireApplication**
2000 Function - mf_df_parse_get_file_ids_response _Bool uint8_t*, uint16_t, MifareDesfireFile**
2001 Function - mf_df_parse_get_file_settings_response _Bool uint8_t*, uint16_t, MifareDesfireFile*

View File

@ -44,10 +44,8 @@ _Static_assert(sizeof(SystemReg) == 4, "SystemReg size mismatch");
#define FURI_HAL_RTC_SECONDS_PER_DAY (FURI_HAL_RTC_SECONDS_PER_HOUR * 24) #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_MONTHS_COUNT 12
#define FURI_HAL_RTC_EPOCH_START_YEAR 1970 #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] = { 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, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
{31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}}; {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}};
@ -395,7 +393,7 @@ uint32_t furi_hal_rtc_datetime_to_timestamp(FuriHalRtcDateTime* datetime) {
uint8_t leap_years = 0; uint8_t leap_years = 0;
for(uint16_t y = FURI_HAL_RTC_EPOCH_START_YEAR; y < datetime->year; y++) { 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++; leap_years++;
} else { } else {
years++; years++;
@ -406,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])) * ((years * furi_hal_rtc_days_per_year[0]) + (leap_years * furi_hal_rtc_days_per_year[1])) *
FURI_HAL_RTC_SECONDS_PER_DAY; 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++) { for(uint8_t m = 1; m < datetime->month; m++) {
timestamp += furi_hal_rtc_days_per_month[year_index][m] * FURI_HAL_RTC_SECONDS_PER_DAY; 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; timestamp += (datetime->day - 1) * FURI_HAL_RTC_SECONDS_PER_DAY;
@ -419,3 +417,15 @@ uint32_t furi_hal_rtc_datetime_to_timestamp(FuriHalRtcDateTime* datetime) {
return timestamp; 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];
}

View File

@ -255,6 +255,30 @@ uint32_t furi_hal_rtc_get_timestamp();
*/ */
uint32_t furi_hal_rtc_datetime_to_timestamp(FuriHalRtcDateTime* datetime); 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 #ifdef __cplusplus
} }
#endif #endif

View File

@ -219,6 +219,19 @@ static bool nfc_worker_read_mf_desfire(NfcWorker* nfc_worker, FuriHalNfcTxRxCont
do { do {
if(!furi_hal_nfc_detect(&nfc_worker->dev_data->nfc_data, 300)) break; if(!furi_hal_nfc_detect(&nfc_worker->dev_data->nfc_data, 300)) break;
if(!mf_df_read_card(tx_rx, data)) 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; read_success = true;
} while(false); } while(false);

View File

@ -6,6 +6,7 @@
#include "troika_4k_parser.h" #include "troika_4k_parser.h"
#include "two_cities.h" #include "two_cities.h"
#include "all_in_one.h" #include "all_in_one.h"
#include "opal.h"
NfcSupportedCard nfc_supported_card[NfcSupportedCardTypeEnd] = { NfcSupportedCard nfc_supported_card[NfcSupportedCardTypeEnd] = {
[NfcSupportedCardTypePlantain] = [NfcSupportedCardTypePlantain] =
@ -50,6 +51,14 @@ NfcSupportedCard nfc_supported_card[NfcSupportedCardTypeEnd] = {
.read = all_in_one_parser_read, .read = all_in_one_parser_read,
.parse = all_in_one_parser_parse, .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) { 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; return card_parsed;
} }
bool stub_parser_verify_read(NfcWorker* nfc_worker, FuriHalNfcTxRxContext* tx_rx) {
UNUSED(nfc_worker);
UNUSED(tx_rx);
return false;
}

View File

@ -11,6 +11,7 @@ typedef enum {
NfcSupportedCardTypeTroika4K, NfcSupportedCardTypeTroika4K,
NfcSupportedCardTypeTwoCities, NfcSupportedCardTypeTwoCities,
NfcSupportedCardTypeAllInOne, NfcSupportedCardTypeAllInOne,
NfcSupportedCardTypeOpal,
NfcSupportedCardTypeEnd, NfcSupportedCardTypeEnd,
} NfcSupportedCardType; } NfcSupportedCardType;
@ -31,3 +32,8 @@ typedef struct {
extern NfcSupportedCard nfc_supported_card[NfcSupportedCardTypeEnd]; extern NfcSupportedCard nfc_supported_card[NfcSupportedCardTypeEnd];
bool nfc_supported_card_verify_and_parse(NfcDeviceData* dev_data); 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);

204
lib/nfc/parsers/opal.c Normal file
View File

@ -0,0 +1,204 @@
/*
* opal.c - Parser for Opal card (Sydney, Australia).
*
* Copyright 2023 Michael Farrell <micolous+git@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "nfc_supported_card.h"
#include "opal.h"
#include <applications/services/locale/locale.h>
#include <gui/modules/widget.h>
#include <nfc_worker_i.h>
#include <furi_hal.h>
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",
};
// 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
// 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 = 1;
// 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_get_days_per_year(out->year);
if(days < diy) break;
days -= diy;
out->year++;
}
// 1-index the day of the year
days++;
// What month is it?
bool is_leap = furi_hal_rtc_is_leap_year(out->year);
for(;;) {
uint8_t dim = furi_hal_rtc_get_days_per_month(is_leap, out->month);
if(days <= dim) break;
days -= dim;
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;
}
OpalFile* o = (OpalFile*)f->contents;
uint8_t serial2 = o->serial / 10000000;
uint16_t serial3 = (o->serial / 1000) % 10000;
uint16_t serial4 = (o->serial % 1000);
if(o->check_digit > 9) {
return false;
}
char* sign = "";
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.
o->balance = abs(o->balance);
sign = "-";
}
uint8_t cents = o->balance % 100;
int32_t dollars = o->balance / 100;
FuriHalRtcDateTime timestamp;
opal_date_time_to_furi(o->days, o->minutes, &timestamp);
if(o->mode >= 3) {
// 3..7 are "reserved", but we use 4 to indicate the Manly Ferry.
o->mode = 3;
}
if(o->usage >= 4 && o->usage <= 6) {
// Usages 4..6 associated with the Manly Ferry, which correspond to
// usages 1..3 for other modes.
o->usage -= 3;
o->mode = 4;
}
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,
"\e#Opal: $%s%ld.%02hu\n3085 22%02hhu %04hu %03hu%01hhu\n%s, %s\n",
sign,
dollars,
cents,
serial2,
serial3,
serial4,
o->check_digit,
mode_str,
usage_str);
FuriString* timestamp_str = furi_string_alloc();
locale_format_date(timestamp_str, &timestamp, 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, &timestamp, 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",
o->weekly_journeys,
o->txn_number);
if(o->auto_topup) {
furi_string_cat_str(dev_data->parsed_data, "Auto-topup enabled\n");
}
if(o->blocked) {
furi_string_cat_str(dev_data->parsed_data, "Card blocked\n");
}
return true;
}

5
lib/nfc/parsers/opal.h Normal file
View File

@ -0,0 +1,5 @@
#pragma once
#include "nfc_supported_card.h"
bool opal_parser_parse(NfcDeviceData* dev_data);

View File

@ -42,6 +42,30 @@ void mf_df_clear(MifareDesfireData* data) {
data->app_head = NULL; 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) { void mf_df_cat_data(MifareDesfireData* data, FuriString* out) {
mf_df_cat_card_info(data, out); mf_df_cat_card_info(data, out);
for(MifareDesfireApplication* app = data->app_head; app; app = app->next) { for(MifareDesfireApplication* app = data->app_head; app; app = app->next) {

View File

@ -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); 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); 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); bool mf_df_parse_get_version_response(uint8_t* buf, uint16_t len, MifareDesfireVersion* out);