public release 0.4

This commit is contained in:
Andrea Santaniello 2025-11-06 17:34:04 +01:00
commit 7ebef9616f
24 changed files with 2565 additions and 0 deletions

39
application.fam Normal file
View File

@ -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",
],
)

137
cogs_mikai.h Normal file
View File

@ -0,0 +1,137 @@
#pragma once
#include <furi.h>
#include <gui/gui.h>
#include <gui/view.h>
#include <gui/view_dispatcher.h>
#include <gui/scene_manager.h>
#include <gui/modules/submenu.h>
#include <gui/modules/variable_item_list.h>
#include <gui/modules/text_input.h>
#include <gui/modules/popup.h>
#include <gui/modules/widget.h>
#include <gui/modules/text_box.h>
#include <dialogs/dialogs.h>
#include <notification/notification_messages.h>
#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);

BIN
cogs_mikai.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 B

106
cogs_mikai_app.c Normal file
View File

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

BIN
dist/cogs_mikai.fap vendored Normal file

Binary file not shown.

BIN
dist/debug/cogs_mikai_d.elf vendored Normal file

Binary file not shown.

0
images/.gitkeep Normal file
View File

492
mykey_core.c Normal file
View File

@ -0,0 +1,492 @@
#include "cogs_mikai.h"
#include <furi.h>
#include <string.h>
#include <machine/endian.h>
#include <storage/storage.h>
// 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(&current);
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(&current_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(&current_block, i);
break;
}
case 0x11:
case 0x15:
case 0x40:
case 0x44:
// Key ID [last three bytes]
current_block = key->eeprom[0x07];
calculate_block_checksum(&current_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(&current_block, i);
encode_decode_block(&current_block);
break;
}
case 0x12:
case 0x16:
case 0x41:
case 0x45:
// Operations counter
current_block = 1;
calculate_block_checksum(&current_block, i);
break;
case 0x13:
case 0x17:
case 0x42:
case 0x46:
// Generic blocks
current_block = 0x00040013;
calculate_block_checksum(&current_block, i);
break;
case 0x18:
case 0x1C:
case 0x47:
case 0x4B:
// Generic blocks
current_block = 0x0000FEDC;
calculate_block_checksum(&current_block, i);
encode_decode_block(&current_block);
break;
case 0x19:
case 0x1D:
case 0x48:
case 0x4C:
// Generic blocks
current_block = 0x00000123;
calculate_block_checksum(&current_block, i);
encode_decode_block(&current_block);
break;
case 0x21:
case 0x25:
// Current credit (0,00€)
mykey_calculate_encryption_key(key);
current_block = 0;
calculate_block_checksum(&current_block, i);
encode_decode_block(&current_block);
current_block ^= key->encryption_key;
break;
case 0x20:
case 0x24:
case 0x4F:
case 0x53:
// Generic blocks
current_block = 0x00010000;
calculate_block_checksum(&current_block, i);
encode_decode_block(&current_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(&current_block, i);
encode_decode_block(&current_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;
}

152
nfc_srix.c Normal file
View File

@ -0,0 +1,152 @@
#include "cogs_mikai.h"
#include <furi.h>
#include <machine/endian.h>
#include <nfc/nfc.h>
#include <nfc/protocols/st25tb/st25tb.h>
#include <nfc/protocols/st25tb/st25tb_poller_sync.h>
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;
}

160
parse_mykey_file.py Normal file
View File

@ -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 <file.myk>")
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

26
scenes/cogs_mikai_scene.c Normal file
View File

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

View File

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

View File

@ -0,0 +1,207 @@
#include "../cogs_mikai.h"
#include <furi_hal_rtc.h>
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, &cents);
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);
}

View File

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

View File

@ -0,0 +1,26 @@
#pragma once
#include <gui/scene_manager.h>
// 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;

View File

@ -0,0 +1,177 @@
#include "../cogs_mikai.h"
#include <storage/storage.h>
#include <machine/endian.h>
#include <furi_hal_rtc.h>
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);
}

View File

@ -0,0 +1,119 @@
#include "../cogs_mikai.h"
#include <machine/endian.h>
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);
}

View File

@ -0,0 +1,191 @@
#include "../cogs_mikai.h"
#include <dialogs/dialogs.h>
#include <storage/storage.h>
#include <toolbox/path.h>
#include <string.h>
// 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);
}

View File

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

View File

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

View File

@ -0,0 +1,158 @@
#include "../cogs_mikai.h"
#include <dialogs/dialogs.h>
#include <storage/storage.h>
#include <toolbox/path.h>
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);
}

View File

@ -0,0 +1,193 @@
#include "../cogs_mikai.h"
#include <furi_hal_rtc.h>
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, &cents);
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);
}

View File

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

View File

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