From 5944cfa7415912806ee9a701d32017c141ad6e15 Mon Sep 17 00:00:00 2001 From: KaraZajac Date: Tue, 17 Feb 2026 19:24:03 -0500 Subject: [PATCH] VAG fix --- IMPORTS/.DS_Store | Bin 0 -> 6148 bytes IMPORTS/FIAT/Fiat Panda.sub | 14 + IMPORTS/FORD/.DS_Store | Bin 0 -> 6148 bytes IMPORTS/HYUNDAI/.DS_Store | Bin 0 -> 6148 bytes IMPORTS/HYUNDAI/desktop.ini | 4 - IMPORTS/Isuzu/Isuzu_Lock.sub | 29 + IMPORTS/Isuzu/Isuzu_Unlock.sub | 28 + IMPORTS/Isuzu/Isuzu_fts_lock.sub | 17 + IMPORTS/Isuzu/Isuzu_fts_unlock.sub | 20 + IMPORTS/KIA/.DS_Store | Bin 0 -> 6148 bytes IMPORTS/KIA/desktop.ini | 5 - IMPORTS/LDV/LDV-T80_lock.sub | 22 + IMPORTS/LDV/LDV-T80_unlock.sub | 20 + IMPORTS/Lamborghini/.DS_Store | Bin 0 -> 6148 bytes IMPORTS/Lamborghini/Lambo_Lock.sub | 6 + IMPORTS/Lamborghini/Lambo_Trunk.sub | 6 + IMPORTS/Lamborghini/Lambo_Unlock.sub | 6 + IMPORTS/Lexus_og_Toyota/desktop.ini | 11 - IMPORTS/Mazda/.DS_Store | Bin 0 -> 6148 bytes IMPORTS/Mazda/ReadMe.md | 2 - IMPORTS/SUBARU/.DS_Store | Bin 0 -> 6148 bytes IMPORTS/SUBARU/desktop.ini | 2 - IMPORTS/Toyota + Lexus/.DS_Store | Bin 0 -> 6148 bytes .../18_toyota_tacoma_l2_ul2_p2.sub | 0 .../19_lexus_is_sk_l_u_t_p.sub | 0 .../19_toyota_camry_hv_ul2_l2_t2_p2.sub | 0 .../19_toyota_camry_l2_u2_t2_p2.sub | 0 .../2017 Lexus NX200t_312.sub | 0 .../2017 Lexus NX200t_314.sub | 0 .../21_lexus_is300_lluulluut.sub | 0 .../21_toyota_tacoma_l_u.sub | 0 .../23_toyota_rav4_l2_ul2_t2_p2.sub | 0 .../23_toyota_tacoma_l2_u2_p2.sub | 0 .../Toyota_Lexus NX200t_2017_312.sub | 0 .../Toyota_Lexus NX200t_2017_314.sub | 0 .../Toyota_Prius2006_lock.sub | 0 .../Toyota_Prius2006_unlock.sub | 0 IMPORTS/VOLKSWAGEN AUDI/desktop.ini | 3 - IMPORTS/honda/.DS_Store | Bin 0 -> 6148 bytes IMPORTS/honda/24_honda_crv_l2_u2_rs_t_p.sub | 5 + IMPORTS/honda/desktop.ini | 8 - IMPORTS/nissan/.DS_Store | Bin 0 -> 6148 bytes IMPORTS/nissan/Skyline_close_US.sub | 11 + IMPORTS/nissan/Skyline_open_US.sub | 8 + scripts/build_keystore_from_mf_keys.py | 111 ++++ src/keystore/embedded.rs | 194 +++--- src/keystore/mod.rs | 90 ++- src/protocols/mod.rs | 616 +++++++++--------- src/protocols/vag.rs | 18 +- 49 files changed, 807 insertions(+), 449 deletions(-) create mode 100644 IMPORTS/.DS_Store create mode 100644 IMPORTS/FIAT/Fiat Panda.sub create mode 100644 IMPORTS/FORD/.DS_Store create mode 100644 IMPORTS/HYUNDAI/.DS_Store delete mode 100644 IMPORTS/HYUNDAI/desktop.ini create mode 100644 IMPORTS/Isuzu/Isuzu_Lock.sub create mode 100644 IMPORTS/Isuzu/Isuzu_Unlock.sub create mode 100644 IMPORTS/Isuzu/Isuzu_fts_lock.sub create mode 100644 IMPORTS/Isuzu/Isuzu_fts_unlock.sub create mode 100644 IMPORTS/KIA/.DS_Store delete mode 100644 IMPORTS/KIA/desktop.ini create mode 100644 IMPORTS/LDV/LDV-T80_lock.sub create mode 100644 IMPORTS/LDV/LDV-T80_unlock.sub create mode 100644 IMPORTS/Lamborghini/.DS_Store create mode 100644 IMPORTS/Lamborghini/Lambo_Lock.sub create mode 100644 IMPORTS/Lamborghini/Lambo_Trunk.sub create mode 100644 IMPORTS/Lamborghini/Lambo_Unlock.sub delete mode 100644 IMPORTS/Lexus_og_Toyota/desktop.ini create mode 100644 IMPORTS/Mazda/.DS_Store delete mode 100644 IMPORTS/Mazda/ReadMe.md create mode 100644 IMPORTS/SUBARU/.DS_Store delete mode 100644 IMPORTS/SUBARU/desktop.ini create mode 100644 IMPORTS/Toyota + Lexus/.DS_Store rename IMPORTS/{Lexus_og_Toyota => Toyota + Lexus}/18_toyota_tacoma_l2_ul2_p2.sub (100%) rename IMPORTS/{Lexus_og_Toyota => Toyota + Lexus}/19_lexus_is_sk_l_u_t_p.sub (100%) rename IMPORTS/{Lexus_og_Toyota => Toyota + Lexus}/19_toyota_camry_hv_ul2_l2_t2_p2.sub (100%) rename IMPORTS/{Lexus_og_Toyota => Toyota + Lexus}/19_toyota_camry_l2_u2_t2_p2.sub (100%) rename IMPORTS/{Lexus_og_Toyota => Toyota + Lexus}/2017 Lexus NX200t_312.sub (100%) rename IMPORTS/{Lexus_og_Toyota => Toyota + Lexus}/2017 Lexus NX200t_314.sub (100%) rename IMPORTS/{Lexus_og_Toyota => Toyota + Lexus}/21_lexus_is300_lluulluut.sub (100%) rename IMPORTS/{Lexus_og_Toyota => Toyota + Lexus}/21_toyota_tacoma_l_u.sub (100%) rename IMPORTS/{Lexus_og_Toyota => Toyota + Lexus}/23_toyota_rav4_l2_ul2_t2_p2.sub (100%) rename IMPORTS/{Lexus_og_Toyota => Toyota + Lexus}/23_toyota_tacoma_l2_u2_p2.sub (100%) rename IMPORTS/{Lexus_og_Toyota => Toyota + Lexus}/Toyota_Lexus NX200t_2017_312.sub (100%) rename IMPORTS/{Lexus_og_Toyota => Toyota + Lexus}/Toyota_Lexus NX200t_2017_314.sub (100%) rename IMPORTS/{Lexus_og_Toyota => Toyota + Lexus}/Toyota_Prius2006_lock.sub (100%) rename IMPORTS/{Lexus_og_Toyota => Toyota + Lexus}/Toyota_Prius2006_unlock.sub (100%) delete mode 100644 IMPORTS/VOLKSWAGEN AUDI/desktop.ini create mode 100644 IMPORTS/honda/.DS_Store create mode 100644 IMPORTS/honda/24_honda_crv_l2_u2_rs_t_p.sub delete mode 100644 IMPORTS/honda/desktop.ini create mode 100644 IMPORTS/nissan/.DS_Store create mode 100644 IMPORTS/nissan/Skyline_close_US.sub create mode 100644 IMPORTS/nissan/Skyline_open_US.sub create mode 100644 scripts/build_keystore_from_mf_keys.py diff --git a/IMPORTS/.DS_Store b/IMPORTS/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..efa3f81697decf494515d3bb5a15337317ad77a1 GIT binary patch literal 6148 zcmeHKu};H441L!Ysi+JU17gfCbYTuv5JFW`3|4l20~q-h*w}c! zv)WWi)U7JWmVEDWwsX$QDUJcS{$kVvx&Yc#!Nxv^3K6?#NgBFxKy>~di`lq0xXsde z$=4QklL2{md$_;@7I;F{`zu(}EJ=p5Y{;1Q$@KB(?d9WiJwDAw8$9Nd(h2F{5;sUO zM235eF~Jmfm{y#n8qZ>0htKDqnpds&D#y*<75O%qZ-T3e{M1s+=j-+R=sobXw669q zTPyb4&68tU83V?^UuJ+cTcq6yXrnP;3>X7T2ITt?Q3YegJfQz{aPTbvvBhOCoJ%hu zF_B`dmi=1N|Gyq&SH^%b@UIwftz?k&`AAw@OAjZtHlkirMZ_-;xD4UMmty)#DL$rp Z;d&$!Vyu`4H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0ByaDKrLeo+uOzJrtfx@VGgs4Yd?`I z*~=i0Qd|EgYuC5!dfTrN)12Qwygom@TukG;e)EmT?(UBEISU?3RyV1VaCLLp|y(XbpHs7eX|-nhm2427-Yp1IN~!^ZviYFH=qOryV+RlCHd{BHfSJ>InmH1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 - This is to help somebody to decode a protocol to bruteforce them or for testing purposes. diff --git a/IMPORTS/SUBARU/.DS_Store b/IMPORTS/SUBARU/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 bytes: + return x.to_bytes(8, "little") + +def build_blob(entries, vag_bytes): + blob = bytearray() + blob.extend(b"KATK") + blob.extend((len(entries)).to_bytes(2, "little")) + for type_id, key_u64, _ in entries: + blob.extend(type_id.to_bytes(4, "little")) + blob.extend(u64_to_le_bytes(key_u64)) + blob.extend(b"VAG ") + assert len(vag_bytes) == 64, f"expected 64 VAG bytes, got {len(vag_bytes)}" + blob.extend(bytes(vag_bytes)) + return bytes(blob) + +def rust_byte_literal(blob: bytes, indent=" ") -> str: + lines = [] + for i in range(0, len(blob), 12): + chunk = blob[i : i + 12] + hexes = ", ".join(f"0x{b:02X}" for b in chunk) + lines.append(indent + hexes + ",") + return "\n".join(lines) + +def main(): + entries, vag_bytes = parse_mf_keys(MF_KEYS_PATH) + blob = build_blob(entries, vag_bytes) + + # embedded.rs + embedded_rs = '''//! Embedded keystore blob (standard encrypted + VAG raw). +//! Data is stored as binary to avoid plain-text keys in config. +//! Format: "KATK" magic, n_entries (u16 LE), then per entry: type_id (u32 LE), key (u64 LE), then "VAG " + 64 bytes. +//! Key bytes are little-endian (LSB first); the resulting u64 matches reference hex (MSB-first notation). +//! Generated from REFERENCES/mf_keys.txt by scripts/build_keystore_from_mf_keys.py + +/// Blob: KATK + ''' + str(len(entries)) + ''' entries + VAG 64 bytes. +#[rustfmt::skip] +pub const KEYSTORE_BLOB: &[u8] = &[ +''' + # Split blob for readable comments: magic + n, then entries (each 12 bytes), then VAG tag + 64 + off = 0 + embedded_rs += " // magic + n_entries\n" + embedded_rs += rust_byte_literal(blob[0:6], " ") + "\n" + off = 6 + entry_size = 12 + for i, (ty, key, name) in enumerate(entries): + if i % 8 == 0: + embedded_rs += f" // entries {i}..\n" + embedded_rs += rust_byte_literal(blob[off : off + entry_size], " ") + "\n" + off += entry_size + embedded_rs += " // VAG tag + 64 bytes\n" + embedded_rs += rust_byte_literal(blob[off:], " ") + "\n" + embedded_rs += "];\n" + + with open("src/keystore/embedded.rs", "w") as f: + f.write(embedded_rs) + + # KEY_ENTRY_NAMES for mod.rs (same order as blob entries) + names = [e[2] for e in entries] + + # Update mod.rs: replace KEY_ENTRY_NAMES + mod_rs_path = "src/keystore/mod.rs" + with open(mod_rs_path) as f: + mod_content = f.read() + escaped = [n.replace("\\", "\\\\").replace('"', '\\"') for n in names] + new_block = "const KEY_ENTRY_NAMES: &[&str] = &[\n " + ",\n ".join(f'"{s}"' for s in escaped) + ",\n];" + pattern = r"const KEY_ENTRY_NAMES: &\[&str\] = &\[\n.*?\n\];" + mod_content = re.sub(pattern, new_block, mod_content, flags=re.DOTALL) + with open(mod_rs_path, "w") as f: + f.write(mod_content) + + print(f"Wrote embedded blob with {len(entries)} entries, {len(vag_bytes)} VAG bytes") + print(f"Updated KEY_ENTRY_NAMES with {len(names)} names") + +if __name__ == "__main__": + main() diff --git a/src/keystore/embedded.rs b/src/keystore/embedded.rs index 78ffae9..9aa44c8 100644 --- a/src/keystore/embedded.rs +++ b/src/keystore/embedded.rs @@ -2,98 +2,112 @@ //! Data is stored as binary to avoid plain-text keys in config. //! Format: "KATK" magic, n_entries (u16 LE), then per entry: type_id (u32 LE), key (u64 LE), then "VAG " + 64 bytes. //! Key bytes are little-endian (LSB first); the resulting u64 matches reference hex (MSB-first notation). -//! All manufacture keys from the encrypted keystore list are included for current and future protocols. -//! Type 20 (Star Line) is also included so KeyStore.star_line_mf_key is populated from the same key as SL_A2-A4. +//! Generated from REFERENCES/mf_keys.txt by scripts/build_keystore_from_mf_keys.py -/// Blob: KATK + 41 entries (30 original + 11 Pandora new keys) + VAG 64 bytes. +/// Blob: KATK + 83 entries + VAG 64 bytes. #[rustfmt::skip] pub const KEYSTORE_BLOB: &[u8] = &[ - b'K', b'A', b'T', b'K', - 0x29, 0x00, // 41 entries (30 original + 11 Pandora) - // type 10 KIA - 0x0A, 0x00, 0x00, 0x00, 0xDB, 0x5C, 0xAA, 0x8D, 0xFC, 0xDF, 0xF5, 0xA8, - // type 11 KIAV6A - 0x0B, 0x00, 0x00, 0x00, 0xFC, 0x23, 0xAA, 0x80, 0xA8, 0x64, 0x86, 0x63, - // type 12 KIAV6B - 0x0C, 0x00, 0x00, 0x00, 0xF4, 0x2B, 0xA2, 0x88, 0xA0, 0x6C, 0x8E, 0x6B, - // type 13 KIAV5 - 0x0D, 0x00, 0x00, 0x00, 0x30, 0x30, 0x45, 0x4B, 0x52, 0x46, 0x54, 0x53, - // type 1 Alligator - 0x01, 0x00, 0x00, 0x00, 0x78, 0x55, 0x6C, 0x62, 0xB2, 0xD4, 0xF3, 0xEE, - // type 2 Mongoose - 0x02, 0x00, 0x00, 0x00, 0xBA, 0xA0, 0xC1, 0xA7, 0xB2, 0xE7, 0xA5, 0xD5, - // type 1 SL_A6-A9/Tomahawk_9010 - 0x01, 0x00, 0x00, 0x00, 0x03, 0x12, 0x30, 0x08, 0x57, 0x04, 0x04, 0x55, - // type 1 Pantera - 0x01, 0x00, 0x00, 0x00, 0x65, 0x6E, 0x79, 0x64, 0x4E, 0x47, 0x41, 0x4D, - // type 1 SL_A2-A4 - 0x01, 0x00, 0x00, 0x00, 0xDA, 0x78, 0xFE, 0xF8, 0x9B, 0xF8, 0xF7, 0x9B, - // type 1 Cenmax_St-5 - 0x01, 0x00, 0x00, 0x00, 0xFC, 0xED, 0xCF, 0xF7, 0xA8, 0xFB, 0xFB, 0xAA, - // type 1 SL_B6,B9_dop - 0x01, 0x00, 0x00, 0x00, 0xFE, 0xDC, 0xBA, 0x2A, 0xB3, 0xD4, 0xE5, 0xF6, - // type 1 Harpoon - 0x01, 0x00, 0x00, 0x00, 0x64, 0x08, 0x07, 0x64, 0x07, 0x01, 0x87, 0x25, - // type 1 Tomahawk_TZ-9030 - 0x01, 0x00, 0x00, 0x00, 0x25, 0x87, 0x01, 0x07, 0x64, 0x07, 0x08, 0x64, - // type 1 Tomahawk_Z,X_3-5 - 0x01, 0x00, 0x00, 0x00, 0xEF, 0xCD, 0xAB, 0x2A, 0x3B, 0x4D, 0x5E, 0x6F, - // type 1 Cenmax_St-7 - 0x01, 0x00, 0x00, 0x00, 0x2B, 0x75, 0xAC, 0x80, 0xB9, 0x46, 0xB4, 0x66, - // type 1 Sheriff - 0x01, 0x00, 0x00, 0x00, 0x78, 0x94, 0x18, 0xA9, 0x94, 0x25, 0x33, 0x12, - // type 1 Pantera_CLK - 0x01, 0x00, 0x00, 0x00, 0x14, 0x0A, 0x61, 0x82, 0x0A, 0x85, 0x30, 0x41, - // type 1 Cenmax - 0x01, 0x00, 0x00, 0x00, 0x56, 0x34, 0x12, 0x90, 0x78, 0x56, 0x34, 0x12, - // type 1 Alligator_S-275 - 0x01, 0x00, 0x00, 0x00, 0xF9, 0x84, 0x62, 0x67, 0xB7, 0x5C, 0x36, 0x54, - // type 2 Guard_RF-311A - 0x02, 0x00, 0x00, 0x00, 0x47, 0x88, 0x19, 0xEB, 0xC2, 0xDA, 0x61, 0x19, - // type 1 Partisan_RX - 0x01, 0x00, 0x00, 0x00, 0x1E, 0xF1, 0x28, 0x70, 0xA1, 0x12, 0x3C, 0xAD, - // type 1 APS-1100_APS-2550 - 0x01, 0x00, 0x00, 0x00, 0x28, 0x47, 0x33, 0x56, 0x50, 0x2C, 0x73, 0x68, - // type 1 Pantera_XS/Jaguar - 0x01, 0x00, 0x00, 0x00, 0x27, 0x46, 0x33, 0x56, 0x50, 0x2C, 0x73, 0x67, - // type 0 Teco - 0x00, 0x00, 0x00, 0x00, 0xD7, 0xD1, 0x26, 0xF1, 0xE8, 0x26, 0x15, 0xB5, - // type 0 Leopard - 0x00, 0x00, 0x00, 0x00, 0x46, 0x8A, 0xD8, 0xE7, 0x33, 0xDC, 0x67, 0x10, - // type 0 Faraon - 0x00, 0x00, 0x00, 0x00, 0xF5, 0x87, 0x22, 0xB6, 0x38, 0x64, 0x32, 0x8A, - // type 0 Reff - 0x00, 0x00, 0x00, 0x00, 0x8A, 0x41, 0xC2, 0x7D, 0x55, 0xFA, 0x12, 0x69, - // type 0 ZX-730-750-1055 - 0x00, 0x00, 0x00, 0x00, 0x96, 0x15, 0x35, 0x27, 0xAB, 0x7A, 0x1A, 0xB5, - // type 20 Star Line (same key as SL_A2-A4, for KeyStore.star_line_mf_key) - 0x14, 0x00, 0x00, 0x00, 0xDA, 0x78, 0xFE, 0xF8, 0x9B, 0xF8, 0xF7, 0x9B, - // type 1 Pandora_M101 - 0x01, 0x00, 0x00, 0x00, 0x99, 0xAE, 0x17, 0x2A, 0x9A, 0x7D, 0x1D, 0x38, - // type 1 Pandora_PRO - 0x01, 0x00, 0x00, 0x00, 0xAC, 0x97, 0x15, 0x28, 0x7B, 0x97, 0x36, 0x1B, - // type 1 Pandora_PRO2 - 0x01, 0x00, 0x00, 0x00, 0xB9, 0x19, 0x2B, 0x39, 0x9A, 0x1E, 0xAF, 0x7E, - // type 1 Pandora_SUBARU - 0x01, 0x00, 0x00, 0x00, 0x95, 0x79, 0x22, 0xA4, 0x34, 0xB9, 0x19, 0x35, - // type 1 Pandora_SUZUKI - 0x01, 0x00, 0x00, 0x00, 0x73, 0xBD, 0xED, 0xDC, 0x06, 0xEF, 0x52, 0x25, - // type 1 Pandora_DEA - 0x01, 0x00, 0x00, 0x00, 0x77, 0xB9, 0xE9, 0xD8, 0x02, 0xEB, 0x56, 0x21, - // type 1 Pandora_GIBIDI - 0x01, 0x00, 0x00, 0x00, 0x76, 0xBA, 0xEA, 0xDB, 0x01, 0xEA, 0x55, 0x22, - // type 1 Pandora_MCODE - 0x01, 0x00, 0x00, 0x00, 0x75, 0xBB, 0xEB, 0xDA, 0x00, 0xE9, 0x54, 0x23, - // type 1 Pandora_Unknown_1 - 0x01, 0x00, 0x00, 0x00, 0x72, 0xBC, 0xEC, 0xDD, 0x07, 0xEE, 0x53, 0x24, - // type 1 Pandora_Unknown_2 - 0x01, 0x00, 0x00, 0x00, 0x70, 0xBE, 0xEE, 0xDF, 0x05, 0xEC, 0x51, 0x26, - // type 1 Pandora_Test_Debug_2 + // magic + n_entries + 0x4B, 0x41, 0x54, 0x4B, 0x53, 0x00, + // entries 0.. + 0x06, 0x00, 0x00, 0x00, 0x58, 0x47, 0x30, 0x23, 0x00, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x48, 0x57, 0x20, 0x33, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x40, 0x30, 0x20, 0x10, 0x04, 0x03, 0x02, 0x01, + 0x02, 0x00, 0x00, 0x00, 0xEF, 0xCD, 0xAB, 0x89, 0x67, 0x45, 0x23, 0x01, 0x01, 0x00, 0x00, 0x00, 0xEF, 0xCD, 0xAB, 0x89, 0x67, 0x45, 0x32, 0x01, - b'V', b'A', b'G', b' ', - // VAG raw 64 bytes - 0x01, 0x37, 0x6C, 0x86, 0xAD, 0xAB, 0xCC, 0x43, 0x07, 0x4D, 0xE8, 0x59, 0xC1, 0x2F, 0x36, 0xAB, - 0x02, 0x37, 0x7C, 0x65, 0xCE, 0xDC, 0x42, 0xEA, 0xA4, 0x53, 0xE8, 0x61, 0xD9, 0xB7, 0x20, 0xFC, - 0x03, 0x8A, 0xA3, 0x7B, 0x1E, 0x56, 0x1F, 0x83, 0x84, 0xB6, 0x19, 0xC5, 0x2E, 0x0A, 0x3F, 0xD7, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x03, 0x19, 0x98, 0xAA, 0xAB, 0xDA, 0xAE, 0x05, + 0x01, 0x00, 0x00, 0x00, 0x03, 0x19, 0x98, 0xAA, 0xAB, 0xDA, 0xAE, 0x08, + 0x00, 0x00, 0x00, 0x00, 0x46, 0x8A, 0xD8, 0xE7, 0x33, 0xDC, 0x67, 0x10, + // entries 8.. + 0x01, 0x00, 0x00, 0x00, 0x78, 0x94, 0x18, 0xA9, 0x94, 0x25, 0x33, 0x12, + 0x01, 0x00, 0x00, 0x00, 0x56, 0x34, 0x12, 0x90, 0x78, 0x56, 0x34, 0x12, + 0x02, 0x00, 0x00, 0x00, 0x14, 0xDF, 0xB1, 0xA9, 0x44, 0x05, 0x8B, 0x18, + 0x02, 0x00, 0x00, 0x00, 0x47, 0x88, 0x19, 0xEB, 0xC2, 0xDA, 0x61, 0x19, + 0x01, 0x00, 0x00, 0x00, 0xAC, 0x97, 0x15, 0x28, 0x7B, 0x97, 0x36, 0x1B, + 0x02, 0x00, 0x00, 0x00, 0x44, 0x6F, 0x38, 0x9D, 0xE5, 0xBB, 0x7D, 0x20, + 0x01, 0x00, 0x00, 0x00, 0x77, 0xB9, 0xE9, 0xD8, 0x02, 0xEB, 0x56, 0x21, + 0x01, 0x00, 0x00, 0x00, 0x76, 0xBA, 0xEA, 0xDB, 0x01, 0xEA, 0x55, 0x22, + // entries 16.. + 0x01, 0x00, 0x00, 0x00, 0x75, 0xBB, 0xEB, 0xDA, 0x00, 0xE9, 0x54, 0x23, + 0x01, 0x00, 0x00, 0x00, 0x72, 0xBC, 0xEC, 0xDD, 0x07, 0xEE, 0x53, 0x24, + 0x01, 0x00, 0x00, 0x00, 0x73, 0xBD, 0xED, 0xDC, 0x06, 0xEF, 0x52, 0x25, + 0x01, 0x00, 0x00, 0x00, 0x64, 0x08, 0x07, 0x64, 0x07, 0x01, 0x87, 0x25, + 0x01, 0x00, 0x00, 0x00, 0x02, 0x19, 0x99, 0x02, 0x19, 0x99, 0x26, 0x26, + 0x01, 0x00, 0x00, 0x00, 0x70, 0xBE, 0xEE, 0xDF, 0x05, 0xEC, 0x51, 0x26, + 0x0B, 0x00, 0x00, 0x00, 0x35, 0x08, 0x7C, 0x11, 0x9B, 0x3A, 0x19, 0x27, + 0x01, 0x00, 0x00, 0x00, 0x71, 0x44, 0x79, 0x7D, 0x30, 0x7B, 0x31, 0x30, + // entries 24.. + 0x01, 0x00, 0x00, 0x00, 0x61, 0x39, 0x4E, 0x30, 0x65, 0x38, 0x4C, 0x31, + 0x01, 0x00, 0x00, 0x00, 0x95, 0x79, 0x22, 0xA4, 0x34, 0xB9, 0x19, 0x35, + 0x01, 0x00, 0x00, 0x00, 0xE0, 0xDF, 0xF4, 0xA5, 0xAC, 0xAB, 0x2B, 0x35, + 0x02, 0x00, 0x00, 0x00, 0x9B, 0xA1, 0x6D, 0xF7, 0xB4, 0x1A, 0x46, 0x3E, + 0x01, 0x00, 0x00, 0x00, 0x99, 0xAE, 0x17, 0x2A, 0x9A, 0x7D, 0x1D, 0x38, + 0x01, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x10, 0x20, 0x30, 0x40, + 0x01, 0x00, 0x00, 0x00, 0x14, 0x0A, 0x61, 0x82, 0x0A, 0x85, 0x30, 0x41, + 0x01, 0x00, 0x00, 0x00, 0x30, 0x38, 0x39, 0x31, 0x58, 0x45, 0x4C, 0x41, + // entries 32.. + 0x02, 0x00, 0x00, 0x00, 0x83, 0x45, 0x13, 0x83, 0x30, 0x90, 0x92, 0x42, + 0x01, 0x00, 0x00, 0x00, 0x69, 0x47, 0x73, 0x65, 0x65, 0x73, 0x72, 0x47, + 0x01, 0x00, 0x00, 0x00, 0x53, 0x52, 0x54, 0x73, 0x6D, 0x6C, 0x4D, 0x48, + 0x03, 0x00, 0x00, 0x00, 0x76, 0x4F, 0x64, 0x55, 0x7A, 0x4D, 0x6D, 0x4C, + 0x01, 0x00, 0x00, 0x00, 0x65, 0x6E, 0x79, 0x64, 0x4E, 0x47, 0x41, 0x4D, + 0x0D, 0x00, 0x00, 0x00, 0x30, 0x30, 0x45, 0x4B, 0x52, 0x46, 0x54, 0x53, + 0x05, 0x00, 0x00, 0x00, 0x14, 0x8C, 0x61, 0x69, 0x76, 0x6C, 0x69, 0x53, + 0x01, 0x00, 0x00, 0x00, 0xF9, 0x84, 0x62, 0x67, 0xB7, 0x5C, 0x36, 0x54, + // entries 40.. + 0x01, 0x00, 0x00, 0x00, 0x4E, 0x49, 0x43, 0x45, 0x53, 0x4D, 0x52, 0x54, + 0x01, 0x00, 0x00, 0x00, 0x03, 0x12, 0x30, 0x08, 0x57, 0x04, 0x04, 0x55, + 0x02, 0x00, 0x00, 0x00, 0xFF, 0xCA, 0x76, 0x94, 0x22, 0x68, 0x27, 0x57, + 0x0B, 0x00, 0x00, 0x00, 0xFC, 0x23, 0xAA, 0x80, 0xA8, 0x64, 0x86, 0x63, + 0x01, 0x00, 0x00, 0x00, 0x25, 0x87, 0x01, 0x07, 0x64, 0x07, 0x08, 0x64, + 0x01, 0x00, 0x00, 0x00, 0x2B, 0x75, 0xAC, 0x80, 0xB9, 0x46, 0xB4, 0x66, + 0x01, 0x00, 0x00, 0x00, 0x27, 0x46, 0x33, 0x56, 0x50, 0x2C, 0x73, 0x67, + 0x01, 0x00, 0x00, 0x00, 0x28, 0x47, 0x33, 0x56, 0x50, 0x2C, 0x73, 0x68, + // entries 48.. + 0x00, 0x00, 0x00, 0x00, 0x8A, 0x41, 0xC2, 0x7D, 0x55, 0xFA, 0x12, 0x69, + 0x0C, 0x00, 0x00, 0x00, 0xF4, 0x2B, 0xA2, 0x88, 0xA0, 0x6C, 0x8E, 0x6B, + 0x09, 0x00, 0x00, 0x00, 0xD2, 0x3E, 0xC6, 0x15, 0x48, 0xAE, 0xB6, 0x6E, + 0x01, 0x00, 0x00, 0x00, 0xEF, 0xCD, 0xAB, 0x2A, 0x3B, 0x4D, 0x5E, 0x6F, + 0x02, 0x00, 0x00, 0x00, 0xBF, 0xDC, 0x6E, 0x37, 0xD4, 0xEE, 0xCB, 0x7B, + 0x01, 0x00, 0x00, 0x00, 0xB9, 0x19, 0x2B, 0x39, 0x9A, 0x1E, 0xAF, 0x7E, + 0x02, 0x00, 0x00, 0x00, 0x23, 0x12, 0x94, 0x84, 0x35, 0xF4, 0x55, 0x84, + 0x02, 0x00, 0x00, 0x00, 0x30, 0x4E, 0x39, 0x61, 0x78, 0x42, 0x07, 0x86, + // entries 56.. + 0x01, 0x00, 0x00, 0x00, 0xB7, 0x03, 0x79, 0x53, 0x59, 0x6E, 0x14, 0x89, + 0x00, 0x00, 0x00, 0x00, 0xF5, 0x87, 0x22, 0xB6, 0x38, 0x64, 0x32, 0x8A, + 0x01, 0x00, 0x00, 0x00, 0x9B, 0x2C, 0x9C, 0xC9, 0x36, 0x8C, 0x63, 0x96, + 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA5, 0x5A, 0x65, 0x04, 0x98, + 0x01, 0x00, 0x00, 0x00, 0xDA, 0x78, 0xFE, 0xF8, 0x9B, 0xF8, 0xF7, 0x9B, + 0x02, 0x00, 0x00, 0x00, 0x5C, 0xE2, 0xB8, 0x47, 0x3A, 0xFD, 0x61, 0x9C, + 0x01, 0x00, 0x00, 0x00, 0xA7, 0x2B, 0x31, 0xCF, 0x53, 0x81, 0xA0, 0x9D, + 0x0A, 0x00, 0x00, 0x00, 0xDB, 0x5C, 0xAA, 0x8D, 0xFC, 0xDF, 0xF5, 0xA8, + // entries 64.. + 0x02, 0x00, 0x00, 0x00, 0x97, 0x52, 0x65, 0xB7, 0x32, 0x85, 0x74, 0xA9, + 0x00, 0x00, 0x00, 0x00, 0xA1, 0xB5, 0x89, 0x21, 0xA3, 0xA7, 0x38, 0xAA, + 0x01, 0x00, 0x00, 0x00, 0xFC, 0xED, 0xCF, 0xF7, 0xA8, 0xFB, 0xFB, 0xAA, + 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x59, 0x27, 0xBB, 0x35, 0xAC, + 0x01, 0x00, 0x00, 0x00, 0x1E, 0xF1, 0x28, 0x70, 0xA1, 0x12, 0x3C, 0xAD, + 0x02, 0x00, 0x00, 0x00, 0x39, 0x71, 0xCD, 0x8C, 0x5A, 0x62, 0xE5, 0xB3, + 0x00, 0x00, 0x00, 0x00, 0xD7, 0xD1, 0x26, 0xF1, 0xE8, 0x26, 0x15, 0xB5, + 0x00, 0x00, 0x00, 0x00, 0x96, 0x15, 0x35, 0x27, 0xAB, 0x7A, 0x1A, 0xB5, + // entries 72.. + 0x01, 0x00, 0x00, 0x00, 0xCC, 0x7E, 0xF9, 0xAA, 0xDD, 0x9E, 0xEE, 0xBB, + 0x04, 0x00, 0x00, 0x00, 0xD2, 0x3E, 0xC6, 0xB5, 0x48, 0xAE, 0xB6, 0xCE, + 0x02, 0x00, 0x00, 0x00, 0xBA, 0xA0, 0xC1, 0xA7, 0xB2, 0xE7, 0xA5, 0xD5, + 0x01, 0x00, 0x00, 0x00, 0x78, 0x55, 0x6C, 0x62, 0xB2, 0xD4, 0xF3, 0xEE, + 0x02, 0x00, 0x00, 0x00, 0x55, 0x7E, 0x64, 0xC8, 0x52, 0xE5, 0xA3, 0xF1, + 0x01, 0x00, 0x00, 0x00, 0xFE, 0xDC, 0xBA, 0x2A, 0xB3, 0xD4, 0xE5, 0xF6, + 0x02, 0x00, 0x00, 0x00, 0xAC, 0xFA, 0x01, 0x00, 0x60, 0x05, 0xAC, 0xFA, + 0x02, 0x00, 0x00, 0x00, 0xAC, 0xFA, 0x02, 0x00, 0x60, 0x05, 0xAC, 0xFA, + // entries 80.. + 0x01, 0x00, 0x00, 0x00, 0xDA, 0xF0, 0x34, 0xDA, 0xFA, 0x12, 0xDA, 0xFA, + 0x01, 0x00, 0x00, 0x00, 0x20, 0x03, 0x37, 0xD5, 0x2C, 0xE2, 0x4D, 0xFC, + 0x14, 0x00, 0x00, 0x00, 0xDA, 0x78, 0xFE, 0xF8, 0x9B, 0xF8, 0xF7, 0x9B, + // VAG tag + 64 bytes + 0x56, 0x41, 0x47, 0x20, 0x01, 0x37, 0x6C, 0x86, 0xAD, 0xAB, 0xCC, 0x43, + 0x07, 0x4D, 0xE8, 0x59, 0xC1, 0x2F, 0x36, 0xAB, 0x02, 0x37, 0x7C, 0x65, + 0xCE, 0xDC, 0x42, 0xEA, 0xA4, 0x53, 0xE8, 0x61, 0xD9, 0xB7, 0x20, 0xFC, + 0x03, 0x8A, 0xA3, 0x7B, 0x1E, 0x56, 0x1F, 0x83, 0x84, 0xB6, 0x19, 0xC5, + 0x2E, 0x0A, 0x3F, 0xD7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; diff --git a/src/keystore/mod.rs b/src/keystore/mod.rs index 6d1fb38..5853426 100644 --- a/src/keystore/mod.rs +++ b/src/keystore/mod.rs @@ -10,15 +10,89 @@ const KEELOQ_MF_TYPES: &[u32] = &[0, 1, 2, 10, 20]; /// Names for each key entry in the blob (same order as entries before VAG). Used for fallback decode display. const KEY_ENTRY_NAMES: &[&str] = &[ - "KIA", "KIAV6A", "KIAV6B", "KIAV5", "Alligator", "Mongoose", - "SL_A6-A9/Tomahawk_9010", "Pantera", "SL_A2-A4", "Cenmax_St-5", "SL_B6,B9_dop", - "Harpoon", "Tomahawk_TZ-9030", "Tomahawk_Z,X_3-5", "Cenmax_St-7", "Sheriff", - "Pantera_CLK", "Cenmax", "Alligator_S-275", "Guard_RF-311A", "Partisan_RX", - "APS-1100_APS-2550", "Pantera_XS/Jaguar", "Teco", "Leopard", "Faraon", "Reff", - "ZX-730-750-1055", "Star Line", - "Pandora_M101", "Pandora_PRO", "Pandora_PRO2", "Pandora_SUBARU", "Pandora_SUZUKI", - "Pandora_DEA", "Pandora_GIBIDI", "Pandora_MCODE", "Pandora_Unknown_1", "Pandora_Unknown_2", + "KGB/Subaru", + "Magic_2", + "IronLogic", + "Stilmatic", "Pandora_Test_Debug_2", + "Rosh", + "Dea_Mio", + "Leopard", + "Sheriff", + "Cenmax", + "Centurion", + "Guard_RF-311A", + "Pandora_PRO", + "Cardin_S449", + "Pandora_DEA", + "Pandora_GIBIDI", + "Pandora_MCODE", + "Pandora_Unknown_1", + "Pandora_SUZUKI", + "Harpoon", + "Gibidi", + "Pandora_Unknown_2", + "Jarolift", + "KEY", + "Novoferm", + "Pandora_SUBARU", + "Pecinin", + "Merlin", + "Pandora_M101", + "IL-100(Smart)", + "Pantera_CLK", + "Kingates_Stylo4k", + "Monarch", + "Aprimatic", + "NICE_MHOUSE", + "BFT", + "Pantera", + "KIAV5", + "FAAC_SLH", + "Alligator_S-275", + "NICE_Smilo", + "SL_A6-A9/Tomahawk_9010", + "Motorline", + "KIAV6A", + "Tomahawk_TZ-9030", + "Cenmax_St-7", + "Pantera_XS/Jaguar", + "APS-1100_APS-2550", + "Reff", + "KIAV6B", + "Beninca_ARC", + "Tomahawk_Z,X_3-5", + "Sommer", + "Pandora_PRO2", + "DoorHan", + "EcoStar", + "JCM_Tech", + "Faraon", + "Came_Space", + "Magic_4", + "SL_A2-A4", + "Elmes_Poland", + "Normstahl", + "KIA", + "GSN", + "Mutanco_Mutancode", + "Cenmax_St-5", + "Magic_3", + "Partisan_RX", + "Steelmate", + "Teco", + "ZX-730-750-1055", + "Jolly_Motors", + "Beninca", + "Mongoose", + "Alligator", + "Comunello", + "SL_B6,B9_dop", + "FAAC_RC,XT", + "Genius_Bravo", + "Rossi", + "DTM_Neo", + "Star Line", ]; const MAGIC: &[u8; 4] = b"KATK"; diff --git a/src/protocols/mod.rs b/src/protocols/mod.rs index 4e9dc02..8e32b12 100644 --- a/src/protocols/mod.rs +++ b/src/protocols/mod.rs @@ -1,308 +1,308 @@ -//! Protocol decoders and encoders for various keyfob systems. -//! -//! Protocols are aligned with the ProtoPirate reference (`REFERENCES/ProtoPirate/protocols/`). -//! Each decoder processes level+duration pairs from the demodulator and optionally supports -//! encoding (replay). Shared pieces: [common], [keeloq_common], [keys], [aut64]. -//! -//! **Manchester decoding**: Ford, Fiat, and common each have separate Manchester state machines -//! (FordV0ManchesterState, FiatV0ManchesterState, CommonManchesterState in common.rs). They are -//! not reused across protocols. Event conventions match the reference per protocol (e.g. Kia V5 -//! opposite polarity; Fiat/Ford/common use Flipper-style: level ? ShortLow : ShortHigh). - -mod common; -pub mod keeloq_common; -mod keeloq_generic; -#[allow(dead_code)] -pub mod aut64; -#[allow(dead_code)] -pub mod keys; -mod kia_v0; -mod kia_v1; -mod kia_v2; -mod kia_v3_v4; -mod kia_v5; -mod kia_v6; -mod subaru; -mod ford_v0; -mod vag; -mod fiat_v0; -mod suzuki; -mod scher_khan; -mod star_line; -mod psa; - -pub use common::DecodedSignal; - -use crate::capture::Capture; -use crate::radio::demodulator::LevelDuration; - -/// Protocol timing constants -#[derive(Debug, Clone, Copy)] -#[allow(dead_code)] -pub struct ProtocolTiming { - /// Short pulse duration in µs - pub te_short: u32, - /// Long pulse duration in µs - pub te_long: u32, - /// Tolerance for timing matching in µs - pub te_delta: u32, - /// Minimum bit count for valid decode - pub min_count_bit: usize, -} - -/// Trait for protocol decoders -/// -/// Each protocol implements a state machine that processes level+duration pairs. -pub trait ProtocolDecoder: Send + Sync { - /// Get the protocol name - fn name(&self) -> &'static str; - - /// Get timing constants - #[allow(dead_code)] - fn timing(&self) -> ProtocolTiming; - - /// Get supported frequencies in Hz - fn supported_frequencies(&self) -> &[u32]; - - /// Reset the decoder state machine - fn reset(&mut self); - - /// Feed a level+duration pair to the decoder - /// Returns Some(DecodedSignal) when a complete valid signal is decoded - fn feed(&mut self, level: bool, duration_us: u32) -> Option; - - /// Check if this protocol supports encoding - fn supports_encoding(&self) -> bool; - - /// Encode a signal with the given button command - fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option>; -} - -/// Registry of all supported protocols -pub struct ProtocolRegistry { - decoders: Vec>, -} - -impl ProtocolRegistry { - /// Create a new protocol registry with all built-in protocols - pub fn new() -> Self { - let decoders: Vec> = vec![ - // Kia protocols - Box::new(kia_v0::KiaV0Decoder::new()), - Box::new(kia_v1::KiaV1Decoder::new()), - Box::new(kia_v2::KiaV2Decoder::new()), - Box::new(kia_v3_v4::KiaV3V4Decoder::new()), - Box::new(kia_v5::KiaV5Decoder::new()), - Box::new(kia_v6::KiaV6Decoder::new()), - // Other protocols (Ford before Subaru so 250/500µs Ford keyfobs decode as Ford) - Box::new(ford_v0::FordV0Decoder::new()), - Box::new(subaru::SubaruDecoder::new()), - Box::new(vag::VagDecoder::new()), - Box::new(fiat_v0::FiatV0Decoder::new()), - Box::new(suzuki::SuzukiDecoder::new()), - Box::new(scher_khan::ScherKhanDecoder::new()), - Box::new(star_line::StarLineDecoder::new()), - Box::new(psa::PsaDecoder::new()), - ]; - - Self { decoders } - } - - /// Process level+duration pairs from demodulator - /// Returns decoded signal info if any protocol matches. - /// Tries normal polarity first, then inverted polarity (so OOK captures where - /// carrier-on is recorded as LOW can still decode as Fiat/Ford etc.). - pub fn process_signal(&mut self, pairs: &[LevelDuration], frequency: u32) -> Option<(String, DecodedSignal)> { - // Try normal polarity first - if let Some(result) = self.process_signal_inner(pairs, frequency, false) { - return Some(result); - } - // Try inverted polarity (capture LOW = RF HIGH) - if let Some(result) = self.process_signal_inner(pairs, frequency, true) { - return Some(result); - } - // No known protocol: try KeeLoq generic (uses keeloq_common with every keystore key) - keeloq_generic::try_decode(pairs, frequency) - } - - /// ProtoPirate-style streaming decode: feed the whole stream, on each decode record and reset decoders, continue. - /// Returns one entry per decode: (protocol name, decoded signal, pairs that produced it). - /// Tries normal polarity first; if no decodes, runs again with inverted polarity. - pub fn process_signal_stream( - &mut self, - pairs: &[LevelDuration], - frequency: u32, - ) -> Vec<(String, DecodedSignal, Vec)> { - let with_normal = self.process_signal_stream_inner(pairs, frequency, false); - if !with_normal.is_empty() { - return with_normal; - } - self.process_signal_stream_inner(pairs, frequency, true) - } - - /// Inner streaming decode with optional level inversion. - fn process_signal_stream_inner( - &mut self, - pairs: &[LevelDuration], - frequency: u32, - invert_level: bool, - ) -> Vec<(String, DecodedSignal, Vec)> { - let mut out = Vec::new(); - let mut segment_start = 0_usize; - - for decoder in &mut self.decoders { - decoder.reset(); - } - - for (i, pair) in pairs.iter().enumerate() { - let level = if invert_level { !pair.level } else { pair.level }; - let duration_us = pair.duration_us; - - let mut hit = None; - for decoder in &mut self.decoders { - let freq_supported = decoder - .supported_frequencies() - .iter() - .any(|&f| { - let diff = if f > frequency { f - frequency } else { frequency - f }; - diff < (f / 50) - }); - if !freq_supported { - continue; - } - if let Some(decoded) = decoder.feed(level, duration_us) { - hit = Some((decoder.name().to_string(), decoded)); - break; - } - } - if let Some((name, decoded)) = hit { - let segment: Vec = pairs[segment_start..=i] - .iter() - .map(|p| LevelDuration::new(p.level, p.duration_us)) - .collect(); - out.push((name, decoded, segment)); - for d in &mut self.decoders { - d.reset(); - } - segment_start = i + 1; - } - } - - out - } - - /// Inner decode: feed pairs (with optional level flip) to decoders that support this frequency. - fn process_signal_inner( - &mut self, - pairs: &[LevelDuration], - frequency: u32, - invert_level: bool, - ) -> Option<(String, DecodedSignal)> { - for decoder in &mut self.decoders { - decoder.reset(); - } - - for pair in pairs { - let level = if invert_level { !pair.level } else { pair.level }; - let duration_us = pair.duration_us; - - for decoder in &mut self.decoders { - let freq_supported = decoder - .supported_frequencies() - .iter() - .any(|&f| { - let diff = if f > frequency { f - frequency } else { frequency - f }; - diff < (f / 50) // 2% tolerance - }); - - if !freq_supported { - continue; - } - - if let Some(decoded) = decoder.feed(level, duration_us) { - return Some((decoder.name().to_string(), decoded)); - } - } - } - - None - } - - /// Try to decode a capture (for compatibility with old interface) - #[allow(dead_code)] - pub fn try_decode(&mut self, capture: &Capture) -> Option<(String, DecodedSignal)> { - // Convert raw pairs to LevelDuration and process - if capture.raw_pairs.is_empty() { - return None; - } - - let pairs: Vec = capture.raw_pairs - .iter() - .map(|p| LevelDuration::new(p.level, p.duration_us)) - .collect(); - - self.process_signal(&pairs, capture.frequency) - } - - /// Get a decoder by name - pub fn get(&self, name: &str) -> Option<&dyn ProtocolDecoder> { - self.decoders - .iter() - .find(|d| d.name().eq_ignore_ascii_case(name)) - .map(|d| d.as_ref()) - } - - /// List all protocol names - #[allow(dead_code)] - pub fn list_protocols(&self) -> Vec<&'static str> { - self.decoders.iter().map(|d| d.name()).collect() - } -} - -impl Default for ProtocolRegistry { - fn default() -> Self { - Self::new() - } -} - -/// Helper macro for duration comparison (matches protopirate's DURATION_DIFF) -#[macro_export] -macro_rules! duration_diff { - ($actual:expr, $expected:expr) => { - if $actual > $expected { - $actual - $expected - } else { - $expected - $actual - } - }; -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::export::flipper::import_sub_raw; - use crate::radio::LevelDuration; - use std::path::Path; - - #[test] - fn ford_v0_decodes_imports_ford_unlock_sub() { - let path = Path::new("IMPORTS/FORD/3_unlock_ford.sub"); - if !path.exists() { - eprintln!("Skip: {:?} not found (run from crate root)", path); - return; - } - let (freq, raw_pairs) = import_sub_raw(path).unwrap(); - let pairs: Vec = raw_pairs - .iter() - .map(|p| LevelDuration::new(p.level, p.duration_us)) - .collect(); - let mut reg = ProtocolRegistry::new(); - let results = reg.process_signal_stream(&pairs, freq); - let ford_decodes: Vec<_> = results.iter().filter(|(name, _, _)| *name == "Ford V0").collect(); - assert!( - !ford_decodes.is_empty(), - "expected at least one Ford V0 decode from 3_unlock_ford.sub, got: {:?}", - results.iter().map(|(n, _, _)| n.as_str()).collect::>() - ); - } -} +//! Protocol decoders and encoders for various keyfob systems. +//! +//! Protocols are aligned with the ProtoPirate reference (`REFERENCES/ProtoPirate/protocols/`). +//! Each decoder processes level+duration pairs from the demodulator and optionally supports +//! encoding (replay). Shared pieces: [common], [keeloq_common], [keys], [aut64]. +//! +//! **Manchester decoding**: Ford, Fiat, and common each have separate Manchester state machines +//! (FordV0ManchesterState, FiatV0ManchesterState, CommonManchesterState in common.rs). They are +//! not reused across protocols. Event conventions match the reference per protocol (e.g. Kia V5 +//! opposite polarity; Fiat/Ford/common use Flipper-style: level ? ShortLow : ShortHigh). + +mod common; +pub mod keeloq_common; +mod keeloq_generic; +#[allow(dead_code)] +pub mod aut64; +#[allow(dead_code)] +pub mod keys; +mod kia_v0; +mod kia_v1; +mod kia_v2; +mod kia_v3_v4; +mod kia_v5; +mod kia_v6; +mod subaru; +mod ford_v0; +mod vag; +mod fiat_v0; +mod suzuki; +mod scher_khan; +mod star_line; +mod psa; + +pub use common::DecodedSignal; + +use crate::capture::Capture; +use crate::radio::demodulator::LevelDuration; + +/// Protocol timing constants +#[derive(Debug, Clone, Copy)] +#[allow(dead_code)] +pub struct ProtocolTiming { + /// Short pulse duration in µs + pub te_short: u32, + /// Long pulse duration in µs + pub te_long: u32, + /// Tolerance for timing matching in µs + pub te_delta: u32, + /// Minimum bit count for valid decode + pub min_count_bit: usize, +} + +/// Trait for protocol decoders +/// +/// Each protocol implements a state machine that processes level+duration pairs. +pub trait ProtocolDecoder: Send + Sync { + /// Get the protocol name + fn name(&self) -> &'static str; + + /// Get timing constants + #[allow(dead_code)] + fn timing(&self) -> ProtocolTiming; + + /// Get supported frequencies in Hz + fn supported_frequencies(&self) -> &[u32]; + + /// Reset the decoder state machine + fn reset(&mut self); + + /// Feed a level+duration pair to the decoder + /// Returns Some(DecodedSignal) when a complete valid signal is decoded + fn feed(&mut self, level: bool, duration_us: u32) -> Option; + + /// Check if this protocol supports encoding + fn supports_encoding(&self) -> bool; + + /// Encode a signal with the given button command + fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option>; +} + +/// Registry of all supported protocols +pub struct ProtocolRegistry { + decoders: Vec>, +} + +impl ProtocolRegistry { + /// Create a new protocol registry with all built-in protocols + pub fn new() -> Self { + let decoders: Vec> = vec![ + // Kia protocols + Box::new(kia_v0::KiaV0Decoder::new()), + Box::new(kia_v1::KiaV1Decoder::new()), + Box::new(kia_v2::KiaV2Decoder::new()), + Box::new(kia_v3_v4::KiaV3V4Decoder::new()), + Box::new(kia_v5::KiaV5Decoder::new()), + Box::new(kia_v6::KiaV6Decoder::new()), + // Other protocols (Ford before Subaru so 250/500µs Ford keyfobs decode as Ford) + Box::new(ford_v0::FordV0Decoder::new()), + Box::new(subaru::SubaruDecoder::new()), + Box::new(vag::VagDecoder::new()), + Box::new(fiat_v0::FiatV0Decoder::new()), + Box::new(suzuki::SuzukiDecoder::new()), + Box::new(scher_khan::ScherKhanDecoder::new()), + Box::new(star_line::StarLineDecoder::new()), + Box::new(psa::PsaDecoder::new()), + ]; + + Self { decoders } + } + + /// Process level+duration pairs from demodulator + /// Returns decoded signal info if any protocol matches. + /// Tries normal polarity first, then inverted polarity (so OOK captures where + /// carrier-on is recorded as LOW can still decode as Fiat/Ford etc.). + pub fn process_signal(&mut self, pairs: &[LevelDuration], frequency: u32) -> Option<(String, DecodedSignal)> { + // Try normal polarity first + if let Some(result) = self.process_signal_inner(pairs, frequency, false) { + return Some(result); + } + // Try inverted polarity (capture LOW = RF HIGH) + if let Some(result) = self.process_signal_inner(pairs, frequency, true) { + return Some(result); + } + // No known protocol: try KeeLoq generic (uses keeloq_common with every keystore key) + keeloq_generic::try_decode(pairs, frequency) + } + + /// ProtoPirate-style streaming decode: feed the whole stream, on each decode record and reset decoders, continue. + /// Returns one entry per decode: (protocol name, decoded signal, pairs that produced it). + /// Tries normal polarity first; if no decodes, runs again with inverted polarity. + pub fn process_signal_stream( + &mut self, + pairs: &[LevelDuration], + frequency: u32, + ) -> Vec<(String, DecodedSignal, Vec)> { + let with_normal = self.process_signal_stream_inner(pairs, frequency, false); + if !with_normal.is_empty() { + return with_normal; + } + self.process_signal_stream_inner(pairs, frequency, true) + } + + /// Inner streaming decode with optional level inversion. + fn process_signal_stream_inner( + &mut self, + pairs: &[LevelDuration], + frequency: u32, + invert_level: bool, + ) -> Vec<(String, DecodedSignal, Vec)> { + let mut out = Vec::new(); + let mut segment_start = 0_usize; + + for decoder in &mut self.decoders { + decoder.reset(); + } + + for (i, pair) in pairs.iter().enumerate() { + let level = if invert_level { !pair.level } else { pair.level }; + let duration_us = pair.duration_us; + + let mut hit = None; + for decoder in &mut self.decoders { + let freq_supported = decoder + .supported_frequencies() + .iter() + .any(|&f| { + let diff = if f > frequency { f - frequency } else { frequency - f }; + diff < (f / 50) + }); + if !freq_supported { + continue; + } + if let Some(decoded) = decoder.feed(level, duration_us) { + hit = Some((decoder.name().to_string(), decoded)); + break; + } + } + if let Some((name, decoded)) = hit { + let segment: Vec = pairs[segment_start..=i] + .iter() + .map(|p| LevelDuration::new(p.level, p.duration_us)) + .collect(); + out.push((name, decoded, segment)); + for d in &mut self.decoders { + d.reset(); + } + segment_start = i + 1; + } + } + + out + } + + /// Inner decode: feed pairs (with optional level flip) to decoders that support this frequency. + fn process_signal_inner( + &mut self, + pairs: &[LevelDuration], + frequency: u32, + invert_level: bool, + ) -> Option<(String, DecodedSignal)> { + for decoder in &mut self.decoders { + decoder.reset(); + } + + for pair in pairs { + let level = if invert_level { !pair.level } else { pair.level }; + let duration_us = pair.duration_us; + + for decoder in &mut self.decoders { + let freq_supported = decoder + .supported_frequencies() + .iter() + .any(|&f| { + let diff = if f > frequency { f - frequency } else { frequency - f }; + diff < (f / 50) // 2% tolerance + }); + + if !freq_supported { + continue; + } + + if let Some(decoded) = decoder.feed(level, duration_us) { + return Some((decoder.name().to_string(), decoded)); + } + } + } + + None + } + + /// Try to decode a capture (for compatibility with old interface) + #[allow(dead_code)] + pub fn try_decode(&mut self, capture: &Capture) -> Option<(String, DecodedSignal)> { + // Convert raw pairs to LevelDuration and process + if capture.raw_pairs.is_empty() { + return None; + } + + let pairs: Vec = capture.raw_pairs + .iter() + .map(|p| LevelDuration::new(p.level, p.duration_us)) + .collect(); + + self.process_signal(&pairs, capture.frequency) + } + + /// Get a decoder by name + pub fn get(&self, name: &str) -> Option<&dyn ProtocolDecoder> { + self.decoders + .iter() + .find(|d| d.name().eq_ignore_ascii_case(name)) + .map(|d| d.as_ref()) + } + + /// List all protocol names + #[allow(dead_code)] + pub fn list_protocols(&self) -> Vec<&'static str> { + self.decoders.iter().map(|d| d.name()).collect() + } +} + +impl Default for ProtocolRegistry { + fn default() -> Self { + Self::new() + } +} + +/// Helper macro for duration comparison (matches protopirate's DURATION_DIFF) +#[macro_export] +macro_rules! duration_diff { + ($actual:expr, $expected:expr) => { + if $actual > $expected { + $actual - $expected + } else { + $expected - $actual + } + }; +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::export::flipper::import_sub_raw; + use crate::radio::LevelDuration; + use std::path::Path; + + #[test] + fn ford_v0_decodes_imports_ford_unlock_sub() { + let path = Path::new("IMPORTS/FORD/3_unlock_ford.sub"); + if !path.exists() { + eprintln!("Skip: {:?} not found (run from crate root)", path); + return; + } + let (freq, raw_pairs) = import_sub_raw(path).unwrap(); + let pairs: Vec = raw_pairs + .iter() + .map(|p| LevelDuration::new(p.level, p.duration_us)) + .collect(); + let mut reg = ProtocolRegistry::new(); + let results = reg.process_signal_stream(&pairs, freq); + let ford_decodes: Vec<_> = results.iter().filter(|(name, _, _)| *name == "Ford V0").collect(); + assert!( + !ford_decodes.is_empty(), + "expected at least one Ford V0 decode from 3_unlock_ford.sub, got: {:?}", + results.iter().map(|(n, _, _)| n.as_str()).collect::>() + ); + } +} diff --git a/src/protocols/vag.rs b/src/protocols/vag.rs index b5fce68..64fac6e 100644 --- a/src/protocols/vag.rs +++ b/src/protocols/vag.rs @@ -899,14 +899,14 @@ impl ProtocolDecoder for VagDecoder { TE_SHORT_12 - duration }; + // Reference: (300-duration)<=79 or (duration-300)<80 -> count pair (check_preamble1_prev) if te_diff < TE_DELTA_12 { - // Check previous pulse let prev_diff = if self.te_last > TE_SHORT_12 { self.te_last - TE_SHORT_12 } else { TE_SHORT_12 - self.te_last }; - if prev_diff <= TE_DELTA_12 { + if prev_diff <= REF_RESET_DELTA { self.te_last = duration; self.header_count += 1; return None; @@ -915,12 +915,13 @@ impl ProtocolDecoder for VagDecoder { return None; } - // Check for gap (end of preamble): 600µs ±79, te_last 300±79 (ref check_gap1) + // Duration not near 300: ref checks for 600µs gap (Preamble1->Data1), then reset + // ref: set step=Reset; if header_count>=201 then duration=|duration-600|; if duration<=79 and te_last 300±79 -> Data1 if self.header_count >= 201 { - let gap_diff = if duration > TE_LONG_12 { - duration - TE_LONG_12 - } else { + let gap_diff = if duration < TE_LONG_12 { TE_LONG_12 - duration + } else { + duration - TE_LONG_12 }; if gap_diff <= REF_GAP1_DELTA { let prev_diff = if self.te_last > TE_SHORT_12 { @@ -952,9 +953,10 @@ impl ProtocolDecoder for VagDecoder { TE_LONG_12 - duration }; - let event = if short_diff <= TE_DELTA_12 { + // Reference Data1: short 300±79 (221..380), long 600±79 (521..680) + let event = if short_diff <= REF_RESET_DELTA { Some(if level { ManchesterEvent::ShortLow } else { ManchesterEvent::ShortHigh }) - } else if long_diff <= TE_DELTA_12 { + } else if long_diff <= REF_RESET_DELTA { Some(if level { ManchesterEvent::LongLow } else { ManchesterEvent::LongHigh }) } else { None