1
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:
gornekich
2023-10-24 07:08:09 +04:00
committed by GitHub
parent 35c903494c
commit d92b0a82cc
514 changed files with 41488 additions and 68125 deletions

View 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;
}

View 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;
}

View File

@@ -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;

View 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, &timestamp);
// 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, &timestamp, locale_get_date_format(), "-");
furi_string_cat(parsed_data, timestamp_str);
furi_string_cat(parsed_data, " at ");
locale_format_time(timestamp_str, &timestamp, 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;
}

View 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;
}

View 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;
}

View 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;
}