From 5d0bd9795438ec4af1b8021deca2596ca66c8979 Mon Sep 17 00:00:00 2001 From: KaraZajac Date: Sun, 22 Feb 2026 21:37:56 -0500 Subject: [PATCH] fob export fixes --- Cargo.lock | 2 +- README.md | 27 ++++++++++--- src/app.rs | 117 +++++++++++++++++++++++++---------------------------- 3 files changed, 77 insertions(+), 69 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7b05075..dc4ea56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -444,7 +444,7 @@ dependencies = [ [[package]] name = "kat" -version = "1.1.2" +version = "1.1.3" dependencies = [ "anyhow", "atty", diff --git a/README.md b/README.md index 30e6a31..c96aa99 100644 --- a/README.md +++ b/README.md @@ -119,11 +119,12 @@ Press `Enter` on a capture to open the action menu. When using RTL-SDR (receive- When exporting to `.fob`, a metadata form collects filename and optional vehicle info: -- **File** — output filename (extension added by format) +- **File** — output filename (extension added by format). For unknown protocol, a unique 8-character hex suffix (e.g. `A1B2C3D4`) is shown in the field and appended to the filename so each export has a distinct file. - **Year** — vehicle model year - **Make** — manufacturer (auto-suggested from protocol) - **Model** — vehicle model - **Region** — region/market +- **Command** — button/command label (e.g. Unlock, Lock, Trunk, Panic); used in the default filename for unknown protocol and stored in the .fob vehicle section. - **Notes** — free-form notes The exported `.fob` file is a versioned JSON document (v2.0, format `kat-fob`) containing: @@ -153,17 +154,22 @@ The exported `.fob` file is a versioned JSON document (v2.0, format `kat-fob`) c "year": 2023, "make": "Kia", "model": "Sportage", - "region": "", + "region": "NA", + "command": "Lock", "notes": "" }, "capture": { "timestamp": "2026-02-07T12:00:00Z", + "raw_data_hex": "0x...", + "raw_pair_count": 0, "raw_pairs": [{"level": true, "duration_us": 400}, {"level": false, "duration_us": 800}] } } ``` -`rf_modulation` is AM, FM, or AM/FM per protocol (from ProtoPirate). Raw pairs are included when config `include_raw_pairs` is true. +- **vehicle.command** — optional; user-editable command label (e.g. Unlock, Lock). Set in the export form or via **i** (capture metadata). Used for unknown-protocol export filenames (`Year_Make_Model_Region_Command_8HEX.fob`). +- **rf_modulation** — AM, FM, or AM/FM per protocol (from ProtoPirate). Omitted when unknown. +- **capture** — `raw_data_hex`, `raw_pair_count`, and optionally `raw_pairs` when config `include_raw_pairs` is true. ### VIM-Style Commands @@ -213,7 +219,7 @@ include_raw_pairs = true ``` - **import_directory** — directory scanned at startup for .fob and .sub files to import (default `~/.config/KAT/import`). Exports are still saved to **export_directory**. -- **research_mode** — when `false` (default), only successfully decoded signals appear in the list; when `true`, unknown (unidentified) signals are also shown. +- **research_mode** — when `true` (default), unknown (unidentified) signals are shown in addition to decoded ones; when `false`, only successfully decoded signals appear. - **include_raw_pairs** — when `true`, .fob exports include raw level/duration pairs for replay. ## Supported Protocols @@ -268,7 +274,7 @@ The **AM/OOK** demodulator turns IQ samples into level/duration pairs for protoc - **Exponential moving average** — magnitude smoothing - **Schmitt trigger hysteresis** — reduces chattering at the decision boundary - **Debounce** — 40µs minimum pulse width to reject noise spikes -- **Gap detection** — 20 ms gap treated as end of signal +- **Gap detection** — 80 ms gap treated as end of signal (multi-burst keyfob presses stay one capture) ## IMPORTS folder @@ -314,6 +320,17 @@ src/ └── status_bar.rs # Status bar ``` +## Call for researchers + +**We need your help.** KAT’s protocol decoders and future analysis depend on real-world keyfob captures. If you use KAT for security research, authorized testing, or protocol development, please consider: + +- **Contributing to protocol analysis** — share timing, encoding, or decoder feedback; report bugs or suggest new protocols. +- **Contributing captures to the research library** — add your `.fob` files to the **[FOB Research Library](https://github.com/KaraZajac/FOB_Research_Library)** so others can use them for decoder development, CVE validation, and protocol studies. + +The [FOB Research Library](https://github.com/KaraZajac/FOB_Research_Library) is a community collection of KAT `.fob` captures organized by manufacturer. Your submissions (with proper metadata: year, make, model, region, command) help grow test vectors and support the wider keyfob research community. + +--- + ## Credits KAT is developed by **Kara Zajac (.leviathan)**. KAT would not be possible without [ProtoPirate](https://protopirate.net/ProtoPirate/ProtoPirate)—the protocol decoders, reference implementations, and community work are the foundation this tool is built on. Truly standing on the shoulders of giants. diff --git a/src/app.rs b/src/app.rs index 5fa132d..23a8e34 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1307,52 +1307,52 @@ impl App { .unwrap_or_else(|_| path.to_string_lossy().to_string()) } - /// For unknown protocol: Year_Make_Model_Region_Command (user can edit; we append _<8 hex> on save). - /// For known protocol: protocol_serial as before. + /// Default export filename for .fob: Year_Make_Model_Region_Command (same format for all captures). + /// Uses capture metadata when set; fallbacks: make from protocol for known, command from button_name(), else "Unknown". fn default_export_filename(capture: &Capture) -> String { - if capture.protocol_name().eq_ignore_ascii_case("unknown") { - let year = capture - .year - .as_deref() - .unwrap_or("Unknown") - .trim() - .replace(' ', "_"); - let make = capture - .make - .as_deref() - .unwrap_or("Unknown") - .trim() - .replace(' ', "_"); - let model = capture - .model - .as_deref() - .unwrap_or("Unknown") - .trim() - .replace(' ', "_"); - let region = capture - .region - .as_deref() - .unwrap_or("Unknown") - .trim() - .replace(' ', "_"); - let cmd_str = capture - .command - .as_deref() - .unwrap_or_else(|| capture.button_name()) - .trim(); - let command = if cmd_str.is_empty() || cmd_str == "-" { - "Unknown".to_string() - } else { - cmd_str.replace(' ', "_") - }; - format!("{}_{}_{}_{}_{}", year, make, model, region, command) + let year = capture + .year + .as_deref() + .unwrap_or("Unknown") + .trim() + .replace(' ', "_"); + let make = capture + .make + .as_deref() + .filter(|s| !s.trim().is_empty()) + .map(|s| s.trim().replace(' ', "_")) + .unwrap_or_else(|| { + if capture.protocol_name().eq_ignore_ascii_case("unknown") { + "Unknown".to_string() + } else { + Self::get_make_for_protocol(capture.protocol_name()) + .trim() + .replace(' ', "_") + } + }); + let model = capture + .model + .as_deref() + .unwrap_or("Unknown") + .trim() + .replace(' ', "_"); + let region = capture + .region + .as_deref() + .unwrap_or("Unknown") + .trim() + .replace(' ', "_"); + let cmd_str = capture + .command + .as_deref() + .unwrap_or_else(|| capture.button_name()) + .trim(); + let command = if cmd_str.is_empty() || cmd_str == "-" { + "Unknown".to_string() } else { - format!( - "{}_{}", - capture.protocol_name().replace(' ', "_").to_lowercase(), - capture.serial_hex() - ) - } + cmd_str.replace(' ', "_") + }; + format!("{}_{}_{}_{}_{}", year, make, model, region, command) } /// Start .fob export by entering filename input mode @@ -1362,21 +1362,17 @@ impl App { return Ok(()); } - // Pre-fill filename from protocol + serial; for unknown captures include 8-hex suffix so user sees it + // Pre-fill filename: Year_Make_Model_Region_Command_8HEX for all .fob exports let capture = self.captures.iter().find(|c| c.id == id); let default_name = capture .map(|c| Self::default_export_filename(c)) .unwrap_or_else(|| format!("capture_{}", id)); - self.export_filename = if capture.map(|c| c.protocol.is_none()).unwrap_or(false) { - let suffix_nanos = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_nanos() as u64; - let suffix = (suffix_nanos.wrapping_add(id as u64 * 2654435761) % 0x100_000_000) as u32; - format!("{}_{:08X}", default_name, suffix) - } else { - default_name.clone() - }; + let suffix_nanos = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() as u64; + let suffix = (suffix_nanos.wrapping_add(id as u64 * 2654435761) % 0x100_000_000) as u32; + self.export_filename = format!("{}_{:08X}", default_name, suffix); // Pre-fill metadata from capture if set, otherwise make from protocol let make = capture @@ -1449,18 +1445,15 @@ impl App { notes: self.fob_meta_notes.clone(), }; - // Unknown captures or user-edited vehicle-style filenames always get 8-hex suffix to avoid overwrites. - let use_hex_suffix = capture.protocol.is_none() - || self.export_filename.ends_with("_Unknown") - || self.export_filename == "Unknown"; + // All .fob exports use Year_Make_Model_Region_Command_8HEX; append 8-hex if user removed it let already_has_8hex = self.export_filename.len() >= 9 && self.export_filename.as_bytes()[self.export_filename.len() - 9] == b'_' && self.export_filename[self.export_filename.len() - 8..] .chars() .all(|c| c.is_ascii_hexdigit()); - let filename = if use_hex_suffix && already_has_8hex { + let filename = if already_has_8hex { format!("{}.fob", self.export_filename.trim()) - } else if use_hex_suffix { + } else { let base = self.export_filename.trim(); let suffix_nanos = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -1473,8 +1466,6 @@ impl App { } else { format!("{}_{}.fob", base, hex_suffix) } - } else { - format!("{}.fob", self.export_filename) }; let path = export_dir.join(&filename);