#include #include "../metroflip_i.h" #include "desfire.h" #include #include static const MfDesfireApplicationId opal_verify_app_id = {.data = {0x31, 0x45, 0x53}}; static const MfDesfireFileId opal_verify_file_id = 0x07; static const MfDesfireApplicationId myki_verify_app_id = {.data = {0x00, 0x11, 0xf2}}; static const MfDesfireFileId myki_verify_file_id = 0x0f; static const MfDesfireApplicationId itso_verify_app_id = {.data = {0x16, 0x02, 0xa0}}; static const MfDesfireFileId itso_verify_file_id = 0x0f; uint64_t itso_swap_uint64(uint64_t val) { val = ((val << 8) & 0xFF00FF00FF00FF00ULL) | ((val >> 8) & 0x00FF00FF00FF00FFULL); val = ((val << 16) & 0xFFFF0000FFFF0000ULL) | ((val >> 16) & 0x0000FFFF0000FFFFULL); return (val << 32) | (val >> 32); } static const struct { const MfDesfireApplicationId app; const char* type; } clipper_verify_types[] = { // Application advertised on classic, plastic cards. {.app = {.data = {0x90, 0x11, 0xf2}}, .type = "Card"}, // Application advertised on a mobile device. {.app = {.data = {0x91, 0x11, 0xf2}}, .type = "Mobile Device"}, }; static const size_t kNumCardVerifyTypes = sizeof(clipper_verify_types) / sizeof(clipper_verify_types[0]); // File ids of important files on the card. static const MfDesfireFileId clipper_ecash_file_id = 2; static const MfDesfireFileId clipper_histidx_file_id = 6; static const MfDesfireFileId clipper_identity_file_id = 8; static const MfDesfireFileId clipper_history_file_id = 14; static bool get_file_contents( const MfDesfireApplication* app, const MfDesfireFileId* id, MfDesfireFileType type, size_t min_size, const uint8_t** out) { const MfDesfireFileSettings* settings = mf_desfire_get_file_settings(app, id); if(settings == NULL) return false; if(settings->type != type) return false; const MfDesfireFileData* file_data = mf_desfire_get_file_data(app, id); if(file_data == NULL) return false; if(simple_array_get_count(file_data->data) < min_size) return false; *out = simple_array_cget_data(file_data->data); return true; } struct ClipperVerifyCardInfo_struct { uint32_t serial_number; uint16_t counter; uint16_t last_txn_id; uint32_t last_updated_tm_1900; uint16_t last_terminal_id; int16_t balance_cents; }; typedef struct ClipperVerifyCardInfo_struct ClipperVerifyCardInfo; // Opal file 0x7 structure. Assumes a little-endian CPU. typedef struct FURI_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; } OpalVerifyFile; static_assert(sizeof(OpalVerifyFile) == 16, "OpalFile"); bool opal_verify(const MfDesfireData* data) { // Check if the card has the expected application const MfDesfireApplication* app = mf_desfire_get_application(data, &opal_verify_app_id); if(app == NULL) { return false; } // Verify the file settings: must be of type standard and have the expected size const MfDesfireFileSettings* file_settings = mf_desfire_get_file_settings(app, &opal_verify_file_id); if(file_settings == NULL || file_settings->type != MfDesfireFileTypeStandard || file_settings->data.size != sizeof(OpalVerifyFile)) { return false; } // Check that the file data exists const MfDesfireFileData* file_data = mf_desfire_get_file_data(app, &opal_verify_file_id); if(file_data == NULL) { return false; } // Retrieve the opal file from the file data const OpalVerifyFile* opal_file = simple_array_cget_data(file_data->data); if(opal_file == NULL) { return false; } // Ensure the check digit is valid (i.e. 0..9) if(opal_file->check_digit > 9) { return false; } // All checks passed, return true return true; } bool myki_verify(const MfDesfireData* data) { // Check if the card contains the expected Myki application. const MfDesfireApplication* app = mf_desfire_get_application(data, &myki_verify_app_id); if(app == NULL) { return false; } // Define the structure for Myki file data. typedef struct { uint32_t top; uint32_t bottom; } mykiFile; // Verify file settings: must be present, of the correct type, and large enough to contain a mykiFile. const MfDesfireFileSettings* file_settings = mf_desfire_get_file_settings(app, &myki_verify_file_id); if(file_settings == NULL || file_settings->type != MfDesfireFileTypeStandard || file_settings->data.size < sizeof(mykiFile)) { return false; } // Verify that the file data is available. const MfDesfireFileData* file_data = mf_desfire_get_file_data(app, &myki_verify_file_id); if(file_data == NULL) { return false; } // Retrieve the Myki file data from the file data array. const mykiFile* myki_file = simple_array_cget_data(file_data->data); if(myki_file == NULL) { return false; } // Check that Myki card numbers are prefixed with "308425". if(myki_file->top != 308425UL) { return false; } // Card numbers are always 15 digits in length. // The bottom field must be within [10000000, 100000000) to meet this requirement. if(myki_file->bottom < 10000000UL || myki_file->bottom >= 100000000UL) { return false; } // All checks passed. return true; } bool itso_verify(const MfDesfireData* data) { // Check if the card contains the expected ITSO application. const MfDesfireApplication* app = mf_desfire_get_application(data, &itso_verify_app_id); if(app == NULL) { return false; } // Define the structure for ITSO file data. typedef struct { uint64_t part1; uint64_t part2; uint64_t part3; uint64_t part4; } ItsoFile; // Verify file settings: must exist, be of standard type, // and have a data size at least as large as an ItsoFile. const MfDesfireFileSettings* file_settings = mf_desfire_get_file_settings(app, &itso_verify_file_id); if(file_settings == NULL || file_settings->type != MfDesfireFileTypeStandard || file_settings->data.size < sizeof(ItsoFile)) { return false; } // Verify that the file data is available. const MfDesfireFileData* file_data = mf_desfire_get_file_data(app, &itso_verify_file_id); if(file_data == NULL) { return false; } // Retrieve the ITSO file from the file data. const ItsoFile* itso_file = simple_array_cget_data(file_data->data); if(itso_file == NULL) { return false; } // Swap bytes for the first two parts. uint64_t x1 = itso_swap_uint64(itso_file->part1); uint64_t x2 = itso_swap_uint64(itso_file->part2); // Prepare buffers for card and date strings. char cardBuff[32]; char dateBuff[18]; // Format the hex strings. snprintf(cardBuff, sizeof(cardBuff), "%llx%llx", x1, x2); snprintf(dateBuff, sizeof(dateBuff), "%llx", x2); // Get pointer to the card number substring (skipping the first 4 characters). char* cardp = cardBuff + 4; cardp[18] = '\0'; // Ensure the substring is null-terminated. // Verify that all ITSO card numbers are prefixed with "633597". if(strncmp(cardp, "633597", 6) != 0) { return false; } // Prepare the date string by advancing 12 characters. char* datep = dateBuff + 12; dateBuff[17] = '\0'; // Ensure termination of the date string. // Convert the date portion (in hexadecimal) to a date stamp. uint32_t dateStamp; if(strint_to_uint32(datep, NULL, &dateStamp, 16) != StrintParseNoError) { return false; } // (Optional) Calculate the Unix timestamp if needed: // uint32_t unixTimestamp = dateStamp * 24 * 60 * 60 + 852076800U; // All checks passed. return true; } bool clipper_verify(const MfDesfireData* data) { bool verified = false; do { FURI_LOG_I("clipper verify", "verifying.."); const MfDesfireApplication* app = NULL; // Try each card type until a matching application is found. for(size_t i = 0; i < kNumCardVerifyTypes; i++) { app = mf_desfire_get_application(data, &clipper_verify_types[i].app); if(app != NULL) { break; } } // If no matching application was found, verification fails. if(app == NULL) { break; } const uint8_t* id_data; if(!get_file_contents( app, &clipper_identity_file_id, MfDesfireFileTypeStandard, 5, &id_data)) { break; } // Get the ecash file contents. const uint8_t* cash_data; if(!get_file_contents( app, &clipper_ecash_file_id, MfDesfireFileTypeBackup, 32, &cash_data)) { break; } // Retrieve ride history file contents. const uint8_t* history_index; const uint8_t* history; if(!get_file_contents( app, &clipper_histidx_file_id, MfDesfireFileTypeBackup, 16, &history_index)) { break; } if(!get_file_contents( app, &clipper_history_file_id, MfDesfireFileTypeStandard, 512, &history)) { break; } // Use a dummy string to verify that the ride history can be decoded. FuriString* dummy_str = furi_string_alloc(); furi_string_free(dummy_str); verified = true; } while(false); return verified; }