From eff68693addaa7043a36efb86cb679133471834b Mon Sep 17 00:00:00 2001 From: lifegame1lu111 <40743123+lifegame1lu111@users.noreply.github.com> Date: Thu, 12 Feb 2026 09:41:56 +0000 Subject: [PATCH] Add support for decrypting and saving KeeLoq signals for SubGhzD (#2982) --- firmware/application/apps/ui_subghzd.cpp | 132 ++++++++++++++++++++++- firmware/application/apps/ui_subghzd.hpp | 29 ++++- firmware/application/file_path.cpp | 2 + firmware/application/file_path.hpp | 2 + firmware/application/keeloq_file.cpp | 69 ++++++++++++ firmware/application/keeloq_file.hpp | 40 +++++++ firmware/application/keeloq_keystore.cpp | 53 +++++++++ firmware/application/keeloq_keystore.hpp | 49 +++++++++ 8 files changed, 374 insertions(+), 2 deletions(-) create mode 100644 firmware/application/keeloq_file.cpp create mode 100644 firmware/application/keeloq_file.hpp create mode 100644 firmware/application/keeloq_keystore.cpp create mode 100644 firmware/application/keeloq_keystore.hpp diff --git a/firmware/application/apps/ui_subghzd.cpp b/firmware/application/apps/ui_subghzd.cpp index 59c485644..379b44762 100644 --- a/firmware/application/apps/ui_subghzd.cpp +++ b/firmware/application/apps/ui_subghzd.cpp @@ -25,6 +25,9 @@ #include "baseband_api.hpp" #include "string_format.hpp" #include "file_path.hpp" +#include "ui_textentry.hpp" +#include "../keeloq_keystore.hpp" +#include "../keeloq_file.hpp" #include "portapack_persistent_memory.hpp" using namespace portapack; @@ -56,6 +59,38 @@ void SubGhzDRecentEntryDetailView::update_data() { if (cnt != SD_NO_CNT) console.writeln("Cnt: " + to_string_dec_uint(cnt)); if (entry_.data != 0) console.writeln("Data: " + to_string_hex(entry_.data)); + + if (entry_.sensorType == FPS_KEELOQ) { + console.writeln("Fix: " + to_string_hex(fix)); + console.writeln("Encrypted: " + to_string_hex(encrypted)); + console.writeln("Manufacturer: " + mf_name); + + if (hop != SD_NO_HOP) { + console.writeln("Hop: " + to_string_hex(hop)); + + add_children({&button_save}); + + button_save.on_select = [this](const Button&) { + keeloq_file_buffer.clear(); + + text_prompt( + nav_, + keeloq_file_buffer, + 64, + ENTER_KEYBOARD_MODE_ALPHA, + [this](std::string& buffer) { + KeeloqData params{ + mf_name, + serial, + cnt, + btn}; + + ensure_directory(keeloq_remotes_dir); + write_keeloq_file(keeloq_remotes_dir / buffer + ".KEELOQ", params); + }); + }; + } + } } SubGhzDRecentEntryDetailView::SubGhzDRecentEntryDetailView(NavigationView& nav, const SubGhzDRecentEntry& entry) @@ -303,6 +338,51 @@ void atomo_decrypt(uint8_t* buff) { } } +bool SubGhzDRecentEntryDetailView::keeloq_check_decrypt(uint32_t decrypt) { + uint16_t end_serial = serial & 0xFF; + + if ((decrypt >> 28 == btn) && (((((uint16_t)(decrypt >> 16)) & 0xFF) == end_serial) || + ((((uint16_t)(decrypt >> 16)) & 0xFF) == 0))) { + cnt = decrypt & 0xFFFF; + + return true; + } + + return false; +} + +bool SubGhzDRecentEntryDetailView::keeloq_check_decrypt_centurion(uint32_t decrypt) { + if ((decrypt >> 28 == btn) && ((((uint16_t)(decrypt >> 16)) & 0x3FF) == 0x1CE)) { + cnt = decrypt & 0xFFFF; + + return true; + } + + return false; +} + +uint32_t keeloq_decrypt(const uint32_t data, const uint64_t key) { + uint32_t x = data, r; + for (r = 0; r < 528; r++) + x = (x << 1) ^ bit(x, 31) ^ bit(x, 15) ^ (uint32_t)bit(key, (15 - r) & 63) ^ + bit(KEELOQ_NLF, g5(x, 0, 8, 19, 25, 30)); + return x; +} + +uint64_t keeloq_normal_learning(uint32_t data, const uint64_t key) { + uint32_t k1, k2; + + data &= 0x0FFFFFFF; + data |= 0x20000000; + k1 = keeloq_decrypt(data, key); + + data &= 0x0FFFFFFF; + data |= 0x60000000; + k2 = keeloq_decrypt(data, key); + + return ((uint64_t)k2 << 32) | k1; +} + const uint32_t came_twee_magic_numbers_xor[15] = { 0x0E0E0E00, 0x1D1D1D11, @@ -591,7 +671,57 @@ void SubGhzDRecentEntryDetailView::parseProtocol() { } if (entry_.sensorType == FPS_KEELOQ) { - // too many sub protocol versions, skipping. maybe in future when we'll have much more fw space + uint64_t data_rev = FProtoGeneral::subghz_protocol_blocks_reverse_key(entry_.data, 64); + + btn = data_rev >> 60; + serial = (data_rev >> 32) & 0xFFFFFFF; + fix = data_rev >> 32; + encrypted = data_rev & 0xFFFFFFFF; + + KeeloqKeystore keystore{}; + + const auto& keys = keystore.get_keys(); + + if (keys.empty()) { + return; + } + + for (const auto& key : keys) { + switch (key.type) { + case KEELOQ_SIMPLE_LEARNING: { + uint32_t decrypted = keeloq_decrypt(encrypted, key.key); + + if (keeloq_check_decrypt(decrypted)) { + mf_name = key.mf_name; + hop = decrypted; + + return; + } + + break; + } + + case KEELOQ_NORMAL_LEARNING: { + uint64_t man = keeloq_normal_learning(fix, key.key); + uint32_t decrypted = keeloq_decrypt(encrypted, man); + + if (key.mf_name == "Centurion" && keeloq_check_decrypt_centurion(decrypted)) { + mf_name = "Centurion"; + hop = decrypted; + + return; + } else if (keeloq_check_decrypt(decrypted)) { + mf_name = key.mf_name; + hop = decrypted; + + return; + } + + break; + } + } + } + return; } diff --git a/firmware/application/apps/ui_subghzd.hpp b/firmware/application/apps/ui_subghzd.hpp index 0c0ed8a3f..77bf5e34d 100644 --- a/firmware/application/apps/ui_subghzd.hpp +++ b/firmware/application/apps/ui_subghzd.hpp @@ -25,7 +25,17 @@ #define SD_NO_SERIAL 0xFFFFFFFF #define SD_NO_BTN 0xFF -#define SD_NO_CNT 0xFF +#define SD_NO_CNT 0xFFFFFFFF + +#define SD_NO_HOP 0xFFFFFFFF +#define SD_NO_FIX 0xFFFFFFFF +#define SD_NO_ENCRYPTED 0xFFFFFFFF + +#define bit(x, n) (((x) >> (n)) & 1) +#define g5(x, a, b, c, d, e) \ + (bit(x, a) + bit(x, b) * 2 + bit(x, c) * 4 + bit(x, d) * 8 + bit(x, e) * 16) + +#define KEELOQ_NLF 0x3A5C742E #include "ui.hpp" #include "ui_navigation.hpp" @@ -191,6 +201,19 @@ class SubGhzDRecentEntryDetailView : public View { uint32_t cnt = SD_NO_CNT; uint32_t seed = 0; + // keeloq specific + std::string keeloq_file_buffer{}; + + uint32_t hop = SD_NO_HOP; + uint32_t fix = SD_NO_FIX; + uint32_t encrypted = SD_NO_ENCRYPTED; + + std::string mf_name = "Unknown"; + + bool keeloq_check_decrypt(uint32_t decrypt); + bool keeloq_check_decrypt_centurion(uint32_t decrypt); + // end keeloq specific + Text text_type{{UI_POS_X(0), 1 * 16, 15 * 8, 16}, "?"}; Text text_id{{6 * 8, 2 * 16, 10 * 8, 16}, "?"}; @@ -207,6 +230,10 @@ class SubGhzDRecentEntryDetailView : public View { {screen_width - 96 - 4, screen_height - 32 - 12, 96, 32}, "Done"}; + Button button_save{ + {4, screen_height - 32 - 12, 96, 32}, + "Save"}; + void parseProtocol(); }; diff --git a/firmware/application/file_path.cpp b/firmware/application/file_path.cpp index a08811fb6..9deeb9e3f 100644 --- a/firmware/application/file_path.cpp +++ b/firmware/application/file_path.cpp @@ -53,3 +53,5 @@ const std::filesystem::path hopper_dir = u"HOPPER"; const std::filesystem::path subghz_dir = u"SUBGHZ"; const std::filesystem::path waterfalls_dir = u"WATERFALLS"; const std::filesystem::path macaddress_dir = u"MACADDRESS"; +const std::filesystem::path keeloq_keys_dir = u"KEELOQKEYS"; +const std::filesystem::path keeloq_remotes_dir = u"KEELOQREMOTES"; diff --git a/firmware/application/file_path.hpp b/firmware/application/file_path.hpp index 0f8548ee0..edc2df0ec 100644 --- a/firmware/application/file_path.hpp +++ b/firmware/application/file_path.hpp @@ -55,5 +55,7 @@ extern const std::filesystem::path hopper_dir; extern const std::filesystem::path subghz_dir; extern const std::filesystem::path waterfalls_dir; extern const std::filesystem::path macaddress_dir; +extern const std::filesystem::path keeloq_keys_dir; +extern const std::filesystem::path keeloq_remotes_dir; #endif /* __FILE_PATH_H__ */ diff --git a/firmware/application/keeloq_file.cpp b/firmware/application/keeloq_file.cpp new file mode 100644 index 000000000..6cc7d2433 --- /dev/null +++ b/firmware/application/keeloq_file.cpp @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2026 lifegame1lu111 + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "keeloq_file.hpp" + +namespace fs = std::filesystem; + +bool read_keeloq_file(const fs::path& path, KeeloqData& data) { + File file{}; + + auto result = file.open(path); + + if (result) { + return false; + } + + FileLineReader reader{file}; + + std::string raw = *reader.begin(); + auto chunks = split_string(raw, ';'); + + if (chunks.size() != 4) { + return false; + } + + data.mf_name = std::string{chunks[0]}; + data.serial = std::strtoul(chunks[1].data(), NULL, 16); + data.counter = (uint16_t)std::atoi(chunks[2].data()); + data.btn = (uint8_t)std::atoi(chunks[3].data()); + + return true; +} + +bool write_keeloq_file(const fs::path& path, const KeeloqData& data) { + delete_file(path); + + File file{}; + + auto result = file.open(path, false, true); + + if (result) { + return false; + } + + std::string formatted = data.mf_name + ";" + to_string_hex(data.serial) + ";" + to_string_dec_uint(data.counter) + ";" + to_string_dec_uint(data.btn); + + file.write_line(formatted); + file.close(); + + return true; +} diff --git a/firmware/application/keeloq_file.hpp b/firmware/application/keeloq_file.hpp new file mode 100644 index 000000000..ca0ff1b42 --- /dev/null +++ b/firmware/application/keeloq_file.hpp @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2026 lifegame1lu111 + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __KEELOQ_FILE__ +#define __KEELOQ_FILE__ + +#include "portapack.hpp" +#include "file_reader.hpp" +#include "string_format.hpp" +#include + +struct KeeloqData { + std::string mf_name{}; + uint32_t serial = 0; + uint16_t counter = 0; + uint8_t btn = 0; +}; + +bool read_keeloq_file(const std::filesystem::path&, KeeloqData&); +bool write_keeloq_file(const std::filesystem::path&, const KeeloqData&); + +#endif diff --git a/firmware/application/keeloq_keystore.cpp b/firmware/application/keeloq_keystore.cpp new file mode 100644 index 000000000..f57839de9 --- /dev/null +++ b/firmware/application/keeloq_keystore.cpp @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2026 lifegame1lu111 + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "keeloq_keystore.hpp" + +KeeloqKeystore::KeeloqKeystore() { + ensure_directory(keeloq_keys_dir); + + File keeloq_keys_file{}; + + if (keeloq_keys_file.open(keeloq_keys_dir / "MFCODES")) { + return; + } + + FileLineReader reader{keeloq_keys_file}; + + for (const std::string& line : reader) { + auto cols = split_string(line, ';'); + + if (cols.size() != 3) { + return; + } + + KeeloqKey key{ + std::string{cols[0]}, + std::strtoull(cols[1].data(), NULL, 16), + (uint8_t)std::atoi(cols[2].data())}; + + keys.push_back(key); + } +} + +const std::vector& KeeloqKeystore::get_keys() { + return keys; +} diff --git a/firmware/application/keeloq_keystore.hpp b/firmware/application/keeloq_keystore.hpp new file mode 100644 index 000000000..91a412f75 --- /dev/null +++ b/firmware/application/keeloq_keystore.hpp @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2026 lifegame1lu111 + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __KEELOQ_KEYSTORE__ +#define __KEELOQ_KEYSTORE__ + +#include "file_reader.hpp" +#include "file_path.hpp" +#include "string_format.hpp" +#include + +#define KEELOQ_SIMPLE_LEARNING 1 +#define KEELOQ_NORMAL_LEARNING 2 + +struct KeeloqKey { + std::string mf_name{}; + uint64_t key = 0; + uint8_t type = 0; +}; + +class KeeloqKeystore { + public: + KeeloqKeystore(); + + const std::vector& get_keys() const; + + private: + std::vector keys{}; +}; + +#endif