/* * Nice O-Code — recover a remote's 16 bit installer code. * * Nice O-Code is Nice Flor-S whose final cipher permutation is XORed with a per remote * installer code instead of being inverted, plus a per parcel mask on the serial. A * single capture is not enough to tell you the code, but four or more parcels from the * same remote are: only the correct installer code makes every parcel decode to the same * serial. This app listens for parcels, brute forces the code, and writes a .sub with * the recovered "IC" key so the sub-ghz app can decode and replay that remote. * * It can also write a .sub for a remote that does not exist yet: the same installer code * and button with a serial of its own, which the gate accepts once it is paired the way a * new remote is. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "helpers/radio_device_loader.h" #include "raw/nice_o_raw.h" #include "nice_o_code_icons.h" #include #define TAG "NiceOCode" #define NICE_O_FOLDER "/ext/subghz" #define NICE_O_EXT ".sub" #define NICE_O_NAME_LEN 32 #define NICE_O_KEYSTORE "/ext/subghz/assets/nice_flor_s" #define NICE_O_MAX_KEYS 16 #define NICE_O_MIN_KEYS 4 #define NICE_O_FREQUENCY 433920000 /* Serial bits a minted remote keeps from the captured one: the top nibble, and the low * two, which carry an ID priority field. */ #define NICE_O_NEW_SERIAL_KEEP 0x0F000003u /* Remotes of one installation share this coset, so a mint skips it. */ #define NICE_O_SIBLING_MASK 0x0837ACu #define NICE_O_SWEEP_TOTAL 0x10000u /* candidates per main loop pass: small enough that the screen keeps redrawing, * large enough that the whole sweep is over in well under a second */ #define NICE_O_SWEEP_CHUNK 4096u typedef enum { NiceOSceneMenu, NiceOSceneCapture, NiceOSceneRaw, NiceOSceneSolve, NiceOSceneResult, } NiceOScene; typedef struct { FuriMutex* mutex; FuriMessageQueue* queue; ViewPort* view_port; Gui* gui; Storage* storage; DialogsApp* dialogs; NotificationApp* notifications; NiceOScene scene; uint8_t menu_index; FuriString* status; // capture SubGhzEnvironment* environment; SubGhzReceiver* receiver; SubGhzWorker* worker; const SubGhzDevice* device; bool device_external; volatile bool rx_active; NiceORaw* raw; FuriString* progress; uint64_t keys[NICE_O_MAX_KEYS]; uint8_t key_count; /* the frame length the remote actually sends, so a saved remote has the same shape */ uint16_t key_bits; uint32_t packets; // sweep, run in chunks from the main loop so the UI never freezes uint32_t mask_32[NICE_O_MAX_KEYS]; uint16_t mask_16[NICE_O_MAX_KEYS]; uint8_t sweep_keys; uint32_t sweep_ic; bool sweep_found; bool sweep_ambiguous; uint16_t sweep_ic_found; uint32_t sweep_serial; /* certificate as the 112 bit frame carries it, masked with the remote's serial; * 0 when none was seen */ uint32_t cert; /* index in keys[] of the first press that carried a certificate, -1 if none */ int8_t cert_key; // result bool solved; uint16_t ic; uint32_t serial; } NiceOCode; /* The receiver's own certificate, which the frame carries masked with the serial the * sweep has to find first. */ static uint32_t nice_o_cert(const NiceOCode* app) { return (app->cert && app->solved) ? (app->cert ^ (app->serial & 0xFFFFFF)) : 0; } /* ------------------------------------------------------------------ drawing */ static void nice_o_draw_callback(Canvas* canvas, void* ctx) { NiceOCode* app = ctx; char buf[48]; furi_mutex_acquire(app->mutex, FuriWaitForever); canvas_clear(canvas); canvas_set_font(canvas, FontPrimary); canvas_draw_str(canvas, 2, 10, "Nice O-Code"); /* Which radio is in use, boxed so it does not read as part of the title. The box is * sized to the text, so Int does not leave a gap. */ canvas_set_font(canvas, FontSecondary); const char* radio = app->device_external ? "Ext" : "Int"; size_t radio_w = canvas_string_width(canvas, radio) + 6; int32_t radio_x = 127 - (int32_t)radio_w; elements_frame(canvas, radio_x, 0, radio_w, 12); canvas_draw_str(canvas, radio_x + 3, 9, radio); canvas_set_font(canvas, FontPrimary); canvas_draw_line(canvas, 0, 13, 128, 13); canvas_set_font(canvas, FontSecondary); switch(app->scene) { case NiceOSceneMenu: { static const char* items[] = {"Capture remote", "Decode RAW file", "Exit"}; for(uint8_t i = 0; i < COUNT_OF(items); i++) { if(i == app->menu_index) { canvas_draw_box(canvas, 0, 17 + i * 12, 128, 11); canvas_set_color(canvas, ColorWhite); } canvas_draw_str(canvas, 4, 26 + i * 12, items[i]); canvas_set_color(canvas, ColorBlack); } elements_frame(canvas, 0, 53, 128, 11); canvas_draw_str(canvas, 4, 62, "4+ presses, do not hold"); break; } case NiceOSceneCapture: { /* The progress bar leaves no room for a line of its own, so a certificate shares * the top line and the wording shortens to make space. */ if(app->cert) { snprintf( buf, sizeof(buf), "%u/%u %lupkt +Crt", app->key_count, NICE_O_MIN_KEYS, app->packets); } else { snprintf( buf, sizeof(buf), "Presses %u/%u %lupkt", app->key_count, NICE_O_MIN_KEYS, app->packets); } canvas_draw_str(canvas, 2, 24, buf); /* fills as the capture approaches the four packets the sweep needs */ const uint8_t have = MIN(app->key_count, (uint8_t)NICE_O_MIN_KEYS); elements_progress_bar(canvas, 2, 28, 124, (float)have / (float)NICE_O_MIN_KEYS); elements_frame(canvas, 0, 40, 128, 13); canvas_draw_str(canvas, 4, 49, furi_string_get_cstr(app->status)); if(app->key_count >= NICE_O_MIN_KEYS) { elements_button_center(canvas, "Solve"); } elements_button_left(canvas, "Back"); break; } case NiceOSceneRaw: { if(app->cert) { snprintf( buf, sizeof(buf), "%u/%u %lupkt +Crt", app->key_count, NICE_O_MIN_KEYS, app->packets); } else { snprintf( buf, sizeof(buf), "Presses %u/%u %lupkt", app->key_count, NICE_O_MIN_KEYS, app->packets); } canvas_draw_str(canvas, 2, 24, buf); /* Here the bar is how far into the file we are, which is the part that takes time; * the press count is already on the line above. The worker reports it as "042%", * so the leading digits are the whole of it. */ uint8_t pct = 0; for(const char* c = furi_string_get_cstr(app->progress); *c >= '0' && *c <= '9'; c++) { pct = (uint8_t)(pct * 10 + (*c - '0')); } elements_progress_bar(canvas, 2, 28, 124, (float)MIN(pct, (uint8_t)100) / 100.0f); elements_frame(canvas, 0, 40, 128, 13); canvas_draw_str(canvas, 4, 49, furi_string_get_cstr(app->status)); elements_button_left(canvas, "Back"); break; } case NiceOSceneSolve: { canvas_draw_str(canvas, 2, 26, "Trying installer codes"); snprintf(buf, sizeof(buf), "%lu / %u", app->sweep_ic, NICE_O_SWEEP_TOTAL); elements_progress_bar_with_text( canvas, 2, 32, 124, (float)app->sweep_ic / (float)NICE_O_SWEEP_TOTAL, buf); elements_button_left(canvas, "Cancel"); break; } default: if(app->solved) { elements_frame(canvas, 0, 17, 128, app->cert ? 34 : 30); canvas_set_font(canvas, FontPrimary); snprintf(buf, sizeof(buf), "IC %04X", app->ic); canvas_draw_str(canvas, 6, 29, buf); canvas_set_font(canvas, FontSecondary); snprintf(buf, sizeof(buf), "Serial %07lX", app->serial); canvas_draw_str(canvas, 6, 40, buf); if(app->cert) { snprintf(buf, sizeof(buf), "Cert %06lX", nice_o_cert(app)); canvas_draw_str(canvas, 6, 49, buf); } elements_button_center(canvas, "Save"); elements_button_right(canvas, "New"); } else { elements_multiline_text_aligned( canvas, 64, 32, AlignCenter, AlignCenter, furi_string_get_cstr(app->status)); } elements_button_left(canvas, "Back"); break; } furi_mutex_release(app->mutex); } static void nice_o_input_callback(InputEvent* event, void* ctx) { FuriMessageQueue* queue = ctx; furi_message_queue_put(queue, event, FuriWaitForever); } /* ------------------------------------------------------------------ capture */ /* * Frames of one press share a counter and only rotate the parcel index, and the parcel * mask cancels between the encrypt and the decrypt side, so they agree on the recovered * serial for every candidate IC and rule nothing out. Two probe ICs tell the cases apart: * a frame agreeing with one already held at both probes is the same press. */ static bool nice_o_key_adds_info(NiceOCode* app, uint64_t key) { static const uint16_t probe[2] = {0x0000u, 0x0001u}; uint32_t mask_32_new = 0; uint16_t mask_16_new = 0; subghz_protocol_nice_o_mask( subghz_protocol_nice_o_get_parcel(key), &mask_32_new, &mask_16_new); for(uint8_t i = 0; i < app->key_count; i++) { if(app->keys[i] == key) return false; uint32_t mask_32_old = 0; uint16_t mask_16_old = 0; subghz_protocol_nice_o_mask( subghz_protocol_nice_o_get_parcel(app->keys[i]), &mask_32_old, &mask_16_old); bool same_press = true; for(uint8_t p = 0; p < COUNT_OF(probe); p++) { const uint64_t a = subghz_protocol_nice_flor_s_decrypt_ic( key, (uint16_t)(probe[p] ^ mask_16_new), NICE_O_KEYSTORE); const uint64_t b = subghz_protocol_nice_flor_s_decrypt_ic( app->keys[i], (uint16_t)(probe[p] ^ mask_16_old), NICE_O_KEYSTORE); if((((uint32_t)(a >> 16) ^ mask_32_new) & 0x0FFFFFFFu) != (((uint32_t)(b >> 16) ^ mask_32_old) & 0x0FFFFFFFu)) { same_press = false; break; } } if(same_press) return false; } return true; } /* Runs on the SubGhzWorker thread, so allocating here is fine. */ static void nice_o_rx_callback(SubGhzReceiver* receiver, SubGhzProtocolDecoderBase* base, void* ctx) { UNUSED(receiver); NiceOCode* app = ctx; if(strcmp(base->protocol->name, SUBGHZ_PROTOCOL_NICE_FLOR_S_NAME) != 0) return; /* Plain Flor-S and Nice One reuse one encrypted body for the whole press, so their * frames carry nothing for the sweep and would only break it. Keep O-Code alone. */ if(!subghz_protocol_decoder_nice_flor_s_is_o_code_no_ic(base)) { furi_mutex_acquire(app->mutex, FuriWaitForever); app->packets++; furi_string_set(app->status, "Not O-Code, ignored"); furi_mutex_release(app->mutex); return; } // pull the raw key out through the public serializer instead of poking at internals uint64_t key = 0; FlipperFormat* fff = flipper_format_string_alloc(); SubGhzRadioPreset preset = { .name = furi_string_alloc_set("AM650"), .frequency = NICE_O_FREQUENCY, .data = NULL, .data_size = 0, }; uint8_t key_data[8] = {0}; uint8_t d2_data[8] = {0}; uint32_t bits = 0; uint32_t cert = 0; if((subghz_protocol_decoder_base_serialize(base, fff, &preset) == SubGhzProtocolStatusOk) && flipper_format_rewind(fff) && flipper_format_read_hex(fff, "Key", key_data, sizeof(key_data))) { for(size_t i = 0; i < sizeof(key_data); i++) { key = (key << 8) | key_data[i]; } if(flipper_format_rewind(fff)) flipper_format_read_uint32(fff, "Bit", &bits, 1); /* The certificate is masked with the serial, which the sweep has not found yet, * so take the raw field out of the frame tail and un-mask it later. */ if(flipper_format_rewind(fff) && flipper_format_read_hex(fff, "Data2", d2_data, sizeof(d2_data))) { uint64_t d2 = 0; for(size_t i = 0; i < sizeof(d2_data); i++) { d2 = (d2 << 8) | d2_data[i]; } cert = (uint32_t)((d2 >> 8) ^ key) & 0xFFFFFF; } } furi_string_free(preset.name); flipper_format_free(fff); if(key == 0) return; furi_mutex_acquire(app->mutex, FuriWaitForever); app->packets++; if(cert) app->cert = cert; if((app->key_count < NICE_O_MAX_KEYS) && nice_o_key_adds_info(app, key)) { /* A remote sending the longer frame also sends 72 bit parcels, and 72 is the * longest the encoder builds, so anything above 52 is saved as 72. */ if(bits > 52) app->key_bits = 72; app->keys[app->key_count++] = key; if(cert && app->cert_key < 0) app->cert_key = (int8_t)(app->key_count - 1); furi_string_printf( app->status, "Press %u: %010llX", app->key_count, key & 0xFFFFFFFFFFULL); notification_message(app->notifications, &sequence_blink_green_10); } else { furi_string_printf(app->status, "Same press, ignored"); } furi_mutex_release(app->mutex); } /* Pick the radio once, when the app opens, so the module in use is known before the * first screen is drawn. The loader does the OTG power handshake and begin() for the * external case. */ static void nice_o_radio_init(NiceOCode* app) { subghz_devices_init(); app->device = radio_device_loader_set(NULL, SubGhzRadioDeviceTypeExternalCC1101); if(!app->device) return; app->device_external = (app->device != subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME)); if(!app->device_external) subghz_devices_begin(app->device); } static void nice_o_radio_deinit(NiceOCode* app) { if(app->device) radio_device_loader_end(app->device); subghz_devices_deinit(); app->device = NULL; } /* The decoder chain on its own. Live capture feeds it from the radio worker, Decode * RAW feeds it from a file, and neither cares which. */ static void nice_o_receiver_alloc(NiceOCode* app) { app->environment = subghz_environment_alloc(); subghz_environment_set_protocol_registry(app->environment, (void*)&subghz_protocol_registry); subghz_environment_set_nice_flor_s_rainbow_table_file_name(app->environment, NICE_O_KEYSTORE); app->receiver = subghz_receiver_alloc_init(app->environment); subghz_receiver_set_filter(app->receiver, SubGhzProtocolFlag_Decodable); subghz_receiver_set_rx_callback(app->receiver, nice_o_rx_callback, app); } static void nice_o_receiver_free(NiceOCode* app) { if(app->receiver) subghz_receiver_free(app->receiver); if(app->environment) subghz_environment_free(app->environment); app->receiver = NULL; app->environment = NULL; } static bool nice_o_rx_start(NiceOCode* app) { if(!app->device) return false; nice_o_receiver_alloc(app); subghz_devices_reset(app->device); subghz_devices_load_preset(app->device, FuriHalSubGhzPresetOok650Async, NULL); subghz_devices_set_frequency(app->device, NICE_O_FREQUENCY); app->worker = subghz_worker_alloc(); subghz_worker_set_overrun_callback( app->worker, (SubGhzWorkerOverrunCallback)subghz_receiver_reset); subghz_worker_set_pair_callback(app->worker, (SubGhzWorkerPairCallback)subghz_receiver_decode); subghz_worker_set_context(app->worker, app->receiver); subghz_devices_start_async_rx(app->device, subghz_worker_rx_callback, app->worker); subghz_worker_start(app->worker); subghz_devices_set_rx(app->device); app->rx_active = true; return true; } static void nice_o_rx_stop(NiceOCode* app) { if(!app->rx_active) return; if(app->worker && subghz_worker_is_running(app->worker)) subghz_worker_stop(app->worker); subghz_devices_stop_async_rx(app->device); subghz_devices_idle(app->device); subghz_devices_sleep(app->device); if(app->worker) { subghz_worker_free(app->worker); app->worker = NULL; } nice_o_receiver_free(app); app->rx_active = false; } static void nice_o_reset_capture(NiceOCode* app) { app->key_count = 0; app->key_bits = 52; app->packets = 0; app->cert = 0; app->cert_key = -1; app->solved = false; furi_string_reset(app->progress); } /* Only a capture carries samples, and a saved key is a .sub as much as a capture is. The * browser cannot tell them apart by name, so the pick is checked here. */ static bool nice_o_raw_file_is_raw(NiceOCode* app, const char* path) { bool is_raw = false; FlipperFormat* fff = flipper_format_file_alloc(app->storage); FuriString* protocol = furi_string_alloc(); if(flipper_format_file_open_existing(fff, path) && flipper_format_read_string(fff, "Protocol", protocol)) { is_raw = furi_string_equal_str(protocol, SUBGHZ_PROTOCOL_RAW_NAME); } furi_string_free(protocol); flipper_format_free(fff); return is_raw; } static void nice_o_raw_complain(NiceOCode* app) { DialogMessage* message = dialog_message_alloc(); dialog_message_set_header(message, "Not a RAW capture", 64, 8, AlignCenter, AlignTop); dialog_message_set_text( message, "Only a Read RAW capture\nholds samples to decode.\nThis file holds a key.", 64, 36, AlignCenter, AlignCenter); dialog_message_set_buttons(message, NULL, "OK", NULL); dialog_message_show(app->dialogs, message); dialog_message_free(message); } /* Ask for a .sub and point the decoder chain at it. No radio is claimed, so this works * with the module unplugged. */ static bool nice_o_raw_begin(NiceOCode* app) { FuriString* path = furi_string_alloc_set(NICE_O_FOLDER); DialogsFileBrowserOptions browser; dialog_file_browser_set_basic_options(&browser, NICE_O_EXT, &I_sub1_10px); browser.base_path = NICE_O_FOLDER; bool ok = false; if(dialog_file_browser_show(app->dialogs, path, path, &browser)) { if(nice_o_raw_file_is_raw(app, furi_string_get_cstr(path))) { nice_o_receiver_alloc(app); ok = nice_o_raw_start(app->raw, furi_string_get_cstr(path), app->receiver); if(!ok) nice_o_receiver_free(app); } else { nice_o_raw_complain(app); } } furi_string_free(path); return ok; } static void nice_o_raw_end(NiceOCode* app) { nice_o_raw_stop(app->raw); nice_o_receiver_free(app); } /* -------------------------------------------------------------------- solve */ /* Set up a sweep over all 65536 installer codes. The rainbow table is read once * here: reading it opens and decrypts a file on the SD card, so doing that per * candidate would turn a sub second search into one that never seems to end. */ static bool nice_o_solve_begin(NiceOCode* app) { if(app->key_count < NICE_O_MIN_KEYS) { furi_string_set(app->status, "Need 4+ packets"); return false; } /* warm the lib side table cache once so the sweep never touches the card */ if(subghz_protocol_nice_flor_s_decrypt_ic(app->keys[0], 0, NICE_O_KEYSTORE) == SUBGHZ_NO_NICE_FLOR_S_RAINBOW_TABLE) { furi_string_set(app->status, "No rainbow table"); return false; } app->sweep_keys = app->key_count; for(uint8_t i = 0; i < app->sweep_keys; i++) { subghz_protocol_nice_o_mask( subghz_protocol_nice_o_get_parcel(app->keys[i]), &app->mask_32[i], &app->mask_16[i]); } app->sweep_ic = 0; app->sweep_found = false; app->sweep_ambiguous = false; return true; } /* Try one chunk of candidates. Returns true once the whole range has been swept. */ static bool nice_o_solve_step(NiceOCode* app) { const uint32_t end = MIN(app->sweep_ic + NICE_O_SWEEP_CHUNK, NICE_O_SWEEP_TOTAL); for(; app->sweep_ic < end; app->sweep_ic++) { const uint16_t ic = (uint16_t)app->sweep_ic; uint64_t d = subghz_protocol_nice_flor_s_decrypt_ic( app->keys[0], (uint16_t)(ic ^ app->mask_16[0]), NICE_O_KEYSTORE); const uint32_t serial = (uint32_t)((d >> 16) ^ app->mask_32[0]) & 0x0FFFFFFFu; uint8_t i = 1; for(; i < app->sweep_keys; i++) { d = subghz_protocol_nice_flor_s_decrypt_ic( app->keys[i], (uint16_t)(ic ^ app->mask_16[i]), NICE_O_KEYSTORE); if(((uint32_t)((d >> 16) ^ app->mask_32[i]) & 0x0FFFFFFFu) != serial) break; } if(i != app->sweep_keys) continue; /* a second hit means the captures cannot tell the codes apart */ if(app->sweep_found) { app->sweep_ambiguous = true; app->sweep_ic = NICE_O_SWEEP_TOTAL; return true; } app->sweep_found = true; app->sweep_ic_found = ic; app->sweep_serial = serial; } return app->sweep_ic >= NICE_O_SWEEP_TOTAL; } static void nice_o_solve_finish(NiceOCode* app) { if(app->sweep_ambiguous) { app->solved = false; furi_string_set(app->status, "Ambiguous, capture more"); } else if(app->sweep_found) { app->solved = true; app->ic = app->sweep_ic_found; app->serial = app->sweep_serial; furi_string_set(app->status, "Found"); } else { app->solved = false; furi_string_set(app->status, "No code fits, not O-Code?"); } } typedef struct { FuriSemaphore* done; bool accepted; } NiceONameCtx; static void nice_o_name_done(void* context) { NiceONameCtx* ctx = context; ctx->accepted = true; furi_semaphore_release(ctx->done); } static void nice_o_name_back(void* context) { NiceONameCtx* ctx = context; ctx->accepted = false; furi_semaphore_release(ctx->done); } /* Hands the screen to the keyboard, blocks until the name is confirmed or dropped, * then gives the screen back. Returns false if the user backed out. */ static bool nice_o_ask_name(NiceOCode* app, char* name, size_t name_size) { gui_remove_view_port(app->gui, app->view_port); NiceONameCtx ctx = {.done = furi_semaphore_alloc(1, 0), .accepted = false}; TextInput* text_input = text_input_alloc(); text_input_set_header_text(text_input, "Name the .sub file"); text_input_set_result_callback(text_input, nice_o_name_done, &ctx, name, name_size, false); ValidatorIsFile* validator = validator_is_file_alloc_init(NICE_O_FOLDER, NICE_O_EXT, NULL); text_input_set_validator(text_input, validator_is_file_callback, validator); ViewHolder* view_holder = view_holder_alloc(); view_holder_attach_to_gui(view_holder, app->gui); view_holder_set_back_callback(view_holder, nice_o_name_back, &ctx); view_holder_set_view(view_holder, text_input_get_view(text_input)); furi_semaphore_acquire(ctx.done, FuriWaitForever); view_holder_set_view(view_holder, NULL); view_holder_free(view_holder); text_input_free(text_input); validator_is_file_free(validator); furi_semaphore_free(ctx.done); gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen); return ctx.accepted; } /* Status is read by the draw callback on the GUI thread, so every write takes the * mutex. It is never held across nice_o_ask_name(), which blocks on the keyboard. */ static void nice_o_set_status(NiceOCode* app, const char* text) { furi_mutex_acquire(app->mutex, FuriWaitForever); furi_string_set(app->status, text); furi_mutex_release(app->mutex); } /* Writes one .sub holding @p key and the recovered installer code. */ /* Let the protocol build the file: it encrypts the parcel, works out the Nice One tail * for the 72 bit frame and writes the installer code, none of which can be assembled * correctly from a captured frame alone. */ static bool nice_o_save_key(NiceOCode* app, uint32_t serial, uint8_t btn, uint16_t cnt, const char* prefix) { char name[NICE_O_NAME_LEN]; name_generator_make_random_prefixed(name, sizeof(name), prefix); if(!nice_o_ask_name(app, name, sizeof(name))) { nice_o_set_status(app, "Save cancelled"); return false; } FuriString* path = furi_string_alloc(); furi_string_printf(path, "%s/%s%s", NICE_O_FOLDER, name, NICE_O_EXT); SubGhzEnvironment* env = subghz_environment_alloc(); subghz_environment_set_protocol_registry(env, (void*)&subghz_protocol_registry); subghz_environment_set_nice_flor_s_rainbow_table_file_name(env, NICE_O_KEYSTORE); SubGhzTransmitter* tx = subghz_transmitter_alloc_init(env, SUBGHZ_PROTOCOL_NICE_FLOR_S_NAME); FlipperFormat* fff = flipper_format_file_alloc(app->storage); SubGhzRadioPreset preset = { .name = furi_string_alloc_set("AM650"), .frequency = NICE_O_FREQUENCY, .data = NULL, .data_size = 0, }; bool ok = false; if(tx && flipper_format_file_open_always(fff, furi_string_get_cstr(path))) { void* protocol = subghz_transmitter_get_protocol_instance(tx); ok = (app->key_bits == 72) ? subghz_protocol_nice_o_one_create_data( protocol, fff, serial, btn, cnt, app->ic, &preset) : subghz_protocol_nice_o_create_data( protocol, fff, serial, btn, cnt, app->ic, &preset); /* A certificate seen in a long frame belongs to the gate, not to the remote, so * the new remote keeps it and enrols itself with it. */ const uint32_t cert = nice_o_cert(app); if(ok && cert) { uint8_t cert_data[3] = {(uint8_t)(cert >> 16), (uint8_t)(cert >> 8), (uint8_t)cert}; ok = flipper_format_rewind(fff) && flipper_format_insert_or_update_hex(fff, "Cert", cert_data, sizeof(cert_data)); } } furi_string_free(preset.name); flipper_format_free(fff); if(tx) subghz_transmitter_free(tx); subghz_environment_free(env); char msg[NICE_O_NAME_LEN + 16]; snprintf(msg, sizeof(msg), ok ? "Saved %s%s" : "Save failed: %s%s", name, NICE_O_EXT); nice_o_set_status(app, msg); furi_string_free(path); return ok; } /* The counter sits in the decrypted parcel; the button is the clear nibble on top. The * raw cipher is used here, so the field still needs unscrambling into a plain count. */ static uint16_t nice_o_cnt_of(NiceOCode* app, uint8_t idx) { uint64_t d = subghz_protocol_nice_flor_s_decrypt_ic( app->keys[idx], (uint16_t)(app->ic ^ app->mask_16[idx]), NICE_O_KEYSTORE); return subghz_protocol_nice_o_cnt_swap((uint16_t)(d & 0xFFFFu)); } static uint16_t nice_o_captured_cnt(NiceOCode* app) { return nice_o_cnt_of(app, 0); } static uint8_t nice_o_captured_btn(NiceOCode* app) { return (uint8_t)((app->keys[0] >> 48) & 0x0Fu); } static bool nice_o_save(NiceOCode* app) { return nice_o_save_key( app, app->serial, nice_o_captured_btn(app), nice_o_captured_cnt(app), "NiceO"); } /* * A second remote for the same gate: the captured installer code and button with a serial * and counter of its own. The gate still has to be taught it, the same as any remote * bought over the counter. * * The counter comes from the first press that carried a certificate, falling back to the * first press of all, because a receiver refuses a count far behind the remotes it * already holds. Counts are per serial, so there is no clash with the captured remote. * * The serial keeps the captured top nibble and priority bits and is random elsewhere, * skipping the sibling coset so a mint cannot land on another remote of the same * installation. Two draws xored so a weak generator cannot narrow the choice. */ static bool nice_o_save_new(NiceOCode* app) { uint32_t serial = app->serial; while(((serial ^ app->serial) & ~NICE_O_SIBLING_MASK) == 0) { serial = (app->serial & NICE_O_NEW_SERIAL_KEEP) | ((furi_hal_random_get() ^ furi_hal_random_get()) & 0x0FFFFFFFu & ~NICE_O_NEW_SERIAL_KEEP); } const uint8_t cnt_key = (app->cert_key >= 0) ? (uint8_t)app->cert_key : 0; return nice_o_save_key( app, serial, nice_o_captured_btn(app), nice_o_cnt_of(app, cnt_key), "NiceO_new"); } /* ---------------------------------------------------------------------- main */ int32_t nice_o_code_app(void* p) { UNUSED(p); NiceOCode* app = malloc(sizeof(NiceOCode)); memset(app, 0, sizeof(NiceOCode)); app->mutex = furi_mutex_alloc(FuriMutexTypeNormal); app->queue = furi_message_queue_alloc(16, sizeof(InputEvent)); app->storage = furi_record_open(RECORD_STORAGE); app->dialogs = furi_record_open(RECORD_DIALOGS); app->notifications = furi_record_open(RECORD_NOTIFICATION); app->status = furi_string_alloc_set("Idle"); app->progress = furi_string_alloc(); app->raw = nice_o_raw_alloc(); app->scene = NiceOSceneMenu; nice_o_radio_init(app); app->view_port = view_port_alloc(); view_port_draw_callback_set(app->view_port, nice_o_draw_callback, app); view_port_input_callback_set(app->view_port, nice_o_input_callback, app->queue); app->gui = furi_record_open(RECORD_GUI); gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen); bool running = true; InputEvent event; while(running) { /* the sweep runs a chunk at a time here rather than in one blocking call, so * the progress bar keeps moving and Back still cancels */ furi_mutex_acquire(app->mutex, FuriWaitForever); const bool solving = (app->scene == NiceOSceneSolve); const bool decoding = (app->scene == NiceOSceneRaw); if(solving) { if(nice_o_solve_step(app)) { nice_o_solve_finish(app); app->scene = NiceOSceneResult; notification_message( app->notifications, app->solved ? &sequence_success : &sequence_error); } } furi_mutex_release(app->mutex); /* The rx callback takes the mutex, so the file is pumped with it released. */ if(decoding) { const bool more = nice_o_raw_next(app->raw); furi_mutex_acquire(app->mutex, FuriWaitForever); nice_o_raw_progress(app->raw, app->progress); if(!more) { furi_mutex_release(app->mutex); nice_o_raw_end(app); furi_mutex_acquire(app->mutex, FuriWaitForever); furi_string_reset(app->progress); if(nice_o_solve_begin(app)) { app->scene = NiceOSceneSolve; } else { app->solved = false; app->scene = NiceOSceneResult; notification_message(app->notifications, &sequence_error); } } furi_mutex_release(app->mutex); } if(solving || decoding) view_port_update(app->view_port); if(furi_message_queue_get(app->queue, &event, (solving || decoding) ? 0 : 200) == FuriStatusOk) { if(event.type == InputTypeShort) { furi_mutex_acquire(app->mutex, FuriWaitForever); switch(app->scene) { case NiceOSceneMenu: if(event.key == InputKeyUp && app->menu_index) app->menu_index--; if(event.key == InputKeyDown && app->menu_index < 2) app->menu_index++; if(event.key == InputKeyBack) running = false; if(event.key == InputKeyOk) { if(app->menu_index == 2) { running = false; } else if(app->menu_index == 1) { nice_o_reset_capture(app); furi_string_set(app->status, "Reading file"); furi_mutex_release(app->mutex); const bool started = nice_o_raw_begin(app); furi_mutex_acquire(app->mutex, FuriWaitForever); if(started) { app->scene = NiceOSceneRaw; } else { furi_string_set(app->status, "No file picked"); } } else { nice_o_reset_capture(app); furi_string_set(app->status, "Listening 433.92"); app->scene = NiceOSceneCapture; furi_mutex_release(app->mutex); if(!nice_o_rx_start(app)) { furi_mutex_acquire(app->mutex, FuriWaitForever); furi_string_set(app->status, "Radio busy"); app->scene = NiceOSceneMenu; furi_mutex_release(app->mutex); } furi_mutex_acquire(app->mutex, FuriWaitForever); } } break; case NiceOSceneCapture: if((event.key == InputKeyBack) || (event.key == InputKeyLeft)) { furi_mutex_release(app->mutex); nice_o_rx_stop(app); furi_mutex_acquire(app->mutex, FuriWaitForever); app->scene = NiceOSceneMenu; } else if((event.key == InputKeyOk) && (app->key_count >= NICE_O_MIN_KEYS)) { furi_mutex_release(app->mutex); nice_o_rx_stop(app); furi_mutex_acquire(app->mutex, FuriWaitForever); if(nice_o_solve_begin(app)) { app->scene = NiceOSceneSolve; } else { app->solved = false; app->scene = NiceOSceneResult; notification_message(app->notifications, &sequence_error); } } break; case NiceOSceneRaw: if((event.key == InputKeyBack) || (event.key == InputKeyLeft)) { furi_mutex_release(app->mutex); nice_o_raw_end(app); furi_mutex_acquire(app->mutex, FuriWaitForever); furi_string_reset(app->progress); app->scene = NiceOSceneMenu; } break; case NiceOSceneSolve: if((event.key == InputKeyBack) || (event.key == InputKeyLeft)) { furi_string_set(app->status, "Cancelled"); app->solved = false; app->scene = NiceOSceneResult; } break; case NiceOSceneResult: if((event.key == InputKeyBack) || (event.key == InputKeyLeft)) { app->scene = NiceOSceneMenu; } else if( ((event.key == InputKeyOk) || (event.key == InputKeyRight)) && app->solved) { const bool as_new = (event.key == InputKeyRight); furi_mutex_release(app->mutex); const bool saved = as_new ? nice_o_save_new(app) : nice_o_save(app); furi_mutex_acquire(app->mutex, FuriWaitForever); notification_message( app->notifications, saved ? &sequence_success : &sequence_error); } break; } furi_mutex_release(app->mutex); } } view_port_update(app->view_port); } nice_o_rx_stop(app); nice_o_raw_end(app); nice_o_raw_free(app->raw); nice_o_radio_deinit(app); gui_remove_view_port(app->gui, app->view_port); view_port_free(app->view_port); furi_record_close(RECORD_GUI); furi_record_close(RECORD_NOTIFICATION); furi_record_close(RECORD_DIALOGS); furi_record_close(RECORD_STORAGE); furi_message_queue_free(app->queue); furi_mutex_free(app->mutex); furi_string_free(app->status); furi_string_free(app->progress); free(app); return 0; }