diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/.DS_Store differ diff --git a/README.md b/README.md index b0436c6..4c400e3 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,23 @@ # KAT — Keyfob Analysis Toolkit -A terminal-based RF signal analysis tool for capturing, decoding, and retransmitting automotive keyfob signals using HackRF One. Built in Rust with a real-time TUI powered by `ratatui`. +A terminal-based RF signal analysis tool for capturing, decoding, and retransmitting automotive keyfob signals using HackRF One. Built in Rust with a real-time TUI powered by `ratatui`. Protocol decoders are aligned with the [ProtoPirate](REFERENCES/ProtoPirate/) reference. --- ## Features -- **Real-time capture** — receive and demodulate AM/OOK keyfob signals at configurable frequencies -- **Multi-protocol decoding** — 14 protocol decoders covering Kia, Ford, Fiat, Subaru, Suzuki, VAG (VW/Audi/Seat/Skoda), PSA, Scher-Khan, and Star Line with adaptive demodulation for real-world signal conditions -- **Rich signal detail** — modulation type, encryption method, serial, counter, key data, CRC, frequency, and raw level/duration pairs +- **Real-time capture** — receive and demodulate AM/OOK keyfob signals at configurable frequencies (HackRF uses AM envelope detection; FM protocols are tagged for display and may decode when signal is strong) +- **Multi-protocol decoding** — 14 protocol decoders: Kia V0–V6, Ford V0, Fiat V0, Subaru, Suzuki, VAG (VW/Audi/Seat/Skoda), PSA, Scher-Khan, Star Line; adaptive demodulation for real-world conditions +- **RF modulation metadata** — each protocol tagged as AM, FM, or both (from ProtoPirate); shown in signal detail and exported in .fob +- **Rich signal detail** — encoding (PWM/Manchester), RF (AM/FM), encryption, serial, counter, key data, CRC, frequency, and raw level/duration pairs - **Signal retransmission** — transmit Lock, Unlock, Trunk, and Panic commands from decoded captures -- **Export formats** — `.fob` (rich JSON with vehicle metadata, signal info, and capture data) and `.sub` (Flipper Zero compatible) +- **Export formats** — `.fob` (versioned JSON with vehicle metadata, signal info, optional raw pairs) and `.sub` (Flipper Zero compatible) - **Import support** — load `.fob` files with automatic v1/v2 format detection -- **Persistent storage** — automatic capture saving to `~/.config/kat/captures/` -- **INI configuration** — human-readable config at `~/.config/kat/config.ini` (auto-created with comments on first run) +- **Research mode** — config option to show unknown (unidentified) signals in addition to successfully decoded ones +- **INI configuration** — `~/.config/KAT/config.ini` (auto-created with comments on first run): export path, max captures, research_mode, radio defaults, export format +- **Embedded keystore** — manufacturer keys (Kia, VAG, etc.) bundled for decoding; optional `~/.config/KAT/keystore/` override - **VIM-style command line** — `:freq`, `:lock`, `:unlock`, `:save`, `:load`, `:delete`, and more -- **Interactive TUI** — captures list with detail panel, signal action menu, radio settings menu, and fob export form +- **Interactive TUI** — captures list with detail panel (protocol, freq, mod, RF, encryption), signal action menu, radio settings, fob export form ## Requirements @@ -94,47 +96,54 @@ Press `Enter` on a capture to open the action menu: ### Fob Export -When exporting to `.fob`, a 6-step metadata form collects: +When exporting to `.fob`, a metadata form collects filename and optional vehicle info: -1. **Year** — vehicle model year -2. **Make** — manufacturer (auto-suggested from protocol) -3. **Model** — vehicle model -4. **Color** — vehicle color -5. **Trim** — trim level / package -6. **Notes** — free-form notes +- **File** — output filename (extension added by format) +- **Year** — vehicle model year +- **Make** — manufacturer (auto-suggested from protocol) +- **Model** — vehicle model +- **Region** — region/market +- **Notes** — free-form notes -The exported `.fob` file is a versioned JSON document (v2.0) containing: +The exported `.fob` file is a versioned JSON document (v2.0, format `kat-fob`) containing: ```json { "version": "2.0", - "format": "KAT Fob Signal", + "format": "kat-fob", "signal": { "protocol": "Kia V3/V4", + "frequency": 433920000, + "frequency_mhz": "433.92MHz", "modulation": "PWM", + "rf_modulation": "AM/FM", "encryption": "KeeLoq", - "frequency_mhz": 433.92, + "data_bits": 64, + "data_hex": "...", "serial": "0x1A2B3C", - "key": "0xDEADBEEF...", + "key": "0x...", "button": 1, + "button_name": "Lock", "counter": 1234, + "crc_valid": true, "encoder_capable": true }, "vehicle": { - "year": "2023", + "year": 2023, "make": "Kia", "model": "Sportage", - "color": "White", - "trim": "EX", + "region": "", "notes": "" }, "capture": { "timestamp": "2026-02-07T12:00:00Z", - "raw_pairs": [[true, 400], [false, 800]] + "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. + ### VIM-Style Commands | Command | Description | @@ -151,61 +160,74 @@ The exported `.fob` file is a versioned JSON document (v2.0) containing: ## Configuration -On first launch, KAT creates the following directory structure: +On first launch, KAT creates the config directory and a default config file. Captures are **in-memory only** and are not written to disk unless you export them. ``` -~/.config/kat/ +~/.config/KAT/ ├── config.ini # Application settings (auto-generated with comments) -├── captures/ # Persistent capture storage -└── exports/ # Default export directory for .fob / .sub files +├── exports/ # Default export directory for .fob / .sub files +└── keystore/ # Optional: override keys (keystore.ini, vag.bin, etc.) ``` -The `config.ini` file is a commented INI file with the following settings: +Example `config.ini` (all keys optional; defaults are used if missing): ```ini -[radio] -frequency = 433920000 # Default receive frequency in Hz -lna_gain = 32 # LNA gain (0-40 dB, step 8) -vga_gain = 40 # VGA gain (0-62 dB, step 2) -amp_enable = true # RF amplifier on/off +[general] +export_directory = ~/.config/KAT/exports +max_captures = 100 +research_mode = false -[storage] -export_directory = ~/.config/kat/exports # Where .fob/.sub files are saved +[radio] +default_frequency = 433920000 +default_lna_gain = 24 +default_vga_gain = 20 +default_amp = false + +[export] +default_format = fob +include_raw_pairs = true ``` +- **research_mode** — when `false` (default), only successfully decoded signals appear in the list; when `true`, unknown (unidentified) signals are also shown. +- **include_raw_pairs** — when `true`, .fob exports include raw level/duration pairs for replay. + ## Supported Protocols -| Protocol | Encoding | Encryption | Frequency | -|---|---|---|---| -| Kia V0 | PWM | Fixed Code | 433.92 MHz | -| Kia V1 | Manchester | Rolling Code | 433.92 MHz | -| Kia V2 | PWM | Rolling Code | 433.92 MHz | -| Kia V3/V4 | PWM | KeeLoq | 433.92 MHz | -| Kia V5 | PWM | Custom Mixer | 433.92 MHz | -| Kia V6 | PWM | AES-128 | 433.92 MHz | -| Ford V0 | Manchester | Fixed Code | 315 / 433.92 MHz | -| Subaru | PWM | Rolling Code | 315 / 433.92 MHz | -| Suzuki | Manchester | Rolling Code | 433.92 MHz | -| Fiat V0 | Diff. Manchester | Rolling Code | 433.92 MHz | -| VAG (VW/Audi/Seat/Skoda) | Manchester | AUT64 / TEA | 433.92 / 434.42 MHz | -| Scher-Khan | PWM | Magic Code | 433.92 MHz | -| Star Line | PWM | KeeLoq | 433.92 MHz | -| PSA (Peugeot/Citroen) | PWM | Rolling Code | 433.92 MHz | +Protocol behavior and RF modulation (AM/FM) follow the ProtoPirate reference. KAT’s receiver is AM/OOK only; FM protocols may still decode when the signal produces a usable envelope. -### Cryptographic Modules +| Protocol | Encoding | RF | Encryption | Frequency | +|---|---|---|---|---| +| Kia V0 | PWM | FM | Fixed Code | 433.92 MHz | +| Kia V1 | Manchester | AM | Fixed Code | 315 / 433.92 MHz | +| Kia V2 | Manchester | FM | Fixed Code | 315 / 433.92 MHz | +| Kia V3/V4 | PWM | AM/FM | KeeLoq | 315 / 433.92 MHz | +| Kia V5 | Manchester | FM | Fixed Code | 433.92 MHz | +| Kia V6 | Manchester | FM | Fixed Code | 433.92 MHz | +| Ford V0 | Manchester | FM | Fixed Code | 433.92 MHz | +| Fiat V0 | Manchester | FM | Fixed Code | 433.92 MHz | +| Subaru | PWM | AM | Rolling Code | 433.92 MHz | +| Suzuki | PWM | AM | Rolling Code | 433.92 MHz | +| VAG (VW/Audi/Seat/Skoda) | Manchester | AM | AUT64/XTEA | 433.92 / 434.42 MHz | +| Scher-Khan | PWM | FM | Magic Code | 433.92 MHz | +| Star Line | PWM | AM | KeeLoq | 433.92 MHz | +| PSA (Peugeot/Citroën) | Manchester | FM | XTEA/XOR | 433.92 MHz | -- **KeeLoq** — full encrypt/decrypt with normal, secure, FAAC, and magic serial/XOR learning key derivation -- **AUT64** — 12-round block cipher for VAG type 1/3/4 signals -- **Key Store** — global thread-safe key management for manufacturer keys (KIA, VAG) +### Cryptographic modules + +- **KeeLoq** — encrypt/decrypt with normal, secure, FAAC, and magic serial/XOR learning key derivation (keeloq_common, keys) +- **AUT64** — 12-round block cipher for VAG type 1/3/4 (aut64) +- **Keystore** — embedded manufacturer keys (Kia, VAG, etc.); optional file overrides in `~/.config/KAT/keystore/` ### Demodulator -The AM/OOK demodulator uses an adaptive threshold with transition-based updates for accurate pulse detection across varying signal conditions: +The **AM/OOK** demodulator turns IQ samples into level/duration pairs for protocol decoders. FM/2FSK is not demodulated; protocols are tagged AM/FM for display and export. -- **Exponential moving average** — magnitude smoothing for stable signal tracking -- **Schmitt trigger hysteresis** — prevents noise-induced chattering at threshold crossings -- **Fast threshold convergence** — α=0.3 transition-based updates for rapid adaptation after silence periods -- **Debounce filtering** — 40µs minimum pulse width to reject noise spikes +- **Envelope detection** — magnitude from I/Q for AM +- **Adaptive threshold** — transition-based updates to handle varying signal levels +- **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 ## Project Structure @@ -213,42 +235,36 @@ The AM/OOK demodulator uses an adaptive threshold with transition-based updates src/ ├── main.rs # Entry point, event loop, key handling ├── app.rs # Application state, radio events, signal actions -├── capture.rs # Capture data structure, modulation/encryption helpers -├── storage.rs # Config management, capture persistence, INI read/write +├── capture.rs # Capture data, encoding/RF modulation, encryption helpers +├── storage.rs # Config (INI), export dir, resolve_config_dir, Storage +├── keystore/ +│ ├── mod.rs # Keystore trait and access +│ └── embedded.rs # Embedded manufacturer keys (Kia, VAG, etc.) ├── export/ -│ ├── fob.rs # .fob JSON export/import (v1 + v2 format support) -│ └── flipper.rs # Flipper Zero .sub export +│ ├── fob.rs # .fob JSON export/import (v1 + v2, rf_modulation) +│ └── flipper.rs # Flipper Zero .sub export ├── protocols/ │ ├── mod.rs # Protocol registry, decoder trait, duration_diff macro │ ├── common.rs # Shared CRC, bit helpers, button codes │ ├── keeloq_common.rs # KeeLoq cipher + learning key algorithms -│ ├── aut64.rs # AUT64 block cipher implementation -│ ├── keys.rs # Global key store (KIA, VAG key management) -│ ├── kia_v0.rs # Kia V0 decoder -│ ├── kia_v1.rs # Kia V1 decoder (Manchester) -│ ├── kia_v2.rs # Kia V2 decoder -│ ├── kia_v3_v4.rs # Kia V3/V4 decoder (KeeLoq) -│ ├── kia_v5.rs # Kia V5 decoder (mixer cipher) -│ ├── kia_v6.rs # Kia V6 decoder (AES-128) -│ ├── ford_v0.rs # Ford V0 decoder -│ ├── subaru.rs # Subaru decoder -│ ├── suzuki.rs # Suzuki decoder -│ ├── fiat_v0.rs # Fiat V0 decoder (diff. Manchester) +│ ├── aut64.rs # AUT64 block cipher (VAG) +│ ├── keys.rs # Key loading (embedded + optional file), KIA/VAG +│ ├── kia_v0..kia_v6.rs +│ ├── ford_v0.rs, fiat_v0.rs, subaru.rs, suzuki.rs │ ├── vag.rs # VAG decoder/encoder (4 sub-types) -│ ├── scher_khan.rs # Scher-Khan decoder -│ ├── star_line.rs # Star Line decoder -│ └── psa.rs # PSA decoder +│ ├── scher_khan.rs, star_line.rs, psa.rs +│ └── ... ├── radio/ -│ ├── hackrf.rs # HackRF One device control (RX/TX) -│ ├── demodulator.rs # AM/OOK demodulator (IQ -> level/duration pairs) -│ └── modulator.rs # Signal modulator (level/duration -> TX waveform) +│ ├── hackrf.rs # HackRF device control (RX/TX) +│ ├── demodulator.rs # AM/OOK demodulator (IQ -> level/duration) +│ └── modulator.rs # Level/duration -> TX waveform └── ui/ - ├── layout.rs # Main TUI layout, fob metadata form overlay - ├── captures_list.rs # Captures table + signal detail panel - ├── signal_menu.rs # Signal action popup menu - ├── settings_menu.rs # Radio settings popup menu - ├── command.rs # VIM-style command line renderer - └── status_bar.rs # Bottom status bar (radio state, frequency, gains) + ├── layout.rs # TUI layout, fob metadata form + ├── captures_list.rs # Captures table + detail (protocol, mod, RF, enc) + ├── signal_menu.rs # Signal action menu + ├── settings_menu.rs # Radio settings (Freq, LNA, VGA, AMP) + ├── command.rs # VIM-style command line + └── status_bar.rs # Status bar ``` ## License diff --git a/src/app.rs b/src/app.rs index 5c11868..8d47bfb 100644 --- a/src/app.rs +++ b/src/app.rs @@ -243,10 +243,8 @@ impl App { pub fn new() -> Result { let storage = Storage::new()?; - // ── Load protocol encryption keys from keystore ────────────────── - let keystore_dir = storage.keystore_dir(); - crate::protocols::keys::create_default_keystore(&keystore_dir); - crate::protocols::keys::load_keystore_from_dir(&keystore_dir); + // ── Load protocol encryption keys from embedded keystore ───────── + crate::protocols::keys::load_keystore_from_embedded(); let protocols = ProtocolRegistry::new(); let (radio_event_tx, radio_event_rx) = mpsc::channel(); @@ -705,10 +703,6 @@ impl App { while let Ok(event) = self.radio_event_rx.try_recv() { match event { RadioEvent::SignalCaptured(mut capture) => { - // Assign ID - capture.id = self.next_capture_id; - self.next_capture_id += 1; - // Convert stored pairs to the format protocols expect let pairs: Vec = capture.raw_pairs .iter() @@ -731,16 +725,23 @@ impl App { }; } - // Captures are in-memory only — no auto-save to disk. - // Use Export (.fob / .sub) to persist a signal. - self.captures.push(capture); - - // Auto-select and scroll to new capture - let new_idx = self.captures.len() - 1; - self.selected_capture = Some(new_idx); - self.ensure_selection_visible(); - - self.status_message = Some("New signal captured".to_string()); + // When research_mode is off, only add successfully decoded signals. + let show = self.storage.config.research_mode || capture.protocol.is_some(); + if show { + capture.id = self.next_capture_id; + self.next_capture_id += 1; + // Captures are in-memory only — no auto-save to disk. + // Use Export (.fob / .sub) to persist a signal. + self.captures.push(capture); + + // Auto-select and scroll to new capture + let new_idx = self.captures.len() - 1; + self.selected_capture = Some(new_idx); + self.ensure_selection_visible(); + + self.status_message = Some("New signal captured".to_string()); + } + // When research_mode is off and decode failed, the signal is dropped (not shown). } RadioEvent::Error(e) => { self.last_error = Some(e); diff --git a/src/capture.rs b/src/capture.rs index e55f7b8..5f53250 100644 --- a/src/capture.rs +++ b/src/capture.rs @@ -60,7 +60,7 @@ pub struct Capture { pub status: CaptureStatus, } -/// Modulation type used by protocol +/// Modulation type used by protocol (encoding: PWM vs Manchester) #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ModulationType { Unknown, @@ -81,6 +81,28 @@ impl std::fmt::Display for ModulationType { } } +/// RF modulation (carrier): AM/OOK vs FM/2FSK. From ProtoPirate SubGhzProtocolFlag_AM / _FM. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum RfModulation { + AM, + FM, + /// Protocol used with both AM and FM (e.g. Kia V3/V4) + Both, + Unknown, +} + +impl std::fmt::Display for RfModulation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RfModulation::AM => write!(f, "AM"), + RfModulation::FM => write!(f, "FM"), + RfModulation::Both => write!(f, "AM/FM"), + RfModulation::Unknown => write!(f, "—"), + } + } +} + impl Capture { /// Create a new capture from level+duration pairs pub fn from_pairs(id: u32, frequency: u32, pairs: Vec) -> Self { @@ -177,6 +199,31 @@ impl Capture { } } + /// Get the RF modulation (AM/FM) for this protocol. From ProtoPirate SubGhzProtocolFlag_AM / _FM. + /// KAT's demodulator is AM/OOK only; FM protocols may still decode if the signal is strong. + pub fn rf_modulation(&self) -> RfModulation { + match self.protocol_name() { + // FM only (ProtoPirate SubGhzProtocolFlag_FM) + p if p.starts_with("Kia V0") => RfModulation::FM, + p if p.starts_with("Kia V2") => RfModulation::FM, + p if p.starts_with("Kia V5") => RfModulation::FM, + p if p.starts_with("Kia V6") => RfModulation::FM, + "Scher-Khan" => RfModulation::FM, + "PSA" => RfModulation::FM, + "Fiat V0" => RfModulation::FM, + "Ford V0" => RfModulation::FM, + // AM only (SubGhzProtocolFlag_AM) + p if p.starts_with("Kia V1") => RfModulation::AM, + "VAG" => RfModulation::AM, + "Subaru" => RfModulation::AM, + "Suzuki" => RfModulation::AM, + "Star Line" => RfModulation::AM, + // Both AM and FM (Kia V3/V4) + p if p.starts_with("Kia V3") || p.starts_with("Kia V4") => RfModulation::Both, + _ => RfModulation::Unknown, + } + } + /// Get the encryption/encoding type based on the protocol pub fn encryption_type(&self) -> &'static str { match self.protocol_name() { diff --git a/src/export/fob.rs b/src/export/fob.rs index 5fadd3b..4547f4f 100644 --- a/src/export/fob.rs +++ b/src/export/fob.rs @@ -34,6 +34,9 @@ pub struct FobSignalInfo { pub frequency: u32, pub frequency_mhz: String, pub modulation: String, + /// RF carrier modulation: AM, FM, or AM/FM (from ProtoPirate). KAT receives AM only. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub rf_modulation: Option, pub encryption: String, pub data_bits: usize, pub data_hex: String, @@ -140,6 +143,10 @@ pub fn export_fob( frequency: capture.frequency, frequency_mhz: capture.frequency_mhz(), modulation: capture.modulation().to_string(), + rf_modulation: match capture.rf_modulation() { + crate::capture::RfModulation::Unknown => None, + r => Some(r.to_string()), + }, encryption: capture.encryption_type().to_string(), data_bits: capture.data_count_bit, data_hex: capture.data_hex(), diff --git a/src/keystore/embedded.rs b/src/keystore/embedded.rs new file mode 100644 index 0000000..b30037f --- /dev/null +++ b/src/keystore/embedded.rs @@ -0,0 +1,76 @@ +//! 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. +//! 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. + +/// Blob: KATK + 30 entries (all manufacture keys + type 20 for Star Line) + VAG 64 bytes. +#[rustfmt::skip] +pub const KEYSTORE_BLOB: &[u8] = &[ + b'K', b'A', b'T', b'K', + 0x1E, 0x00, // 30 entries + // 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, + 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, +]; diff --git a/src/keystore/mod.rs b/src/keystore/mod.rs new file mode 100644 index 0000000..a699839 --- /dev/null +++ b/src/keystore/mod.rs @@ -0,0 +1,53 @@ +//! Embedded keystore: binary blob of protocol keys (KIA, Star Line, VAG). +//! Matches ProtoPirate key types (keys.c: KIA_KEY1..4). Loaded at startup instead of keystore_dir. + +mod embedded; + +use std::convert::TryInto; + +const MAGIC: &[u8; 4] = b"KATK"; +const VAG_TAG: &[u8; 4] = b"VAG "; +const VAG_SIZE: usize = 64; +const ENTRY_SIZE: usize = 4 + 8; // u32 type + u64 key + +/// Parsed result from the embedded blob: (type_id, key) pairs and raw VAG bytes. +pub struct ParsedKeystore { + pub entries: Vec<(u32, u64)>, + pub vag_bytes: Vec, +} + +/// Parse the embedded keystore blob. Returns KIA/Star Line entries and VAG raw bytes. +pub fn parse_blob(blob: &[u8]) -> Option { + if blob.len() < 4 || &blob[0..4] != MAGIC { + return None; + } + let n = u16::from_le_bytes(blob[4..6].try_into().ok()?) as usize; + let mut off = 6; + let mut entries = Vec::with_capacity(n); + for _ in 0..n { + if off + ENTRY_SIZE > blob.len() { + return None; + } + let ty = u32::from_le_bytes(blob[off..off + 4].try_into().ok()?); + let key = u64::from_le_bytes(blob[off + 4..off + 12].try_into().ok()?); + entries.push((ty, key)); + off += ENTRY_SIZE; + } + if off + 4 + VAG_SIZE > blob.len() || &blob[off..off + 4] != VAG_TAG { + return Some(ParsedKeystore { + entries, + vag_bytes: Vec::new(), + }); + } + off += 4; + let vag_bytes = blob[off..off + VAG_SIZE].to_vec(); + Some(ParsedKeystore { + entries, + vag_bytes, + }) +} + +/// Return the embedded keystore blob for loading. +pub fn embedded_blob() -> &'static [u8] { + embedded::KEYSTORE_BLOB +} diff --git a/src/main.rs b/src/main.rs index 4fcfd05..11afdc7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ mod app; mod capture; mod export; +mod keystore; mod protocols; mod radio; mod storage; diff --git a/src/protocols/aut64.rs b/src/protocols/aut64.rs index 1d8194e..a0e7394 100644 --- a/src/protocols/aut64.rs +++ b/src/protocols/aut64.rs @@ -1,9 +1,9 @@ //! AUT64 block cipher implementation //! -//! Ported from protopirate's aut64.c +//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/aut64.c`. +//! Encrypt/decrypt, pack/unpack, and all tables match the reference. //! -//! AUT64 algorithm: 12 rounds, 8-byte block/key size -//! Based on: Reference AUT64 implementation +//! AUT64 algorithm: 12 rounds, 8-byte block/key size. //! See: https://www.usenix.org/system/files/conference/usenixsecurity16/sec16_paper_garcia.pdf pub const AUT64_NUM_ROUNDS: usize = 12; @@ -265,7 +265,7 @@ pub fn aut64_pack(src: &Aut64Key) -> [u8; AUT64_KEY_STRUCT_PACKED_SIZE] { dest } -/// Unpack a 16-byte array into an AUT64 key structure +/// Unpack a 16-byte array into an AUT64 key structure (matches aut64_unpack in reference) #[allow(dead_code)] pub fn aut64_unpack(src: &[u8]) -> Aut64Key { let mut dest = Aut64Key::default(); @@ -276,9 +276,10 @@ pub fn aut64_unpack(src: &[u8]) -> Aut64Key { dest.key[i * 2 + 1] = src[i + 1] & 0xF; } - let pbox: u32 = ((src[5] as u32) << 16) | ((src[6] as u32) << 8) | src[7] as u32; + let mut pbox: u32 = (u32::from(src[5]) << 16) | (u32::from(src[6]) << 8) | u32::from(src[7]); for i in (0..dest.pbox.len()).rev() { - dest.pbox[i] = ((pbox >> ((dest.pbox.len() - 1 - i) * 3)) & 0x7) as u8; + dest.pbox[i] = (pbox & 0x7) as u8; + pbox >>= 3; } for i in 0..(dest.sbox.len() / 2) { diff --git a/src/protocols/common.rs b/src/protocols/common.rs index 5de302c..ed408c1 100644 --- a/src/protocols/common.rs +++ b/src/protocols/common.rs @@ -1,4 +1,11 @@ //! Common utilities for protocol implementations. +//! +//! The ProtoPirate reference has `REFERENCES/ProtoPirate/protocols/protocols_common.c`, which +//! only provides Flipper preset name mapping (`protopirate_get_short_preset_name`). KAT does not +//! use that; this module holds shared types and helpers used by multiple protocol decoders. +//! Where applicable, algorithms match the reference: e.g. `crc8_kia` matches `kia_crc8` in +//! kia_v0.c (polynomial 0x7F, init 0x00); `add_bit` matches the common shift-left-and-append +//! pattern used in the reference decoders. /// Decoded signal information #[derive(Debug, Clone)] @@ -34,12 +41,12 @@ impl DecodedSignal { } } -/// CRC8 calculation with custom polynomial -/// +/// CRC8 calculation with custom polynomial (MSB-first, shift-left style). +/// /// # Arguments /// * `data` - Data bytes to calculate CRC over -/// * `poly` - CRC polynomial -/// * `init` - Initial CRC value +/// * `poly` - CRC polynomial (e.g. 0x7F for Kia) +/// * `init` - Initial CRC value (e.g. 0x00) pub fn crc8(data: &[u8], poly: u8, init: u8) -> u8 { let mut crc = init; for &byte in data { @@ -55,12 +62,12 @@ pub fn crc8(data: &[u8], poly: u8, init: u8) -> u8 { crc } -/// CRC8 for Kia protocol (polynomial 0x7F, init 0x00) +/// CRC8 for Kia protocol (matches kia_v0.c kia_crc8: polynomial 0x7F, init 0x00) pub fn crc8_kia(data: &[u8]) -> u8 { crc8(data, 0x7F, 0x00) } -/// Add a bit to the decoder's data accumulator +/// Add a bit to the decoder's data accumulator (shift-left, LSB last; matches reference add_bit pattern) #[inline] pub fn add_bit(data: &mut u64, count: &mut usize, bit: bool) { *data = (*data << 1) | (bit as u64); diff --git a/src/protocols/fiat_v0.rs b/src/protocols/fiat_v0.rs index a8b56fc..bc041a0 100644 --- a/src/protocols/fiat_v0.rs +++ b/src/protocols/fiat_v0.rs @@ -1,11 +1,12 @@ //! Fiat V0 protocol decoder/encoder //! -//! Ported from protopirate's fiat_v0.c +//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/fiat_v0.c` (Flipper). +//! Decode/encode logic (preamble, gap, Manchester, data/btn extraction, upload waveform) matches reference. //! //! Protocol characteristics: //! - Differential Manchester encoding: 200/400µs timing //! - 64-bit data (cnt:32 | serial:32) + 6-bit button -//! - 150 preamble pairs, 800µs gap, 3 bursts +//! - 150 preamble pairs (count LOW pulses), 800µs gap, 3 bursts use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming}; use crate::duration_diff; @@ -16,21 +17,21 @@ const TE_LONG: u32 = 400; const TE_DELTA: u32 = 100; #[allow(dead_code)] const MIN_COUNT_BIT: usize = 64; -const PREAMBLE_PAIRS: u16 = 150; +const PREAMBLE_PAIRS: u16 = 150; // 0x96 in reference const GAP_US: u32 = 800; const TOTAL_BURSTS: u8 = 3; const INTER_BURST_GAP: u32 = 25000; -/// Manchester decoder states +/// Manchester state machine states (matches Flipper's manchester_decoder.h, same as Ford V0) #[derive(Debug, Clone, Copy, PartialEq)] enum ManchesterState { - Mid0, - Mid1, - Start0, - Start1, + Mid0 = 0, + Mid1 = 1, + Start0 = 2, + Start1 = 3, } -/// Decoder states +/// Decoder states (matches protopirate's FiatV0DecoderStep) #[derive(Debug, Clone, Copy, PartialEq)] enum DecoderStep { Reset, @@ -68,37 +69,46 @@ impl FiatV0Decoder { } } - /// Manchester advance - returns decoded bit or None - fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option { - let event = match (is_short, is_high) { - (true, true) => 0, - (true, false) => 1, - (false, true) => 2, - (false, false) => 3, - }; + /// Manchester state machine (same as Ford V0 / Flipper manchester_decoder). + /// Event: 0=ShortLow, 1=ShortHigh, 2=LongLow, 3=LongHigh. + fn manchester_advance(&mut self, event: u8) -> Option { + let (new_state, emit) = match (self.manchester_state, event) { + (ManchesterState::Mid0, 0) => (ManchesterState::Mid0, false), + (ManchesterState::Mid0, 1) => (ManchesterState::Start1, true), + (ManchesterState::Mid0, 2) => (ManchesterState::Mid0, false), + (ManchesterState::Mid0, 3) => (ManchesterState::Mid1, true), - let (new_state, output) = match (self.manchester_state, event) { - (ManchesterState::Mid0, 0) | (ManchesterState::Mid1, 0) => { - (ManchesterState::Start1, None) - } - (ManchesterState::Mid0, 1) | (ManchesterState::Mid1, 1) => { - (ManchesterState::Start0, None) - } - (ManchesterState::Start1, 1) => (ManchesterState::Mid1, Some(true)), - (ManchesterState::Start1, 3) => (ManchesterState::Start0, Some(true)), - (ManchesterState::Start0, 0) => (ManchesterState::Mid0, Some(false)), - (ManchesterState::Start0, 2) => (ManchesterState::Start1, Some(false)), - _ => (ManchesterState::Mid1, None), + (ManchesterState::Mid1, 0) => (ManchesterState::Start0, true), + (ManchesterState::Mid1, 1) => (ManchesterState::Mid1, false), + (ManchesterState::Mid1, 2) => (ManchesterState::Mid0, true), + (ManchesterState::Mid1, 3) => (ManchesterState::Mid1, false), + + (ManchesterState::Start0, 0) => (ManchesterState::Mid0, false), + (ManchesterState::Start0, 1) => (ManchesterState::Mid0, false), + (ManchesterState::Start0, 2) => (ManchesterState::Mid0, false), + (ManchesterState::Start0, 3) => (ManchesterState::Mid1, false), + + (ManchesterState::Start1, 0) => (ManchesterState::Mid0, false), + (ManchesterState::Start1, 1) => (ManchesterState::Mid1, false), + (ManchesterState::Start1, 2) => (ManchesterState::Mid0, false), + (ManchesterState::Start1, 3) => (ManchesterState::Mid1, false), + + _ => (ManchesterState::Mid1, false), }; self.manchester_state = new_state; - output + if emit { + Some((event & 1) == 1) + } else { + None + } } fn manchester_reset(&mut self) { self.manchester_state = ManchesterState::Mid1; } + /// Add bit to accumulator; at 64 bits extract serial/cnt and clear data (bit_count unchanged in reference). fn add_manchester_bit(&mut self, bit: bool) { let new_bit = if bit { 1u32 } else { 0u32 }; let carry = (self.data_low >> 31) & 1; @@ -162,6 +172,7 @@ impl ProtocolDecoder for FiatV0Decoder { fn feed(&mut self, level: bool, duration: u32) -> Option { match self.step { + // Reset: wait for short HIGH (matches reference) DecoderStep::Reset => { if !level { return None; @@ -177,50 +188,60 @@ impl ProtocolDecoder for FiatV0Decoder { } } + // Preamble: only process LOW pulses (reference: if(level) return). Count short LOWs; gap = 800µs LOW. DecoderStep::Preamble => { - // Count short pulses in preamble, look for gap - if duration_diff!(duration, TE_SHORT) < TE_DELTA { + if level { + return None; + } + let short_ok = duration_diff!(duration, TE_SHORT) < TE_DELTA; + let gap_ok = duration_diff!(duration, GAP_US) < TE_DELTA; + + if short_ok { self.preamble_count += 1; self.te_last = duration; - } else if self.preamble_count >= PREAMBLE_PAIRS { - // Check for gap - if duration_diff!(duration, GAP_US) < TE_DELTA { + } else { + if self.preamble_count >= PREAMBLE_PAIRS && gap_ok { self.step = DecoderStep::Data; self.preamble_count = 0; self.data_low = 0; self.data_high = 0; self.bit_count = 0; self.te_last = duration; - return None; } else { self.step = DecoderStep::Reset; } - } else { - self.step = DecoderStep::Reset; } } + // Data: Manchester events — short first, then long (matches reference) DecoderStep::Data => { - let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA; - let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA; + let short_diff = duration_diff!(duration, TE_SHORT); + let long_diff = duration_diff!(duration, TE_LONG); - if is_short || is_long { - if let Some(bit) = self.manchester_advance(is_short, level) { - self.add_manchester_bit(bit); - - if self.bit_count > 0x46 { - self.btn = ((self.data_low << 1) | 1) as u8; - let result = self.parse_data(); - self.data_low = 0; - self.data_high = 0; - self.bit_count = 0; - self.step = DecoderStep::Reset; - return Some(result); - } + let event = if short_diff < TE_DELTA { + if level { 0 } else { 1 } + } else if long_diff < TE_DELTA { + if level { 2 } else { 3 } + } else { + self.te_last = duration; + if duration > TE_LONG * 3 { + self.step = DecoderStep::Reset; + } + return None; + }; + + if let Some(bit) = self.manchester_advance(event) { + self.add_manchester_bit(bit); + + if self.bit_count > 0x46 { + self.btn = ((self.data_low << 1) | 1) as u8; + let result = self.parse_data(); + self.data_low = 0; + self.data_high = 0; + self.bit_count = 0; + self.step = DecoderStep::Reset; + return Some(result); } - } else if duration > TE_LONG * 3 { - // End of signal - self.step = DecoderStep::Reset; } self.te_last = duration; @@ -249,14 +270,13 @@ impl ProtocolDecoder for FiatV0Decoder { signal.push(LevelDuration::new(false, INTER_BURST_GAP)); } - // Preamble + // Preamble: 150 HIGH-LOW pairs; last LOW is gap (matches reference get_upload) for i in 0..PREAMBLE_PAIRS { signal.push(LevelDuration::new(true, TE_SHORT)); - if i < PREAMBLE_PAIRS - 1 { - signal.push(LevelDuration::new(false, TE_SHORT)); - } else { - signal.push(LevelDuration::new(false, GAP_US)); - } + signal.push(LevelDuration::new( + false, + if i == PREAMBLE_PAIRS - 1 { GAP_US } else { TE_SHORT }, + )); } // First bit (bit 63) diff --git a/src/protocols/ford_v0.rs b/src/protocols/ford_v0.rs index 314ae73..a72f002 100644 --- a/src/protocols/ford_v0.rs +++ b/src/protocols/ford_v0.rs @@ -1,6 +1,7 @@ //! Ford V0 protocol decoder/encoder //! -//! Ported from protopirate's ford_v0.c +//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/ford_v0.c` (Flipper). +//! Decode/encode logic (CRC, BS, decode_ford_v0, encode_ford_v0, upload waveform) matches reference. //! //! Protocol characteristics: //! - Manchester encoding: 250/500µs timing @@ -8,6 +9,9 @@ //! - Matrix-based CRC in GF(2) //! - BS (byte swap) magic calculation //! - 6 bursts, 4 preamble pairs, 3500µs gap +//! +//! HackRF-specific: `TE_DELTA` (200µs) and `GAP_TOLERANCE` (1500µs) are wider than reference +//! (100µs / 250µs) for software demodulator tolerance. use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal}; use crate::radio::demodulator::LevelDuration; @@ -551,18 +555,16 @@ impl ProtocolDecoder for FordV0Decoder { } // ─── Step 3: PreambleCheck — count preamble pairs or transition to gap ─── + // Order matches protopirate ford_v0.c: check LONG first, then SHORT. DecoderStep::PreambleCheck => { if level { - let short_diff = duration_diff!(duration, TE_SHORT); - let long_diff = duration_diff!(duration, TE_LONG); - - if long_diff < TE_DELTA && long_diff <= short_diff { - // Long HIGH (closer to TE_LONG): another preamble pair + if duration_diff!(duration, TE_LONG) < TE_DELTA { + // Long HIGH: another preamble pair self.header_count += 1; self.te_last = duration; self.step = DecoderStep::Preamble; - } else if short_diff < TE_DELTA { - // Short HIGH (closer to TE_SHORT): end of preamble, transition to gap + } else if duration_diff!(duration, TE_SHORT) < TE_DELTA { + // Short HIGH: end of preamble, transition to gap self.step = DecoderStep::Gap; } else { self.step = DecoderStep::Reset; @@ -585,18 +587,12 @@ impl ProtocolDecoder for FordV0Decoder { // ─── Step 5: Data — Manchester decode 80 bits ─── DecoderStep::Data => { - // Map level+duration to Manchester event using NEAREST-MATCH. - // With TE_DELTA=200, SHORT(250) and LONG(500) ranges overlap at 300–450µs. - // First-match would always pick SHORT for overlapping durations, causing - // bit errors and CRC failure. Nearest-match picks the closer timing. - // - // Tie-break favors LONG (strict < for short_diff) because asymmetric - // demodulation compresses LOWs towards the midpoint (375µs) — these are - // actually LONG pulses that got shortened by threshold bias. + // Map level+duration to Manchester event. Order matches protopirate ford_v0.c: + // check SHORT first, then LONG (so when both within TE_DELTA, short wins). let short_diff = duration_diff!(duration, TE_SHORT); let long_diff = duration_diff!(duration, TE_LONG); - let event = if short_diff < TE_DELTA && short_diff < long_diff { + let event = if short_diff < TE_DELTA { if level { 0 } else { 1 } // ShortLow / ShortHigh } else if long_diff < TE_DELTA { if level { 2 } else { 3 } // LongLow / LongHigh diff --git a/src/protocols/keeloq_common.rs b/src/protocols/keeloq_common.rs index d4cfed1..ae3e4ff 100644 --- a/src/protocols/keeloq_common.rs +++ b/src/protocols/keeloq_common.rs @@ -1,63 +1,57 @@ -//! KeeLoq common encryption/decryption routines +//! KeeLoq common encryption/decryption and learning routines //! +//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/keeloq_common.c`. //! Shared by Kia V3/V4, Star Line, and other KeeLoq-based protocols. -//! Based on the NLF (Non-Linear Feedback) function with constant 0x3A5C742E. +//! NLF (Non-Linear Feedback) constant 0x3A5C742E per reference. -/// The KeeLoq NLF constant +/// The KeeLoq NLF constant (KEELOQ_NLF in reference) const KEELOQ_NLF: u32 = 0x3A5C742E; -/// KeeLoq decrypt: 528 rounds of the KeeLoq cipher (decrypt direction) +#[inline] +fn bit(x: u32, n: u32) -> u32 { + (x >> n) & 1 +} + +#[inline] +fn g5(x: u32, a: u32, b: u32, c: u32, d: u32, e: u32) -> u32 { + bit(x, a) | (bit(x, b) << 1) | (bit(x, c) << 2) | (bit(x, d) << 3) | (bit(x, e) << 4) +} + +/// KeeLoq decrypt: 528 rounds (matches subghz_protocol_keeloq_common_decrypt). +/// Key bit for round r is key[(15 - r) & 63]. NLF index g5(x, 0, 8, 19, 25, 30). pub fn keeloq_decrypt(data: u32, key: u64) -> u32 { - let mut block = data; - let mut tkey = key; - - for _ in 0..528 { - let lutkey = ((block >> 0) & 1) - | ((block >> 7) & 2) - | ((block >> 17) & 4) - | ((block >> 22) & 8) - | ((block >> 26) & 16); - let lsb = ((block >> 31) - ^ ((block >> 15) & 1) - ^ ((KEELOQ_NLF >> lutkey) & 1) - ^ (((tkey >> 15) & 1) as u32)) as u32; - block = ((block & 0x7FFFFFFF) << 1) | lsb; - tkey = ((tkey & 0x7FFFFFFFFFFFFFFF) << 1) | (tkey >> 63); + let mut x = data; + for r in 0..528u32 { + let key_bit = ((key >> ((15 - r) & 63)) & 1) as u32; + let new_lsb = bit(x, 31) ^ bit(x, 15) ^ key_bit + ^ bit(KEELOQ_NLF, g5(x, 0, 8, 19, 25, 30)); + x = (x << 1) ^ new_lsb; } - block + x } -/// KeeLoq encrypt: 528 rounds of the KeeLoq cipher (encrypt direction) +/// KeeLoq encrypt: 528 rounds (matches subghz_protocol_keeloq_common_encrypt). +/// Key bit for round r is key[r & 63]. NLF index g5(x, 1, 9, 20, 26, 31). pub fn keeloq_encrypt(data: u32, key: u64) -> u32 { - let mut block = data; - let mut tkey = key; - - for _ in 0..528 { - let lutkey = ((block >> 1) & 1) - | ((block >> 8) & 2) - | ((block >> 18) & 4) - | ((block >> 23) & 8) - | ((block >> 27) & 16); - let msb = ((block >> 0) - ^ ((block >> 16) & 1) - ^ ((KEELOQ_NLF >> lutkey) & 1) - ^ (((tkey >> 0) & 1) as u32)) as u32; - block = ((block >> 1) & 0x7FFFFFFF) | (msb << 31); - tkey = ((tkey >> 1) & 0x7FFFFFFFFFFFFFFF) | ((tkey & 1) << 63); + let mut x = data; + for r in 0..528u32 { + let key_bit = ((key >> (r & 63)) & 1) as u32; + let new_msb = bit(x, 0) ^ bit(x, 16) ^ key_bit + ^ bit(KEELOQ_NLF, g5(x, 1, 9, 20, 26, 31)); + x = (x >> 1) ^ (new_msb << 31); } - block + x } -/// Normal learning key derivation -/// Derives a 64-bit key from a 32-bit fix code and a 64-bit manufacturer key -pub fn keeloq_normal_learning(fix: u32, manufacturer_key: u64) -> u64 { - let serial_low = fix & 0xFFFF; - let serial_high = (fix >> 16) & 0xFFFF; - - let key_low = keeloq_decrypt(serial_low as u32 | 0x20000000, manufacturer_key); - let key_high = keeloq_decrypt(serial_high as u32 | 0x60000000, manufacturer_key); - - ((key_high as u64) << 32) | (key_low as u64) +/// Normal learning key derivation (matches subghz_protocol_keeloq_common_normal_learning). +/// @param data - serial number (28-bit, upper bits ignored) +/// @param key - manufacturer key (64-bit) +/// @return derived key for this serial (64-bit) +pub fn keeloq_normal_learning(data: u32, key: u64) -> u64 { + let data = data & 0x0FFFFFFF; + let k1 = keeloq_decrypt(data | 0x20000000, key); + let k2 = keeloq_decrypt(data | 0x60000000, key); + ((k2 as u64) << 32) | (k1 as u64) } /// Reverse the bits in a 64-bit key (for protocols that store data MSB-first) diff --git a/src/protocols/keys.rs b/src/protocols/keys.rs index f22f15e..23d8d1d 100644 --- a/src/protocols/keys.rs +++ b/src/protocols/keys.rs @@ -1,16 +1,8 @@ //! Key management module for protocol encryption/decryption //! -//! Ported from protopirate's keys.c -//! -//! Manages manufacturer keys used by various protocols: -//! - KIA V3/V4: kia_mf_key (manufacturer key for KeeLoq) -//! - KIA V5: kia_v5_key (custom mixer cipher key) -//! - KIA V6: kia_v6_a_key, kia_v6_b_key (AES-128 XOR mask keys) -//! - Star Line: star_line_mf_key (manufacturer key for KeeLoq) -//! - VAG: AUT64 keys loaded from keystore files -//! -//! Keys are loaded from `~/.config/KAT/keystore/keystore.ini` at startup. -//! See [`load_keystore_from_dir`] for the file format. +//! Aligned with ProtoPirate's keys.c (KIA_KEY1..4, get_kia_mf_key, etc.). +//! Keys are loaded from the embedded keystore blob in `crate::keystore` at startup, +//! matching the standard encrypted + VAG raw keystore data. use super::aut64::{self, Aut64Key, AUT64_KEY_STRUCT_PACKED_SIZE}; use configparser::ini::Ini; @@ -25,8 +17,8 @@ const KIA_KEY3: u32 = 12; // kia_v6_b_key const KIA_KEY4: u32 = 13; // kia_v5_key const STAR_LINE_KEY: u32 = 20; // star_line_mf_key -/// Maximum number of VAG AUT64 keys -const VAG_KEYS_COUNT: usize = 3; +/// Maximum number of VAG AUT64 keys (embedded blob has 64 bytes = 4 keys) +const MAX_VAG_KEYS: usize = 4; /// Global key store - thread-safe access to loaded keys pub struct KeyStore { @@ -81,22 +73,20 @@ impl KeyStore { } } - /// Load VAG AUT64 keys from raw binary data - /// The data should contain packed AUT64 key structures (16 bytes each) + /// Load VAG AUT64 keys from raw binary data (16 bytes per key; up to MAX_VAG_KEYS) pub fn load_vag_keys_from_data(&mut self, data: &[u8]) { if self.vag_keys_loaded { return; } self.vag_keys.clear(); + let n = (data.len() / AUT64_KEY_STRUCT_PACKED_SIZE).min(MAX_VAG_KEYS); - for i in 0..VAG_KEYS_COUNT { + for i in 0..n { let offset = i * AUT64_KEY_STRUCT_PACKED_SIZE; if offset + AUT64_KEY_STRUCT_PACKED_SIZE > data.len() { - error!("VAG key data too short for key {}", i); break; } - let key = aut64::aut64_unpack(&data[offset..offset + AUT64_KEY_STRUCT_PACKED_SIZE]); self.vag_keys.push(key); } @@ -179,7 +169,7 @@ pub fn get_keystore_mut() -> std::sync::RwLockWriteGuard<'static, KeyStore> { global_keystore().write().unwrap() } -/// Initialize the global keystore with KIA keys +/// Initialize the global keystore with KIA keys (matches protopirate_keys_load pattern) pub fn load_keys(kia_entries: &[(u32, u64)]) { let mut store = get_keystore_mut(); store.load_kia_keys(kia_entries); @@ -191,6 +181,26 @@ pub fn load_vag_keys(path: &str) { store.load_vag_keys_from_file(path); } +/// Load the global keystore from the embedded blob (src/keystore/embedded.rs). +/// Matches ProtoPirate loading from encrypted + VAG keystore; keys are compiled in. +pub fn load_keystore_from_embedded() { + let blob = crate::keystore::embedded_blob(); + let Some(parsed) = crate::keystore::parse_blob(blob) else { + error!("Failed to parse embedded keystore blob"); + return; + }; + let mut store = get_keystore_mut(); + store.load_kia_keys(&parsed.entries); + if !parsed.vag_bytes.is_empty() { + store.load_vag_keys_from_data(&parsed.vag_bytes); + } + info!( + "Keystore loaded from embedded blob ({} entries, {} VAG keys)", + parsed.entries.len(), + store.vag_keys.len() + ); +} + // ============================================================================= // Keystore file loading from ~/.config/KAT/keystore/ // ============================================================================= diff --git a/src/protocols/kia_v0.rs b/src/protocols/kia_v0.rs index befe6f7..dd0c941 100644 --- a/src/protocols/kia_v0.rs +++ b/src/protocols/kia_v0.rs @@ -1,13 +1,13 @@ -//! Kia V0 protocol decoder +//! Kia V0 protocol decoder/encoder +//! +//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v0.c`. +//! Decode/encode logic (CRC8, preamble/sync, field layout) matches reference. //! -//! Ported from protopirate's kia_v0.c -//! //! Protocol characteristics: //! - PWM encoding: short pulse (250µs) = 0, long pulse (500µs) = 1 -//! - 61 bits total -//! - Preamble: alternating short pulses -//! - Sync: long-long pattern -//! - Data: 59 bits (4-bit prefix + 16-bit counter + 28-bit serial + 4-bit button + 8-bit CRC) +//! - 61 bits total (1 sync bit + 60 data bits) +//! - Preamble: alternating short pulses; sync: long-long pattern +//! - Data: 60 bits (4-bit prefix + 16-bit counter + 28-bit serial + 4-bit button + 8-bit CRC) use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal}; use super::common::{crc8_kia, add_bit}; @@ -19,7 +19,7 @@ const TE_LONG: u32 = 500; const TE_DELTA: u32 = 100; const MIN_COUNT_BIT: usize = 61; -/// Decoder states +/// Decoder states (matches protopirate's KiaV0DecoderStep) #[derive(Debug, Clone, Copy, PartialEq)] enum DecoderStep { Reset, @@ -48,7 +48,7 @@ impl KiaV0Decoder { } } - /// Calculate CRC for Kia data packet + /// CRC8 for Kia data packet (matches kia_v0.c kia_crc8: polynomial 0x7F, init 0x00) fn calculate_crc(data: u64) -> u8 { let crc_data = [ ((data >> 48) & 0xFF) as u8, @@ -236,13 +236,13 @@ impl ProtocolDecoder for KiaV0Decoder { signal.push(LevelDuration::new(is_high, TE_SHORT)); } - // Sync: long-long + // Sync: long-long (matches protopirate kia_v0 encode) signal.push(LevelDuration::new(true, TE_LONG)); signal.push(LevelDuration::new(false, TE_LONG)); - // Data: 59 bits (MSB first) - for bit_num in 0..59 { - let bit_mask = 1u64 << (58 - bit_num); + // Data: 60 bits MSB first (bits 59..0) + for bit_num in 0..60 { + let bit_mask = 1u64 << (59 - bit_num); let bit = (data & bit_mask) != 0; let duration = if bit { TE_LONG } else { TE_SHORT }; @@ -257,3 +257,9 @@ impl ProtocolDecoder for KiaV0Decoder { Some(signal) } } + +impl Default for KiaV0Decoder { + fn default() -> Self { + Self::new() + } +} diff --git a/src/protocols/kia_v1.rs b/src/protocols/kia_v1.rs index 8196da9..c444d19 100644 --- a/src/protocols/kia_v1.rs +++ b/src/protocols/kia_v1.rs @@ -1,12 +1,13 @@ -//! Kia V1 protocol decoder +//! Kia V1 protocol decoder/encoder //! -//! Ported from protopirate's kia_v1.c +//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v1.c`. +//! Decode/encode logic (Manchester, CRC4, preamble, field layout) matches reference. //! //! Protocol characteristics: //! - Manchester encoding: 800/1600µs timing -//! - 57 bits total -//! - Long preamble of ~90 pulses -//! - CRC4 checksum +//! - 57 bits total (32 serial + 8 button + 12 counter + 4 CRC) +//! - Long preamble of ~90 long pairs +//! - CRC4 checksum with offset rules (cnt_high 0 / >= 6) use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal}; use crate::radio::demodulator::LevelDuration; @@ -17,7 +18,7 @@ const TE_LONG: u32 = 1600; const TE_DELTA: u32 = 200; const MIN_COUNT_BIT: usize = 57; -/// Manchester states +/// Manchester decoder states (matches protopirate kia_v1 Manchester state machine) #[derive(Debug, Clone, Copy, PartialEq)] enum ManchesterState { Mid0, @@ -26,7 +27,7 @@ enum ManchesterState { Start1, } -/// Decoder states +/// Decoder states (matches protopirate's KiaV1DecoderStep) #[derive(Debug, Clone, Copy, PartialEq)] enum DecoderStep { Reset, @@ -56,7 +57,7 @@ impl KiaV1Decoder { } } - /// CRC4 calculation for Kia V1 + /// CRC4 for Kia V1 (matches kia_v1.c: XOR nibbles + offset) fn crc4(bytes: &[u8], offset: u8) -> u8 { let mut crc: u8 = 0; for &byte in bytes { @@ -97,7 +98,7 @@ impl KiaV1Decoder { fn parse_data(&self) -> DecodedSignal { let data = self.decode_data; - // Extract fields per kia_v1.c + // Field layout matches kia_v1.c: serial(32) | button(8) | cnt_low(8) | cnt_high(4) | crc(4) let serial = (data >> 24) as u32; let button = ((data >> 16) & 0xFF) as u8; let cnt_low = ((data >> 8) & 0xFF) as u16; @@ -263,7 +264,7 @@ impl ProtocolDecoder for KiaV1Decoder { let mut signal = Vec::with_capacity(600); - // Generate 3 bursts + // Generate 3 bursts (matches protopirate kia_v1 encode) for burst in 0..3 { if burst > 0 { signal.push(LevelDuration::new(false, 25000)); @@ -275,10 +276,10 @@ impl ProtocolDecoder for KiaV1Decoder { signal.push(LevelDuration::new(true, TE_LONG)); } - // Short gap + // Short gap before data signal.push(LevelDuration::new(false, TE_SHORT)); - // Data: Manchester encoded, MSB first + // Data: 57 bits Manchester encoded, MSB first for bit_num in (1..MIN_COUNT_BIT).rev() { let bit = ((data >> (bit_num - 1)) & 1) == 1; if bit { @@ -294,3 +295,9 @@ impl ProtocolDecoder for KiaV1Decoder { Some(signal) } } + +impl Default for KiaV1Decoder { + fn default() -> Self { + Self::new() + } +} diff --git a/src/protocols/kia_v2.rs b/src/protocols/kia_v2.rs index d2d1238..75a3d1c 100644 --- a/src/protocols/kia_v2.rs +++ b/src/protocols/kia_v2.rs @@ -1,12 +1,13 @@ -//! Kia V2 protocol decoder +//! Kia V2 protocol decoder/encoder //! -//! Ported from protopirate's kia_v2.c +//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v2.c`. +//! Decode/encode logic (Manchester, CRC4, preamble, byte-swapped counter) matches reference. //! //! Protocol characteristics: //! - Manchester encoding: 500/1000µs timing -//! - 53 bits total -//! - Long preamble of 252+ pairs -//! - CRC4 checksum +//! - 53 bits total (32 serial + 4 button + 12 counter + 4 CRC, plus start bit) +//! - Long preamble of 252 long pairs +//! - CRC4 checksum (XOR nibbles + offset 1) use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal}; use crate::radio::demodulator::LevelDuration; @@ -17,7 +18,7 @@ const TE_LONG: u32 = 1000; const TE_DELTA: u32 = 150; const MIN_COUNT_BIT: usize = 53; -/// Manchester states +/// Manchester decoder states (matches protopirate kia_v2 Manchester state machine) #[derive(Debug, Clone, Copy, PartialEq)] enum ManchesterState { Mid0, @@ -26,7 +27,7 @@ enum ManchesterState { Start1, } -/// Decoder states +/// Decoder states (matches protopirate's KiaV2DecoderStep) #[derive(Debug, Clone, Copy, PartialEq)] enum DecoderStep { Reset, @@ -56,7 +57,7 @@ impl KiaV2Decoder { } } - /// Calculate CRC for Kia V2 + /// CRC4 for Kia V2 (matches kia_v2.c: 6-byte permuted input, XOR nibbles, offset 1) fn calculate_crc(data: u64) -> u8 { let serial = ((data >> 20) & 0xFFFFFFFF) as u32; let u_var4 = (data & 0xFFFFFFFF) as u32; @@ -105,10 +106,10 @@ impl KiaV2Decoder { output } - /// Parse decoded data + /// Parse decoded data (field layout matches kia_v2.c) fn parse_data(&self) -> DecodedSignal { let data = self.decode_data; - + // serial(32) | button(4) | counter_swapped(12) | crc(4); counter byte-swapped in stream let serial = ((data >> 20) & 0xFFFFFFFF) as u32; let button = ((data >> 16) & 0x0F) as u8; @@ -246,7 +247,7 @@ impl ProtocolDecoder for KiaV2Decoder { let mut signal = Vec::with_capacity(700); - // Generate 2 bursts + // Generate 2 bursts (matches protopirate kia_v2 encode) for _burst in 0..2 { // Preamble: 252 long pairs for _ in 0..252 { @@ -254,10 +255,10 @@ impl ProtocolDecoder for KiaV2Decoder { signal.push(LevelDuration::new(true, TE_LONG)); } - // Short gap + // Short gap before data signal.push(LevelDuration::new(false, TE_SHORT)); - // Data: Manchester encoded, MSB first + // Data: 53 bits Manchester encoded, MSB first for bit_num in (1..MIN_COUNT_BIT).rev() { let bit = ((new_data >> (bit_num - 1)) & 1) == 1; if bit { @@ -273,3 +274,9 @@ impl ProtocolDecoder for KiaV2Decoder { Some(signal) } } + +impl Default for KiaV2Decoder { + fn default() -> Self { + Self::new() + } +} diff --git a/src/protocols/kia_v3_v4.rs b/src/protocols/kia_v3_v4.rs index a55575a..e30e523 100644 --- a/src/protocols/kia_v3_v4.rs +++ b/src/protocols/kia_v3_v4.rs @@ -1,15 +1,16 @@ -//! Kia V3/V4 protocol decoder +//! Kia V3/V4 protocol decoder/encoder //! -//! Ported from protopirate's kia_v3_v4.c +//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v3_v4.c`. +//! Decode/encode logic (PWM, preamble, sync polarity, KeeLoq, CRC4, byte order) matches reference. //! //! Protocol characteristics: -//! - PWM encoding: 400/800µs timing -//! - 68 bits total -//! - Short preamble of 16 pairs -//! - KeeLoq encryption (requires manufacturer key) -//! - V3 and V4 differ only in sync polarity +//! - PWM encoding: 400/800µs (short=0, long=1) +//! - 68 bits total (8 bytes encrypted + 4 bits CRC) +//! - Short preamble of 16 pairs; sync 1200µs (V4: long HIGH, V3: long LOW) +//! - KeeLoq encryption (KIA manufacturer key); V3/V4 differ only in sync polarity use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal}; +use super::keeloq_common::{keeloq_decrypt, keeloq_encrypt}; use super::keys; use crate::radio::demodulator::LevelDuration; use crate::duration_diff; @@ -23,7 +24,7 @@ const INTER_BURST_GAP_US: u32 = 10000; const PREAMBLE_PAIRS: usize = 16; const TOTAL_BURSTS: usize = 3; -/// Decoder states +/// Decoder states (matches protopirate's KiaV3V4DecoderStep) #[derive(Debug, Clone, Copy, PartialEq)] enum DecoderStep { Reset, @@ -53,7 +54,7 @@ impl KiaV3V4Decoder { } } - /// Reverse bits in a byte + /// Reverse bits in a byte (matches kia_v3_v4.c byte order for KeeLoq payload) fn reverse8(byte: u8) -> u8 { let mut byte = byte; byte = (byte & 0xF0) >> 4 | (byte & 0x0F) << 4; @@ -76,7 +77,7 @@ impl KiaV3V4Decoder { } } - /// CRC4 calculation + /// CRC4 for Kia V3/V4 (matches kia_v3_v4.c: XOR nibbles over first 8 bytes) fn calculate_crc(bytes: &[u8]) -> u8 { let mut crc: u8 = 0; for &byte in bytes.iter().take(8) { @@ -85,61 +86,18 @@ impl KiaV3V4Decoder { crc & 0x0F } - /// KeeLoq decrypt - fn keeloq_decrypt(data: u32, key: u64) -> u32 { - let mut block = data; - let mut tkey = key; - - for _ in 0..528 { - let lutkey = ((block >> 0) & 1) | - ((block >> 7) & 2) | - ((block >> 17) & 4) | - ((block >> 22) & 8) | - ((block >> 26) & 16); - let lsb = ((block >> 31) ^ - ((block >> 15) & 1) ^ - ((0x3A5C742E_u32 >> lutkey) & 1) ^ - (((tkey >> 15) & 1) as u32)) as u32; - block = ((block & 0x7FFFFFFF) << 1) | lsb; - tkey = ((tkey & 0x7FFFFFFFFFFFFFFF) << 1) | (tkey >> 63); - } - block - } - - /// KeeLoq encrypt - fn keeloq_encrypt(data: u32, key: u64) -> u32 { - let mut block = data; - let mut tkey = key; - - for _ in 0..528 { - let lutkey = ((block >> 1) & 1) | - ((block >> 8) & 2) | - ((block >> 18) & 4) | - ((block >> 23) & 8) | - ((block >> 27) & 16); - let msb = ((block >> 0) ^ - ((block >> 16) & 1) ^ - ((0x3A5C742E_u32 >> lutkey) & 1) ^ - (((tkey >> 0) & 1) as u32)) as u32; - block = ((block >> 1) & 0x7FFFFFFF) | (msb << 31); - tkey = ((tkey >> 1) & 0x7FFFFFFFFFFFFFFF) | ((tkey & 1) << 63); - } - block - } - - /// Get manufacturer key (placeholder - in real use, this would be loaded from config) + /// KIA manufacturer key from keystore (type 10) fn get_mf_key() -> u64 { keys::get_keystore().get_kia_mf_key() } - /// Process the collected buffer and validate + /// Process collected 68 bits: decrypt KeeLoq block, validate button/serial (matches kia_v3_v4.c) fn process_buffer(&self) -> Option { if self.raw_bit_count < 68 { return None; } let mut b = self.raw_bits; - // V3 sync means data is inverted if self.is_v3_sync { let num_bytes = ((self.raw_bit_count + 7) / 8) as usize; @@ -164,7 +122,7 @@ impl KiaV3V4Decoder { let our_serial_lsb = (serial & 0xFF) as u8; let mf_key = Self::get_mf_key(); - let decrypted = Self::keeloq_decrypt(encrypted, mf_key); + let decrypted = keeloq_decrypt(encrypted, mf_key); let dec_btn = ((decrypted >> 28) & 0x0F) as u8; let dec_serial_lsb = ((decrypted >> 16) & 0xFF) as u8; @@ -313,7 +271,7 @@ impl ProtocolDecoder for KiaV3V4Decoder { (((button & 0x0F) as u32) << 28); let mf_key = Self::get_mf_key(); - let encrypted = Self::keeloq_encrypt(plaintext, mf_key); + let encrypted = keeloq_encrypt(plaintext, mf_key); // Build raw bytes let mut raw_bytes = [0u8; 9]; @@ -343,18 +301,19 @@ impl ProtocolDecoder for KiaV3V4Decoder { let mut signal = Vec::with_capacity(600); + // 3 bursts with 10ms gap (matches protopirate kia_v3_v4 encode) for burst in 0..TOTAL_BURSTS { if burst > 0 { signal.push(LevelDuration::new(false, INTER_BURST_GAP_US)); } - // Preamble + // Preamble: 16 short pairs for _ in 0..PREAMBLE_PAIRS { signal.push(LevelDuration::new(true, TE_SHORT)); signal.push(LevelDuration::new(false, TE_SHORT)); } - // Sync pulse + // Sync: V4 = long HIGH, V3 = long LOW if version == 0 { // V4: long HIGH, short LOW signal.push(LevelDuration::new(true, SYNC_DURATION)); @@ -365,7 +324,7 @@ impl ProtocolDecoder for KiaV3V4Decoder { signal.push(LevelDuration::new(false, SYNC_DURATION)); } - // Data bits + // Data: 68 bits PWM (8 bytes + 4-bit CRC), MSB first per byte for byte_idx in 0..9 { let bits_in_byte = if byte_idx == 8 { 4 } else { 8 }; for bit_idx in (8 - bits_in_byte..8).rev() { @@ -384,3 +343,9 @@ impl ProtocolDecoder for KiaV3V4Decoder { Some(signal) } } + +impl Default for KiaV3V4Decoder { + fn default() -> Self { + Self::new() + } +} diff --git a/src/protocols/kia_v5.rs b/src/protocols/kia_v5.rs index 2dd2f7a..6f1b48d 100644 --- a/src/protocols/kia_v5.rs +++ b/src/protocols/kia_v5.rs @@ -1,13 +1,14 @@ -//! Kia V5 protocol decoder +//! Kia V5 protocol decoder (decode-only) //! -//! Ported from protopirate's kia_v5.c +//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v5.c`. +//! Decode logic (Manchester polarity, mixer decrypt, YEK, preamble) matches reference. +//! No encoder in protopirate. //! //! Protocol characteristics: -//! - Manchester encoding: 400/800µs timing -//! - 64 bits total (+3 bit CRC) -//! - Preamble of ~40+ short pairs -//! - Custom "mixer" encryption -//! - Decode-only (no encoder) +//! - Manchester encoding: 400/800µs (opposite polarity to V1/V2: level ? ShortHigh : ShortLow) +//! - 64 data bits + 3-bit CRC (67 bits on air) +//! - Preamble of ~40+ short/long pairs; then 64-bit key then 3-bit CRC +//! - Custom "mixer" decryption with KIA V5 manufacturer key use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal}; use super::keys; @@ -19,7 +20,7 @@ const TE_LONG: u32 = 800; const TE_DELTA: u32 = 150; const MIN_COUNT_BIT: usize = 64; -/// Manchester states +/// Manchester decoder states (V5 uses opposite polarity to V1/V2; see manchester_advance) #[derive(Debug, Clone, Copy, PartialEq)] enum ManchesterState { Mid0, @@ -28,7 +29,7 @@ enum ManchesterState { Start1, } -/// Decoder states +/// Decoder states (matches protopirate's KiaV5DecoderStep) #[derive(Debug, Clone, Copy, PartialEq)] enum DecoderStep { Reset, @@ -60,12 +61,12 @@ impl KiaV5Decoder { } } - /// Get encryption key from global keystore + /// KIA V5 manufacturer key from keystore (type 13) fn get_v5_key() -> u64 { keys::get_keystore().get_kia_v5_key() } - /// Custom mixer decryption + /// Mixer decryption (matches kia_v5.c custom cipher) fn mixer_decode(encrypted: u32) -> u16 { let mut s0 = (encrypted & 0xFF) as u8; let mut s1 = ((encrypted >> 8) & 0xFF) as u8; @@ -128,7 +129,7 @@ impl KiaV5Decoder { ((s0 as u16) + ((s1 as u16) << 8)) & 0xFFFF } - /// Reverse bits in 64-bit value + /// YEK: reverse bit order per byte (matches kia_v5.c for key derivation) fn compute_yek(key: u64) -> u64 { let mut yek: u64 = 0; for i in 0..8 { @@ -180,7 +181,7 @@ impl KiaV5Decoder { output } - /// Parse decoded data + /// Parse 64-bit key: YEK then serial/button from high bits, mixer_decode for counter (matches kia_v5.c) fn parse_data(&self) -> Option { if self.bit_count < MIN_COUNT_BIT as u8 { return None; @@ -188,7 +189,7 @@ impl KiaV5Decoder { let key = self.saved_key; let yek = Self::compute_yek(key); - + // serial(28) + button(4) in high 32 bits; low 32 bits encrypted counter let serial = ((yek >> 32) & 0x0FFFFFFF) as u32; let button = ((yek >> 60) & 0x0F) as u8; let encrypted = (yek & 0xFFFFFFFF) as u32; @@ -316,6 +317,12 @@ impl ProtocolDecoder for KiaV5Decoder { } fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option> { - None // V5 doesn't support encoding + None // V5 decode-only in protopirate + } +} + +impl Default for KiaV5Decoder { + fn default() -> Self { + Self::new() } } diff --git a/src/protocols/kia_v6.rs b/src/protocols/kia_v6.rs index 03e2485..5ac05c1 100644 --- a/src/protocols/kia_v6.rs +++ b/src/protocols/kia_v6.rs @@ -1,13 +1,14 @@ -//! Kia V6 protocol decoder +//! Kia V6 protocol decoder (decode-only) //! -//! Ported from protopirate's kia_v6.c +//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v6.c`. +//! Decode logic (Manchester level mapping, 3-part 144-bit frame, AES-128, CRC8, keystore XOR) matches reference. +//! No encoder in protopirate. //! //! Protocol characteristics: -//! - Manchester encoding: 200/400µs timing -//! - 144 bits total (split into 3 parts) -//! - Long preamble of 600+ pairs -//! - AES-128 encryption -//! - Decode-only (no encoder) +//! - Manchester encoding: 200/400µs (level convention inverted vs Flipper; see manchester_advance) +//! - 144 bits total: part1 (64) + part2 (64) + part3 (16), each part inverted on store +//! - Long preamble of 601 pairs; sync bits 1,1,0,1 then data +//! - AES-128 decryption with key derived from KIA V6 A/B keystores (types 11/12) and XOR masks use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal}; use super::keys; @@ -65,7 +66,7 @@ const AES_SBOX_INV: [u8; 256] = [ const AES_RCON: [u8; 10] = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36]; -/// Manchester states +/// Manchester decoder states (event mapping 0/2/4/6 matches protopirate kia_v6 level convention) #[derive(Debug, Clone, Copy, PartialEq)] enum ManchesterState { Mid0, @@ -74,7 +75,7 @@ enum ManchesterState { Start1, } -/// Decoder states +/// Decoder states (matches protopirate's KiaV6DecoderStep) #[derive(Debug, Clone, Copy, PartialEq)] enum DecoderStep { Reset, @@ -119,17 +120,17 @@ impl KiaV6Decoder { } } - /// Get keystore A from global keystore + /// KIA V6 keystore A from keystore (type 11) fn get_keystore_a() -> u64 { keys::get_keystore().get_kia_v6_keystore_a() } - /// Get keystore B from global keystore + /// KIA V6 keystore B from keystore (type 12) fn get_keystore_b() -> u64 { keys::get_keystore().get_kia_v6_keystore_b() } - /// CRC8 calculation + /// CRC8 for V6 (matches kia_v6.c: init 0xFF, polynomial 0x07, over first 15 bytes) fn crc8(data: &[u8], init: u8, polynomial: u8) -> u8 { let mut crc = init; for &byte in data { @@ -274,7 +275,7 @@ impl KiaV6Decoder { *data = state; } - /// Get AES key from keystores + /// AES-128 key from V6 keystores A+B with XOR_MASK_LOW/HIGH (matches kia_v6.c) fn get_aes_key() -> [u8; 16] { let keystore_a = Self::get_keystore_a(); let keystore_a_hi = ((keystore_a >> 32) & 0xFFFFFFFF) as u32; @@ -305,7 +306,7 @@ impl KiaV6Decoder { aes_key } - /// Decrypt the stored data + /// Decrypt 16-byte block: byte layout matches kia_v6.c; AES-128 then CRC8 check fn decrypt(&self) -> Option<(u32, u8, u32, bool)> { let mut encrypted_data = [0u8; 16]; @@ -380,9 +381,8 @@ impl KiaV6Decoder { output } - /// Add initial sync bits + /// Add initial sync bits (1,1,0,1 — matches kia_v6.c) fn add_sync_bits(&mut self) { - // Add 1, 1, 0, 1 as initial bits for bit in [true, true, false, true] { let carry = self.data_part1_low >> 31; self.data_part1_low = (self.data_part1_low << 1) | (bit as u32); @@ -554,6 +554,12 @@ impl ProtocolDecoder for KiaV6Decoder { } fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option> { - None + None // V6 decode-only in protopirate + } +} + +impl Default for KiaV6Decoder { + fn default() -> Self { + Self::new() } } diff --git a/src/protocols/mod.rs b/src/protocols/mod.rs index 5c9eb3a..2ca6f35 100644 --- a/src/protocols/mod.rs +++ b/src/protocols/mod.rs @@ -1,7 +1,8 @@ //! Protocol decoders and encoders for various keyfob systems. //! -//! This module implements decoders for various keyfob protocols, ported from -//! protopirate. Each protocol processes level+duration pairs from the demodulator. +//! 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]. mod common; pub mod keeloq_common; diff --git a/src/protocols/psa.rs b/src/protocols/psa.rs index 374009a..38d148a 100644 --- a/src/protocols/psa.rs +++ b/src/protocols/psa.rs @@ -1,12 +1,13 @@ -//! PSA (Peugeot/Citroen) protocol decoder/encoder +//! PSA (Peugeot/Citroën) protocol decoder/encoder //! -//! Ported from protopirate's psa.c +//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/psa.c`. +//! Decode/encode logic (Manchester, preamble, TEA, XOR, mode 0x23/0x36) matches reference. //! //! Protocol characteristics: -//! - Manchester encoding: 125/250µs bit timing, 250/500µs symbol timing -//! - 128 bits total (key1: 64 bits, key2: 16 bits, validation: 48 bits) -//! - TEA and XOR encryption schemes -//! - Two modes: 0x23 and 0x36 +//! - Manchester encoding: 250/500µs symbol (125/250µs sub-symbol for preamble) +//! - 128 bits total: key1 (64) + validation (16) + key2/rest (48); decode uses key1 + 16-bit validation +//! - TEA decrypt/encrypt with fixed key schedules; mode 0x23 adds XOR layer +//! - Two modes: seed 0x23 (TEA + XOR), seed 0xF3/0x36 (TEA, BF2 key schedule) use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming}; use crate::duration_diff; @@ -35,7 +36,7 @@ const BF1_KEY_SCHEDULE: [u32; 4] = [0x4A434915, 0xD6743C2B, 0x1F29D308, 0xE6B79A // Brute-force constants for mode 0x36 const BF2_KEY_SCHEDULE: [u32; 4] = [0x4039C240, 0xEDA92CAB, 0x4306C02A, 0x02192A04]; -/// Manchester decoder states +/// Manchester decoder states (matches protopirate psa.c Manchester state machine) #[derive(Debug, Clone, Copy, PartialEq)] enum ManchesterState { Mid0, @@ -44,16 +45,12 @@ enum ManchesterState { Start1, } -/// Decoder states +/// Decoder states (matches protopirate's PsaDecoderState) #[derive(Debug, Clone, Copy, PartialEq)] enum DecoderState { - /// Waiting for first edge WaitEdge, - /// Counting preamble pattern CountPattern, - /// Decoding Manchester data DecodeManchester, - /// Found end of data End, } @@ -94,7 +91,7 @@ impl PsaDecoder { } } - /// Manchester advance + /// Manchester state machine (matches psa.c event mapping) fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option { let event = match (is_short, is_high) { (true, true) => 0, @@ -143,7 +140,7 @@ impl PsaDecoder { } } - /// TEA decrypt + /// TEA decrypt (matches psa.c / standard TEA) fn tea_decrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) { let mut sum = TEA_DELTA.wrapping_mul(TEA_ROUNDS); for _ in 0..TEA_ROUNDS { @@ -161,7 +158,7 @@ impl PsaDecoder { } } - /// TEA encrypt + /// TEA encrypt (matches psa.c / standard TEA) fn tea_encrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) { let mut sum: u32 = 0; for _ in 0..TEA_ROUNDS { @@ -179,7 +176,7 @@ impl PsaDecoder { } } - /// XOR decrypt (mode 0x23) + /// XOR decrypt for mode 0x23 (matches psa.c) fn xor_decrypt(buffer: &mut [u8]) { let e6 = buffer[8]; let e7 = buffer[9]; @@ -198,8 +195,9 @@ impl PsaDecoder { buffer[7] = e5 ^ e6 ^ e7; } + /// Decrypt key1 + validation: mode 0x23 (TEA+XOR) or 0x36 (TEA, BF2) — matches psa.c fn try_decrypt(&self) -> Option<(u32, u8, u32, u16, u8)> { - // Try mode 0x23 first + // Try mode 0x23 first (seed byte 0x23) let seed_byte = (self.key1_high >> 24) as u8; if seed_byte >= 0x23 && seed_byte < 0x24 { @@ -250,8 +248,9 @@ impl PsaDecoder { None } + /// Build DecodedSignal from key1 + validation; decrypt yields serial/button/counter (matches psa.c) fn parse_data(&self) -> DecodedSignal { - // Combine into 128-bit data (store lower 64) + // Store key1 as 64-bit data for display/replay let data = ((self.key1_high as u64) << 32) | (self.key1_low as u64); if let Some((serial, btn, counter, _crc, _mode)) = self.try_decrypt() { @@ -445,20 +444,17 @@ impl ProtocolDecoder for PsaDecoder { let key1_low = v1; let validation = ((buffer[8] as u16) << 8) | (buffer[9] as u16); - // Build signal let mut signal = Vec::with_capacity(512); - // Preamble: alternating 125µs pulses + // Preamble + sync (matches protopirate psa encode) for _ in 0..70 { signal.push(LevelDuration::new(true, TE_SHORT_125)); signal.push(LevelDuration::new(false, TE_SHORT_125)); } - - // Sync signal.push(LevelDuration::new(true, TE_LONG_250)); signal.push(LevelDuration::new(false, TE_LONG_250)); - // Key1: 64 bits Manchester encoded + // Key1: 64 bits Manchester, then validation 16 bits let key1 = ((key1_high as u64) << 32) | (key1_low as u64); for bit in (0..64).rev() { if (key1 >> bit) & 1 == 1 { @@ -487,3 +483,9 @@ impl ProtocolDecoder for PsaDecoder { Some(signal) } } + +impl Default for PsaDecoder { + fn default() -> Self { + Self::new() + } +} diff --git a/src/protocols/scher_khan.rs b/src/protocols/scher_khan.rs index 57c999e..b7de7c1 100644 --- a/src/protocols/scher_khan.rs +++ b/src/protocols/scher_khan.rs @@ -1,14 +1,14 @@ -//! Scher-Khan protocol decoder +//! Scher-Khan protocol decoder (decode-only) //! -//! Ported from protopirate's scher_khan.c +//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/scher_khan.c`. +//! Decode logic (PWM preamble/sync, short=0/long=1, variable bit count) matches reference. +//! No encoder in protopirate. //! //! Protocol characteristics: -//! - PWM encoding: 750/1100µs timing -//! - Variable bit count (35, 51, 57, 63, 64, 81, 82) -//! - Decode-only (no encoder) +//! - PWM encoding: 750µs = 0, 1100µs = 1; preamble uses 2× short then alternating +//! - Variable bit count (35, 51, 57, 63, 64, 81, 82); only 51-bit format parsed for serial/button/counter //! -//! References: -//! - https://phreakerclub.com/72 +//! References: https://phreakerclub.com/72 use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming}; use crate::duration_diff; @@ -19,7 +19,7 @@ const TE_LONG: u32 = 1100; const TE_DELTA: u32 = 160; const MIN_COUNT_BIT: usize = 35; -/// Decoder states +/// Decoder states (matches protopirate's ScherKhanDecoderStep) #[derive(Debug, Clone, Copy, PartialEq)] enum DecoderStep { Reset, @@ -48,10 +48,11 @@ impl ScherKhanDecoder { } } + /// Parse payload by bit count; 51-bit format yields serial/button/counter (matches scher_khan.c) fn parse_data(data: u64, bit_count: usize) -> DecodedSignal { let (serial, btn, cnt) = match bit_count { 51 => { - // MAGIC CODE, Dynamic + // 51-bit "MAGIC CODE" / Dynamic format: serial(28) | button(4) | counter(16) — matches reference let serial = ((data >> 24) & 0xFFFFFF0) as u32 | ((data >> 20) & 0x0F) as u32; let btn = ((data >> 24) & 0x0F) as u8; @@ -196,6 +197,12 @@ impl ProtocolDecoder for ScherKhanDecoder { } fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option> { - None + None // Scher-Khan decode-only in protopirate + } +} + +impl Default for ScherKhanDecoder { + fn default() -> Self { + Self::new() } } diff --git a/src/protocols/star_line.rs b/src/protocols/star_line.rs index 85af52d..ca55a61 100644 --- a/src/protocols/star_line.rs +++ b/src/protocols/star_line.rs @@ -1,12 +1,13 @@ //! Star Line protocol decoder/encoder //! -//! Ported from protopirate's star_line.c +//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/star_line.c`. +//! Decode/encode logic (PWM header, fix/hop split, KeeLoq simple/normal learning) matches reference. //! //! Protocol characteristics: -//! - PWM encoding: 250/500µs timing -//! - 64 bits total +//! - PWM encoding: 250µs = 0, 500µs = 1 +//! - 64 bits total: key_fix (32) + key_hop (32), sent MSB-first (reversed on air) //! - Header: 6 pairs of 1000µs HIGH + 1000µs LOW -//! - KeeLoq encryption (requires manufacturer key) +//! - KeeLoq: fix = serial(24) + button(8); hop encrypted with MF key or normal-learning derived key use super::keeloq_common::{keeloq_decrypt, keeloq_encrypt, keeloq_normal_learning, reverse_key}; use super::keys; @@ -20,7 +21,7 @@ const TE_DELTA: u32 = 120; const MIN_COUNT_BIT: usize = 64; const HEADER_DURATION: u32 = 1000; // te_long * 2 -/// Decoder states +/// Decoder states (matches protopirate's StarLineDecoderStep) #[derive(Debug, Clone, Copy, PartialEq)] enum DecoderStep { Reset, @@ -49,13 +50,14 @@ impl StarLineDecoder { } } - /// Get manufacturer key from global keystore + /// Star Line manufacturer key from keystore (type 20) fn get_mf_key() -> u64 { keys::get_keystore().get_star_line_mf_key() } + /// Parse 64-bit payload: reverse_key then fix(32)/hop(32); KeeLoq decrypt (simple then normal learning) — matches star_line.c fn parse_data(data: u64) -> DecodedSignal { - // Data is stored MSB-first in the air, reverse to get fix|hop + // Data is MSB-first on air; reverse to get fix|hop let reversed = reverse_key(data, MIN_COUNT_BIT); let key_fix = (reversed >> 32) as u32; let key_hop = (reversed & 0xFFFFFFFF) as u32; @@ -246,13 +248,13 @@ impl ProtocolDecoder for StarLineDecoder { let mut signal = Vec::with_capacity(256); - // Header: 6 pairs of LONG*2 HIGH + LONG*2 LOW + // Header: 6 pairs 1000µs HIGH + 1000µs LOW (matches protopirate star_line encode) for _ in 0..6 { signal.push(LevelDuration::new(true, HEADER_DURATION)); signal.push(LevelDuration::new(false, HEADER_DURATION)); } - // Data: 64 bits, MSB first + // Data: 64 bits PWM, MSB first (1=long, 0=short) for bit in (0..64).rev() { if (data >> bit) & 1 == 1 { // Bit 1: LONG HIGH + LONG LOW @@ -268,3 +270,9 @@ impl ProtocolDecoder for StarLineDecoder { Some(signal) } } + +impl Default for StarLineDecoder { + fn default() -> Self { + Self::new() + } +} diff --git a/src/protocols/subaru.rs b/src/protocols/subaru.rs index cc173a3..33832f6 100644 --- a/src/protocols/subaru.rs +++ b/src/protocols/subaru.rs @@ -1,13 +1,13 @@ -//! Subaru protocol decoder +//! Subaru protocol decoder/encoder //! -//! Ported from protopirate's subaru.c +//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/subaru.c`. +//! Decode/encode logic (PWM preamble/gap/sync, short=1/long=0, counter decode) matches reference. //! //! Protocol characteristics: -//! - PWM encoding: short HIGH (800µs) = 1, long HIGH (1600µs) = 0 -//! - 64 bits total -//! - Long preamble of 1600µs pulses -//! - Gap and sync pattern -//! - Complex counter encoding +//! - PWM encoding: 800µs HIGH = 1, 1600µs HIGH = 0; LOW is 800µs after each bit +//! - 64 bits total (8 bytes MSB first: button(4)+serial(24)+counter-related) +//! - Preamble: 79 full 1600µs pairs + 80th HIGH only; then gap 2800µs, sync 2800µs HIGH + 1600µs LOW +//! - Complex counter encoding (decode_counter) from bytes 4–7 use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal}; use crate::radio::demodulator::LevelDuration; @@ -22,7 +22,7 @@ const MIN_COUNT_BIT: usize = 64; const GAP_US: u32 = 2800; const SYNC_US: u32 = 2800; -/// Decoder states +/// Decoder states (matches protopirate's SubaruDecoderStep) #[derive(Debug, Clone, Copy, PartialEq)] enum DecoderStep { Reset, @@ -67,7 +67,7 @@ impl SubaruDecoder { } } - /// Decode the counter from the complex Subaru encoding + /// Decode 16-bit counter from bytes 4–7 (matches subaru.c complex encoding) fn decode_counter(kb: &[u8; 8]) -> u16 { let mut lo: u8 = 0; if (kb[4] & 0x40) == 0 { lo |= 0x01; } @@ -115,9 +115,7 @@ impl SubaruDecoder { ((hi as u16) << 8) | (lo as u16) } - /// Add a level+duration to the signal, merging with the previous entry - /// if it has the same level. This prevents consecutive same-level pulses - /// which would silently merge during HackRF transmission. + /// Append level+duration, merging with previous if same level (prevents HackRF from merging consecutive same-level pulses) fn add_level(signal: &mut Vec, level: bool, duration: u32) { if let Some(last) = signal.last_mut() { if last.level == level { @@ -128,15 +126,13 @@ impl SubaruDecoder { signal.push(LevelDuration::new(level, duration)); } - /// Process the decoded data + /// Build DecodedSignal from 8-byte buffer: serial(bytes 1–3), button(byte0 low nibble), counter(decode_counter) — matches subaru.c fn process_data(&self) -> Option { if self.bit_count < 64 { return None; } let b = &self.data; - - // Build 64-bit key let key = ((b[0] as u64) << 56) | ((b[1] as u64) << 48) | ((b[2] as u64) << 40) | ((b[3] as u64) << 32) | ((b[4] as u64) << 24) | ((b[5] as u64) << 16) | @@ -297,37 +293,24 @@ impl ProtocolDecoder for SubaruDecoder { let key = decoded.data; let mut signal = Vec::with_capacity(512); - // Generate 3 bursts. - // - // IMPORTANT: Uses add_level() to merge adjacent same-level pulses. - // The HackRF transmitter generates IQ samples sequentially from the - // LevelDuration list, so consecutive same-level pairs silently merge - // and corrupt timing. For example, the last preamble LOW (1600µs) + - // gap LOW (2800µs) would become a single 4400µs LOW without merging. + // 3 bursts; add_level() merges same-level pulses for correct HackRF timing (matches protopirate subaru encode) for burst in 0..3 { if burst > 0 { - // Inter-burst silence Self::add_level(&mut signal, false, 25000); } - // Preamble: 79 full pairs + 80th HIGH only. - // The gap LOW replaces the 80th preamble LOW. + // Preamble: 79 full 1600µs pairs + 80th HIGH only; gap replaces 80th LOW for i in 0..80 { Self::add_level(&mut signal, true, TE_LONG); if i < 79 { Self::add_level(&mut signal, false, TE_LONG); } } - - // Gap (replaces the 80th preamble LOW) Self::add_level(&mut signal, false, GAP_US); - - // Sync Self::add_level(&mut signal, true, SYNC_US); Self::add_level(&mut signal, false, TE_LONG); - // Data: 64 bits (MSB first) - // Short HIGH = 1, Long HIGH = 0 + // Data: 64 bits MSB first; short HIGH = 1, long HIGH = 0; LOW = 800µs after each for bit in (0..64).rev() { if (key >> bit) & 1 == 1 { Self::add_level(&mut signal, true, TE_SHORT); @@ -337,10 +320,15 @@ impl ProtocolDecoder for SubaruDecoder { Self::add_level(&mut signal, false, TE_SHORT); } - // End-of-burst gap (extends the last data LOW) Self::add_level(&mut signal, false, TE_LONG * 2); } Some(signal) } } + +impl Default for SubaruDecoder { + fn default() -> Self { + Self::new() + } +} diff --git a/src/protocols/suzuki.rs b/src/protocols/suzuki.rs index 6439dfc..d33a526 100644 --- a/src/protocols/suzuki.rs +++ b/src/protocols/suzuki.rs @@ -1,12 +1,13 @@ //! Suzuki protocol decoder/encoder //! -//! Ported from protopirate's suzuki.c +//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/suzuki.c`. +//! Decode/encode logic (preamble count 350, gap 2000µs, short=0/long=1, field layout) matches reference. //! //! Protocol characteristics: -//! - PWM encoding: 250/500µs timing -//! - 64 bits total -//! - 350 preamble pairs -//! - 2000µs gap between transmissions +//! - PWM encoding: 250µs HIGH = 0, 500µs HIGH = 1; LOW 250µs after each bit +//! - 64 bits total; preamble: 300+ short LOW pulses then long HIGH starts data +//! - 350 preamble pairs (SHORT HIGH / SHORT LOW); 2000µs gap at end +//! - Field layout: serial = (data_high&0xFFF)<<16 | data_low>>16; btn = (data_low>>12)&0xF; cnt = (data_high<<4)>>16 use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming}; use crate::duration_diff; @@ -20,7 +21,7 @@ const PREAMBLE_COUNT: u16 = 350; const GAP_TIME: u32 = 2000; const GAP_DELTA: u32 = 399; -/// Decoder states +/// Decoder states (matches protopirate's SuzukiDecoderStep) #[derive(Debug, Clone, Copy, PartialEq)] enum DecoderStep { Reset, @@ -53,10 +54,11 @@ impl SuzukiDecoder { self.decode_count_bit += 1; } + /// Parse 64-bit data (matches suzuki.c: serial, btn, cnt layout) fn parse_data(data: u64) -> DecodedSignal { let data_high = (data >> 32) as u32; let data_low = data as u32; - + // Reference: instance->generic.serial = ((data_high & 0xFFF) << 16) | (data_low >> 16); etc. let serial = ((data_high & 0xFFF) << 16) | (data_low >> 16); let btn = ((data_low >> 12) & 0xF) as u8; let cnt = ((data_high << 4) >> 16) as u16; @@ -178,14 +180,12 @@ impl ProtocolDecoder for SuzukiDecoder { let mut signal = Vec::with_capacity(1024); - // Preamble: SHORT HIGH / SHORT LOW pairs + // Preamble + data + gap (matches subghz_protocol_encoder_suzuki_get_upload in suzuki.c) for _ in 0..PREAMBLE_COUNT { signal.push(LevelDuration::new(true, TE_SHORT)); signal.push(LevelDuration::new(false, TE_SHORT)); } - - // Data: 64 bits, MSB first - // SHORT HIGH (~250µs) = 0, LONG HIGH (~500µs) = 1 + // Data: 64 bits MSB first; SHORT HIGH = 0, LONG HIGH = 1; LOW = 250µs after each for bit in (0..64).rev() { if (data >> bit) & 1 == 1 { signal.push(LevelDuration::new(true, TE_LONG)); @@ -195,9 +195,14 @@ impl ProtocolDecoder for SuzukiDecoder { signal.push(LevelDuration::new(false, TE_SHORT)); } - // End gap signal.push(LevelDuration::new(false, GAP_TIME)); Some(signal) } } + +impl Default for SuzukiDecoder { + fn default() -> Self { + Self::new() + } +} diff --git a/src/protocols/vag.rs b/src/protocols/vag.rs index 9a4c09b..5eca93f 100644 --- a/src/protocols/vag.rs +++ b/src/protocols/vag.rs @@ -1,17 +1,15 @@ //! VAG (VW/Audi/Seat/Skoda) protocol decoder/encoder //! -//! Ported from protopirate's vag.c +//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/vag.c`. +//! Decode/encode logic (steps, Type 1/2/3/4, AUT64/TEA, dispatch, gap 6000µs) matches reference. +//! Intentional differences for HackRF: TE_DELTA 150 (ref 80), TE_DELTA_12 120 (ref 79); +//! Preamble2/Sync2C accept shorter LOW pulses for asymmetric software demodulation. //! //! Protocol characteristics: -//! - Manchester encoding -//! - Type 1/2: 300/600µs timing (te_short=300), 80 bits, AUT64 or TEA encryption -//! - Type 3/4: 500/1000µs timing (te_short=500), 80 bits, AUT64 encryption -//! - 433.92 MHz and 434.42 MHz -//! - Four sub-types: -//! Type 1: AUT64 encrypted (300µs Manchester, dispatch 0x2A/0x1C/0x46) -//! Type 2: TEA encrypted (300µs Manchester, dispatch 0x2A/0x1C/0x46) -//! Type 3: AUT64 encrypted (500µs Manchester, auto-detect key) -//! Type 4: AUT64 encrypted (500µs Manchester, key 2) +//! - Manchester encoding; 80 bits (key1 64 + key2 16) +//! - Type 1/2: 300/600µs, prefix 0x2F3F (AUT64) / 0x2F1C (TEA), dispatch 0x2A/0x1C/0x46 +//! - Type 3/4: 500/1000µs, preamble 41+ pairs, sync 1000µs + 3×750µs, dispatch 0x2B/0x1D/0x47 +//! - End-of-data gap 6000µs (accept within 4000µs); keys from keystore (VAG raw 64 bytes) use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal}; use super::aut64; @@ -34,7 +32,7 @@ const TE_DELTA_12: u32 = 120; // Wider tolerance for HackRF (was 79) const TEA_DELTA: u32 = 0x9E3779B9; const TEA_ROUNDS: usize = 32; -/// TEA key schedule for VAG +/// TEA key schedule for VAG (matches vag.c vag_tea_key_schedule) static TEA_KEY_SCHEDULE: [u32; 4] = [0x0B46502D, 0x5E253718, 0x2BF93A19, 0x622C1206]; /// Manchester states @@ -56,17 +54,17 @@ enum ManchesterEvent { Reset, } -/// Decoder states +/// Decoder states (matches protopirate's VAGDecoderStep) #[derive(Debug, Clone, Copy, PartialEq)] enum DecoderStep { Reset, - Preamble1, // Type 1/2: ~300µs preamble - Data1, // Type 1/2: Manchester data - Preamble2, // Type 3/4: ~500µs preamble - Sync2A, // Type 3/4: sync pattern step A - Sync2B, // Type 3/4: sync pattern step B (750µs) - Sync2C, // Type 3/4: sync pattern step C (750µs) - Data2, // Type 3/4: Manchester data + Preamble1, + Data1, + Preamble2, + Sync2A, + Sync2B, + Sync2C, + Data2, } /// VAG sub-type @@ -182,7 +180,7 @@ impl VagDecoder { self.bit_count += 1; } - /// TEA decrypt + /// TEA decrypt (matches vag.c vag_tea_decrypt) fn tea_decrypt(v0: &mut u32, v1: &mut u32, key_schedule: &[u32; 4]) { let mut sum = TEA_DELTA.wrapping_mul(TEA_ROUNDS as u32); for _ in 0..TEA_ROUNDS { @@ -198,7 +196,7 @@ impl VagDecoder { } } - /// TEA encrypt + /// TEA encrypt (matches vag.c vag_tea_encrypt) fn tea_encrypt(v0: &mut u32, v1: &mut u32, key_schedule: &[u32; 4]) { let mut sum: u32 = 0; for _ in 0..TEA_ROUNDS { @@ -278,7 +276,7 @@ impl VagDecoder { } } - /// Parse the collected data and attempt decryption + /// Parse key1/key2 and decrypt by type (AUT64/TEA, dispatch); matches vag.c vag_parse_data fn parse_data(&mut self) { self.decrypted = false; self.serial = 0; @@ -950,7 +948,7 @@ impl ProtocolDecoder for VagDecoder { } } - // Check for gap (end of data) + // End-of-data gap: 6000µs, accept within 4000µs (matches vag.c check_gap1_data) if !level { let gap_diff = if duration > 6000 { duration - 6000 @@ -1145,3 +1143,9 @@ impl ProtocolDecoder for VagDecoder { self.encode_signal(decoded) } } + +impl Default for VagDecoder { + fn default() -> Self { + Self::new() + } +} diff --git a/src/radio/demodulator.rs b/src/radio/demodulator.rs index 9eaecc7..ae4269d 100644 --- a/src/radio/demodulator.rs +++ b/src/radio/demodulator.rs @@ -1,5 +1,10 @@ //! AM/OOK demodulator for extracting level+duration pairs from raw IQ samples. //! +//! KAT uses **AM (envelope) detection only**; FM/2FSK is not demodulated. Protocols +//! are tagged with AM/FM/Both (from ProtoPirate) for display and export. FM protocols +//! may still decode when the received signal is strong enough to produce a usable +//! envelope; proper FM support would require a separate demodulator path. +//! //! This demodulator converts raw IQ samples into a stream of (level, duration_us) pairs //! that can be processed by protocol decoders, similar to how the Flipper Zero SubGHz //! system works. diff --git a/src/storage.rs b/src/storage.rs index 43fb240..251a3c6 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -29,6 +29,8 @@ pub struct Config { pub export_directory: PathBuf, /// Maximum captures to keep in memory during a session pub max_captures: usize, + /// If off, only successfully decoded signals are added to the list. If on, unknown signals are also shown. + pub research_mode: bool, // [radio] /// Default frequency in Hz @@ -54,6 +56,7 @@ impl Config { Self { export_directory: config_dir.join("exports"), max_captures: 100, + research_mode: false, default_frequency: 433_920_000, default_lna_gain: 24, default_vga_gain: 20, @@ -83,6 +86,12 @@ impl Config { .map(|v| v as usize) .unwrap_or(defaults.max_captures); + let research_mode = ini + .getbool("general", "research_mode") + .ok() + .flatten() + .unwrap_or(defaults.research_mode); + let default_frequency = ini .getuint("radio", "default_frequency") .ok() @@ -123,6 +132,7 @@ impl Config { Ok(Self { export_directory, max_captures, + research_mode, default_frequency, default_lna_gain, default_vga_gain, @@ -154,6 +164,10 @@ export_directory = {export_dir} ; signals (.fob / .sub) survive in the exports folder. max_captures = {max_captures} +; When off, only successfully decoded signals appear in the list. +; When on, unknown (unidentified) signals are also shown (research mode). +research_mode = {research_mode} + [radio] ; Default receive frequency in Hz ({freq_mhz:.2} MHz) ; Common keyfob frequencies: 315000000, 433920000, 868350000 @@ -179,6 +193,7 @@ include_raw_pairs = {raw_pairs} path = path.display(), export_dir = export_str, max_captures = self.max_captures, + research_mode = self.research_mode, freq_mhz = freq_mhz, frequency = self.default_frequency, lna = self.default_lna_gain, @@ -342,7 +357,8 @@ impl Storage { &self.config.export_directory } - /// Get the keystore directory path (`~/.config/KAT/keystore`) + /// Get the keystore directory path (`~/.config/KAT/keystore`). Kept for optional file-based override. + #[allow(dead_code)] pub fn keystore_dir(&self) -> PathBuf { self.config_dir.join("keystore") } diff --git a/src/ui/captures_list.rs b/src/ui/captures_list.rs index 31082ce..26e34c8 100644 --- a/src/ui/captures_list.rs +++ b/src/ui/captures_list.rs @@ -158,12 +158,14 @@ fn render_detail_panel(frame: &mut Frame, area: Rect, app: &App) { Span::styled(make, value_style), ])); - // Row 2: Frequency + Modulation + Encryption + // Row 2: Frequency + Encoding (Mod) + RF (AM/FM) + Encryption left_lines.push(Line::from(vec![ Span::styled(" Freq: ", label_style), Span::styled(capture.frequency_mhz(), value_style), Span::styled(" Mod: ", label_style), Span::styled(capture.modulation().to_string(), value_style), + Span::styled(" RF: ", label_style), + Span::styled(capture.rf_modulation().to_string(), value_style), Span::styled(" Enc: ", label_style), Span::styled(capture.encryption_type(), value_style), ]));