-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Parser for Santiago, Chile BIP transit card #3456
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 4 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
d6141fa
Parser for Santiago, Chile BIP transit card
8cb775e
Update to use bit_lib instead of nfc_util
1844117
Merge branch 'dev' into dev
gornekich 6b560c0
Merge branch 'dev' into dev
gornekich b8bf873
PR feedback
rbasoalto da09499
Merge branch 'dev' into dev
gornekich File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,372 @@ | ||
| #include "nfc_supported_card_plugin.h" | ||
|
|
||
| #include <bit_lib/bit_lib.h> | ||
| #include <flipper_application/flipper_application.h> | ||
| #include <nfc/nfc_device.h> | ||
| #include <nfc/protocols/mf_classic/mf_classic_poller_sync.h> | ||
|
|
||
| #define TAG "Bip" | ||
|
|
||
| #define SECTOR_BLOCK_OFFSET(sector, block) (((sector) * 4) + (block)) | ||
|
|
||
| static const uint64_t bip_keys_a[] = { | ||
| 0x3a42f33af429, | ||
| 0x6338a371c0ed, | ||
| 0xf124c2578ad0, | ||
| 0x32ac3b90ac13, | ||
| 0x4ad1e273eaf1, | ||
| 0xe2c42591368a, | ||
| 0x2a3c347a1200, | ||
| 0x16f3d5ab1139, | ||
| 0x937a4fff3011, | ||
| 0x35c3d2caee88, | ||
| 0x693143f10368, | ||
| 0xa3f97428dd01, | ||
| 0x63f17a449af0, | ||
| 0xc4652c54261c, | ||
| 0xd49e2826664f, | ||
| 0x3df14c8000a1, | ||
| }; | ||
|
|
||
| static const uint64_t bip_keys_b[] = { | ||
| 0x1fc235ac1309, | ||
| 0x243f160918d1, | ||
| 0x9afc42372af1, | ||
| 0x682d401abb09, | ||
| 0x067db45454a9, | ||
| 0x15fc4c7613fe, | ||
| 0x68d30288910a, | ||
| 0xf59a36a2546d, | ||
| 0x64e3c10394c2, | ||
| 0xb736412614af, | ||
| 0x324f5df65310, | ||
| 0x643fb6de2217, | ||
| 0x82f435dedf01, | ||
| 0x0263de1278f3, | ||
| 0x51284c3686a6, | ||
| 0x6a470d54127c, | ||
| }; | ||
|
|
||
| bool bip_verify(Nfc* nfc) { | ||
| bool verified = true; | ||
|
|
||
| const uint8_t verify_sector = 0; | ||
| uint8_t block_num = mf_classic_get_first_block_num_of_sector(verify_sector); | ||
| FURI_LOG_D(TAG, "Verifying sector %u", verify_sector); | ||
|
|
||
| MfClassicKey key_a_0 = {}; | ||
| bit_lib_num_to_bytes_be(bip_keys_a[0], COUNT_OF(key_a_0.data), key_a_0.data); | ||
|
|
||
| MfClassicAuthContext auth_ctx = {}; | ||
| MfClassicError error = | ||
| mf_classic_poller_sync_auth(nfc, block_num, &key_a_0, MfClassicKeyTypeA, &auth_ctx); | ||
|
|
||
| if(error == MfClassicErrorNotPresent) { | ||
| FURI_LOG_D(TAG, "Failed to read block %u: %d", block_num, error); | ||
| verified = false; | ||
| } | ||
|
|
||
| return verified; | ||
| } | ||
|
|
||
| static bool bip_read(Nfc* nfc, NfcDevice* device) { | ||
| furi_assert(nfc); | ||
| furi_assert(device); | ||
|
|
||
| bool is_read = false; | ||
|
|
||
| MfClassicData* data = mf_classic_alloc(); | ||
| nfc_device_copy_data(device, NfcProtocolMfClassic, data); | ||
|
|
||
| do { | ||
| MfClassicType type = MfClassicType1k; | ||
| MfClassicError error = mf_classic_poller_sync_detect_type(nfc, &type); | ||
| if(error == MfClassicErrorNotPresent) { | ||
| FURI_LOG_W(TAG, "Card not MIFARE Classic 1k"); | ||
| break; | ||
| } | ||
|
|
||
| data->type = type; | ||
| MfClassicDeviceKeys keys = {}; | ||
| for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) { | ||
| bit_lib_num_to_bytes_be(bip_keys_a[i], sizeof(MfClassicKey), keys.key_a[i].data); | ||
| FURI_BIT_SET(keys.key_a_mask, i); | ||
| bit_lib_num_to_bytes_be(bip_keys_b[i], sizeof(MfClassicKey), keys.key_b[i].data); | ||
| FURI_BIT_SET(keys.key_b_mask, i); | ||
| } | ||
|
|
||
| error = mf_classic_poller_sync_read(nfc, &keys, data); | ||
| if(error == MfClassicErrorNotPresent) { | ||
| FURI_LOG_W(TAG, "Failed to read data. Bad keys?"); | ||
| break; | ||
| } | ||
|
|
||
| nfc_device_set_data(device, NfcProtocolMfClassic, data); | ||
|
|
||
| is_read = true; | ||
| } while(false); | ||
|
|
||
| mf_classic_free(data); | ||
|
|
||
| return is_read; | ||
| } | ||
|
|
||
| typedef struct { | ||
| uint16_t year; | ||
| uint8_t month; | ||
| uint8_t day; | ||
| uint8_t hour; | ||
| uint8_t minute; | ||
| uint8_t second; | ||
| } BipTimestamp; | ||
|
|
||
| static void parse_bip_timestamp(const MfClassicBlock* block, BipTimestamp* timestamp) { | ||
| furi_assert(block); | ||
| furi_assert(timestamp); | ||
|
|
||
| timestamp->day = (((block->data[1] << 8) + block->data[0]) >> 6) & 0x1f; | ||
| timestamp->month = (((block->data[1] << 8) + block->data[0]) >> 11) & 0xf; | ||
| timestamp->year = 2000 + ((((block->data[2] << 8) + block->data[1]) >> 7) & 0x1f); | ||
| timestamp->hour = (((block->data[3] << 8) + block->data[2]) >> 4) & 0x1f; | ||
| timestamp->minute = (((block->data[3] << 8) + block->data[2]) >> 9) & 0x3f; | ||
| timestamp->second = (((block->data[4] << 8) + block->data[3]) >> 7) & 0x3f; | ||
| } | ||
|
|
||
| static int compare_bip_timestamp(const BipTimestamp* t1, const BipTimestamp* t2) { | ||
| furi_assert(t1); | ||
| furi_assert(t2); | ||
| if(t1->year != t2->year) { | ||
| return t1->year - t2->year; | ||
| } | ||
| if(t1->month != t2->month) { | ||
| return t1->month - t2->month; | ||
| } | ||
| if(t1->day != t2->day) { | ||
| return t1->day - t2->day; | ||
| } | ||
| if(t1->hour != t2->hour) { | ||
| return t1->hour - t2->hour; | ||
| } | ||
| if(t1->minute != t2->minute) { | ||
| return t1->minute - t2->minute; | ||
| } | ||
| if(t1->second != t2->second) { | ||
| return t1->second - t2->second; | ||
| } | ||
| return 0; | ||
| } | ||
|
|
||
| static void print_bip_timestamp(const BipTimestamp* timestamp, FuriString* str) { | ||
| furi_assert(timestamp); | ||
| furi_assert(str); | ||
| furi_string_cat_printf( | ||
| str, | ||
| "%04u-%02u-%02u %02u:%02u:%02u", | ||
| timestamp->year, | ||
| timestamp->month, | ||
| timestamp->day, | ||
| timestamp->hour, | ||
| timestamp->minute, | ||
| timestamp->second); | ||
| } | ||
|
|
||
| static bool is_bip_block_empty(const MfClassicBlock* block) { | ||
| furi_assert(block); | ||
| // check if all but last byte are zero (last is checksum) | ||
| for(size_t i = 0; i < sizeof(block->data) - 1; i++) { | ||
| if(block->data[i] != 0) { | ||
| return false; | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| static void parse_uint16_le(const uint8_t* data, uint16_t* value) { | ||
| furi_assert(data); | ||
| furi_assert(value); | ||
|
|
||
| *value = (data[0]) | (data[1] << 8); | ||
| } | ||
|
|
||
| static void parse_uint32_le(const uint8_t* data, uint32_t* value) { | ||
| furi_assert(data); | ||
| furi_assert(value); | ||
|
|
||
| *value = (data[0]) | (data[1] << 8) | (data[2] << 16) | (data[3] << 24); | ||
| } | ||
|
|
||
| static void parse_uint16_txn_amount(const uint8_t* data, uint16_t* value) { | ||
| furi_assert(data); | ||
| furi_assert(value); | ||
|
|
||
| parse_uint16_le(data, value); | ||
| *value = *value >> 2; | ||
| } | ||
|
|
||
| typedef struct { | ||
| BipTimestamp timestamp; | ||
| uint16_t amount; | ||
| } BipTransaction; | ||
|
|
||
| static bool bip_parse(const NfcDevice* device, FuriString* parsed_data) { | ||
| furi_assert(device); | ||
| furi_assert(parsed_data); | ||
|
|
||
| bool parsed = true; | ||
|
|
||
| struct { | ||
| uint32_t card_id; | ||
| uint16_t balance; | ||
| uint16_t flags; | ||
| BipTimestamp trip_time_window; | ||
| BipTransaction top_ups[3]; | ||
| BipTransaction charges[3]; | ||
| } bip_data = { | ||
| .card_id = 0, | ||
| .balance = 0, | ||
| .flags = 0, | ||
| .trip_time_window = {0, 0, 0, 0, 0, 0}, | ||
| .top_ups = | ||
| { | ||
| {{0, 0, 0, 0, 0, 0}, 0}, | ||
| {{0, 0, 0, 0, 0, 0}, 0}, | ||
| {{0, 0, 0, 0, 0, 0}, 0}, | ||
| }, | ||
| .charges = | ||
| { | ||
| {{0, 0, 0, 0, 0, 0}, 0}, | ||
| {{0, 0, 0, 0, 0, 0}, 0}, | ||
| {{0, 0, 0, 0, 0, 0}, 0}, | ||
| }, | ||
| }; | ||
|
|
||
| const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic); | ||
|
|
||
| do { | ||
| // verify all sectors' keys | ||
| for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) { | ||
| MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(data, i); | ||
| uint64_t key = bit_lib_bytes_to_num_be(sec_tr->key_a.data, 6); | ||
| if(key != bip_keys_a[i]) { | ||
| parsed = false; | ||
| break; | ||
| } | ||
| key = bit_lib_bytes_to_num_be(sec_tr->key_b.data, 6); | ||
| if(key != bip_keys_b[i]) { | ||
| parsed = false; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| // Get Card ID, little-endian 4 bytes at sector 0 block 1, bytes 4-7 | ||
| parse_uint32_le(&data->block[SECTOR_BLOCK_OFFSET(0, 1)].data[4], &bip_data.card_id); | ||
|
|
||
| // Get balance, little-endian 2 bytes at sector 8 block 1, bytes 0-1 | ||
| parse_uint16_le(&data->block[SECTOR_BLOCK_OFFSET(8, 1)].data[0], &bip_data.balance); | ||
|
|
||
| // Get balance flags (negative balance, etc.), little-endian 2 bytes at sector 8 block 1, bytes 2-3 | ||
| parse_uint16_le(&data->block[SECTOR_BLOCK_OFFSET(8, 1)].data[2], &bip_data.flags); | ||
|
|
||
| // Get trip time window, proprietary format, at sector 5 block 1, bytes 0-7 | ||
| parse_bip_timestamp(&data->block[SECTOR_BLOCK_OFFSET(5, 1)], &bip_data.trip_time_window); | ||
|
|
||
| // Last 3 top-ups: sector 10, ring-buffer of 3 blocks, timestamp in bytes 0-7, amount in bytes 9-10 | ||
| for(size_t i = 0; i < 3; i++) { | ||
| if(is_bip_block_empty(&data->block[SECTOR_BLOCK_OFFSET(10, i)])) { | ||
| continue; | ||
| } | ||
| BipTransaction* top_up = &bip_data.top_ups[i]; | ||
| parse_bip_timestamp(&data->block[SECTOR_BLOCK_OFFSET(10, i)], &top_up->timestamp); | ||
| parse_uint16_txn_amount( | ||
| &data->block[SECTOR_BLOCK_OFFSET(10, i)].data[9], &top_up->amount); | ||
| } | ||
|
|
||
| // Last 3 charges (i.e. trips), sector 11, ring-buffer of 3 blocks, timestamp in bytes 0-7, amount in bytes 10-11 | ||
| for(size_t i = 0; i < 3; i++) { | ||
| if(is_bip_block_empty(&data->block[SECTOR_BLOCK_OFFSET(11, i)])) { | ||
| continue; | ||
| } | ||
| BipTransaction* charge = &bip_data.charges[i]; | ||
| parse_bip_timestamp(&data->block[SECTOR_BLOCK_OFFSET(11, i)], &charge->timestamp); | ||
| parse_uint16_txn_amount( | ||
| &data->block[SECTOR_BLOCK_OFFSET(11, i)].data[10], &charge->amount); | ||
| } | ||
|
|
||
| parsed = true; | ||
| } while(false); | ||
|
|
||
| if(parsed) { | ||
gornekich marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // Print basic info | ||
| furi_string_printf( | ||
| parsed_data, | ||
| "\e#Tarjeta Bip!\n" | ||
| "Card Number: %ld\n" | ||
| "Balance: $%d (flags %x)\n" | ||
| "Current Trip Window Ends:\n @", | ||
| bip_data.card_id, | ||
| bip_data.balance, | ||
| bip_data.flags); | ||
|
|
||
| print_bip_timestamp(&bip_data.trip_time_window, parsed_data); | ||
|
|
||
| // Find newest top-up | ||
| size_t newest_top_up = 0; | ||
| for(size_t i = 1; i < 3; i++) { | ||
| const BipTimestamp* newest = &bip_data.top_ups[newest_top_up].timestamp; | ||
| const BipTimestamp* current = &bip_data.top_ups[i].timestamp; | ||
| if(compare_bip_timestamp(current, newest) > 0) { | ||
| newest_top_up = i; | ||
| } | ||
| } | ||
|
|
||
| // Print top-ups, newest first | ||
| furi_string_cat_printf(parsed_data, "\n\e#Last Top-ups"); | ||
| for(size_t i = 0; i < 3; i++) { | ||
| const BipTransaction* top_up = &bip_data.top_ups[(3u + newest_top_up - i) % 3]; | ||
| furi_string_cat_printf(parsed_data, "\n+$%d\n @", top_up->amount); | ||
| print_bip_timestamp(&top_up->timestamp, parsed_data); | ||
| } | ||
|
|
||
| // Find newest charge | ||
| size_t newest_charge = 0; | ||
| for(size_t i = 1; i < 3; i++) { | ||
| const BipTimestamp* newest = &bip_data.charges[newest_charge].timestamp; | ||
| const BipTimestamp* current = &bip_data.charges[i].timestamp; | ||
| if(compare_bip_timestamp(current, newest) > 0) { | ||
| newest_charge = i; | ||
| } | ||
| } | ||
|
|
||
| // Print charges | ||
| furi_string_cat_printf(parsed_data, "\n\e#Last Charges (Trips)"); | ||
| for(size_t i = 0; i < 3; i++) { | ||
| const BipTransaction* charge = &bip_data.charges[(3u + newest_charge - i) % 3]; | ||
| furi_string_cat_printf(parsed_data, "\n-$%d\n @", charge->amount); | ||
| print_bip_timestamp(&charge->timestamp, parsed_data); | ||
| } | ||
|
|
||
| return true; | ||
| } else { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /* Actual implementation of app<>plugin interface */ | ||
| static const NfcSupportedCardsPlugin bip_plugin = { | ||
| .protocol = NfcProtocolMfClassic, | ||
| .verify = bip_verify, | ||
| .read = bip_read, | ||
| .parse = bip_parse, | ||
| }; | ||
|
|
||
| /* Plugin descriptor to comply with basic plugin specification */ | ||
| static const FlipperAppPluginDescriptor bip_plugin_descriptor = { | ||
| .appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID, | ||
| .ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION, | ||
| .entry_point = &bip_plugin, | ||
| }; | ||
|
|
||
| /* Plugin entry point - must return a pointer to const descriptor */ | ||
| const FlipperAppPluginDescriptor* bip_plugin_ep() { | ||
| return &bip_plugin_descriptor; | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.