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( |     } 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); | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -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); | ||||||
|  | |||||||
| @ -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"); | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -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); | ||||||
|             } |             } | ||||||
|  | |||||||
| @ -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,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*" | ||||||
|  | |||||||
| 
 | 
| @ -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]; | ||||||
|  | } | ||||||
|  | |||||||
| @ -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 | ||||||
|  | |||||||
| @ -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); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -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; | ||||||
|  | } | ||||||
|  | |||||||
| @ -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
									
								
							
							
						
						
									
										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; |     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) { | ||||||
|  | |||||||
| @ -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); | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 micolous
						micolous