commit 7ebef9616f30f2ab651786bc5d24d55a15f6be3e Author: Andrea Santaniello Date: Thu Nov 6 17:34:04 2025 +0100 public release 0.4 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 0000000..cabbae5 Binary files /dev/null and b/cogs_mikai.png differ 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 0000000..2a808dd Binary files /dev/null and b/dist/cogs_mikai.fap differ diff --git a/dist/debug/cogs_mikai_d.elf b/dist/debug/cogs_mikai_d.elf new file mode 100644 index 0000000..a0d55a0 Binary files /dev/null and b/dist/debug/cogs_mikai_d.elf differ 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); +}