From 7ebef9616f30f2ab651786bc5d24d55a15f6be3e Mon Sep 17 00:00:00 2001 From: Andrea Santaniello Date: Thu, 6 Nov 2025 17:34:04 +0100 Subject: [PATCH] public release 0.4 --- application.fam | 39 +++ cogs_mikai.h | 137 ++++++++ cogs_mikai.png | Bin 0 -> 134 bytes cogs_mikai_app.c | 106 ++++++ dist/cogs_mikai.fap | Bin 0 -> 34496 bytes dist/debug/cogs_mikai_d.elf | Bin 0 -> 213644 bytes images/.gitkeep | 0 mykey_core.c | 492 +++++++++++++++++++++++++++ nfc_srix.c | 152 +++++++++ parse_mykey_file.py | 160 +++++++++ scenes/cogs_mikai_scene.c | 26 ++ scenes/cogs_mikai_scene_about.c | 25 ++ scenes/cogs_mikai_scene_add_credit.c | 207 +++++++++++ scenes/cogs_mikai_scene_config.c | 11 + scenes/cogs_mikai_scene_config.h | 26 ++ scenes/cogs_mikai_scene_debug.c | 177 ++++++++++ scenes/cogs_mikai_scene_info.c | 119 +++++++ scenes/cogs_mikai_scene_load_file.c | 191 +++++++++++ scenes/cogs_mikai_scene_read.c | 57 ++++ scenes/cogs_mikai_scene_reset.c | 57 ++++ scenes/cogs_mikai_scene_save_file.c | 158 +++++++++ scenes/cogs_mikai_scene_set_credit.c | 193 +++++++++++ scenes/cogs_mikai_scene_start.c | 158 +++++++++ scenes/cogs_mikai_scene_write_card.c | 74 ++++ 24 files changed, 2565 insertions(+) create mode 100644 application.fam create mode 100644 cogs_mikai.h create mode 100644 cogs_mikai.png create mode 100644 cogs_mikai_app.c create mode 100644 dist/cogs_mikai.fap create mode 100644 dist/debug/cogs_mikai_d.elf create mode 100644 images/.gitkeep create mode 100644 mykey_core.c create mode 100644 nfc_srix.c create mode 100644 parse_mykey_file.py create mode 100644 scenes/cogs_mikai_scene.c create mode 100644 scenes/cogs_mikai_scene_about.c create mode 100644 scenes/cogs_mikai_scene_add_credit.c create mode 100644 scenes/cogs_mikai_scene_config.c create mode 100644 scenes/cogs_mikai_scene_config.h create mode 100644 scenes/cogs_mikai_scene_debug.c create mode 100644 scenes/cogs_mikai_scene_info.c create mode 100644 scenes/cogs_mikai_scene_load_file.c create mode 100644 scenes/cogs_mikai_scene_read.c create mode 100644 scenes/cogs_mikai_scene_reset.c create mode 100644 scenes/cogs_mikai_scene_save_file.c create mode 100644 scenes/cogs_mikai_scene_set_credit.c create mode 100644 scenes/cogs_mikai_scene_start.c create mode 100644 scenes/cogs_mikai_scene_write_card.c diff --git a/application.fam b/application.fam new file mode 100644 index 0000000..852a7d6 --- /dev/null +++ b/application.fam @@ -0,0 +1,39 @@ +# COGS MyKai +App( + appid="cogs_mikai", + name="COGS - MyKey", + apptype=FlipperAppType.EXTERNAL, + entry_point="cogs_mikai_app", + stack_size=4 * 1024, + fap_category="NFC", + fap_version="1.1", + fap_icon="cogs_mikai.png", + fap_description="Read and edit COGES MyKey NFC cards.", + fap_author="luhf", + fap_weburl="https://monocul.us/", + fap_icon_assets="images", + sources=[ + "cogs_mikai_app.c", + "mykey_core.c", + "nfc_srix.c", + "scenes/cogs_mikai_scene.c", + "scenes/cogs_mikai_scene_start.c", + "scenes/cogs_mikai_scene_read.c", + "scenes/cogs_mikai_scene_info.c", + "scenes/cogs_mikai_scene_write_card.c", + "scenes/cogs_mikai_scene_add_credit.c", + "scenes/cogs_mikai_scene_set_credit.c", + "scenes/cogs_mikai_scene_reset.c", + "scenes/cogs_mikai_scene_save_file.c", + "scenes/cogs_mikai_scene_load_file.c", + "scenes/cogs_mikai_scene_debug.c", + "scenes/cogs_mikai_scene_about.c", + ], + requires=[ + "gui", + "dialogs", + ], + provides=[ + "cogs_mikai", + ], +) diff --git a/cogs_mikai.h b/cogs_mikai.h new file mode 100644 index 0000000..961281c --- /dev/null +++ b/cogs_mikai.h @@ -0,0 +1,137 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define TAG "COGSMyKai" + +// SRIX4K Constants +#define SRIX_BLOCK_LENGTH 4 +#define SRIX_UID_LENGTH 8 +#define SRIX4K_BLOCKS 128 +#define SRIX4K_BYTES 512 + +typedef enum { + COGSMyKaiViewSubmenu, + COGSMyKaiViewTextInput, + COGSMyKaiViewPopup, + COGSMyKaiViewWidget, + COGSMyKaiViewTextBox, +} COGSMyKaiView; + +typedef enum { + COGSMyKaiSceneStart, + COGSMyKaiSceneRead, + COGSMyKaiSceneInfo, + COGSMyKaiSceneWriteCard, + COGSMyKaiSceneAddCredit, + COGSMyKaiSceneSetCredit, + COGSMyKaiSceneReset, + COGSMyKaiSceneSaveFile, + COGSMyKaiSceneLoadFile, + COGSMyKaiSceneDebug, + COGSMyKaiSceneAbout, + COGSMyKaiSceneCount, +} COGSMyKaiScene; + +typedef struct { + uint32_t eeprom[SRIX4K_BLOCKS]; + uint64_t uid; + uint32_t encryption_key; + bool is_loaded; + bool is_reset; + bool is_modified; + uint16_t current_credit; +} MyKeyData; + +typedef struct { + Gui* gui; + ViewDispatcher* view_dispatcher; + SceneManager* scene_manager; + Submenu* submenu; + TextInput* text_input; + Popup* popup; + Widget* widget; + TextBox* text_box; + FuriString* text_box_store; + DialogsApp* dialogs; + NotificationApp* notifications; + + MyKeyData mykey; + char text_buffer[32]; + uint32_t temp_credit_value; +} COGSMyKaiApp; + +// Scene handler function declarations +void cogs_mikai_scene_start_on_enter(void* context); +bool cogs_mikai_scene_start_on_event(void* context, SceneManagerEvent event); +void cogs_mikai_scene_start_on_exit(void* context); + +void cogs_mikai_scene_read_on_enter(void* context); +bool cogs_mikai_scene_read_on_event(void* context, SceneManagerEvent event); +void cogs_mikai_scene_read_on_exit(void* context); + +void cogs_mikai_scene_info_on_enter(void* context); +bool cogs_mikai_scene_info_on_event(void* context, SceneManagerEvent event); +void cogs_mikai_scene_info_on_exit(void* context); + +void cogs_mikai_scene_add_credit_on_enter(void* context); +bool cogs_mikai_scene_add_credit_on_event(void* context, SceneManagerEvent event); +void cogs_mikai_scene_add_credit_on_exit(void* context); + +void cogs_mikai_scene_set_credit_on_enter(void* context); +bool cogs_mikai_scene_set_credit_on_event(void* context, SceneManagerEvent event); +void cogs_mikai_scene_set_credit_on_exit(void* context); + +void cogs_mikai_scene_reset_on_enter(void* context); +bool cogs_mikai_scene_reset_on_event(void* context, SceneManagerEvent event); +void cogs_mikai_scene_reset_on_exit(void* context); + +void cogs_mikai_scene_write_card_on_enter(void* context); +bool cogs_mikai_scene_write_card_on_event(void* context, SceneManagerEvent event); +void cogs_mikai_scene_write_card_on_exit(void* context); + +void cogs_mikai_scene_save_file_on_enter(void* context); +bool cogs_mikai_scene_save_file_on_event(void* context, SceneManagerEvent event); +void cogs_mikai_scene_save_file_on_exit(void* context); + +void cogs_mikai_scene_load_file_on_enter(void* context); +bool cogs_mikai_scene_load_file_on_event(void* context, SceneManagerEvent event); +void cogs_mikai_scene_load_file_on_exit(void* context); + +void cogs_mikai_scene_debug_on_enter(void* context); +bool cogs_mikai_scene_debug_on_event(void* context, SceneManagerEvent event); +void cogs_mikai_scene_debug_on_exit(void* context); + +void cogs_mikai_scene_about_on_enter(void* context); +bool cogs_mikai_scene_about_on_event(void* context, SceneManagerEvent event); +void cogs_mikai_scene_about_on_exit(void* context); + +// Scene handlers declaration +extern const SceneManagerHandlers cogs_mikai_scene_handlers; + +// MyKey operations +bool mykey_read_from_nfc(COGSMyKaiApp* app); +bool mykey_write_to_nfc(COGSMyKaiApp* app); +void mykey_calculate_encryption_key(MyKeyData* key); +bool mykey_is_reset(MyKeyData* key); +uint16_t mykey_get_current_credit(MyKeyData* key); +uint16_t mykey_get_credit_from_history(MyKeyData* key); +void mykey_encode_decode_block(uint32_t* block); +bool mykey_add_cents(MyKeyData* key, uint16_t cents, uint8_t day, uint8_t month, uint8_t year); +bool mykey_set_cents(MyKeyData* key, uint16_t cents, uint8_t day, uint8_t month, uint8_t year); +void mykey_reset(MyKeyData* key); +uint32_t mykey_get_block(MyKeyData* key, uint8_t block_num); +void mykey_modify_block(MyKeyData* key, uint32_t block, uint8_t block_num); +bool mykey_save_raw_data(COGSMyKaiApp* app, const char* path); diff --git a/cogs_mikai.png b/cogs_mikai.png new file mode 100644 index 0000000000000000000000000000000000000000..cabbae58c353a94b6da348b3f868e31707d54e1b GIT binary patch literal 134 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2xGmzZ=C-xtZVk{1FcVfJGQl}os;VkfoEM{Qf z76xHPhFNnYfP!oRJ|V9E|NjRvLl0f915%QnE{-7;bCOdM5~friSy3_n;y;BB7q@Th ZjOD7F+}Uv}xPgioJYD@<);T3K0RRf4Bi;Z2 literal 0 HcmV?d00001 diff --git a/cogs_mikai_app.c b/cogs_mikai_app.c new file mode 100644 index 0000000..7d14c68 --- /dev/null +++ b/cogs_mikai_app.c @@ -0,0 +1,106 @@ +#include "cogs_mikai.h" + +static bool cogs_mikai_custom_event_callback(void* context, uint32_t event) { + furi_assert(context); + COGSMyKaiApp* app = context; + return scene_manager_handle_custom_event(app->scene_manager, event); +} + +static bool cogs_mikai_back_event_callback(void* context) { + furi_assert(context); + COGSMyKaiApp* app = context; + return scene_manager_handle_back_event(app->scene_manager); +} + +static COGSMyKaiApp* cogs_mikai_app_alloc() { + COGSMyKaiApp* app = malloc(sizeof(COGSMyKaiApp)); + + app->gui = furi_record_open(RECORD_GUI); + app->dialogs = furi_record_open(RECORD_DIALOGS); + app->notifications = furi_record_open(RECORD_NOTIFICATION); + + app->view_dispatcher = view_dispatcher_alloc(); + app->scene_manager = scene_manager_alloc(&cogs_mikai_scene_handlers, app); + view_dispatcher_set_event_callback_context(app->view_dispatcher, app); + + view_dispatcher_set_custom_event_callback( + app->view_dispatcher, cogs_mikai_custom_event_callback); + view_dispatcher_set_navigation_event_callback( + app->view_dispatcher, cogs_mikai_back_event_callback); + + view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); + + // Allocate views + app->submenu = submenu_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, COGSMyKaiViewSubmenu, submenu_get_view(app->submenu)); + + app->text_input = text_input_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, COGSMyKaiViewTextInput, text_input_get_view(app->text_input)); + + app->popup = popup_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, COGSMyKaiViewPopup, popup_get_view(app->popup)); + + app->widget = widget_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, COGSMyKaiViewWidget, widget_get_view(app->widget)); + + app->text_box = text_box_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, COGSMyKaiViewTextBox, text_box_get_view(app->text_box)); + app->text_box_store = furi_string_alloc(); + + // Initialize MyKey data + memset(&app->mykey, 0, sizeof(MyKeyData)); + app->mykey.is_loaded = false; + + scene_manager_next_scene(app->scene_manager, COGSMyKaiSceneStart); + + return app; +} + +static void cogs_mikai_app_free(COGSMyKaiApp* app) { + furi_assert(app); + + // Remove views + view_dispatcher_remove_view(app->view_dispatcher, COGSMyKaiViewSubmenu); + view_dispatcher_remove_view(app->view_dispatcher, COGSMyKaiViewTextInput); + view_dispatcher_remove_view(app->view_dispatcher, COGSMyKaiViewPopup); + view_dispatcher_remove_view(app->view_dispatcher, COGSMyKaiViewWidget); + view_dispatcher_remove_view(app->view_dispatcher, COGSMyKaiViewTextBox); + + submenu_free(app->submenu); + text_input_free(app->text_input); + popup_free(app->popup); + widget_free(app->widget); + text_box_free(app->text_box); + furi_string_free(app->text_box_store); + + // Free view dispatcher and scene manager + view_dispatcher_free(app->view_dispatcher); + scene_manager_free(app->scene_manager); + + // Close records + furi_record_close(RECORD_GUI); + furi_record_close(RECORD_DIALOGS); + furi_record_close(RECORD_NOTIFICATION); + + free(app); +} + +int32_t cogs_mikai_app(void* p) { + UNUSED(p); + COGSMyKaiApp* app = cogs_mikai_app_alloc(); + + FURI_LOG_I(TAG, "COGS MyKai app started"); + + view_dispatcher_run(app->view_dispatcher); + + FURI_LOG_I(TAG, "COGS MyKai app stopped"); + + cogs_mikai_app_free(app); + + return 0; +} diff --git a/dist/cogs_mikai.fap b/dist/cogs_mikai.fap new file mode 100644 index 0000000000000000000000000000000000000000..2a808dd84786d4f27457ec26ab8d7e12256c6526 GIT binary patch literal 34496 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 Ncj3Nl5CIJ4``^KDV9@{o literal 0 HcmV?d00001 diff --git a/dist/debug/cogs_mikai_d.elf b/dist/debug/cogs_mikai_d.elf new file mode 100644 index 0000000000000000000000000000000000000000..a0d55a03021fbe23dce5d61206fefa4f804cd42b GIT binary patch literal 213644 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 Ox!YJyWNsv8ZudWW{8f(t literal 0 HcmV?d00001 diff --git a/images/.gitkeep b/images/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/mykey_core.c b/mykey_core.c new file mode 100644 index 0000000..0fcb80a --- /dev/null +++ b/mykey_core.c @@ -0,0 +1,492 @@ +#include "cogs_mikai.h" +#include +#include +#include +#include + +// Encode or decode a MyKey block (XOR bit manipulation) +static inline void encode_decode_block(uint32_t* block) { + *block ^= (*block & 0x00C00000) << 6 | (*block & 0x0000C000) << 12 | (*block & 0x000000C0) << 18 | + (*block & 0x000C0000) >> 6 | (*block & 0x00030000) >> 12 | (*block & 0x00000300) >> 6; + *block ^= (*block & 0x30000000) >> 6 | (*block & 0x0C000000) >> 12 | (*block & 0x03000000) >> 18 | + (*block & 0x00003000) << 6 | (*block & 0x00000030) << 12 | (*block & 0x0000000C) << 6; + *block ^= (*block & 0x00C00000) << 6 | (*block & 0x0000C000) << 12 | (*block & 0x000000C0) << 18 | + (*block & 0x000C0000) >> 6 | (*block & 0x00030000) >> 12 | (*block & 0x00000300) >> 6; +} + +// Public wrapper for debug purposes +void mykey_encode_decode_block(uint32_t* block) { + encode_decode_block(block); +} + +// Calculate checksum of a generic block +static inline void calculate_block_checksum(uint32_t* block, const uint8_t block_num) { + uint8_t checksum = 0xFF - block_num - (*block & 0x0F) - (*block >> 4 & 0x0F) - + (*block >> 8 & 0x0F) - (*block >> 12 & 0x0F) - (*block >> 16 & 0x0F) - + (*block >> 20 & 0x0F); + + // Clear first byte and set to checksum value + *block &= 0x00FFFFFF; + *block |= checksum << 24; +} + +// Return the number of days between 1/1/1995 and a specified date +static uint32_t days_difference(uint8_t day, uint8_t month, uint16_t year) { + if(month < 3) { + year--; + month += 12; + } + return year * 365 + year / 4 - year / 100 + year / 400 + (month * 153 + 3) / 5 + day - 728692; +} + +// Get current transaction offset +static uint8_t get_current_transaction_offset(MyKeyData* key) { + uint32_t block3C = key->eeprom[0x3C]; + + // If first transaction, set the pointer to 7 to fill the first transaction block + if(block3C == 0xFFFFFFFF) { + return 0x07; + } + + // Decode transaction pointer + uint32_t current = block3C ^ (key->eeprom[0x07] & 0x00FFFFFF); + encode_decode_block(¤t); + + if((current & 0x00FF0000 >> 16) > 0x07) { + // Out of range + return 0x07; + } else { + // Return result (a value between 0x00 and 0x07) + return current >> 16; + } +} + +// Calculate the encryption key and save the result in key struct +void mykey_calculate_encryption_key(MyKeyData* key) { + FURI_LOG_I(TAG, "=== Encryption Key Calculation ==="); + FURI_LOG_I(TAG, "UID (as stored): 0x%016llX", key->uid); + + // OTP calculation (reverse block 6 + 1, incremental. 1,2,3, etc.) + uint32_t block6 = key->eeprom[0x06]; + FURI_LOG_I(TAG, "Block 0x06 raw: 0x%08lX", block6); + + uint32_t block6_reversed = __bswap32(block6); + FURI_LOG_I(TAG, "Block 0x06 reversed: 0x%08lX", block6_reversed); + + uint32_t otp = ~block6_reversed + 1; + FURI_LOG_I(TAG, "OTP (~reversed + 1): 0x%08lX", otp); + + // Encryption key calculation + // MK = UID * VENDOR + // SK (Encryption key) = MK * OTP + uint32_t block18_raw = key->eeprom[0x18]; + uint32_t block19_raw = key->eeprom[0x19]; + FURI_LOG_I(TAG, "Block 0x18 raw: 0x%08lX", block18_raw); + FURI_LOG_I(TAG, "Block 0x19 raw: 0x%08lX", block19_raw); + + uint32_t block18 = block18_raw; + uint32_t block19 = block19_raw; + encode_decode_block(&block18); + encode_decode_block(&block19); + FURI_LOG_I(TAG, "Block 0x18 decoded: 0x%08lX", block18); + FURI_LOG_I(TAG, "Block 0x19 decoded: 0x%08lX", block19); + + uint64_t vendor = (((uint64_t)block18 << 16) | (block19 & 0x0000FFFF)) + 1; + FURI_LOG_I(TAG, "Vendor: 0x%llX", vendor); + + // Calculate encryption key: UID * vendor * OTP + // UID is now correctly stored in big-endian format, no swapping needed + key->encryption_key = (key->uid * vendor * otp) & 0xFFFFFFFF; + FURI_LOG_I(TAG, "Encryption Key: 0x%08lX", key->encryption_key); + FURI_LOG_I(TAG, "==================================="); +} + +// Check if MyKey is reset (no vendor bound) +bool mykey_is_reset(MyKeyData* key) { + static const uint32_t block18_reset = 0x8FCD0F48; + static const uint32_t block19_reset = 0xC0820007; + return key->eeprom[0x18] == block18_reset && key->eeprom[0x19] == block19_reset; +} + +// Get block value +uint32_t mykey_get_block(MyKeyData* key, uint8_t block_num) { + if(block_num >= SRIX4K_BLOCKS) return 0; + return key->eeprom[block_num]; +} + +// Modify block +void mykey_modify_block(MyKeyData* key, uint32_t block, uint8_t block_num) { + if(block_num < SRIX4K_BLOCKS) { + key->eeprom[block_num] = block; + } +} + +// Extract current credit using libmikai method (block 0x21) - PRIMARY METHOD +uint16_t mykey_get_current_credit(MyKeyData* key) { + FURI_LOG_I(TAG, "=== Credit Decoding (libmikai method) ==="); + + // Use libmikai approach: read from block 0x21 + uint32_t block21_raw = key->eeprom[0x21]; + FURI_LOG_I(TAG, "Block 0x21 raw: 0x%08lX", block21_raw); + FURI_LOG_I(TAG, " Bytes: [%02X %02X %02X %02X]", + (uint8_t)(block21_raw & 0xFF), + (uint8_t)((block21_raw >> 8) & 0xFF), + (uint8_t)((block21_raw >> 16) & 0xFF), + (uint8_t)((block21_raw >> 24) & 0xFF)); + + FURI_LOG_I(TAG, "Encryption key: 0x%08lX", key->encryption_key); + + uint32_t after_xor = block21_raw ^ key->encryption_key; + FURI_LOG_I(TAG, "After XOR: 0x%08lX", after_xor); + + uint32_t current_credit = after_xor; + encode_decode_block(¤t_credit); + FURI_LOG_I(TAG, "After encode_decode: 0x%08lX", current_credit); + + uint16_t credit_lower = current_credit & 0xFFFF; + uint16_t credit_upper = (current_credit >> 16) & 0xFFFF; + FURI_LOG_I(TAG, "Lower 16 bits: %u cents (%u.%02u EUR)", + credit_lower, credit_lower / 100, credit_lower % 100); + FURI_LOG_I(TAG, "Upper 16 bits: %u cents (%u.%02u EUR)", + credit_upper, credit_upper / 100, credit_upper % 100); + FURI_LOG_I(TAG, "========================================="); + + return credit_lower; +} + +// Get credit from transaction history (for comparison/debugging) +uint16_t mykey_get_credit_from_history(MyKeyData* key) { + uint32_t block3C = key->eeprom[0x3C]; + if(block3C == 0xFFFFFFFF) { + return 0xFFFF; // No history available + } + + // Decrypt block 0x3C to get starting offset + uint32_t decrypted_3C = block3C ^ key->eeprom[0x07]; + uint32_t starting_offset = ((decrypted_3C & 0x30000000) >> 28) | + ((decrypted_3C & 0x00100000) >> 18); + + if(starting_offset >= 8) { + return 0xFFFF; // Invalid offset + } + + // Get most recent transaction (offset 8 in the circular buffer) + // Blocks are already in big-endian format, credit is in lower 16 bits + uint32_t txn_block = key->eeprom[0x34 + ((starting_offset + 8) % 8)]; + uint16_t credit = txn_block & 0xFFFF; + + FURI_LOG_D(TAG, "Credit from transaction history: %d cents", credit); + return credit; +} + +// Add N cents to MyKey actual credit +bool mykey_add_cents(MyKeyData* key, uint16_t cents, uint8_t day, uint8_t month, uint8_t year) { + FURI_LOG_I(TAG, "=== Adding %u cents (%u.%02u EUR) ===", cents, cents / 100, cents % 100); + + // Check reset key + if(mykey_is_reset(key)) { + FURI_LOG_E(TAG, "Key is reset, cannot add credit"); + return false; + } + + if(key->eeprom[0x06] == 0 || key->eeprom[0x06] == 0xFFFFFFFF) { + FURI_LOG_E(TAG, "Key has no vendor"); + return false; + } + + // Calculate current credit + uint16_t precedent_credit; + uint16_t actual_credit = mykey_get_current_credit(key); + FURI_LOG_I(TAG, "Current credit: %u cents", actual_credit); + + // Get current transaction position + uint8_t current = get_current_transaction_offset(key); + FURI_LOG_I(TAG, "Current transaction offset: %u", current); + + // Split credit into multiple transactions. Stop at 5 cent. + do { + // Save current credit to precedent + precedent_credit = actual_credit; + + // Choose current recharge + if(cents / 200 > 0) { + cents -= 200; + actual_credit += 200; + } else if(cents / 100 > 0) { + cents -= 100; + actual_credit += 100; + } else if(cents / 50 > 0) { + cents -= 50; + actual_credit += 50; + } else if(cents / 20 > 0) { + cents -= 20; + actual_credit += 20; + } else if(cents / 10 > 0) { + cents -= 10; + actual_credit += 10; + } else if(cents / 5 > 0) { + cents -= 5; + actual_credit += 5; + } else { + // Less than 5 cents + actual_credit += cents; + cents = 0; + } + + // Point to new credit position + current = (current == 7) ? 0 : current + 1; + + // Save new credit to history blocks + uint32_t txn_block = (uint32_t)day << 27 | (uint32_t)month << 23 | + (uint32_t)year << 16 | actual_credit; + key->eeprom[0x34 + current] = txn_block; + + FURI_LOG_I(TAG, "Transaction %u: %u cents at block 0x%02X", + current, actual_credit, 0x34 + current); + } while(cents > 5); + + FURI_LOG_I(TAG, "Final credit: %u cents, precedent: %u cents", + actual_credit, precedent_credit); + + // Save new credit to 21 and 25 + key->eeprom[0x21] = actual_credit; + calculate_block_checksum(&key->eeprom[0x21], 0x21); + encode_decode_block(&key->eeprom[0x21]); + key->eeprom[0x21] ^= key->encryption_key; + + key->eeprom[0x25] = actual_credit; + calculate_block_checksum(&key->eeprom[0x25], 0x25); + encode_decode_block(&key->eeprom[0x25]); + key->eeprom[0x25] ^= key->encryption_key; + + // Save precedent credit to 23 and 27 + key->eeprom[0x23] = precedent_credit; + calculate_block_checksum(&key->eeprom[0x23], 0x23); + encode_decode_block(&key->eeprom[0x23]); + + key->eeprom[0x27] = precedent_credit; + calculate_block_checksum(&key->eeprom[0x27], 0x27); + encode_decode_block(&key->eeprom[0x27]); + + // Save transaction pointer to block 3C + key->eeprom[0x3C] = current << 16; + calculate_block_checksum(&key->eeprom[0x3C], 0x3C); + encode_decode_block(&key->eeprom[0x3C]); + key->eeprom[0x3C] ^= key->eeprom[0x07] & 0x00FFFFFF; + + // Increment operation counter (block 0x12, lower 24 bits) + uint32_t op_count = (key->eeprom[0x12] & 0x00FFFFFF) + 1; + key->eeprom[0x12] = (key->eeprom[0x12] & 0xFF000000) | (op_count & 0x00FFFFFF); + FURI_LOG_I(TAG, "Operation counter incremented to: %lu", op_count); + + // Mark as modified + key->is_modified = true; + + return true; +} + +// Reset credit history and charge N cents +bool mykey_set_cents(MyKeyData* key, uint16_t cents, uint8_t day, uint8_t month, uint8_t year) { + // Backup precedent blocks + uint32_t dump[10]; + memcpy(dump, &key->eeprom[0x21], SRIX_BLOCK_LENGTH); + memcpy(dump + 1, &key->eeprom[0x34], 9 * SRIX_BLOCK_LENGTH); + + key->eeprom[0x21] = 0; + calculate_block_checksum(&key->eeprom[0x21], 0x21); + encode_decode_block(&key->eeprom[0x21]); + key->eeprom[0x21] ^= key->encryption_key; + + // Reset transaction history and pointer (0x34-0x3C) + memset(&key->eeprom[0x34], 0xFF, 9 * SRIX_BLOCK_LENGTH); + + // If there is an error, restore precedent dump + if(!mykey_add_cents(key, cents, day, month, year)) { + memcpy(&key->eeprom[0x21], dump, SRIX_BLOCK_LENGTH); + memcpy(&key->eeprom[0x34], dump + 1, 9 * SRIX_BLOCK_LENGTH); + return false; + } + + return true; +} + +// Reset a MyKey to associate it with another vendor +void mykey_reset(MyKeyData* key) { + for(uint8_t i = 0x10; i < SRIX4K_BLOCKS; i++) { + uint32_t current_block; + + switch(i) { + case 0x10: + case 0x14: + case 0x3F: + case 0x43: { + // Key ID (first byte) + days elapsed from production + uint32_t production_date = key->eeprom[0x08]; + + // Decode BCD (Binary Coded Decimal) production date + uint8_t pday = (production_date >> 28 & 0x0F) * 10 + (production_date >> 24 & 0x0F); + uint8_t pmonth = (production_date >> 20 & 0x0F) * 10 + (production_date >> 16 & 0x0F); + uint16_t pyear = (production_date & 0x0F) * 1000 + + (production_date >> 4 & 0x0F) * 100 + + (production_date >> 12 & 0x0F) * 10 + + (production_date >> 8 & 0x0F); + + uint32_t elapsed = days_difference(pday, pmonth, pyear); + current_block = ((key->eeprom[0x07] & 0xFF000000) >> 8) | + (((elapsed / 1000 % 10) << 12) + ((elapsed / 100 % 10) << 8)) | + (((elapsed / 10 % 10) << 4) + (elapsed % 10)); + calculate_block_checksum(¤t_block, i); + break; + } + + case 0x11: + case 0x15: + case 0x40: + case 0x44: + // Key ID [last three bytes] + current_block = key->eeprom[0x07]; + calculate_block_checksum(¤t_block, i); + break; + + case 0x22: + case 0x26: + case 0x51: + case 0x55: { + // Production date (last three bytes) + uint32_t production_date = key->eeprom[0x08]; + current_block = (production_date & 0x0000FF00) << 8 | (production_date & 0x00FF0000) >> 8 | + (production_date & 0xFF000000) >> 24; + calculate_block_checksum(¤t_block, i); + encode_decode_block(¤t_block); + break; + } + + case 0x12: + case 0x16: + case 0x41: + case 0x45: + // Operations counter + current_block = 1; + calculate_block_checksum(¤t_block, i); + break; + + case 0x13: + case 0x17: + case 0x42: + case 0x46: + // Generic blocks + current_block = 0x00040013; + calculate_block_checksum(¤t_block, i); + break; + + case 0x18: + case 0x1C: + case 0x47: + case 0x4B: + // Generic blocks + current_block = 0x0000FEDC; + calculate_block_checksum(¤t_block, i); + encode_decode_block(¤t_block); + break; + + case 0x19: + case 0x1D: + case 0x48: + case 0x4C: + // Generic blocks + current_block = 0x00000123; + calculate_block_checksum(¤t_block, i); + encode_decode_block(¤t_block); + break; + + case 0x21: + case 0x25: + // Current credit (0,00€) + mykey_calculate_encryption_key(key); + current_block = 0; + calculate_block_checksum(¤t_block, i); + encode_decode_block(¤t_block); + current_block ^= key->encryption_key; + break; + + case 0x20: + case 0x24: + case 0x4F: + case 0x53: + // Generic blocks + current_block = 0x00010000; + calculate_block_checksum(¤t_block, i); + encode_decode_block(¤t_block); + break; + + case 0x1A: + case 0x1B: + case 0x1E: + case 0x1F: + case 0x23: + case 0x27: + case 0x49: + case 0x4A: + case 0x4D: + case 0x4E: + case 0x50: + case 0x52: + case 0x54: + case 0x56: + // Generic blocks + current_block = 0; + calculate_block_checksum(¤t_block, i); + encode_decode_block(¤t_block); + break; + + default: + current_block = 0xFFFFFFFF; + break; + } + + // If this block has a different value than EEPROM, modify it + if(key->eeprom[i] != current_block) { + key->eeprom[i] = current_block; + } + } + + // Mark as modified + key->is_modified = true; +} + + +// Save raw card data to file for debugging +bool mykey_save_raw_data(COGSMyKaiApp* app, const char* path) { + if(!app->mykey.is_loaded) { + return false; + } + + Storage* storage = furi_record_open(RECORD_STORAGE); + File* file = storage_file_alloc(storage); + + if(!storage_file_open(file, path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return false; + } + + // Write header + FuriString* line = furi_string_alloc(); + furi_string_printf(line, "MyKey Raw Data Dump\n"); + furi_string_cat_printf(line, "UID: %016llX\n", (unsigned long long)app->mykey.uid); + furi_string_cat_printf(line, "Encryption Key: 0x%08lX\n\n", app->mykey.encryption_key); + storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line)); + + // Write all blocks + for(size_t i = 0; i < SRIX4K_BLOCKS; i++) { + furi_string_printf(line, "Block 0x%02zX: 0x%08lX\n", i, app->mykey.eeprom[i]); + storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line)); + } + + furi_string_free(line); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + + return true; +} diff --git a/nfc_srix.c b/nfc_srix.c new file mode 100644 index 0000000..4368711 --- /dev/null +++ b/nfc_srix.c @@ -0,0 +1,152 @@ +#include "cogs_mikai.h" +#include +#include +#include +#include +#include + +bool mykey_read_from_nfc(COGSMyKaiApp* app) { + FURI_LOG_I(TAG, "Reading SRIX4K from NFC..."); + + bool success = false; + Nfc* nfc = nfc_alloc(); + + // Detect ST25TB card type + St25tbType type; + St25tbError error = st25tb_poller_sync_detect_type(nfc, &type); + + if(error != St25tbErrorNone) { + FURI_LOG_E(TAG, "Failed to detect ST25TB card: %d", error); + nfc_free(nfc); + return false; + } + + // Check if it's SRIX4K (ST25TBX512 or ST25TB04K or ST25TBX4K) + if(type != St25tbTypeX512 && type != St25tbType04k && type != St25tbTypeX4k) { + FURI_LOG_E(TAG, "Card is not SRIX4K compatible, type: %d", type); + nfc_free(nfc); + return false; + } + + FURI_LOG_I(TAG, "Detected ST25TB card type: %d", type); + + // Allocate ST25TB data structure + St25tbData* st25tb_data = st25tb_alloc(); + + // Read entire card + error = st25tb_poller_sync_read(nfc, st25tb_data); + + if(error != St25tbErrorNone) { + FURI_LOG_E(TAG, "Failed to read ST25TB card: %d", error); + st25tb_free(st25tb_data); + nfc_free(nfc); + return false; + } + + // Extract UID (8 bytes for ST25TB) + // ST25TB UID bytes are in order [0..7], we need to assemble them big-endian + // to match libmikai: uid[0] is MSB (bits 56-63), uid[7] is LSB (bits 0-7) + app->mykey.uid = 0; + for(size_t i = 0; i < ST25TB_UID_SIZE && i < 8; i++) { + app->mykey.uid |= ((uint64_t)st25tb_data->uid[i]) << ((7 - i) * 8); + } + + FURI_LOG_I(TAG, "Card UID (big-endian): %016llX", app->mykey.uid); + FURI_LOG_I(TAG, "UID bytes: %02X %02X %02X %02X %02X %02X %02X %02X", + st25tb_data->uid[0], st25tb_data->uid[1], st25tb_data->uid[2], st25tb_data->uid[3], + st25tb_data->uid[4], st25tb_data->uid[5], st25tb_data->uid[6], st25tb_data->uid[7]); + + // Copy blocks to MyKey data structure + // ST25TB stores data in blocks, we need to read all 128 blocks (512 bytes total) + size_t num_blocks = st25tb_get_block_count(type); + if(num_blocks > SRIX4K_BLOCKS) { + num_blocks = SRIX4K_BLOCKS; + } + + // ST25TB blocks need byte-swapping to match libmikai's big-endian format + // Flipper SDK stores blocks in little-endian, libmikai expects big-endian + for(size_t i = 0; i < num_blocks; i++) { + app->mykey.eeprom[i] = __bswap32(st25tb_data->blocks[i]); + } + + FURI_LOG_I(TAG, "Blocks byte-swapped to big-endian format"); + + // Calculate encryption key from UID + mykey_calculate_encryption_key(&app->mykey); + + // Update cached values + app->mykey.is_loaded = true; + app->mykey.is_modified = false; // Fresh read from card + app->mykey.is_reset = mykey_is_reset(&app->mykey); + app->mykey.current_credit = mykey_get_current_credit(&app->mykey); + + FURI_LOG_I(TAG, "Card loaded successfully. Credit: %d cents, Reset: %s", + app->mykey.current_credit, + app->mykey.is_reset ? "Yes" : "No"); + + success = true; + + st25tb_free(st25tb_data); + nfc_free(nfc); + + return success; +} + +bool mykey_write_to_nfc(COGSMyKaiApp* app) { + FURI_LOG_I(TAG, "Writing to SRIX4K via NFC..."); + + if(!app->mykey.is_loaded) { + FURI_LOG_E(TAG, "No card data loaded, cannot write"); + return false; + } + + bool success = true; + Nfc* nfc = nfc_alloc(); + + // Detect ST25TB card type + St25tbType type; + St25tbError error = st25tb_poller_sync_detect_type(nfc, &type); + + if(error != St25tbErrorNone) { + FURI_LOG_E(TAG, "Failed to detect ST25TB card: %d", error); + nfc_free(nfc); + return false; + } + + // Check if it's SRIX4K + if(type != St25tbTypeX512 && type != St25tbType04k && type != St25tbTypeX4k) { + FURI_LOG_E(TAG, "Card is not SRIX4K compatible, type: %d", type); + nfc_free(nfc); + return false; + } + + size_t num_blocks = st25tb_get_block_count(type); + if(num_blocks > SRIX4K_BLOCKS) { + num_blocks = SRIX4K_BLOCKS; + } + + // Write each block + // Note: Block 0 (UID) is typically read-only, so we skip it + for(size_t i = 1; i < num_blocks; i++) { + // Byte-swap block back to little-endian for ST25TB card + // Our internal format is big-endian, ST25TB expects little-endian + uint32_t block_to_write = __bswap32(app->mykey.eeprom[i]); + error = st25tb_poller_sync_write_block(nfc, i, block_to_write); + + if(error != St25tbErrorNone) { + FURI_LOG_E(TAG, "Failed to write block %zu: %d", i, error); + success = false; + // Continue trying to write remaining blocks + } + } + + if(success) { + FURI_LOG_I(TAG, "Card written successfully"); + } else { + FURI_LOG_W(TAG, "Card write completed with errors"); + } + + nfc_free(nfc); + + return success; +} diff --git a/parse_mykey_file.py b/parse_mykey_file.py new file mode 100644 index 0000000..c8cba6b --- /dev/null +++ b/parse_mykey_file.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +""" +COGES MyKey File Parser +Parses .myk files created by the COGES MyKai Flipper Zero application. + - a luhf shitscript +""" + +import sys +from datetime import datetime + +def bswap32(val): + """Byte swap 32-bit value""" + return ((val & 0xFF) << 24) | ((val & 0xFF00) << 8) | ((val & 0xFF0000) >> 8) | ((val >> 24) & 0xFF) + +def encode_decode_block(block): + """libmikai encode_decode function""" + block ^= ((block & 0x00C00000) << 6 | (block & 0x0000C000) << 12 | (block & 0x000000C0) << 18 | + (block & 0x000C0000) >> 6 | (block & 0x00030000) >> 12 | (block & 0x00000300) >> 6) + block ^= ((block & 0x30000000) >> 6 | (block & 0x0C000000) >> 12 | (block & 0x03000000) >> 18 | + (block & 0x00003000) << 6 | (block & 0x00000030) << 12 | (block & 0x0000000C) << 6) + block ^= ((block & 0x00C00000) << 6 | (block & 0x0000C000) << 12 | (block & 0x000000C0) << 18 | + (block & 0x000C0000) >> 6 | (block & 0x00030000) >> 12 | (block & 0x00000300) >> 6) + return block & 0xFFFFFFFF + +def parse_mykey_file(filename): + """Parse a .myk file and display its contents""" + try: + with open(filename, 'r') as f: + lines = f.readlines() + except FileNotFoundError: + print(f"Error: File '{filename}' not found") + return False + except Exception as e: + print(f"Error reading file: {e}") + return False + + # Verify header + if not lines[0].startswith("COGES_MYKEY_V1"): + print("Error: Invalid file format (missing header)") + return False + + # Parse UID + uid_line = lines[1].strip() + if not uid_line.startswith("UID:"): + print("Error: Invalid file format (missing UID)") + return False + uid = int(uid_line.split(":")[1].strip(), 16) + + # Parse encryption key + key_line = lines[2].strip() + if not key_line.startswith("ENCRYPTION_KEY:"): + print("Error: Invalid file format (missing encryption key)") + return False + encryption_key = int(key_line.split(":")[1].strip(), 16) + + # Parse blocks + blocks = {} + for line in lines[3:]: + line = line.strip() + if line.startswith("BLOCK_"): + parts = line.split(":") + block_num = int(parts[0].split("_")[1]) + block_val = int(parts[1].strip(), 16) + blocks[block_num] = block_val + + if len(blocks) != 128: + print(f"Warning: Expected 128 blocks, found {len(blocks)}") + + # Display information + print("=" * 60) + print(" COGES MyKey Card Information") + print("=" * 60) + print(f"\nUID: 0x{uid:016X}") + print(f"Encryption Key: 0x{encryption_key:08X}") + + # Serial number (block 0x07 in BCD format) + serial = blocks.get(0x07, 0) + print(f"Serial Number: {serial:08X}") + + # Decode credit from block 0x21 + block_21 = blocks.get(0x21, 0) + current_credit_raw = block_21 ^ encryption_key + current_credit_decoded = encode_decode_block(current_credit_raw) + credit_value = current_credit_decoded & 0xFFFF + print(f"\nCurrent Credit: {credit_value} cents ({credit_value/100:.2f} EUR)") + + # Operation counter (block 0x12, lower 24 bits) + block_12 = blocks.get(0x12, 0) + op_count = block_12 & 0x00FFFFFF + print(f"Operations: {op_count}") + + # Check if reset + block_18 = blocks.get(0x18, 0) + block_19 = blocks.get(0x19, 0) + is_reset = (block_18 == 0x8FCD0F48 and block_19 == 0xC0820007) + print(f"Status: {'Reset' if is_reset else 'Active'}") + + # Parse transaction history + block_3C = blocks.get(0x3C, 0xFFFFFFFF) + block_07 = blocks.get(0x07, 0) + + if block_3C != 0xFFFFFFFF: + block_3C_decrypted = block_3C ^ block_07 + starting_offset = ((block_3C_decrypted & 0x30000000) >> 28) | \ + ((block_3C_decrypted & 0x00100000) >> 18) + + if starting_offset < 8: + # Count transactions + transactions = [] + for i in range(8): + txn_block = blocks.get(0x34 + ((starting_offset + i) % 8), 0xFFFFFFFF) + if txn_block == 0xFFFFFFFF: + break + + day = txn_block >> 27 + month = (txn_block >> 23) & 0xF + year = 2000 + ((txn_block >> 16) & 0x7F) + credit = txn_block & 0xFFFF + + transactions.append({ + 'date': f"{day:02d}/{month:02d}/{year}", + 'credit': credit + }) + + if transactions: + print("\n" + "=" * 60) + print(" Transaction History (Newest First)") + print("=" * 60) + for i, txn in enumerate(reversed(transactions), 1): + print(f"{i}. {txn['date']} - {txn['credit']} cents ({txn['credit']/100:.2f} EUR)") + + # Display interesting blocks + print("\n" + "=" * 60) + print(" Key Blocks") + print("=" * 60) + interesting_blocks = [0x06, 0x07, 0x12, 0x18, 0x19, 0x21, 0x23, 0x25, 0x27, 0x3C] + for block_num in interesting_blocks: + if block_num in blocks: + print(f"Block 0x{block_num:02X}: 0x{blocks[block_num]:08X}") + + return True + +def main(): + if len(sys.argv) < 2: + print("Usage: python3 parse_mykey_file.py ") + print("\nThis script parses COGES MyKey files created by the Flipper Zero app") + print("and displays card information in a human-readable format.") + sys.exit(1) + + filename = sys.argv[1] + if parse_mykey_file(filename): + print("\n" + "=" * 60) + print(" Parsing completed successfully") + print("=" * 60) + else: + sys.exit(1) + +if __name__ == "__main__": + main() +#culo \ No newline at end of file diff --git a/scenes/cogs_mikai_scene.c b/scenes/cogs_mikai_scene.c new file mode 100644 index 0000000..da90c77 --- /dev/null +++ b/scenes/cogs_mikai_scene.c @@ -0,0 +1,26 @@ +#include "../cogs_mikai.h" + +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter, +void (* const cogs_mikai_scene_on_enter_handlers[])(void*) = { +#include "cogs_mikai_scene_config.c" +}; +#undef ADD_SCENE + +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event, +bool (* const cogs_mikai_scene_on_event_handlers[])(void*, SceneManagerEvent) = { +#include "cogs_mikai_scene_config.c" +}; +#undef ADD_SCENE + +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit, +void (* const cogs_mikai_scene_on_exit_handlers[])(void*) = { +#include "cogs_mikai_scene_config.c" +}; +#undef ADD_SCENE + +const SceneManagerHandlers cogs_mikai_scene_handlers = { + .on_enter_handlers = cogs_mikai_scene_on_enter_handlers, + .on_event_handlers = cogs_mikai_scene_on_event_handlers, + .on_exit_handlers = cogs_mikai_scene_on_exit_handlers, + .scene_num = COGSMyKaiSceneCount, +}; diff --git a/scenes/cogs_mikai_scene_about.c b/scenes/cogs_mikai_scene_about.c new file mode 100644 index 0000000..f39e39b --- /dev/null +++ b/scenes/cogs_mikai_scene_about.c @@ -0,0 +1,25 @@ +#include "../cogs_mikai.h" + +void cogs_mikai_scene_about_on_enter(void* context) { + COGSMyKaiApp* app = context; + Widget* widget = app->widget; + + widget_add_string_element(widget, 64, 5, AlignCenter, AlignTop, FontPrimary, "COGS MyKai"); + widget_add_string_element(widget, 64, 18, AlignCenter, AlignTop, FontSecondary, "v0.4"); + widget_add_string_element(widget, 64, 30, AlignCenter, AlignTop, FontSecondary, "COGES MyKey NFC"); + widget_add_string_element(widget, 64, 40, AlignCenter, AlignTop, FontSecondary, "Reader/Writer"); + widget_add_string_element(widget, 64, 52, AlignCenter, AlignTop, FontSecondary, "Based on libmikai, built by luhf"); + + view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewWidget); +} + +bool cogs_mikai_scene_about_on_event(void* context, SceneManagerEvent event) { + UNUSED(context); + UNUSED(event); + return false; +} + +void cogs_mikai_scene_about_on_exit(void* context) { + COGSMyKaiApp* app = context; + widget_reset(app->widget); +} diff --git a/scenes/cogs_mikai_scene_add_credit.c b/scenes/cogs_mikai_scene_add_credit.c new file mode 100644 index 0000000..e798128 --- /dev/null +++ b/scenes/cogs_mikai_scene_add_credit.c @@ -0,0 +1,207 @@ +#include "../cogs_mikai.h" +#include + +enum { + AddCreditSceneEventInput, + AddCreditSceneEventWrite, + AddCreditSceneEventSave, + AddCreditSceneEventDiscard, +}; + +static bool cogs_mikai_scene_add_credit_validator(const char* text, FuriString* error, void* context) { + UNUSED(context); + + if(strlen(text) == 0) { + return true; + } + + // digits and at most one decimal point + bool has_decimal = false; + for(size_t i = 0; text[i] != '\0'; i++) { + if(text[i] == '.' || text[i] == ',') { + if(has_decimal) { + furi_string_set(error, "Only one decimal point"); + return false; + } + has_decimal = true; + } else if(text[i] < '0' || text[i] > '9') { + furi_string_set(error, "Only numbers and '.'"); + return false; + } + } + + return true; +} + +static bool parse_euros_to_cents(const char* text, uint16_t* cents) { + if(!text || !cents) return false; + + uint32_t integer_part = 0; + uint32_t decimal_part = 0; + uint32_t decimal_digits = 0; + bool in_decimal = false; + + for(size_t i = 0; text[i] != '\0'; i++) { + if(text[i] == '.' || text[i] == ',') { + in_decimal = true; + } else if(text[i] >= '0' && text[i] <= '9') { + if(in_decimal) { + if(decimal_digits < 2) { + decimal_part = decimal_part * 10 + (text[i] - '0'); + decimal_digits++; + } + } else { + integer_part = integer_part * 10 + (text[i] - '0'); + } + } + } + if(decimal_digits == 1) { + decimal_part *= 10; + } + + uint32_t total_cents = integer_part * 100 + decimal_part; + if(total_cents > 99999) return false; // Max 999.99 EUR + + *cents = (uint16_t)total_cents; + return true; +} + +static void cogs_mikai_scene_add_credit_text_input_callback(void* context) { + COGSMyKaiApp* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, AddCreditSceneEventInput); +} + +static void cogs_mikai_scene_add_credit_popup_callback(void* context) { + COGSMyKaiApp* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, 0); +} + +void cogs_mikai_scene_add_credit_on_enter(void* context) { + COGSMyKaiApp* app = context; + + if(!app->mykey.is_loaded) { + Popup* popup = app->popup; + popup_set_header(popup, "Error", 64, 10, AlignCenter, AlignTop); + popup_set_text(popup, "No card loaded\nRead a card first", 64, 25, AlignCenter, AlignTop); + popup_set_callback(popup, cogs_mikai_scene_add_credit_popup_callback); + popup_set_context(popup, app); + popup_set_timeout(popup, 2000); + popup_enable_timeout(popup); + view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup); + return; + } + + TextInput* text_input = app->text_input; + snprintf(app->text_buffer, sizeof(app->text_buffer), "5.00"); + + text_input_set_header_text(text_input, "Add Credit (EUR)"); + text_input_set_validator(text_input, cogs_mikai_scene_add_credit_validator, NULL); + text_input_set_result_callback( + text_input, + cogs_mikai_scene_add_credit_text_input_callback, + app, + app->text_buffer, + sizeof(app->text_buffer), + false); + + view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewTextInput); +} + +bool cogs_mikai_scene_add_credit_on_event(void* context, SceneManagerEvent event) { + COGSMyKaiApp* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == AddCreditSceneEventInput) { + if(app->text_buffer[0] == '\0') { + FURI_LOG_W(TAG, "Ignoring empty text_buffer (already processed)"); + consumed = true; + return consumed; + } + + FURI_LOG_I(TAG, "Add credit input: '%s'", app->text_buffer); + uint16_t cents = 0; + bool parse_ok = parse_euros_to_cents(app->text_buffer, ¢s); + FURI_LOG_I(TAG, "Parse result: %d, cents: %d", parse_ok, cents); + + if(parse_ok && cents > 0) { + FURI_LOG_I(TAG, "Valid amount: %d cents", cents); + + DateTime datetime; + furi_hal_rtc_get_datetime(&datetime); + + bool success = mykey_add_cents( + &app->mykey, + cents, + datetime.day, + datetime.month, + datetime.year - 2000); + + if(success) { + // cache updated credit + app->mykey.current_credit = mykey_get_current_credit(&app->mykey); + + // reset text input to prevent any further callbacks + text_input_reset(app->text_input); + + // clear text buffer to prevent re-triggering + memset(app->text_buffer, 0, sizeof(app->text_buffer)); + + Popup* popup = app->popup; + popup_set_header(popup, "Credit Added!", 64, 10, AlignCenter, AlignTop); + popup_set_text(popup, "Saved in memory\nUse 'Write to Card'", 64, 25, AlignCenter, AlignTop); + popup_set_callback(popup, cogs_mikai_scene_add_credit_popup_callback); + popup_set_context(popup, app); + popup_set_timeout(popup, 2000); + popup_enable_timeout(popup); + view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup); + notification_message(app->notifications, &sequence_success); + } else { + Popup* popup = app->popup; + popup_set_header(popup, "Error", 64, 10, AlignCenter, AlignTop); + popup_set_text(popup, "Failed to add credit", 64, 25, AlignCenter, AlignTop); + popup_set_callback(popup, cogs_mikai_scene_add_credit_popup_callback); + popup_set_context(popup, app); + popup_set_timeout(popup, 2000); + popup_enable_timeout(popup); + view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup); + notification_message(app->notifications, &sequence_error); + } + } else { + FURI_LOG_E(TAG, "Invalid amount: parse_ok=%d, cents=%d", + parse_ok, cents); + Popup* popup = app->popup; + popup_set_header(popup, "Error", 64, 10, AlignCenter, AlignTop); + popup_set_text( + popup, + "Invalid amount\nEnter 0.01-999.99", + 64, + 25, + AlignCenter, + AlignTop); + popup_set_callback(popup, cogs_mikai_scene_add_credit_popup_callback); + popup_set_context(popup, app); + popup_set_timeout(popup, 2000); + popup_enable_timeout(popup); + view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup); + } + consumed = true; + } else { + scene_manager_search_and_switch_to_previous_scene(app->scene_manager, COGSMyKaiSceneStart); + consumed = true; + } + } else if(event.type == SceneManagerEventTypeBack) { + if(app->mykey.is_modified) { + scene_manager_search_and_switch_to_previous_scene(app->scene_manager, COGSMyKaiSceneStart); + consumed = true; + } + } + + return consumed; +} + +void cogs_mikai_scene_add_credit_on_exit(void* context) { + COGSMyKaiApp* app = context; + text_input_reset(app->text_input); + popup_reset(app->popup); +} diff --git a/scenes/cogs_mikai_scene_config.c b/scenes/cogs_mikai_scene_config.c new file mode 100644 index 0000000..a7f84b1 --- /dev/null +++ b/scenes/cogs_mikai_scene_config.c @@ -0,0 +1,11 @@ +ADD_SCENE(cogs_mikai, start, Start) +ADD_SCENE(cogs_mikai, read, Read) +ADD_SCENE(cogs_mikai, info, Info) +ADD_SCENE(cogs_mikai, write_card, WriteCard) +ADD_SCENE(cogs_mikai, add_credit, AddCredit) +ADD_SCENE(cogs_mikai, set_credit, SetCredit) +ADD_SCENE(cogs_mikai, reset, Reset) +ADD_SCENE(cogs_mikai, save_file, SaveFile) +ADD_SCENE(cogs_mikai, load_file, LoadFile) +ADD_SCENE(cogs_mikai, debug, Debug) +ADD_SCENE(cogs_mikai, about, About) diff --git a/scenes/cogs_mikai_scene_config.h b/scenes/cogs_mikai_scene_config.h new file mode 100644 index 0000000..85f46c8 --- /dev/null +++ b/scenes/cogs_mikai_scene_config.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +// Generate scene on_enter handlers declaration +#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*); +#include "cogs_mikai_scene_config.c" +#undef ADD_SCENE + +// Generate scene on_event handlers declaration +#define ADD_SCENE(prefix, name, id) \ + bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event); +#include "cogs_mikai_scene_config.c" +#undef ADD_SCENE + +// Generate scene on_exit handlers declaration +#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context); +#include "cogs_mikai_scene_config.c" +#undef ADD_SCENE + +// Generate scene configuration array +#define ADD_SCENE(prefix, name, id) \ + {.on_enter = prefix##_scene_##name##_on_enter, .on_event = prefix##_scene_##name##_on_event, \ + .on_exit = prefix##_scene_##name##_on_exit}, + +extern const SceneManagerHandlers cogs_mikai_scene_handlers; diff --git a/scenes/cogs_mikai_scene_debug.c b/scenes/cogs_mikai_scene_debug.c new file mode 100644 index 0000000..1965eca --- /dev/null +++ b/scenes/cogs_mikai_scene_debug.c @@ -0,0 +1,177 @@ +#include "../cogs_mikai.h" +#include +#include +#include + +enum { + DebugSceneEventSaveData = 1, +}; + +void cogs_mikai_scene_debug_on_enter(void* context) { + COGSMyKaiApp* app = context; + TextBox* text_box = app->text_box; + FuriString* text = app->text_box_store; + + furi_string_reset(text); + + if(!app->mykey.is_loaded) { + furi_string_cat(text, "No Card Loaded\n\nPlease read a card first."); + } else { + furi_string_cat(text, "=== DEBUG INFO ===\n\n"); + + // UID + furi_string_cat_printf( + text, "UID: %016llX\n", (unsigned long long)app->mykey.uid); + + // lower 32 bits of UID + furi_string_cat_printf( + text, "UID (lower 32): 0x%08lX\n", (uint32_t)app->mykey.uid); + + // encryption key + + furi_string_cat_printf( + text, "Encryption Key: 0x%08lX\n\n", app->mykey.encryption_key); + + // block 0x21 (credit block) analysis + furi_string_cat(text, "--- Block 0x21 Analysis ---\n"); + uint32_t block21_raw = app->mykey.eeprom[0x21]; + furi_string_cat_printf(text, "Raw: 0x%08lX\n", block21_raw); + + // show individual bytes + furi_string_cat_printf(text, "Bytes: [%02X %02X %02X %02X]\n", + (uint8_t)(block21_raw & 0xFF), + (uint8_t)((block21_raw >> 8) & 0xFF), + (uint8_t)((block21_raw >> 16) & 0xFF), + (uint8_t)((block21_raw >> 24) & 0xFF)); + + // uint32_t block21_xor = block21_raw ^ app->mykey.encryption_key; + // furi_string_cat_printf(text, "After XOR: 0x%08lX\n", block21_xor); + + + // uint32_t block21_swapped = __bswap32(block21_raw); + // furi_string_cat_printf(text, "Byte-swapped: 0x%08lX\n", block21_swapped); + + // uint32_t block21_xor_swapped = block21_swapped ^ app->mykey.encryption_key; + // furi_string_cat_printf(text, "Swap then XOR: 0x%08lX\n\n", block21_xor_swapped); + + + // furi_string_cat(text, "--- Test Combinations ---\n"); + + // uint32_t test_a = block21_raw ^ app->mykey.encryption_key; + // mykey_encode_decode_block(&test_a); + // uint16_t credit_a = test_a & 0xFFFF; + // furi_string_cat_printf(text, "A (Raw->XOR->Dec): %u.%02u\n", credit_a / 100, credit_a % 100); + // uint32_t test_b = block21_swapped ^ app->mykey.encryption_key; + // mykey_encode_decode_block(&test_b); + // uint16_t credit_b = test_b & 0xFFFF; + // furi_string_cat_printf(text, "B (Swap->XOR->Dec): %u.%02u\n", credit_b / 100, credit_b % 100); + + // uint32_t test_c = __bswap32(block21_raw ^ app->mykey.encryption_key); + // mykey_encode_decode_block(&test_c); + // uint16_t credit_c = test_c & 0xFFFF; + // furi_string_cat_printf(text, "C (XOR->Swap->Dec): %u.%02u\n", credit_c / 100, credit_c % 100); + + // uint32_t test_d = block21_raw; + // mykey_encode_decode_block(&test_d); + // test_d ^= app->mykey.encryption_key; + // uint16_t credit_d = test_d & 0xFFFF; + // furi_string_cat_printf(text, "D (Dec->XOR): %u.%02u\n\n", credit_d / 100, credit_d % 100); + + // credit calculations + furi_string_cat(text, "--- Credit Readings ---\n"); + + // libmikai + uint16_t credit_libmikai = mykey_get_current_credit(&app->mykey); + furi_string_cat_printf( + text, "libmikai (0x21): %u.%02u EUR\n", credit_libmikai / 100, credit_libmikai % 100); + + // transaction history + uint16_t credit_history = mykey_get_credit_from_history(&app->mykey); + if(credit_history != 0xFFFF) { + furi_string_cat_printf( + text, + "History (0x34+): %u.%02u EUR\n\n", + credit_history / 100, + credit_history % 100); + } else { + furi_string_cat(text, "History: Not available\n\n"); + } + + // key blocks + furi_string_cat(text, "--- Key Blocks ---\n"); + furi_string_cat_printf(text, "Block 0x05: 0x%08lX\n", app->mykey.eeprom[0x05]); + furi_string_cat_printf(text, "Block 0x06: 0x%08lX\n", app->mykey.eeprom[0x06]); + furi_string_cat_printf(text, "Block 0x07: 0x%08lX\n", app->mykey.eeprom[0x07]); + furi_string_cat_printf(text, "Block 0x12: 0x%08lX\n", app->mykey.eeprom[0x12]); + furi_string_cat_printf(text, "Block 0x18: 0x%08lX\n", app->mykey.eeprom[0x18]); + furi_string_cat_printf(text, "Block 0x19: 0x%08lX\n", app->mykey.eeprom[0x19]); + furi_string_cat_printf(text, "Block 0x21: 0x%08lX\n", app->mykey.eeprom[0x21]); + furi_string_cat_printf(text, "Block 0x3C: 0x%08lX\n\n", app->mykey.eeprom[0x3C]); + + furi_string_cat(text, "\n--- Raw data saved in SD ---\n"); + furi_string_cat(text, "Use back to exit, or\n"); + furi_string_cat(text, "check logs for raw data"); + } + + text_box_set_text(text_box, furi_string_get_cstr(text)); + text_box_set_font(text_box, TextBoxFontText); + text_box_set_focus(text_box, TextBoxFocusStart); + + + if(app->mykey.is_loaded) { + DateTime datetime; + furi_hal_rtc_get_datetime(&datetime); + + FuriString* file_path = furi_string_alloc(); + furi_string_printf( + file_path, + "/ext/mykey_debug_%04d%02d%02d_%02d%02d%02d.txt", + datetime.year, + datetime.month, + datetime.day, + datetime.hour, + datetime.minute, + datetime.second); + + Storage* storage = furi_record_open(RECORD_STORAGE); + File* file = storage_file_alloc(storage); + + if(storage_file_open(file, furi_string_get_cstr(file_path), FSAM_WRITE, FSOM_CREATE_ALWAYS)) { + + storage_file_write(file, furi_string_get_cstr(text), furi_string_size(text)); + + + storage_file_write(file, "\n\n--- Raw Data Dump ---\n", strlen("\n\n--- Raw Data Dump ---\n")); + + FuriString* line = furi_string_alloc(); + for(size_t i = 0; i < SRIX4K_BLOCKS; i++) { + furi_string_printf(line, "Block 0x%02zX: 0x%08lX\n", i, app->mykey.eeprom[i]); + storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line)); + } + furi_string_free(line); + + FURI_LOG_I(TAG, "Debug data saved to: %s", furi_string_get_cstr(file_path)); + storage_file_close(file); + } else { + FURI_LOG_E(TAG, "Failed to save debug data"); + } + + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + furi_string_free(file_path); + } + + view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewTextBox); +} + +bool cogs_mikai_scene_debug_on_event(void* context, SceneManagerEvent event) { + UNUSED(context); + UNUSED(event); + return false; +} + +void cogs_mikai_scene_debug_on_exit(void* context) { + COGSMyKaiApp* app = context; + text_box_reset(app->text_box); + furi_string_reset(app->text_box_store); +} diff --git a/scenes/cogs_mikai_scene_info.c b/scenes/cogs_mikai_scene_info.c new file mode 100644 index 0000000..79944b6 --- /dev/null +++ b/scenes/cogs_mikai_scene_info.c @@ -0,0 +1,119 @@ +#include "../cogs_mikai.h" +#include + +void cogs_mikai_scene_info_on_enter(void* context) { + COGSMyKaiApp* app = context; + TextBox* text_box = app->text_box; + FuriString* text = app->text_box_store; + + furi_string_reset(text); + + if(!app->mykey.is_loaded) { + furi_string_cat(text, "No Card Loaded\n\nPlease read a card first."); + } else { + + furi_string_cat_printf(text, "Serial: %08lX\n", (uint32_t)app->mykey.eeprom[0x07]); + + // vendor ID - calculated from blocks 0x18 and 0x19 + uint32_t block18 = app->mykey.eeprom[0x18]; + uint32_t block19 = app->mykey.eeprom[0x19]; + mykey_encode_decode_block(&block18); + mykey_encode_decode_block(&block19); + uint64_t vendor = (((uint64_t)block18 << 16) | (block19 & 0x0000FFFF)) + 1; + + furi_string_cat_printf(text, "Vendor: %llX\n", vendor); + + // current credit + furi_string_cat_printf( + text, + "Credit: %u.%02u EUR\n", + app->mykey.current_credit / 100, + app->mykey.current_credit % 100); + + // card status + furi_string_cat_printf(text, "Status: %s\n", app->mykey.is_reset ? "Reset" : "Active"); + + // operation count (block 0x12, lower 24 bits) + uint32_t op_count = app->mykey.eeprom[0x12] & 0x00FFFFFF; + furi_string_cat_printf(text, "Operations: %lu\n", (unsigned long)op_count); + + // UID + furi_string_cat_printf( + text, + "UID: %08lX%08lX\n", + (uint32_t)(app->mykey.uid >> 32), + (uint32_t)(app->mykey.uid & 0xFFFFFFFF)); + + // parse and display full transaction history + uint32_t block3C = app->mykey.eeprom[0x3C]; + if(block3C != 0xFFFFFFFF) { + block3C ^= app->mykey.eeprom[0x07]; + uint32_t starting_offset = + ((block3C & 0x30000000) >> 28) | ((block3C & 0x00100000) >> 18); + + if(starting_offset < 8) { + // first, find how many transactions exist by going forward from starting_offset + int num_transactions = 0; + for(int i = 0; i < 8; i++) { + uint32_t txn_block = app->mykey.eeprom[0x34 + ((starting_offset + i) % 8)]; + if(txn_block == 0xFFFFFFFF) { + break; + } + num_transactions++; + } + + if(num_transactions > 0) { + furi_string_cat(text, "\n=== Transaction History ===\n"); + furi_string_cat(text, "(Newest first)\n\n"); + + // display transactions in reverse order (newest first) + for(int i = num_transactions - 1; i >= 0; i--) { + uint32_t txn_block = app->mykey.eeprom[0x34 + ((starting_offset + i) % 8)]; + + // extract transaction fields directly from big-endian block + uint8_t day = txn_block >> 27; + uint8_t month = (txn_block >> 23) & 0xF; + uint16_t year = 2000 + ((txn_block >> 16) & 0x7F); + uint16_t credit = txn_block & 0xFFFF; + + furi_string_cat_printf( + text, + "%d. %02d/%02d/%04d - %d.%02d EUR\n", + num_transactions - i, + day, + month, + year, + credit / 100, + credit % 100); + } + } else { + furi_string_cat(text, "\nNo transaction history\n"); + } + } else { + furi_string_cat(text, "\nTransaction history:\n"); + furi_string_cat(text, "Invalid offset\n"); + } + } else { + furi_string_cat(text, "\nTransaction history:\n"); + furi_string_cat(text, "Not available\n"); + } + } + + text_box_set_text(text_box, furi_string_get_cstr(text)); + text_box_set_font(text_box, TextBoxFontText); + text_box_set_focus(text_box, TextBoxFocusStart); + + view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewTextBox); +} + +bool cogs_mikai_scene_info_on_event(void* context, SceneManagerEvent event) { + UNUSED(context); + UNUSED(event); + return false; +} + +void cogs_mikai_scene_info_on_exit(void* context) { + COGSMyKaiApp* app = context; + text_box_reset(app->text_box); + furi_string_reset(app->text_box_store); +} diff --git a/scenes/cogs_mikai_scene_load_file.c b/scenes/cogs_mikai_scene_load_file.c new file mode 100644 index 0000000..fe6f510 --- /dev/null +++ b/scenes/cogs_mikai_scene_load_file.c @@ -0,0 +1,191 @@ +#include "../cogs_mikai.h" +#include +#include +#include +#include + +// Manual hex parser (sscanf %X doesn't work reliably on Flipper) +static bool parse_hex64(const char* str, uint64_t* value) { + if(!str || !value) return false; + *value = 0; + for(size_t i = 0; str[i] != '\0' && i < 16; i++) { + char c = str[i]; + uint8_t digit; + if(c >= '0' && c <= '9') digit = c - '0'; + else if(c >= 'A' && c <= 'F') digit = c - 'A' + 10; + else if(c >= 'a' && c <= 'f') digit = c - 'a' + 10; + else break; + *value = (*value << 4) | digit; + } + return true; +} + +static bool parse_hex32(const char* str, uint32_t* value) { + if(!str || !value) return false; + *value = 0; + for(size_t i = 0; str[i] != '\0' && i < 8; i++) { + char c = str[i]; + uint8_t digit; + if(c >= '0' && c <= '9') digit = c - '0'; + else if(c >= 'A' && c <= 'F') digit = c - 'A' + 10; + else if(c >= 'a' && c <= 'f') digit = c - 'a' + 10; + else break; + *value = (*value << 4) | digit; + } + return true; +} + +static void cogs_mikai_scene_load_file_popup_callback(void* context) { + COGSMyKaiApp* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, 0); +} + +void cogs_mikai_scene_load_file_on_enter(void* context) { + COGSMyKaiApp* app = context; + Popup* popup = app->popup; + + // Show file browser starting in the app's data folder + FuriString* file_path = furi_string_alloc(); + furi_string_set(file_path, "/ext/apps_data/cogs_mikai"); + + // Ensure directory exists + Storage* storage_mkdir = furi_record_open(RECORD_STORAGE); + storage_simply_mkdir(storage_mkdir, "/ext/apps_data/cogs_mikai"); + furi_record_close(RECORD_STORAGE); + + DialogsFileBrowserOptions browser_options; + dialog_file_browser_set_basic_options(&browser_options, ".myk", NULL); + browser_options.hide_ext = false; + + if(dialog_file_browser_show(app->dialogs, file_path, file_path, &browser_options)) { + // User selected a file + Storage* storage = furi_record_open(RECORD_STORAGE); + File* file = storage_file_alloc(storage); + + if(storage_file_open(file, furi_string_get_cstr(file_path), FSAM_READ, FSOM_OPEN_EXISTING)) { + // Read entire file + size_t file_size = storage_file_size(file); + char* file_buffer = malloc(file_size + 1); + bool success = false; + + if(file_buffer) { + size_t bytes_read = storage_file_read(file, file_buffer, file_size); + file_buffer[bytes_read] = '\0'; + + // Parse the file - manual line parsing without strtok + if(bytes_read > 14 && strncmp(file_buffer, "COGES_MYKEY_V1", 14) == 0) { + char* ptr = file_buffer; + char line[128]; + + // Helper to read next line + auto bool read_line(char** p, char* buf, size_t max_len) { + size_t i = 0; + while(**p && **p != '\n' && i < max_len - 1) { + buf[i++] = *(*p)++; + } + buf[i] = '\0'; + if(**p == '\n') (*p)++; + return i > 0; + }; + + // Skip header line + read_line(&ptr, line, sizeof(line)); + + // Read UID + if(read_line(&ptr, line, sizeof(line))) { + char* uid_str = strstr(line, "UID: "); + if(uid_str && parse_hex64(uid_str + 5, &app->mykey.uid)) { + FURI_LOG_I(TAG, "Loaded UID: %016llX", app->mykey.uid); + + // Read encryption key + if(read_line(&ptr, line, sizeof(line))) { + char* key_str = strstr(line, "ENCRYPTION_KEY: "); + if(key_str && parse_hex32(key_str + 16, &app->mykey.encryption_key)) { + FURI_LOG_I(TAG, "Loaded key: %08lX", app->mykey.encryption_key); + success = true; + + // Read blocks + for(size_t i = 0; i < SRIX4K_BLOCKS && success; i++) { + if(read_line(&ptr, line, sizeof(line))) { + char* block_str = strstr(line, ": "); + if(block_str && parse_hex32(block_str + 2, &app->mykey.eeprom[i])) { + // Success + } else { + FURI_LOG_E(TAG, "Failed to parse block %zu: %s", i, line); + success = false; + } + } else { + FURI_LOG_E(TAG, "Failed to read line for block %zu", i); + success = false; + } + } + } else { + FURI_LOG_E(TAG, "Failed to parse encryption key: %s", line); + } + } + } else { + FURI_LOG_E(TAG, "Failed to parse UID: %s", line); + } + } + } + + free(file_buffer); + } + + storage_file_close(file); + + if(success) { + app->mykey.is_loaded = true; + app->mykey.is_modified = false; // Fresh load from file + app->mykey.is_reset = mykey_is_reset(&app->mykey); + app->mykey.current_credit = mykey_get_current_credit(&app->mykey); + + popup_set_header(popup, "Success!", 64, 10, AlignCenter, AlignTop); + popup_set_text(popup, "Card loaded from file", 64, 25, AlignCenter, AlignTop); + notification_message(app->notifications, &sequence_success); + } else { + popup_set_header(popup, "Error", 64, 10, AlignCenter, AlignTop); + popup_set_text(popup, "Invalid file format", 64, 25, AlignCenter, AlignTop); + notification_message(app->notifications, &sequence_error); + } + } else { + popup_set_header(popup, "Error", 64, 10, AlignCenter, AlignTop); + popup_set_text(popup, "Failed to open file", 64, 25, AlignCenter, AlignTop); + notification_message(app->notifications, &sequence_error); + } + + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + } else { + // User cancelled - search back to start scene and switch (forces menu rebuild) + furi_string_free(file_path); + scene_manager_search_and_switch_to_previous_scene(app->scene_manager, COGSMyKaiSceneStart); + return; + } + + furi_string_free(file_path); + + popup_set_callback(popup, cogs_mikai_scene_load_file_popup_callback); + popup_set_context(popup, app); + popup_set_timeout(popup, 2000); + popup_enable_timeout(popup); + view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup); +} + +bool cogs_mikai_scene_load_file_on_event(void* context, SceneManagerEvent event) { + COGSMyKaiApp* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + // Popup timeout - search back to start scene and switch (forces menu rebuild) + scene_manager_search_and_switch_to_previous_scene(app->scene_manager, COGSMyKaiSceneStart); + consumed = true; + } + + return consumed; +} + +void cogs_mikai_scene_load_file_on_exit(void* context) { + COGSMyKaiApp* app = context; + popup_reset(app->popup); +} diff --git a/scenes/cogs_mikai_scene_read.c b/scenes/cogs_mikai_scene_read.c new file mode 100644 index 0000000..edf9b32 --- /dev/null +++ b/scenes/cogs_mikai_scene_read.c @@ -0,0 +1,57 @@ +#include "../cogs_mikai.h" + +static void cogs_mikai_scene_read_popup_callback(void* context) { + COGSMyKaiApp* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, 0); +} + +void cogs_mikai_scene_read_on_enter(void* context) { + COGSMyKaiApp* app = context; + + // Show popup for card detection + Popup* popup = app->popup; + popup_set_header(popup, "Reading Card", 64, 10, AlignCenter, AlignTop); + popup_set_text(popup, "Place COGES MyKey\non Flipper's back", 64, 25, AlignCenter, AlignTop); + popup_set_icon(popup, 0, 0, NULL); + popup_set_callback(popup, cogs_mikai_scene_read_popup_callback); + popup_set_context(popup, app); + popup_set_timeout(popup, 3000); + popup_enable_timeout(popup); + + view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup); + + // Attempt to read NFC card + if(mykey_read_from_nfc(app)) { + // Calculate encryption key + mykey_calculate_encryption_key(&app->mykey); + app->mykey.is_loaded = true; + app->mykey.is_modified = false; // Fresh read from card + app->mykey.is_reset = mykey_is_reset(&app->mykey); + app->mykey.current_credit = mykey_get_current_credit(&app->mykey); + + popup_set_header(popup, "Success!", 64, 10, AlignCenter, AlignTop); + popup_set_text(popup, "Card read successfully", 64, 25, AlignCenter, AlignTop); + notification_message(app->notifications, &sequence_success); + } else { + popup_set_header(popup, "Error", 64, 10, AlignCenter, AlignTop); + popup_set_text(popup, "Failed to read card\nsegmentaion fault", 64, 25, AlignCenter, AlignTop); + notification_message(app->notifications, &sequence_error); + } +} + +bool cogs_mikai_scene_read_on_event(void* context, SceneManagerEvent event) { + COGSMyKaiApp* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + consumed = true; + scene_manager_previous_scene(app->scene_manager); + } + + return consumed; +} + +void cogs_mikai_scene_read_on_exit(void* context) { + COGSMyKaiApp* app = context; + popup_reset(app->popup); +} diff --git a/scenes/cogs_mikai_scene_reset.c b/scenes/cogs_mikai_scene_reset.c new file mode 100644 index 0000000..8c8d173 --- /dev/null +++ b/scenes/cogs_mikai_scene_reset.c @@ -0,0 +1,57 @@ +#include "../cogs_mikai.h" + +static void cogs_mikai_scene_reset_popup_callback(void* context) { + COGSMyKaiApp* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, 0); +} + +void cogs_mikai_scene_reset_on_enter(void* context) { + COGSMyKaiApp* app = context; + Popup* popup = app->popup; + + if(!app->mykey.is_loaded) { + popup_set_header(popup, "Error", 64, 10, AlignCenter, AlignTop); + popup_set_text(popup, "No card loaded\nRead a card first", 64, 25, AlignCenter, AlignTop); + popup_set_callback(popup, cogs_mikai_scene_reset_popup_callback); + popup_set_context(popup, app); + popup_set_timeout(popup, 2000); + popup_enable_timeout(popup); + view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup); + return; + } + + // Reset the card + mykey_reset(&app->mykey); + + // Update cached values + app->mykey.is_reset = mykey_is_reset(&app->mykey); + app->mykey.current_credit = mykey_get_current_credit(&app->mykey); + + // Show confirmation - saved in memory, not written to card + popup_set_header(popup, "Card Reset!", 64, 10, AlignCenter, AlignTop); + popup_set_text(popup, "Reset in memory\nUse 'Write to Card'", 64, 25, AlignCenter, AlignTop); + popup_set_callback(popup, cogs_mikai_scene_reset_popup_callback); + popup_set_context(popup, app); + popup_set_timeout(popup, 2000); + popup_enable_timeout(popup); + view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup); + notification_message(app->notifications, &sequence_success); +} + +bool cogs_mikai_scene_reset_on_event(void* context, SceneManagerEvent event) { + COGSMyKaiApp* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + consumed = true; + // Search back to start scene and switch (forces menu rebuild) + scene_manager_search_and_switch_to_previous_scene(app->scene_manager, COGSMyKaiSceneStart); + } + + return consumed; +} + +void cogs_mikai_scene_reset_on_exit(void* context) { + COGSMyKaiApp* app = context; + popup_reset(app->popup); +} diff --git a/scenes/cogs_mikai_scene_save_file.c b/scenes/cogs_mikai_scene_save_file.c new file mode 100644 index 0000000..e591bce --- /dev/null +++ b/scenes/cogs_mikai_scene_save_file.c @@ -0,0 +1,158 @@ +#include "../cogs_mikai.h" +#include +#include +#include + +enum { + SaveFileSceneEventInput, +}; + +static bool cogs_mikai_scene_save_file_validator(const char* text, FuriString* error, void* context) { + UNUSED(context); + + if(strlen(text) == 0) { + return true; + } + + for(size_t i = 0; text[i] != '\0'; i++) { + char c = text[i]; + if(!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '_' || c == '-')) { + furi_string_set(error, "Only a-z, 0-9, _, -"); + return false; + } + } + + return true; +} + +static void cogs_mikai_scene_save_file_text_input_callback(void* context) { + COGSMyKaiApp* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, SaveFileSceneEventInput); +} + +static void cogs_mikai_scene_save_file_popup_callback(void* context) { + COGSMyKaiApp* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, 0); +} + +void cogs_mikai_scene_save_file_on_enter(void* context) { + COGSMyKaiApp* app = context; + Popup* popup = app->popup; + + if(!app->mykey.is_loaded) { + popup_set_header(popup, "Error", 64, 10, AlignCenter, AlignTop); + popup_set_text(popup, "No card loaded\nRead a card first", 64, 25, AlignCenter, AlignTop); + popup_set_callback(popup, cogs_mikai_scene_save_file_popup_callback); + popup_set_context(popup, app); + popup_set_timeout(popup, 2000); + popup_enable_timeout(popup); + view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup); + return; + } + + TextInput* text_input = app->text_input; + snprintf(app->text_buffer, sizeof(app->text_buffer), "mykey_save"); + + text_input_set_header_text(text_input, "Enter filename (.myk)"); + text_input_set_validator(text_input, cogs_mikai_scene_save_file_validator, NULL); + text_input_set_result_callback( + text_input, + cogs_mikai_scene_save_file_text_input_callback, + app, + app->text_buffer, + sizeof(app->text_buffer), + false); + + view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewTextInput); +} + +bool cogs_mikai_scene_save_file_on_event(void* context, SceneManagerEvent event) { + COGSMyKaiApp* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == SaveFileSceneEventInput) { + if(app->text_buffer[0] == '\0') { + FURI_LOG_W(TAG, "Ignoring empty text_buffer (already processed)"); + consumed = true; + return consumed; + } + + char filename[32]; + strncpy(filename, app->text_buffer, sizeof(filename) - 1); + filename[sizeof(filename) - 1] = '\0'; + + text_input_reset(app->text_input); + memset(app->text_buffer, 0, sizeof(app->text_buffer)); + + FuriString* file_path = furi_string_alloc(); + furi_string_printf(file_path, "/ext/apps_data/cogs_mikai/%s.myk", filename); + + FURI_LOG_I(TAG, "Attempting to save file: %s", furi_string_get_cstr(file_path)); + Storage* storage = furi_record_open(RECORD_STORAGE); + storage_simply_mkdir(storage, "/ext/apps_data/cogs_mikai"); + + File* file = storage_file_alloc(storage); + bool success = false; + + if(storage_file_open(file, furi_string_get_cstr(file_path), FSAM_WRITE, FSOM_CREATE_ALWAYS)) { + const char* header = "COGES_MYKEY_V1\n"; + storage_file_write(file, header, strlen(header)); + + FuriString* line = furi_string_alloc(); + furi_string_printf(line, "UID: %016llX\n", app->mykey.uid); + storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line)); + + furi_string_printf(line, "ENCRYPTION_KEY: %08lX\n", app->mykey.encryption_key); + storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line)); + + for(size_t i = 0; i < SRIX4K_BLOCKS; i++) { + furi_string_printf(line, "BLOCK_%03zu: %08lX\n", i, app->mykey.eeprom[i]); + storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line)); + } + + furi_string_free(line); + storage_file_close(file); + success = true; + } + + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + + Popup* popup = app->popup; + if(success) { + popup_set_header(popup, "Success!", 64, 10, AlignCenter, AlignTop); + popup_set_text(popup, "File saved to\napps_data/cogs_mikai/", 64, 25, AlignCenter, AlignTop); + notification_message(app->notifications, &sequence_success); + FURI_LOG_I(TAG, "File saved: %s", furi_string_get_cstr(file_path)); + } else { + popup_set_header(popup, "Error", 64, 10, AlignCenter, AlignTop); + popup_set_text(popup, "Failed to create file", 64, 25, AlignCenter, AlignTop); + notification_message(app->notifications, &sequence_error); + FURI_LOG_E(TAG, "Failed to save file: %s", furi_string_get_cstr(file_path)); + } + + furi_string_free(file_path); + + popup_set_callback(popup, cogs_mikai_scene_save_file_popup_callback); + popup_set_context(popup, app); + popup_set_timeout(popup, 3000); + popup_enable_timeout(popup); + view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup); + + consumed = true; + } else { + scene_manager_search_and_switch_to_previous_scene(app->scene_manager, COGSMyKaiSceneStart); + consumed = true; + } + } + + return consumed; +} + +void cogs_mikai_scene_save_file_on_exit(void* context) { + COGSMyKaiApp* app = context; + text_input_reset(app->text_input); + popup_reset(app->popup); +} diff --git a/scenes/cogs_mikai_scene_set_credit.c b/scenes/cogs_mikai_scene_set_credit.c new file mode 100644 index 0000000..d29176d --- /dev/null +++ b/scenes/cogs_mikai_scene_set_credit.c @@ -0,0 +1,193 @@ +#include "../cogs_mikai.h" +#include + +enum { + SetCreditSceneEventInput, +}; + +static bool cogs_mikai_scene_set_credit_validator(const char* text, FuriString* error, void* context) { + UNUSED(context); + + if(strlen(text) == 0) { + return true; + } + + bool has_decimal = false; + for(size_t i = 0; text[i] != '\0'; i++) { + if(text[i] == '.' || text[i] == ',') { + if(has_decimal) { + furi_string_set(error, "Only one decimal point"); + return false; + } + has_decimal = true; + } else if(text[i] < '0' || text[i] > '9') { + furi_string_set(error, "Only numbers and '.'"); + return false; + } + } + + return true; +} + + +static bool parse_euros_to_cents(const char* text, uint16_t* cents) { + if(!text || !cents) return false; + + uint32_t integer_part = 0; + uint32_t decimal_part = 0; + uint32_t decimal_digits = 0; + bool in_decimal = false; + + for(size_t i = 0; text[i] != '\0'; i++) { + if(text[i] == '.' || text[i] == ',') { + in_decimal = true; + } else if(text[i] >= '0' && text[i] <= '9') { + if(in_decimal) { + if(decimal_digits < 2) { + decimal_part = decimal_part * 10 + (text[i] - '0'); + decimal_digits++; + } + } else { + integer_part = integer_part * 10 + (text[i] - '0'); + } + } + } + if(decimal_digits == 1) { + decimal_part *= 10; + } + + uint32_t total_cents = integer_part * 100 + decimal_part; + if(total_cents > 99999) return false; // Max 999.99 EUR + + *cents = (uint16_t)total_cents; + return true; +} + +static void cogs_mikai_scene_set_credit_text_input_callback(void* context) { + COGSMyKaiApp* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, SetCreditSceneEventInput); +} + +static void cogs_mikai_scene_set_credit_popup_callback(void* context) { + COGSMyKaiApp* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, 0); +} + +void cogs_mikai_scene_set_credit_on_enter(void* context) { + COGSMyKaiApp* app = context; + + if(!app->mykey.is_loaded) { + Popup* popup = app->popup; + popup_set_header(popup, "Error", 64, 10, AlignCenter, AlignTop); + popup_set_text(popup, "No card loaded\nRead a card first", 64, 25, AlignCenter, AlignTop); + popup_set_callback(popup, cogs_mikai_scene_set_credit_popup_callback); + popup_set_context(popup, app); + popup_set_timeout(popup, 2000); + popup_enable_timeout(popup); + view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup); + return; + } + + TextInput* text_input = app->text_input; + snprintf(app->text_buffer, sizeof(app->text_buffer), "10.00"); + + text_input_set_header_text(text_input, "Set Credit (EUR)"); + text_input_set_validator(text_input, cogs_mikai_scene_set_credit_validator, NULL); + text_input_set_result_callback( + text_input, + cogs_mikai_scene_set_credit_text_input_callback, + app, + app->text_buffer, + sizeof(app->text_buffer), + false); + + view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewTextInput); +} + +bool cogs_mikai_scene_set_credit_on_event(void* context, SceneManagerEvent event) { + COGSMyKaiApp* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == SetCreditSceneEventInput) { + if(app->text_buffer[0] == '\0') { + FURI_LOG_W(TAG, "Ignoring empty text_buffer (already processed)"); + consumed = true; + return consumed; + } + FURI_LOG_I(TAG, "Set credit input: '%s'", app->text_buffer); + uint16_t cents = 0; + bool parse_ok = parse_euros_to_cents(app->text_buffer, ¢s); + FURI_LOG_I(TAG, "Parse result: %d, cents: %d", parse_ok, cents); + + if(parse_ok) { + FURI_LOG_I(TAG, "Valid amount: %d cents", cents); + + DateTime datetime; + furi_hal_rtc_get_datetime(&datetime); + + bool success = mykey_set_cents( + &app->mykey, + cents, + datetime.day, + datetime.month, + datetime.year - 2000); + + if(success) { + app->mykey.current_credit = mykey_get_current_credit(&app->mykey); + text_input_reset(app->text_input); + + memset(app->text_buffer, 0, sizeof(app->text_buffer)); + Popup* popup = app->popup; + popup_set_header(popup, "Credit Set!", 64, 10, AlignCenter, AlignTop); + popup_set_text(popup, "Saved in memory\nUse 'Write to Card'", 64, 25, AlignCenter, AlignTop); + popup_set_callback(popup, cogs_mikai_scene_set_credit_popup_callback); + popup_set_context(popup, app); + popup_set_timeout(popup, 2000); + popup_enable_timeout(popup); + view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup); + notification_message(app->notifications, &sequence_success); + } else { + Popup* popup = app->popup; + popup_set_header(popup, "Error", 64, 10, AlignCenter, AlignTop); + popup_set_text(popup, "Failed to set credit", 64, 25, AlignCenter, AlignTop); + popup_set_callback(popup, cogs_mikai_scene_set_credit_popup_callback); + popup_set_context(popup, app); + popup_set_timeout(popup, 2000); + popup_enable_timeout(popup); + view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup); + notification_message(app->notifications, &sequence_error); + } + } else { + FURI_LOG_E(TAG, "Invalid amount: parse_ok=%d, cents=%d", + parse_ok, cents); + Popup* popup = app->popup; + popup_set_header(popup, "Error", 64, 10, AlignCenter, AlignTop); + popup_set_text( + popup, + "Invalid amount\nEnter 0.00-999.99", + 64, + 25, + AlignCenter, + AlignTop); + popup_set_callback(popup, cogs_mikai_scene_set_credit_popup_callback); + popup_set_context(popup, app); + popup_set_timeout(popup, 2000); + popup_enable_timeout(popup); + view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup); + } + consumed = true; + } else { + scene_manager_search_and_switch_to_previous_scene(app->scene_manager, COGSMyKaiSceneStart); + consumed = true; + } + } + + return consumed; +} + +void cogs_mikai_scene_set_credit_on_exit(void* context) { + COGSMyKaiApp* app = context; + text_input_reset(app->text_input); + popup_reset(app->popup); +} diff --git a/scenes/cogs_mikai_scene_start.c b/scenes/cogs_mikai_scene_start.c new file mode 100644 index 0000000..41ddd9a --- /dev/null +++ b/scenes/cogs_mikai_scene_start.c @@ -0,0 +1,158 @@ +#include "../cogs_mikai.h" + +typedef enum { + SubmenuIndexRead, + SubmenuIndexInfo, + SubmenuIndexWriteCard, + SubmenuIndexAddCredit, + SubmenuIndexSetCredit, + SubmenuIndexReset, + SubmenuIndexSaveFile, + SubmenuIndexLoadFile, + SubmenuIndexDebug, + SubmenuIndexAbout, +} SubmenuIndex; + +static void cogs_mikai_scene_start_submenu_callback(void* context, uint32_t index) { + COGSMyKaiApp* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, index); +} + +void cogs_mikai_scene_start_on_enter(void* context) { + COGSMyKaiApp* app = context; + Submenu* submenu = app->submenu; + + // Always rebuild menu to reflect current state (e.g., is_modified flag) + submenu_reset(submenu); + + if(app->mykey.is_loaded) { + submenu_set_header(submenu, "[Card Loaded]"); + } + + submenu_add_item( + submenu, + "Read Card", + SubmenuIndexRead, + cogs_mikai_scene_start_submenu_callback, + app); + + submenu_add_item( + submenu, + "View Info", + SubmenuIndexInfo, + cogs_mikai_scene_start_submenu_callback, + app); + + // Show "Write to Card" only if data has been modified + if(app->mykey.is_modified) { + submenu_add_item( + submenu, + ">>> Write to Card <<<", + SubmenuIndexWriteCard, + cogs_mikai_scene_start_submenu_callback, + app); + } + + submenu_add_item( + submenu, + "Add Credit", + SubmenuIndexAddCredit, + cogs_mikai_scene_start_submenu_callback, + app); + + submenu_add_item( + submenu, + "Set Credit", + SubmenuIndexSetCredit, + cogs_mikai_scene_start_submenu_callback, + app); + + submenu_add_item( + submenu, + "Reset Card", + SubmenuIndexReset, + cogs_mikai_scene_start_submenu_callback, + app); + + submenu_add_item( + submenu, + "Save to File", + SubmenuIndexSaveFile, + cogs_mikai_scene_start_submenu_callback, + app); + + submenu_add_item( + submenu, + "Load from File", + SubmenuIndexLoadFile, + cogs_mikai_scene_start_submenu_callback, + app); + + submenu_add_item( + submenu, + "Debug Info", + SubmenuIndexDebug, + cogs_mikai_scene_start_submenu_callback, + app); + + submenu_add_item( + submenu, + "About", + SubmenuIndexAbout, + cogs_mikai_scene_start_submenu_callback, + app); + + submenu_set_selected_item( + submenu, scene_manager_get_scene_state(app->scene_manager, COGSMyKaiSceneStart)); + + view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewSubmenu); +} + +bool cogs_mikai_scene_start_on_event(void* context, SceneManagerEvent event) { + COGSMyKaiApp* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + scene_manager_set_scene_state(app->scene_manager, COGSMyKaiSceneStart, event.event); + consumed = true; + switch(event.event) { + case SubmenuIndexRead: + scene_manager_next_scene(app->scene_manager, COGSMyKaiSceneRead); + break; + case SubmenuIndexInfo: + scene_manager_next_scene(app->scene_manager, COGSMyKaiSceneInfo); + break; + case SubmenuIndexWriteCard: + scene_manager_next_scene(app->scene_manager, COGSMyKaiSceneWriteCard); + break; + case SubmenuIndexAddCredit: + scene_manager_next_scene(app->scene_manager, COGSMyKaiSceneAddCredit); + break; + case SubmenuIndexSetCredit: + scene_manager_next_scene(app->scene_manager, COGSMyKaiSceneSetCredit); + break; + case SubmenuIndexReset: + scene_manager_next_scene(app->scene_manager, COGSMyKaiSceneReset); + break; + case SubmenuIndexSaveFile: + scene_manager_next_scene(app->scene_manager, COGSMyKaiSceneSaveFile); + break; + case SubmenuIndexLoadFile: + scene_manager_next_scene(app->scene_manager, COGSMyKaiSceneLoadFile); + break; + case SubmenuIndexDebug: + scene_manager_next_scene(app->scene_manager, COGSMyKaiSceneDebug); + break; + case SubmenuIndexAbout: + scene_manager_next_scene(app->scene_manager, COGSMyKaiSceneAbout); + break; + } + } + + return consumed; +} + +void cogs_mikai_scene_start_on_exit(void* context) { + COGSMyKaiApp* app = context; + submenu_reset(app->submenu); +} diff --git a/scenes/cogs_mikai_scene_write_card.c b/scenes/cogs_mikai_scene_write_card.c new file mode 100644 index 0000000..efc8f79 --- /dev/null +++ b/scenes/cogs_mikai_scene_write_card.c @@ -0,0 +1,74 @@ +#include "../cogs_mikai.h" + +static void cogs_mikai_scene_write_card_popup_callback(void* context) { + COGSMyKaiApp* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, 0); +} + +void cogs_mikai_scene_write_card_on_enter(void* context) { + COGSMyKaiApp* app = context; + Popup* popup = app->popup; + + if(!app->mykey.is_loaded) { + popup_set_header(popup, "Error", 64, 10, AlignCenter, AlignTop); + popup_set_text(popup, "No card loaded", 64, 25, AlignCenter, AlignTop); + popup_set_callback(popup, cogs_mikai_scene_write_card_popup_callback); + popup_set_context(popup, app); + popup_set_timeout(popup, 2000); + popup_enable_timeout(popup); + view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup); + return; + } + + if(!app->mykey.is_modified) { + popup_set_header(popup, "No Changes", 64, 10, AlignCenter, AlignTop); + popup_set_text(popup, "Card data not modified", 64, 25, AlignCenter, AlignTop); + popup_set_callback(popup, cogs_mikai_scene_write_card_popup_callback); + popup_set_context(popup, app); + popup_set_timeout(popup, 2000); + popup_enable_timeout(popup); + view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup); + return; + } + + popup_set_header(popup, "Writing...", 64, 10, AlignCenter, AlignTop); + popup_set_text(popup, "Place card on reader", 64, 25, AlignCenter, AlignTop); + popup_set_callback(popup, cogs_mikai_scene_write_card_popup_callback); + popup_set_context(popup, app); + popup_set_timeout(popup, 5000); + popup_enable_timeout(popup); + + view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup); + + if(mykey_write_to_nfc(app)) { + app->mykey.is_modified = false; + + popup_set_header(popup, "Success!", 64, 10, AlignCenter, AlignTop); + popup_set_text(popup, "Card updated", 64, 25, AlignCenter, AlignTop); + notification_message(app->notifications, &sequence_success); + } else { + popup_set_header(popup, "Error", 64, 10, AlignCenter, AlignTop); + popup_set_text(popup, "Write failed\nTry again", 64, 25, AlignCenter, AlignTop); + notification_message(app->notifications, &sequence_error); + } + + popup_set_timeout(popup, 2000); + popup_enable_timeout(popup); +} + +bool cogs_mikai_scene_write_card_on_event(void* context, SceneManagerEvent event) { + COGSMyKaiApp* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + scene_manager_search_and_switch_to_previous_scene(app->scene_manager, COGSMyKaiSceneStart); + consumed = true; + } + + return consumed; +} + +void cogs_mikai_scene_write_card_on_exit(void* context) { + COGSMyKaiApp* app = context; + popup_reset(app->popup); +}