mirror of
https://github.com/flipperdevices/flipperzero-firmware.git
synced 2025-12-12 04:41:26 +04:00
NFC refactoring (#3050)
"A long time ago in a galaxy far, far away...." we started NFC subsystem refactoring. Starring: - @gornekich - NFC refactoring project lead, architect, senior developer - @gsurkov - architect, senior developer - @RebornedBrain - senior developer Supporting roles: - @skotopes, @DrZlo13, @hedger - general architecture advisors, code review - @Astrrra, @doomwastaken, @Hellitron, @ImagineVagon333 - quality assurance Special thanks: @bettse, @pcunning, @nxv, @noproto, @AloneLiberty and everyone else who has been helping us all this time and contributing valuable knowledges, ideas and source code.
This commit is contained in:
107
applications/main/nfc/plugins/supported_cards/all_in_one.c
Normal file
107
applications/main/nfc/plugins/supported_cards/all_in_one.c
Normal file
@@ -0,0 +1,107 @@
|
||||
#include "nfc_supported_card_plugin.h"
|
||||
|
||||
#include <flipper_application/flipper_application.h>
|
||||
#include <nfc/protocols/mf_ultralight/mf_ultralight.h>
|
||||
|
||||
#define TAG "AllInOne"
|
||||
|
||||
typedef enum {
|
||||
AllInOneLayoutTypeA,
|
||||
AllInOneLayoutTypeD,
|
||||
AllInOneLayoutTypeE2,
|
||||
AllInOneLayoutTypeE3,
|
||||
AllInOneLayoutTypeE5,
|
||||
AllInOneLayoutType2,
|
||||
AllInOneLayoutTypeUnknown,
|
||||
} AllInOneLayoutType;
|
||||
|
||||
static AllInOneLayoutType all_in_one_get_layout(const MfUltralightData* data) {
|
||||
// Switch on the second half of the third byte of page 5
|
||||
const uint8_t layout_byte = data->page[5].data[2];
|
||||
const uint8_t layout_half_byte = data->page[5].data[2] & 0x0F;
|
||||
|
||||
FURI_LOG_I(TAG, "Layout byte: %02x", layout_byte);
|
||||
FURI_LOG_I(TAG, "Layout half-byte: %02x", layout_half_byte);
|
||||
|
||||
switch(layout_half_byte) {
|
||||
// If it is A, the layout type is a type A layout
|
||||
case 0x0A:
|
||||
return AllInOneLayoutTypeA;
|
||||
case 0x0D:
|
||||
return AllInOneLayoutTypeD;
|
||||
case 0x02:
|
||||
return AllInOneLayoutType2;
|
||||
default:
|
||||
FURI_LOG_I(TAG, "Unknown layout type: %d", layout_half_byte);
|
||||
return AllInOneLayoutTypeUnknown;
|
||||
}
|
||||
}
|
||||
|
||||
static bool all_in_one_parse(const NfcDevice* device, FuriString* parsed_data) {
|
||||
furi_assert(device);
|
||||
furi_assert(parsed_data);
|
||||
|
||||
const MfUltralightData* data = nfc_device_get_data(device, NfcProtocolMfUltralight);
|
||||
|
||||
bool parsed = false;
|
||||
|
||||
do {
|
||||
if(data->page[4].data[0] != 0x45 || data->page[4].data[1] != 0xD9) {
|
||||
FURI_LOG_I(TAG, "Pass not verified");
|
||||
break;
|
||||
}
|
||||
|
||||
uint8_t ride_count = 0;
|
||||
uint32_t serial = 0;
|
||||
|
||||
const AllInOneLayoutType layout_type = all_in_one_get_layout(data);
|
||||
|
||||
if(layout_type == AllInOneLayoutTypeA) {
|
||||
// If the layout is A then the ride count is stored in the first byte of page 8
|
||||
ride_count = data->page[8].data[0];
|
||||
} else if(layout_type == AllInOneLayoutTypeD) {
|
||||
// If the layout is D, the ride count is stored in the second byte of page 9
|
||||
ride_count = data->page[9].data[1];
|
||||
} else {
|
||||
FURI_LOG_I(TAG, "Unknown layout: %d", layout_type);
|
||||
ride_count = 137;
|
||||
}
|
||||
|
||||
// // The number starts at the second half of the third byte on page 4, and is 32 bits long
|
||||
// // So we get the second half of the third byte, then bytes 4-6, and then the first half of the 7th byte
|
||||
// // B8 17 A2 A4 BD becomes 81 7A 2A 4B
|
||||
const uint8_t* serial_data_lo = data->page[4].data;
|
||||
const uint8_t* serial_data_hi = data->page[5].data;
|
||||
|
||||
serial = (serial_data_lo[2] & 0x0F) << 28 | serial_data_lo[3] << 20 |
|
||||
serial_data_hi[0] << 12 | serial_data_hi[1] << 4 | serial_data_hi[2] >> 4;
|
||||
|
||||
// Format string for rides count
|
||||
furi_string_printf(
|
||||
parsed_data, "\e#All-In-One\nNumber: %lu\nRides left: %u", serial, ride_count);
|
||||
|
||||
parsed = true;
|
||||
} while(false);
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/* Actual implementation of app<>plugin interface */
|
||||
static const NfcSupportedCardsPlugin all_in_one_plugin = {
|
||||
.protocol = NfcProtocolMfUltralight,
|
||||
.verify = NULL,
|
||||
.read = NULL,
|
||||
.parse = all_in_one_parse,
|
||||
};
|
||||
|
||||
/* Plugin descriptor to comply with basic plugin specification */
|
||||
static const FlipperAppPluginDescriptor all_in_one_plugin_descriptor = {
|
||||
.appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID,
|
||||
.ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION,
|
||||
.entry_point = &all_in_one_plugin,
|
||||
};
|
||||
|
||||
/* Plugin entry point - must return a pointer to const descriptor */
|
||||
const FlipperAppPluginDescriptor* all_in_one_plugin_ep() {
|
||||
return &all_in_one_plugin_descriptor;
|
||||
}
|
||||
116
applications/main/nfc/plugins/supported_cards/myki.c
Normal file
116
applications/main/nfc/plugins/supported_cards/myki.c
Normal file
@@ -0,0 +1,116 @@
|
||||
/* myki.c - Parser for myki cards (Melbourne, Australia).
|
||||
*
|
||||
* Based on the code by Emily Trau (https://github.com/emilytrau)
|
||||
* Original pull request URL: https://github.com/flipperdevices/flipperzero-firmware/pull/2326
|
||||
* Reference: https://github.com/metrodroid/metrodroid/wiki/Myki
|
||||
*/
|
||||
#include "nfc_supported_card_plugin.h"
|
||||
|
||||
#include <flipper_application/flipper_application.h>
|
||||
#include <lib/nfc/protocols/mf_desfire/mf_desfire.h>
|
||||
|
||||
static const MfDesfireApplicationId myki_app_id = {.data = {0x00, 0x11, 0xf2}};
|
||||
static const MfDesfireFileId myki_file_id = 0x0f;
|
||||
|
||||
static uint8_t myki_calculate_luhn(uint64_t number) {
|
||||
// https://en.wikipedia.org/wiki/Luhn_algorithm
|
||||
// Drop existing check digit to form payload
|
||||
uint64_t payload = number / 10;
|
||||
int sum = 0;
|
||||
int position = 0;
|
||||
|
||||
while(payload > 0) {
|
||||
int digit = payload % 10;
|
||||
if(position % 2 == 0) {
|
||||
digit *= 2;
|
||||
}
|
||||
if(digit > 9) {
|
||||
digit = (digit / 10) + (digit % 10);
|
||||
}
|
||||
sum += digit;
|
||||
payload /= 10;
|
||||
position++;
|
||||
}
|
||||
|
||||
return (10 - (sum % 10)) % 10;
|
||||
}
|
||||
|
||||
static bool myki_parse(const NfcDevice* device, FuriString* parsed_data) {
|
||||
furi_assert(device);
|
||||
furi_assert(parsed_data);
|
||||
|
||||
bool parsed = false;
|
||||
|
||||
do {
|
||||
const MfDesfireData* data = nfc_device_get_data(device, NfcProtocolMfDesfire);
|
||||
|
||||
const MfDesfireApplication* app = mf_desfire_get_application(data, &myki_app_id);
|
||||
if(app == NULL) break;
|
||||
|
||||
typedef struct {
|
||||
uint32_t top;
|
||||
uint32_t bottom;
|
||||
} MykiFile;
|
||||
|
||||
const MfDesfireFileSettings* file_settings =
|
||||
mf_desfire_get_file_settings(app, &myki_file_id);
|
||||
|
||||
if(file_settings == NULL || file_settings->type != MfDesfireFileTypeStandard ||
|
||||
file_settings->data.size < sizeof(MykiFile))
|
||||
break;
|
||||
|
||||
const MfDesfireFileData* file_data = mf_desfire_get_file_data(app, &myki_file_id);
|
||||
if(file_data == NULL) break;
|
||||
|
||||
const MykiFile* myki_file = simple_array_cget_data(file_data->data);
|
||||
|
||||
// All myki card numbers are prefixed with "308425"
|
||||
if(myki_file->top != 308425UL) break;
|
||||
// Card numbers are always 15 digits in length
|
||||
if(myki_file->bottom < 10000000UL || myki_file->bottom >= 100000000UL) break;
|
||||
|
||||
uint64_t card_number = myki_file->top * 1000000000ULL + myki_file->bottom * 10UL;
|
||||
// Stored card number doesn't include check digit
|
||||
card_number += myki_calculate_luhn(card_number);
|
||||
|
||||
furi_string_set(parsed_data, "\e#myki\n");
|
||||
|
||||
// Stylise card number according to the physical card
|
||||
char card_string[20];
|
||||
snprintf(card_string, sizeof(card_string), "%llu", card_number);
|
||||
|
||||
// Digit count in each space-separated group
|
||||
static const uint8_t digit_count[] = {1, 5, 4, 4, 1};
|
||||
|
||||
for(uint32_t i = 0, k = 0; i < COUNT_OF(digit_count); k += digit_count[i++]) {
|
||||
for(uint32_t j = 0; j < digit_count[i]; ++j) {
|
||||
furi_string_push_back(parsed_data, card_string[j + k]);
|
||||
}
|
||||
furi_string_push_back(parsed_data, ' ');
|
||||
}
|
||||
|
||||
parsed = true;
|
||||
} while(false);
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/* Actual implementation of app<>plugin interface */
|
||||
static const NfcSupportedCardsPlugin myki_plugin = {
|
||||
.protocol = NfcProtocolMfDesfire,
|
||||
.verify = NULL,
|
||||
.read = NULL,
|
||||
.parse = myki_parse,
|
||||
};
|
||||
|
||||
/* Plugin descriptor to comply with basic plugin specification */
|
||||
static const FlipperAppPluginDescriptor myki_plugin_descriptor = {
|
||||
.appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID,
|
||||
.ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION,
|
||||
.entry_point = &myki_plugin,
|
||||
};
|
||||
|
||||
/* Plugin entry point - must return a pointer to const descriptor */
|
||||
const FlipperAppPluginDescriptor* myki_plugin_ep() {
|
||||
return &myki_plugin_descriptor;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* @file nfc_supported_card_plugin.h
|
||||
* @brief Supported card plugin abstract interface.
|
||||
*
|
||||
* Supported card plugins are dynamically loaded libraries that help making sense of
|
||||
* a particular card's raw data, if a suitable plugin exists.
|
||||
*
|
||||
* For example, if some card serves as a bus ticket, instead of just displaying a data dump,
|
||||
* a suitable plugin will transform that data into a human-readable format, showing the number
|
||||
* of rides or balance left.
|
||||
* Because of the highly specialised nature of application-specific cards, a separate plugin
|
||||
* for each such card type must be implemented.
|
||||
*
|
||||
* To add a new plugin, create a uniquely-named .c file in the `supported_cards` directory
|
||||
* and implement at least the parse() function in the NfcSupportedCardsPlugin structure.
|
||||
* Then, register the plugin in the `application.fam` file in the `nfc` directory. Use the existing
|
||||
* entries as an example. After being registered, the plugin will be automatically deployed with the application.
|
||||
*
|
||||
* @note the APPID field MUST end with `_parser` so the applicaton would know that this particular file
|
||||
* is a supported card plugin.
|
||||
*
|
||||
* @see nfc_supported_cards.h
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <furi/core/string.h>
|
||||
|
||||
#include <nfc/nfc.h>
|
||||
#include <nfc/nfc_device.h>
|
||||
|
||||
/**
|
||||
* @brief Unique string identifier for supported card plugins.
|
||||
*/
|
||||
#define NFC_SUPPORTED_CARD_PLUGIN_APP_ID "NfcSupportedCardPlugin"
|
||||
|
||||
/**
|
||||
* @brief Currently supported plugin API version.
|
||||
*/
|
||||
#define NFC_SUPPORTED_CARD_PLUGIN_API_VERSION 1
|
||||
|
||||
/**
|
||||
* @brief Verify that the card is of a supported type.
|
||||
*
|
||||
* This function should be implemented if a quick check exists
|
||||
* allowing to verify that the plugin is working with the appropriate card type.
|
||||
* Such checks may include, but are not limited to: reading a specific sector,
|
||||
* performing a certain read operation, etc.
|
||||
*
|
||||
* @param[in,out] nfc pointer to an Nfc instance.
|
||||
* @returns true if the card was successfully verified, false otherwise.
|
||||
*/
|
||||
typedef bool (*NfcSupportedCardPluginVerify)(Nfc* nfc);
|
||||
|
||||
/**
|
||||
* @brief Read the card using a custom procedure.
|
||||
*
|
||||
* This function should be implemented if a card requires some special reading
|
||||
* procedure not covered in the vanilla poller. Examples include, but are not
|
||||
* limited to: reading with particular security keys, mandatory order of read
|
||||
* operations, etc.
|
||||
*
|
||||
* @param[in,out] nfc pointer to an Nfc instance.
|
||||
* @param[in,out] device pointer to a device instance to hold the read data.
|
||||
* @returns true if the card was successfully read, false otherwise.
|
||||
*/
|
||||
typedef bool (*NfcSupportedCardPluginRead)(Nfc* nfc, NfcDevice* device);
|
||||
|
||||
/**
|
||||
* @brief Parse raw data into human-readable representation.
|
||||
*
|
||||
* A supported card plugin may contain only this function, if no special verification
|
||||
* or reading procedures are not required. In any case, the data must be complete and
|
||||
* available through the `device` parameter at the time of calling.
|
||||
*
|
||||
* The output format is free and application-dependent. Multiple lines should
|
||||
* be separated by newline character.
|
||||
*
|
||||
* @param[in] device pointer to a device instance holding the data is to be parsed.
|
||||
* @param[out] parsed_data pointer to the string to contain the formatted result.
|
||||
* @returns true if the card was successfully parsed, false otherwise.
|
||||
*/
|
||||
typedef bool (*NfcSupportedCardPluginParse)(const NfcDevice* device, FuriString* parsed_data);
|
||||
|
||||
/**
|
||||
* @brief Supported card plugin interface.
|
||||
*
|
||||
* For a minimally functional plugin, only the parse() function must be implemented.
|
||||
*/
|
||||
typedef struct {
|
||||
NfcProtocol protocol; /**< Identifier of the protocol this card type works on top of. */
|
||||
NfcSupportedCardPluginVerify verify; /**< Pointer to the verify() function. */
|
||||
NfcSupportedCardPluginRead read; /**< Pointer to the read() function. */
|
||||
NfcSupportedCardPluginParse parse; /**< Pointer to the parse() function. */
|
||||
} NfcSupportedCardsPlugin;
|
||||
233
applications/main/nfc/plugins/supported_cards/opal.c
Normal file
233
applications/main/nfc/plugins/supported_cards/opal.c
Normal file
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
* 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_plugin.h"
|
||||
|
||||
#include <flipper_application/flipper_application.h>
|
||||
#include <applications/services/locale/locale.h>
|
||||
#include <furi_hal_rtc.h>
|
||||
|
||||
#include <lib/nfc/protocols/mf_desfire/mf_desfire.h>
|
||||
|
||||
static const MfDesfireApplicationId opal_app_id = {.data = {0x31, 0x45, 0x53}};
|
||||
|
||||
static const MfDesfireFileId opal_file_id = 0x07;
|
||||
|
||||
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",
|
||||
NULL, // Manly Ferry: new journey
|
||||
NULL, // Manly Ferry: transfer from ferry
|
||||
NULL, // 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, "OpalFile");
|
||||
|
||||
// Converts an Opal timestamp to FuriHalRtcDateTime.
|
||||
//
|
||||
// Opal measures days since 1980-01-01 and minutes since midnight, and presumes
|
||||
// all days are 1440 minutes.
|
||||
static void opal_date_time_to_furi(uint16_t days, uint16_t minutes, FuriHalRtcDateTime* out) {
|
||||
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(;;) {
|
||||
const uint16_t num_days_in_year = furi_hal_rtc_get_days_per_year(out->year);
|
||||
if(days < num_days_in_year) break;
|
||||
days -= num_days_in_year;
|
||||
out->year++;
|
||||
}
|
||||
|
||||
// 1-index the day of the year
|
||||
days++;
|
||||
|
||||
for(;;) {
|
||||
// What month is it?
|
||||
const bool is_leap = furi_hal_rtc_is_leap_year(out->year);
|
||||
const uint8_t num_days_in_month = furi_hal_rtc_get_days_per_month(is_leap, out->month);
|
||||
if(days <= num_days_in_month) break;
|
||||
days -= num_days_in_month;
|
||||
out->month++;
|
||||
}
|
||||
|
||||
out->day = days;
|
||||
}
|
||||
|
||||
static bool opal_parse(const NfcDevice* device, FuriString* parsed_data) {
|
||||
furi_assert(device);
|
||||
furi_assert(parsed_data);
|
||||
|
||||
const MfDesfireData* data = nfc_device_get_data(device, NfcProtocolMfDesfire);
|
||||
|
||||
bool parsed = false;
|
||||
|
||||
do {
|
||||
const MfDesfireApplication* app = mf_desfire_get_application(data, &opal_app_id);
|
||||
if(app == NULL) break;
|
||||
|
||||
const MfDesfireFileSettings* file_settings =
|
||||
mf_desfire_get_file_settings(app, &opal_file_id);
|
||||
if(file_settings == NULL || file_settings->type != MfDesfireFileTypeStandard ||
|
||||
file_settings->data.size != sizeof(OpalFile))
|
||||
break;
|
||||
|
||||
const MfDesfireFileData* file_data = mf_desfire_get_file_data(app, &opal_file_id);
|
||||
if(file_data == NULL) break;
|
||||
|
||||
const OpalFile* opal_file = simple_array_cget_data(file_data->data);
|
||||
|
||||
const uint8_t serial2 = opal_file->serial / 10000000;
|
||||
const uint16_t serial3 = (opal_file->serial / 1000) % 10000;
|
||||
const uint16_t serial4 = (opal_file->serial % 1000);
|
||||
|
||||
if(opal_file->check_digit > 9) break;
|
||||
|
||||
// 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.
|
||||
const bool is_negative_balance = (opal_file->balance < 0);
|
||||
const char* sign = is_negative_balance ? "-" : "";
|
||||
const int32_t balance = is_negative_balance ? labs(opal_file->balance) : //-V1081
|
||||
opal_file->balance;
|
||||
const uint8_t balance_cents = balance % 100;
|
||||
const int32_t balance_dollars = balance / 100;
|
||||
|
||||
FuriHalRtcDateTime timestamp;
|
||||
opal_date_time_to_furi(opal_file->days, opal_file->minutes, ×tamp);
|
||||
|
||||
// Usages 4..6 associated with the Manly Ferry, which correspond to
|
||||
// usages 1..3 for other modes.
|
||||
const bool is_manly_ferry = (opal_file->usage >= 4) && (opal_file->usage <= 6);
|
||||
|
||||
// 3..7 are "reserved", but we use 4 to indicate the Manly Ferry.
|
||||
const uint8_t mode = is_manly_ferry ? 4 : opal_file->mode;
|
||||
const uint8_t usage = is_manly_ferry ? opal_file->usage - 3 : opal_file->usage;
|
||||
|
||||
const char* mode_str = opal_modes[mode > 4 ? 3 : mode];
|
||||
const char* usage_str = opal_usages[usage > 12 ? 13 : usage];
|
||||
|
||||
furi_string_printf(
|
||||
parsed_data,
|
||||
"\e#Opal: $%s%ld.%02hu\n3085 22%02hhu %04hu %03hu%01hhu\n%s, %s\n",
|
||||
sign,
|
||||
balance_dollars,
|
||||
balance_cents,
|
||||
serial2,
|
||||
serial3,
|
||||
serial4,
|
||||
opal_file->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(parsed_data, timestamp_str);
|
||||
furi_string_cat(parsed_data, " at ");
|
||||
|
||||
locale_format_time(timestamp_str, ×tamp, locale_get_time_format(), false);
|
||||
furi_string_cat(parsed_data, timestamp_str);
|
||||
|
||||
furi_string_free(timestamp_str);
|
||||
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\nWeekly journeys: %hhu, Txn #%hu\n",
|
||||
opal_file->weekly_journeys,
|
||||
opal_file->txn_number);
|
||||
|
||||
if(opal_file->auto_topup) {
|
||||
furi_string_cat_str(parsed_data, "Auto-topup enabled\n");
|
||||
}
|
||||
|
||||
if(opal_file->blocked) {
|
||||
furi_string_cat_str(parsed_data, "Card blocked\n");
|
||||
}
|
||||
|
||||
parsed = true;
|
||||
} while(false);
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/* Actual implementation of app<>plugin interface */
|
||||
static const NfcSupportedCardsPlugin opal_plugin = {
|
||||
.protocol = NfcProtocolMfDesfire,
|
||||
.verify = NULL,
|
||||
.read = NULL,
|
||||
.parse = opal_parse,
|
||||
};
|
||||
|
||||
/* Plugin descriptor to comply with basic plugin specification */
|
||||
static const FlipperAppPluginDescriptor opal_plugin_descriptor = {
|
||||
.appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID,
|
||||
.ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION,
|
||||
.entry_point = &opal_plugin,
|
||||
};
|
||||
|
||||
/* Plugin entry point - must return a pointer to const descriptor */
|
||||
const FlipperAppPluginDescriptor* opal_plugin_ep() {
|
||||
return &opal_plugin_descriptor;
|
||||
}
|
||||
219
applications/main/nfc/plugins/supported_cards/plantain.c
Normal file
219
applications/main/nfc/plugins/supported_cards/plantain.c
Normal file
@@ -0,0 +1,219 @@
|
||||
#include "nfc_supported_card_plugin.h"
|
||||
|
||||
#include <flipper_application/flipper_application.h>
|
||||
|
||||
#include <nfc/nfc_device.h>
|
||||
#include <nfc/helpers/nfc_util.h>
|
||||
#include <nfc/protocols/mf_classic/mf_classic_poller_sync_api.h>
|
||||
|
||||
#define TAG "Plantain"
|
||||
|
||||
typedef struct {
|
||||
uint64_t a;
|
||||
uint64_t b;
|
||||
} MfClassicKeyPair;
|
||||
|
||||
typedef struct {
|
||||
const MfClassicKeyPair* keys;
|
||||
uint32_t data_sector;
|
||||
} PlantainCardConfig;
|
||||
|
||||
static const MfClassicKeyPair plantain_1k_keys[] = {
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xe56ac127dd45, .b = 0x19fc84a3784b},
|
||||
{.a = 0x77dabc9825e1, .b = 0x9764fec3154a},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0x26973ea74321, .b = 0xd27058c6e2c7},
|
||||
{.a = 0xeb0a8ff88ade, .b = 0x578a9ada41e3},
|
||||
{.a = 0xea0fd73cb149, .b = 0x29c35fa068fb},
|
||||
{.a = 0xc76bf71a2509, .b = 0x9ba241db3f56},
|
||||
{.a = 0xacffffffffff, .b = 0x71f3a315ad26},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
};
|
||||
|
||||
static const MfClassicKeyPair plantain_4k_keys[] = {
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xe56ac127dd45, .b = 0x19fc84a3784b}, {.a = 0x77dabc9825e1, .b = 0x9764fec3154a},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0x26973ea74321, .b = 0xd27058c6e2c7}, {.a = 0xeb0a8ff88ade, .b = 0x578a9ada41e3},
|
||||
{.a = 0xea0fd73cb149, .b = 0x29c35fa068fb}, {.a = 0xc76bf71a2509, .b = 0x9ba241db3f56},
|
||||
{.a = 0xacffffffffff, .b = 0x71f3a315ad26}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0x72f96bdd3714, .b = 0x462225cd34cf}, {.a = 0x044ce1872bc3, .b = 0x8c90c70cff4a},
|
||||
{.a = 0xbc2d1791dec1, .b = 0xca96a487de0b}, {.a = 0x8791b2ccb5c4, .b = 0xc956c3b80da3},
|
||||
{.a = 0x8e26e45e7d65, .b = 0x8e65b3af7d22}, {.a = 0x0f318130ed18, .b = 0x0c420a20e056},
|
||||
{.a = 0x045ceca15535, .b = 0x31bec3d9e510}, {.a = 0x9d993c5d4ef4, .b = 0x86120e488abf},
|
||||
{.a = 0xc65d4eaa645b, .b = 0xb69d40d1a439}, {.a = 0x3a8a139c20b4, .b = 0x8818a9c5d406},
|
||||
{.a = 0xbaff3053b496, .b = 0x4b7cb25354d3}, {.a = 0x7413b599c4ea, .b = 0xb0a2AAF3A1BA},
|
||||
{.a = 0x0ce7cd2cc72b, .b = 0xfa1fbb3f0f1f}, {.a = 0x0be5fac8b06a, .b = 0x6f95887a4fd3},
|
||||
{.a = 0x0eb23cc8110b, .b = 0x04dc35277635}, {.a = 0xbc4580b7f20b, .b = 0xd0a4131fb290},
|
||||
{.a = 0x7a396f0d633d, .b = 0xad2bdc097023}, {.a = 0xa3faa6daff67, .b = 0x7600e889adf9},
|
||||
{.a = 0xfd8705e721b0, .b = 0x296fc317a513}, {.a = 0x22052b480d11, .b = 0xe19504c39461},
|
||||
{.a = 0xa7141147d430, .b = 0xff16014fefc7}, {.a = 0x8a8d88151a00, .b = 0x038b5f9b5a2a},
|
||||
{.a = 0xb27addfb64b0, .b = 0x152fd0c420a7}, {.a = 0x7259fa0197c6, .b = 0x5583698df085},
|
||||
};
|
||||
|
||||
static bool plantain_get_card_config(PlantainCardConfig* config, MfClassicType type) {
|
||||
bool success = true;
|
||||
|
||||
if(type == MfClassicType1k) {
|
||||
config->data_sector = 8;
|
||||
config->keys = plantain_1k_keys;
|
||||
} else if(type == MfClassicType4k) {
|
||||
config->data_sector = 8;
|
||||
config->keys = plantain_4k_keys;
|
||||
} else {
|
||||
success = false;
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
static bool plantain_verify_type(Nfc* nfc, MfClassicType type) {
|
||||
bool verified = false;
|
||||
|
||||
do {
|
||||
PlantainCardConfig cfg = {};
|
||||
if(!plantain_get_card_config(&cfg, type)) break;
|
||||
|
||||
const uint8_t block_num = mf_classic_get_first_block_num_of_sector(cfg.data_sector);
|
||||
FURI_LOG_D(TAG, "Verifying sector %lu", cfg.data_sector);
|
||||
|
||||
MfClassicKey key = {0};
|
||||
nfc_util_num2bytes(cfg.keys[cfg.data_sector].a, COUNT_OF(key.data), key.data);
|
||||
|
||||
MfClassicAuthContext auth_context;
|
||||
MfClassicError error =
|
||||
mf_classic_poller_auth(nfc, block_num, &key, MfClassicKeyTypeA, &auth_context);
|
||||
if(error != MfClassicErrorNone) {
|
||||
FURI_LOG_D(TAG, "Failed to read block %u: %d", block_num, error);
|
||||
break;
|
||||
}
|
||||
|
||||
verified = true;
|
||||
} while(false);
|
||||
|
||||
return verified;
|
||||
}
|
||||
|
||||
static bool plantain_verify(Nfc* nfc) {
|
||||
return plantain_verify_type(nfc, MfClassicType1k) ||
|
||||
plantain_verify_type(nfc, MfClassicType4k);
|
||||
}
|
||||
|
||||
static bool plantain_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 = MfClassicTypeMini;
|
||||
MfClassicError error = mf_classic_poller_detect_type(nfc, &type);
|
||||
if(error != MfClassicErrorNone) break;
|
||||
|
||||
data->type = type;
|
||||
PlantainCardConfig cfg = {};
|
||||
if(!plantain_get_card_config(&cfg, data->type)) break;
|
||||
|
||||
MfClassicDeviceKeys keys = {};
|
||||
for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) {
|
||||
nfc_util_num2bytes(cfg.keys[i].a, sizeof(MfClassicKey), keys.key_a[i].data);
|
||||
FURI_BIT_SET(keys.key_a_mask, i);
|
||||
nfc_util_num2bytes(cfg.keys[i].b, sizeof(MfClassicKey), keys.key_b[i].data);
|
||||
FURI_BIT_SET(keys.key_b_mask, i);
|
||||
}
|
||||
|
||||
error = mf_classic_poller_read(nfc, &keys, data);
|
||||
if(error != MfClassicErrorNone) {
|
||||
FURI_LOG_W(TAG, "Failed to read data");
|
||||
break;
|
||||
}
|
||||
|
||||
nfc_device_set_data(device, NfcProtocolMfClassic, data);
|
||||
|
||||
is_read = true;
|
||||
} while(false);
|
||||
|
||||
mf_classic_free(data);
|
||||
|
||||
return is_read;
|
||||
}
|
||||
|
||||
static bool plantain_parse(const NfcDevice* device, FuriString* parsed_data) {
|
||||
furi_assert(device);
|
||||
|
||||
const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic);
|
||||
|
||||
bool parsed = false;
|
||||
|
||||
do {
|
||||
// Verify card type
|
||||
PlantainCardConfig cfg = {};
|
||||
if(!plantain_get_card_config(&cfg, data->type)) break;
|
||||
|
||||
// Verify key
|
||||
const MfClassicSectorTrailer* sec_tr =
|
||||
mf_classic_get_sector_trailer_by_sector(data, cfg.data_sector);
|
||||
|
||||
const uint64_t key = nfc_util_bytes2num(sec_tr->key_a.data, COUNT_OF(sec_tr->key_a.data));
|
||||
if(key != cfg.keys[cfg.data_sector].a) break;
|
||||
|
||||
// Point to block 0 of sector 4, value 0
|
||||
const uint8_t* temp_ptr = data->block[16].data;
|
||||
// Read first 4 bytes of block 0 of sector 4 from last to first and convert them to uint32_t
|
||||
// 38 18 00 00 becomes 00 00 18 38, and equals to 6200 decimal
|
||||
uint32_t balance =
|
||||
((temp_ptr[3] << 24) | (temp_ptr[2] << 16) | (temp_ptr[1] << 8) | temp_ptr[0]) / 100;
|
||||
// Read card number
|
||||
// Point to block 0 of sector 0, value 0
|
||||
temp_ptr = data->block[0].data;
|
||||
// Read first 7 bytes of block 0 of sector 0 from last to first and convert them to uint64_t
|
||||
// 04 31 16 8A 23 5C 80 becomes 80 5C 23 8A 16 31 04, and equals to 36130104729284868 decimal
|
||||
uint8_t card_number_arr[7];
|
||||
for(size_t i = 0; i < 7; i++) {
|
||||
card_number_arr[i] = temp_ptr[6 - i];
|
||||
}
|
||||
// Copy card number to uint64_t
|
||||
uint64_t card_number = 0;
|
||||
for(size_t i = 0; i < 7; i++) {
|
||||
card_number = (card_number << 8) | card_number_arr[i];
|
||||
}
|
||||
|
||||
furi_string_printf(
|
||||
parsed_data, "\e#Plantain\nN:%llu-\nBalance:%lu\n", card_number, balance);
|
||||
parsed = true;
|
||||
} while(false);
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/* Actual implementation of app<>plugin interface */
|
||||
static const NfcSupportedCardsPlugin plantain_plugin = {
|
||||
.protocol = NfcProtocolMfClassic,
|
||||
.verify = plantain_verify,
|
||||
.read = plantain_read,
|
||||
.parse = plantain_parse,
|
||||
};
|
||||
|
||||
/* Plugin descriptor to comply with basic plugin specification */
|
||||
static const FlipperAppPluginDescriptor plantain_plugin_descriptor = {
|
||||
.appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID,
|
||||
.ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION,
|
||||
.entry_point = &plantain_plugin,
|
||||
};
|
||||
|
||||
/* Plugin entry point - must return a pointer to const descriptor */
|
||||
const FlipperAppPluginDescriptor* plantain_plugin_ep() {
|
||||
return &plantain_plugin_descriptor;
|
||||
}
|
||||
214
applications/main/nfc/plugins/supported_cards/troika.c
Normal file
214
applications/main/nfc/plugins/supported_cards/troika.c
Normal file
@@ -0,0 +1,214 @@
|
||||
#include "nfc_supported_card_plugin.h"
|
||||
|
||||
#include <flipper_application/flipper_application.h>
|
||||
|
||||
#include <nfc/nfc_device.h>
|
||||
#include <nfc/helpers/nfc_util.h>
|
||||
#include <nfc/protocols/mf_classic/mf_classic_poller_sync_api.h>
|
||||
|
||||
#define TAG "Troika"
|
||||
|
||||
typedef struct {
|
||||
uint64_t a;
|
||||
uint64_t b;
|
||||
} MfClassicKeyPair;
|
||||
|
||||
typedef struct {
|
||||
const MfClassicKeyPair* keys;
|
||||
uint32_t data_sector;
|
||||
} TroikaCardConfig;
|
||||
|
||||
static const MfClassicKeyPair troika_1k_keys[] = {
|
||||
{.a = 0xa0a1a2a3a4a5, .b = 0xfbf225dc5d58},
|
||||
{.a = 0xa82607b01c0d, .b = 0x2910989b6880},
|
||||
{.a = 0x2aa05ed1856f, .b = 0xeaac88e5dc99},
|
||||
{.a = 0x2aa05ed1856f, .b = 0xeaac88e5dc99},
|
||||
{.a = 0x73068f118c13, .b = 0x2b7f3253fac5},
|
||||
{.a = 0xfbc2793d540b, .b = 0xd3a297dc2698},
|
||||
{.a = 0x2aa05ed1856f, .b = 0xeaac88e5dc99},
|
||||
{.a = 0xae3d65a3dad4, .b = 0x0f1c63013dba},
|
||||
{.a = 0xa73f5dc1d333, .b = 0xe35173494a81},
|
||||
{.a = 0x69a32f1c2f19, .b = 0x6b8bd9860763},
|
||||
{.a = 0x9becdf3d9273, .b = 0xf8493407799d},
|
||||
{.a = 0x08b386463229, .b = 0x5efbaecef46b},
|
||||
{.a = 0xcd4c61c26e3d, .b = 0x31c7610de3b0},
|
||||
{.a = 0xa82607b01c0d, .b = 0x2910989b6880},
|
||||
{.a = 0x0e8f64340ba4, .b = 0x4acec1205d75},
|
||||
{.a = 0x2aa05ed1856f, .b = 0xeaac88e5dc99},
|
||||
};
|
||||
|
||||
static const MfClassicKeyPair troika_4k_keys[] = {
|
||||
{.a = 0xa0a1a2a3a4a5, .b = 0xfbf225dc5d58}, {.a = 0xa82607b01c0d, .b = 0x2910989b6880},
|
||||
{.a = 0x2aa05ed1856f, .b = 0xeaac88e5dc99}, {.a = 0x2aa05ed1856f, .b = 0xeaac88e5dc99},
|
||||
{.a = 0x73068f118c13, .b = 0x2b7f3253fac5}, {.a = 0xfbc2793d540b, .b = 0xd3a297dc2698},
|
||||
{.a = 0x2aa05ed1856f, .b = 0xeaac88e5dc99}, {.a = 0xae3d65a3dad4, .b = 0x0f1c63013dbb},
|
||||
{.a = 0xa73f5dc1d333, .b = 0xe35173494a81}, {.a = 0x69a32f1c2f19, .b = 0x6b8bd9860763},
|
||||
{.a = 0x9becdf3d9273, .b = 0xf8493407799d}, {.a = 0x08b386463229, .b = 0x5efbaecef46b},
|
||||
{.a = 0xcd4c61c26e3d, .b = 0x31c7610de3b0}, {.a = 0xa82607b01c0d, .b = 0x2910989b6880},
|
||||
{.a = 0x0e8f64340ba4, .b = 0x4acec1205d75}, {.a = 0x2aa05ed1856f, .b = 0xeaac88e5dc99},
|
||||
{.a = 0x6b02733bb6ec, .b = 0x7038cd25c408}, {.a = 0x403d706ba880, .b = 0xb39d19a280df},
|
||||
{.a = 0xc11f4597efb5, .b = 0x70d901648cb9}, {.a = 0x0db520c78c1c, .b = 0x73e5b9d9d3a4},
|
||||
{.a = 0x3ebce0925b2f, .b = 0x372cc880f216}, {.a = 0x16a27af45407, .b = 0x9868925175ba},
|
||||
{.a = 0xaba208516740, .b = 0xce26ecb95252}, {.a = 0xcd64e567abcd, .b = 0x8f79c4fd8a01},
|
||||
{.a = 0x764cd061f1e6, .b = 0xa74332f74994}, {.a = 0x1cc219e9fec1, .b = 0xb90de525ceb6},
|
||||
{.a = 0x2fe3cb83ea43, .b = 0xfba88f109b32}, {.a = 0x07894ffec1d6, .b = 0xefcb0e689db3},
|
||||
{.a = 0x04c297b91308, .b = 0xc8454c154cb5}, {.a = 0x7a38e3511a38, .b = 0xab16584c972a},
|
||||
{.a = 0x7545df809202, .b = 0xecf751084a80}, {.a = 0x5125974cd391, .b = 0xd3eafb5df46d},
|
||||
{.a = 0x7a86aa203788, .b = 0xe41242278ca2}, {.a = 0xafcef64c9913, .b = 0x9db96dca4324},
|
||||
{.a = 0x04eaa462f70b, .b = 0xac17b93e2fae}, {.a = 0xe734c210f27e, .b = 0x29ba8c3e9fda},
|
||||
{.a = 0xd5524f591eed, .b = 0x5daf42861b4d}, {.a = 0xe4821a377b75, .b = 0xe8709e486465},
|
||||
{.a = 0x518dc6eea089, .b = 0x97c64ac98ca4}, {.a = 0xbb52f8cce07f, .b = 0x6b6119752c70},
|
||||
};
|
||||
|
||||
static bool troika_get_card_config(TroikaCardConfig* config, MfClassicType type) {
|
||||
bool success = true;
|
||||
|
||||
if(type == MfClassicType1k) {
|
||||
config->data_sector = 8;
|
||||
config->keys = troika_1k_keys;
|
||||
} else if(type == MfClassicType4k) {
|
||||
config->data_sector = 4;
|
||||
config->keys = troika_4k_keys;
|
||||
} else {
|
||||
success = false;
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
static bool troika_verify_type(Nfc* nfc, MfClassicType type) {
|
||||
bool verified = false;
|
||||
|
||||
do {
|
||||
TroikaCardConfig cfg = {};
|
||||
if(!troika_get_card_config(&cfg, type)) break;
|
||||
|
||||
const uint8_t block_num = mf_classic_get_first_block_num_of_sector(cfg.data_sector);
|
||||
FURI_LOG_D(TAG, "Verifying sector %lu", cfg.data_sector);
|
||||
|
||||
MfClassicKey key = {0};
|
||||
nfc_util_num2bytes(cfg.keys[cfg.data_sector].a, COUNT_OF(key.data), key.data);
|
||||
|
||||
MfClassicAuthContext auth_context;
|
||||
MfClassicError error =
|
||||
mf_classic_poller_auth(nfc, block_num, &key, MfClassicKeyTypeA, &auth_context);
|
||||
if(error != MfClassicErrorNone) {
|
||||
FURI_LOG_D(TAG, "Failed to read block %u: %d", block_num, error);
|
||||
break;
|
||||
}
|
||||
|
||||
verified = true;
|
||||
} while(false);
|
||||
|
||||
return verified;
|
||||
}
|
||||
|
||||
static bool troika_verify(Nfc* nfc) {
|
||||
return troika_verify_type(nfc, MfClassicType1k) || troika_verify_type(nfc, MfClassicType4k);
|
||||
}
|
||||
|
||||
static bool troika_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 = MfClassicTypeMini;
|
||||
MfClassicError error = mf_classic_poller_detect_type(nfc, &type);
|
||||
if(error != MfClassicErrorNone) break;
|
||||
|
||||
data->type = type;
|
||||
TroikaCardConfig cfg = {};
|
||||
if(!troika_get_card_config(&cfg, data->type)) break;
|
||||
|
||||
MfClassicDeviceKeys keys = {
|
||||
.key_a_mask = 0,
|
||||
.key_b_mask = 0,
|
||||
};
|
||||
for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) {
|
||||
nfc_util_num2bytes(cfg.keys[i].a, sizeof(MfClassicKey), keys.key_a[i].data);
|
||||
FURI_BIT_SET(keys.key_a_mask, i);
|
||||
nfc_util_num2bytes(cfg.keys[i].b, sizeof(MfClassicKey), keys.key_b[i].data);
|
||||
FURI_BIT_SET(keys.key_b_mask, i);
|
||||
}
|
||||
|
||||
error = mf_classic_poller_read(nfc, &keys, data);
|
||||
if(error != MfClassicErrorNone) {
|
||||
FURI_LOG_W(TAG, "Failed to read data");
|
||||
break;
|
||||
}
|
||||
|
||||
nfc_device_set_data(device, NfcProtocolMfClassic, data);
|
||||
|
||||
is_read = true;
|
||||
} while(false);
|
||||
|
||||
mf_classic_free(data);
|
||||
|
||||
return is_read;
|
||||
}
|
||||
|
||||
static bool troika_parse(const NfcDevice* device, FuriString* parsed_data) {
|
||||
furi_assert(device);
|
||||
|
||||
const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic);
|
||||
|
||||
bool parsed = false;
|
||||
|
||||
do {
|
||||
// Verify card type
|
||||
TroikaCardConfig cfg = {};
|
||||
if(!troika_get_card_config(&cfg, data->type)) break;
|
||||
|
||||
// Verify key
|
||||
const MfClassicSectorTrailer* sec_tr =
|
||||
mf_classic_get_sector_trailer_by_sector(data, cfg.data_sector);
|
||||
|
||||
const uint64_t key = nfc_util_bytes2num(sec_tr->key_a.data, COUNT_OF(sec_tr->key_a.data));
|
||||
if(key != cfg.keys[cfg.data_sector].a) break;
|
||||
|
||||
// Parse data
|
||||
const uint8_t start_block_num = mf_classic_get_first_block_num_of_sector(cfg.data_sector);
|
||||
|
||||
const uint8_t* temp_ptr = &data->block[start_block_num + 1].data[5];
|
||||
uint16_t balance = ((temp_ptr[0] << 8) | temp_ptr[1]) / 25;
|
||||
temp_ptr = &data->block[start_block_num].data[2];
|
||||
|
||||
uint32_t number = 0;
|
||||
for(size_t i = 1; i < 5; i++) {
|
||||
number <<= 8;
|
||||
number |= temp_ptr[i];
|
||||
}
|
||||
number >>= 4;
|
||||
number |= (temp_ptr[0] & 0xf) << 28;
|
||||
|
||||
furi_string_printf(parsed_data, "\e#Troika\nNum: %lu\nBalance: %u RUR", number, balance);
|
||||
parsed = true;
|
||||
} while(false);
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/* Actual implementation of app<>plugin interface */
|
||||
static const NfcSupportedCardsPlugin troika_plugin = {
|
||||
.protocol = NfcProtocolMfClassic,
|
||||
.verify = troika_verify,
|
||||
.read = troika_read,
|
||||
.parse = troika_parse,
|
||||
};
|
||||
|
||||
/* Plugin descriptor to comply with basic plugin specification */
|
||||
static const FlipperAppPluginDescriptor troika_plugin_descriptor = {
|
||||
.appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID,
|
||||
.ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION,
|
||||
.entry_point = &troika_plugin,
|
||||
};
|
||||
|
||||
/* Plugin entry point - must return a pointer to const descriptor */
|
||||
const FlipperAppPluginDescriptor* troika_plugin_ep() {
|
||||
return &troika_plugin_descriptor;
|
||||
}
|
||||
189
applications/main/nfc/plugins/supported_cards/two_cities.c
Normal file
189
applications/main/nfc/plugins/supported_cards/two_cities.c
Normal file
@@ -0,0 +1,189 @@
|
||||
#include "nfc_supported_card_plugin.h"
|
||||
|
||||
#include <flipper_application/flipper_application.h>
|
||||
|
||||
#include <nfc/nfc_device.h>
|
||||
#include <nfc/helpers/nfc_util.h>
|
||||
#include <nfc/protocols/mf_classic/mf_classic_poller_sync_api.h>
|
||||
|
||||
#define TAG "TwoCities"
|
||||
|
||||
typedef struct {
|
||||
uint64_t a;
|
||||
uint64_t b;
|
||||
} MfClassicKeyPair;
|
||||
|
||||
static const MfClassicKeyPair two_cities_4k_keys[] = {
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0x2aa05ed1856f, .b = 0xeaac88e5dc99}, {.a = 0x2aa05ed1856f, .b = 0xeaac88e5dc99},
|
||||
{.a = 0xe56ac127dd45, .b = 0x19fc84a3784b}, {.a = 0x77dabc9825e1, .b = 0x9764fec3154a},
|
||||
{.a = 0x2aa05ed1856f, .b = 0xeaac88e5dc99}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xa73f5dc1d333, .b = 0xe35173494a81}, {.a = 0x69a32f1c2f19, .b = 0x6b8bd9860763},
|
||||
{.a = 0xea0fd73cb149, .b = 0x29c35fa068fb}, {.a = 0xc76bf71a2509, .b = 0x9ba241db3f56},
|
||||
{.a = 0xacffffffffff, .b = 0x71f3a315ad26}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0x2aa05ed1856f, .b = 0xeaac88e5dc99},
|
||||
{.a = 0x72f96bdd3714, .b = 0x462225cd34cf}, {.a = 0x044ce1872bc3, .b = 0x8c90c70cff4a},
|
||||
{.a = 0xbc2d1791dec1, .b = 0xca96a487de0b}, {.a = 0x8791b2ccb5c4, .b = 0xc956c3b80da3},
|
||||
{.a = 0x8e26e45e7d65, .b = 0x8e65b3af7d22}, {.a = 0x0f318130ed18, .b = 0x0c420a20e056},
|
||||
{.a = 0x045ceca15535, .b = 0x31bec3d9e510}, {.a = 0x9d993c5d4ef4, .b = 0x86120e488abf},
|
||||
{.a = 0xc65d4eaa645b, .b = 0xb69d40d1a439}, {.a = 0x3a8a139c20b4, .b = 0x8818a9c5d406},
|
||||
{.a = 0xbaff3053b496, .b = 0x4b7cb25354d3}, {.a = 0x7413b599c4ea, .b = 0xb0a2AAF3A1BA},
|
||||
{.a = 0x0ce7cd2cc72b, .b = 0xfa1fbb3f0f1f}, {.a = 0x0be5fac8b06a, .b = 0x6f95887a4fd3},
|
||||
{.a = 0x26973ea74321, .b = 0xd27058c6e2c7}, {.a = 0xeb0a8ff88ade, .b = 0x578a9ada41e3},
|
||||
{.a = 0x7a396f0d633d, .b = 0xad2bdc097023}, {.a = 0xa3faa6daff67, .b = 0x7600e889adf9},
|
||||
{.a = 0x2aa05ed1856f, .b = 0xeaac88e5dc99}, {.a = 0x2aa05ed1856f, .b = 0xeaac88e5dc99},
|
||||
{.a = 0xa7141147d430, .b = 0xff16014fefc7}, {.a = 0x8a8d88151a00, .b = 0x038b5f9b5a2a},
|
||||
{.a = 0xb27addfb64b0, .b = 0x152fd0c420a7}, {.a = 0x7259fa0197c6, .b = 0x5583698df085},
|
||||
};
|
||||
|
||||
bool two_cities_verify(Nfc* nfc) {
|
||||
bool verified = false;
|
||||
|
||||
do {
|
||||
const uint8_t verify_sector = 4;
|
||||
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 = {};
|
||||
nfc_util_num2bytes(two_cities_4k_keys[verify_sector].a, COUNT_OF(key.data), key.data);
|
||||
|
||||
MfClassicAuthContext auth_ctx = {};
|
||||
MfClassicError error =
|
||||
mf_classic_poller_auth(nfc, block_num, &key, MfClassicKeyTypeA, &auth_ctx);
|
||||
if(error != MfClassicErrorNone) {
|
||||
FURI_LOG_D(TAG, "Failed to read block %u: %d", block_num, error);
|
||||
break;
|
||||
}
|
||||
|
||||
verified = true;
|
||||
} while(false);
|
||||
|
||||
return verified;
|
||||
}
|
||||
|
||||
static bool two_cities_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 = MfClassicTypeMini;
|
||||
MfClassicError error = mf_classic_poller_detect_type(nfc, &type);
|
||||
if(error != MfClassicErrorNone) break;
|
||||
|
||||
data->type = type;
|
||||
MfClassicDeviceKeys keys = {};
|
||||
for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) {
|
||||
nfc_util_num2bytes(two_cities_4k_keys[i].a, sizeof(MfClassicKey), keys.key_a[i].data);
|
||||
FURI_BIT_SET(keys.key_a_mask, i);
|
||||
nfc_util_num2bytes(two_cities_4k_keys[i].b, sizeof(MfClassicKey), keys.key_b[i].data);
|
||||
FURI_BIT_SET(keys.key_b_mask, i);
|
||||
}
|
||||
|
||||
error = mf_classic_poller_read(nfc, &keys, data);
|
||||
if(error != MfClassicErrorNone) {
|
||||
FURI_LOG_W(TAG, "Failed to read data");
|
||||
break;
|
||||
}
|
||||
|
||||
nfc_device_set_data(device, NfcProtocolMfClassic, data);
|
||||
|
||||
is_read = true;
|
||||
} while(false);
|
||||
|
||||
mf_classic_free(data);
|
||||
|
||||
return is_read;
|
||||
}
|
||||
|
||||
static bool two_cities_parse(const NfcDevice* device, FuriString* parsed_data) {
|
||||
furi_assert(device);
|
||||
|
||||
const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic);
|
||||
|
||||
bool parsed = false;
|
||||
|
||||
do {
|
||||
// Verify key
|
||||
MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(data, 4);
|
||||
uint64_t key = nfc_util_bytes2num(sec_tr->key_a.data, 6);
|
||||
if(key != two_cities_4k_keys[4].a) return false;
|
||||
|
||||
// =====
|
||||
// PLANTAIN
|
||||
// =====
|
||||
|
||||
// Point to block 0 of sector 4, value 0
|
||||
const uint8_t* temp_ptr = data->block[16].data;
|
||||
// Read first 4 bytes of block 0 of sector 4 from last to first and convert them to uint32_t
|
||||
// 38 18 00 00 becomes 00 00 18 38, and equals to 6200 decimal
|
||||
uint32_t balance =
|
||||
((temp_ptr[3] << 24) | (temp_ptr[2] << 16) | (temp_ptr[1] << 8) | temp_ptr[0]) / 100;
|
||||
// Read card number
|
||||
// Point to block 0 of sector 0, value 0
|
||||
temp_ptr = data->block[0].data;
|
||||
// Read first 7 bytes of block 0 of sector 0 from last to first and convert them to uint64_t
|
||||
// 04 31 16 8A 23 5C 80 becomes 80 5C 23 8A 16 31 04, and equals to 36130104729284868 decimal
|
||||
uint8_t card_number_arr[7];
|
||||
for(size_t i = 0; i < 7; i++) {
|
||||
card_number_arr[i] = temp_ptr[6 - i];
|
||||
}
|
||||
// Copy card number to uint64_t
|
||||
uint64_t card_number = 0;
|
||||
for(size_t i = 0; i < 7; i++) {
|
||||
card_number = (card_number << 8) | card_number_arr[i];
|
||||
}
|
||||
|
||||
// =====
|
||||
// --PLANTAIN--
|
||||
// =====
|
||||
// TROIKA
|
||||
// =====
|
||||
|
||||
const uint8_t* troika_temp_ptr = &data->block[33].data[5];
|
||||
uint16_t troika_balance = ((troika_temp_ptr[0] << 8) | troika_temp_ptr[1]) / 25;
|
||||
troika_temp_ptr = &data->block[32].data[2];
|
||||
uint32_t troika_number = 0;
|
||||
for(size_t i = 0; i < 4; i++) {
|
||||
troika_number <<= 8;
|
||||
troika_number |= troika_temp_ptr[i];
|
||||
}
|
||||
troika_number >>= 4;
|
||||
|
||||
furi_string_printf(
|
||||
parsed_data,
|
||||
"\e#Troika+Plantain\nPN: %llu-\nPB: %lu rur.\nTN: %lu\nTB: %u rur.\n",
|
||||
card_number,
|
||||
balance,
|
||||
troika_number,
|
||||
troika_balance);
|
||||
|
||||
parsed = true;
|
||||
} while(false);
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/* Actual implementation of app<>plugin interface */
|
||||
static const NfcSupportedCardsPlugin two_cities_plugin = {
|
||||
.protocol = NfcProtocolMfClassic,
|
||||
.verify = two_cities_verify,
|
||||
.read = two_cities_read,
|
||||
.parse = two_cities_parse,
|
||||
};
|
||||
|
||||
/* Plugin descriptor to comply with basic plugin specification */
|
||||
static const FlipperAppPluginDescriptor two_cities_plugin_descriptor = {
|
||||
.appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID,
|
||||
.ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION,
|
||||
.entry_point = &two_cities_plugin,
|
||||
};
|
||||
|
||||
/* Plugin entry point - must return a pointer to const descriptor */
|
||||
const FlipperAppPluginDescriptor* two_cities_plugin_ep() {
|
||||
return &two_cities_plugin_descriptor;
|
||||
}
|
||||
Reference in New Issue
Block a user