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:
		
							parent
							
								
									66961dab06
								
							
						
					
					
						commit
						363f555ed7
					
				| @ -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); | ||||
|     } | ||||
|  | ||||
| @ -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]); | ||||
|     } | ||||
| 
 | ||||
|     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++; | ||||
|     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++; | ||||
|             } | ||||
|         } | ||||
|         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); | ||||
|  | ||||
| @ -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"); | ||||
|     } | ||||
|  | ||||
| @ -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); | ||||
|             } | ||||
|  | ||||
| @ -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* | ||||
|  | ||||
| 
 | 
| @ -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* | ||||
| @ -1991,6 +1994,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*" | ||||
|  | ||||
| 
 | 
| @ -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_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] = { | ||||
| 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}}; | ||||
| 
 | ||||
| @ -395,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++; | ||||
| @ -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])) * | ||||
|         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; | ||||
| @ -419,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]; | ||||
| } | ||||
|  | ||||
| @ -255,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 | ||||
|  | ||||
| @ -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); | ||||
| 
 | ||||
|  | ||||
| @ -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, FuriHalNfcTxRxContext* tx_rx) { | ||||
|     UNUSED(nfc_worker); | ||||
|     UNUSED(tx_rx); | ||||
|     return false; | ||||
| } | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
							
								
								
									
										204
									
								
								lib/nfc/parsers/opal.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										204
									
								
								lib/nfc/parsers/opal.c
									
									
									
									
									
										Normal 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, ×tamp); | ||||
| 
 | ||||
|     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, ×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", | ||||
|         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
									
								
							
							
						
						
									
										5
									
								
								lib/nfc/parsers/opal.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| #pragma once | ||||
| 
 | ||||
| #include "nfc_supported_card.h" | ||||
| 
 | ||||
| bool opal_parser_parse(NfcDeviceData* dev_data); | ||||
| @ -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) { | ||||
|  | ||||
| @ -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); | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 micolous
						micolous