Protocol updates
This commit is contained in:
@@ -1,21 +1,23 @@
|
|||||||
# KAT — Keyfob Analysis Toolkit
|
# 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
|
## Features
|
||||||
|
|
||||||
- **Real-time capture** — receive and demodulate AM/OOK keyfob signals at configurable frequencies
|
- **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 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
|
- **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
|
||||||
- **Rich signal detail** — modulation type, encryption method, serial, counter, key data, CRC, frequency, and raw level/duration pairs
|
- **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
|
- **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
|
- **Import support** — load `.fob` files with automatic v1/v2 format detection
|
||||||
- **Persistent storage** — automatic capture saving to `~/.config/kat/captures/`
|
- **Research mode** — config option to show unknown (unidentified) signals in addition to successfully decoded ones
|
||||||
- **INI configuration** — human-readable config at `~/.config/kat/config.ini` (auto-created with comments on first run)
|
- **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
|
- **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
|
## Requirements
|
||||||
|
|
||||||
@@ -94,47 +96,54 @@ Press `Enter` on a capture to open the action menu:
|
|||||||
|
|
||||||
### Fob Export
|
### 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
|
- **File** — output filename (extension added by format)
|
||||||
2. **Make** — manufacturer (auto-suggested from protocol)
|
- **Year** — vehicle model year
|
||||||
3. **Model** — vehicle model
|
- **Make** — manufacturer (auto-suggested from protocol)
|
||||||
4. **Color** — vehicle color
|
- **Model** — vehicle model
|
||||||
5. **Trim** — trim level / package
|
- **Region** — region/market
|
||||||
6. **Notes** — free-form notes
|
- **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
|
```json
|
||||||
{
|
{
|
||||||
"version": "2.0",
|
"version": "2.0",
|
||||||
"format": "KAT Fob Signal",
|
"format": "kat-fob",
|
||||||
"signal": {
|
"signal": {
|
||||||
"protocol": "Kia V3/V4",
|
"protocol": "Kia V3/V4",
|
||||||
|
"frequency": 433920000,
|
||||||
|
"frequency_mhz": "433.92MHz",
|
||||||
"modulation": "PWM",
|
"modulation": "PWM",
|
||||||
|
"rf_modulation": "AM/FM",
|
||||||
"encryption": "KeeLoq",
|
"encryption": "KeeLoq",
|
||||||
"frequency_mhz": 433.92,
|
"data_bits": 64,
|
||||||
|
"data_hex": "...",
|
||||||
"serial": "0x1A2B3C",
|
"serial": "0x1A2B3C",
|
||||||
"key": "0xDEADBEEF...",
|
"key": "0x...",
|
||||||
"button": 1,
|
"button": 1,
|
||||||
|
"button_name": "Lock",
|
||||||
"counter": 1234,
|
"counter": 1234,
|
||||||
|
"crc_valid": true,
|
||||||
"encoder_capable": true
|
"encoder_capable": true
|
||||||
},
|
},
|
||||||
"vehicle": {
|
"vehicle": {
|
||||||
"year": "2023",
|
"year": 2023,
|
||||||
"make": "Kia",
|
"make": "Kia",
|
||||||
"model": "Sportage",
|
"model": "Sportage",
|
||||||
"color": "White",
|
"region": "",
|
||||||
"trim": "EX",
|
|
||||||
"notes": ""
|
"notes": ""
|
||||||
},
|
},
|
||||||
"capture": {
|
"capture": {
|
||||||
"timestamp": "2026-02-07T12:00:00Z",
|
"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
|
### VIM-Style Commands
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
@@ -151,61 +160,74 @@ The exported `.fob` file is a versioned JSON document (v2.0) containing:
|
|||||||
|
|
||||||
## Configuration
|
## 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)
|
├── 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
|
```ini
|
||||||
[radio]
|
[general]
|
||||||
frequency = 433920000 # Default receive frequency in Hz
|
export_directory = ~/.config/KAT/exports
|
||||||
lna_gain = 32 # LNA gain (0-40 dB, step 8)
|
max_captures = 100
|
||||||
vga_gain = 40 # VGA gain (0-62 dB, step 2)
|
research_mode = false
|
||||||
amp_enable = true # RF amplifier on/off
|
|
||||||
|
|
||||||
[storage]
|
[radio]
|
||||||
export_directory = ~/.config/kat/exports # Where .fob/.sub files are saved
|
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
|
## Supported Protocols
|
||||||
|
|
||||||
| Protocol | Encoding | Encryption | Frequency |
|
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.
|
||||||
|---|---|---|---|
|
|
||||||
| 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 |
|
|
||||||
|
|
||||||
### 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
|
### Cryptographic modules
|
||||||
- **AUT64** — 12-round block cipher for VAG type 1/3/4 signals
|
|
||||||
- **Key Store** — global thread-safe key management for manufacturer keys (KIA, VAG)
|
- **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
|
### 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
|
- **Envelope detection** — magnitude from I/Q for AM
|
||||||
- **Schmitt trigger hysteresis** — prevents noise-induced chattering at threshold crossings
|
- **Adaptive threshold** — transition-based updates to handle varying signal levels
|
||||||
- **Fast threshold convergence** — α=0.3 transition-based updates for rapid adaptation after silence periods
|
- **Exponential moving average** — magnitude smoothing
|
||||||
- **Debounce filtering** — 40µs minimum pulse width to reject noise spikes
|
- **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
|
## Project Structure
|
||||||
|
|
||||||
@@ -213,42 +235,36 @@ The AM/OOK demodulator uses an adaptive threshold with transition-based updates
|
|||||||
src/
|
src/
|
||||||
├── main.rs # Entry point, event loop, key handling
|
├── main.rs # Entry point, event loop, key handling
|
||||||
├── app.rs # Application state, radio events, signal actions
|
├── app.rs # Application state, radio events, signal actions
|
||||||
├── capture.rs # Capture data structure, modulation/encryption helpers
|
├── capture.rs # Capture data, encoding/RF modulation, encryption helpers
|
||||||
├── storage.rs # Config management, capture persistence, INI read/write
|
├── 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/
|
├── export/
|
||||||
│ ├── fob.rs # .fob JSON export/import (v1 + v2 format support)
|
│ ├── fob.rs # .fob JSON export/import (v1 + v2, rf_modulation)
|
||||||
│ └── flipper.rs # Flipper Zero .sub export
|
│ └── flipper.rs # Flipper Zero .sub export
|
||||||
├── protocols/
|
├── protocols/
|
||||||
│ ├── mod.rs # Protocol registry, decoder trait, duration_diff macro
|
│ ├── mod.rs # Protocol registry, decoder trait, duration_diff macro
|
||||||
│ ├── common.rs # Shared CRC, bit helpers, button codes
|
│ ├── common.rs # Shared CRC, bit helpers, button codes
|
||||||
│ ├── keeloq_common.rs # KeeLoq cipher + learning key algorithms
|
│ ├── keeloq_common.rs # KeeLoq cipher + learning key algorithms
|
||||||
│ ├── aut64.rs # AUT64 block cipher implementation
|
│ ├── aut64.rs # AUT64 block cipher (VAG)
|
||||||
│ ├── keys.rs # Global key store (KIA, VAG key management)
|
│ ├── keys.rs # Key loading (embedded + optional file), KIA/VAG
|
||||||
│ ├── kia_v0.rs # Kia V0 decoder
|
│ ├── kia_v0..kia_v6.rs
|
||||||
│ ├── kia_v1.rs # Kia V1 decoder (Manchester)
|
│ ├── ford_v0.rs, fiat_v0.rs, subaru.rs, suzuki.rs
|
||||||
│ ├── 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)
|
|
||||||
│ ├── vag.rs # VAG decoder/encoder (4 sub-types)
|
│ ├── vag.rs # VAG decoder/encoder (4 sub-types)
|
||||||
│ ├── scher_khan.rs # Scher-Khan decoder
|
│ ├── scher_khan.rs, star_line.rs, psa.rs
|
||||||
│ ├── star_line.rs # Star Line decoder
|
│ └── ...
|
||||||
│ └── psa.rs # PSA decoder
|
|
||||||
├── radio/
|
├── radio/
|
||||||
│ ├── hackrf.rs # HackRF One device control (RX/TX)
|
│ ├── hackrf.rs # HackRF device control (RX/TX)
|
||||||
│ ├── demodulator.rs # AM/OOK demodulator (IQ -> level/duration pairs)
|
│ ├── demodulator.rs # AM/OOK demodulator (IQ -> level/duration)
|
||||||
│ └── modulator.rs # Signal modulator (level/duration -> TX waveform)
|
│ └── modulator.rs # Level/duration -> TX waveform
|
||||||
└── ui/
|
└── ui/
|
||||||
├── layout.rs # Main TUI layout, fob metadata form overlay
|
├── layout.rs # TUI layout, fob metadata form
|
||||||
├── captures_list.rs # Captures table + signal detail panel
|
├── captures_list.rs # Captures table + detail (protocol, mod, RF, enc)
|
||||||
├── signal_menu.rs # Signal action popup menu
|
├── signal_menu.rs # Signal action menu
|
||||||
├── settings_menu.rs # Radio settings popup menu
|
├── settings_menu.rs # Radio settings (Freq, LNA, VGA, AMP)
|
||||||
├── command.rs # VIM-style command line renderer
|
├── command.rs # VIM-style command line
|
||||||
└── status_bar.rs # Bottom status bar (radio state, frequency, gains)
|
└── status_bar.rs # Status bar
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
+9
-8
@@ -243,10 +243,8 @@ impl App {
|
|||||||
pub fn new() -> Result<Self> {
|
pub fn new() -> Result<Self> {
|
||||||
let storage = Storage::new()?;
|
let storage = Storage::new()?;
|
||||||
|
|
||||||
// ── Load protocol encryption keys from keystore ──────────────────
|
// ── Load protocol encryption keys from embedded keystore ─────────
|
||||||
let keystore_dir = storage.keystore_dir();
|
crate::protocols::keys::load_keystore_from_embedded();
|
||||||
crate::protocols::keys::create_default_keystore(&keystore_dir);
|
|
||||||
crate::protocols::keys::load_keystore_from_dir(&keystore_dir);
|
|
||||||
|
|
||||||
let protocols = ProtocolRegistry::new();
|
let protocols = ProtocolRegistry::new();
|
||||||
let (radio_event_tx, radio_event_rx) = mpsc::channel();
|
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() {
|
while let Ok(event) = self.radio_event_rx.try_recv() {
|
||||||
match event {
|
match event {
|
||||||
RadioEvent::SignalCaptured(mut capture) => {
|
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
|
// Convert stored pairs to the format protocols expect
|
||||||
let pairs: Vec<crate::radio::LevelDuration> = capture.raw_pairs
|
let pairs: Vec<crate::radio::LevelDuration> = capture.raw_pairs
|
||||||
.iter()
|
.iter()
|
||||||
@@ -731,6 +725,11 @@ impl App {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
// Captures are in-memory only — no auto-save to disk.
|
||||||
// Use Export (.fob / .sub) to persist a signal.
|
// Use Export (.fob / .sub) to persist a signal.
|
||||||
self.captures.push(capture);
|
self.captures.push(capture);
|
||||||
@@ -742,6 +741,8 @@ impl App {
|
|||||||
|
|
||||||
self.status_message = Some("New signal captured".to_string());
|
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) => {
|
RadioEvent::Error(e) => {
|
||||||
self.last_error = Some(e);
|
self.last_error = Some(e);
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-1
@@ -60,7 +60,7 @@ pub struct Capture {
|
|||||||
pub status: CaptureStatus,
|
pub status: CaptureStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Modulation type used by protocol
|
/// Modulation type used by protocol (encoding: PWM vs Manchester)
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum ModulationType {
|
pub enum ModulationType {
|
||||||
Unknown,
|
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 {
|
impl Capture {
|
||||||
/// Create a new capture from level+duration pairs
|
/// Create a new capture from level+duration pairs
|
||||||
pub fn from_pairs(id: u32, frequency: u32, pairs: Vec<StoredLevelDuration>) -> Self {
|
pub fn from_pairs(id: u32, frequency: u32, pairs: Vec<StoredLevelDuration>) -> 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
|
/// Get the encryption/encoding type based on the protocol
|
||||||
pub fn encryption_type(&self) -> &'static str {
|
pub fn encryption_type(&self) -> &'static str {
|
||||||
match self.protocol_name() {
|
match self.protocol_name() {
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ pub struct FobSignalInfo {
|
|||||||
pub frequency: u32,
|
pub frequency: u32,
|
||||||
pub frequency_mhz: String,
|
pub frequency_mhz: String,
|
||||||
pub modulation: 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<String>,
|
||||||
pub encryption: String,
|
pub encryption: String,
|
||||||
pub data_bits: usize,
|
pub data_bits: usize,
|
||||||
pub data_hex: String,
|
pub data_hex: String,
|
||||||
@@ -140,6 +143,10 @@ pub fn export_fob(
|
|||||||
frequency: capture.frequency,
|
frequency: capture.frequency,
|
||||||
frequency_mhz: capture.frequency_mhz(),
|
frequency_mhz: capture.frequency_mhz(),
|
||||||
modulation: capture.modulation().to_string(),
|
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(),
|
encryption: capture.encryption_type().to_string(),
|
||||||
data_bits: capture.data_count_bit,
|
data_bits: capture.data_count_bit,
|
||||||
data_hex: capture.data_hex(),
|
data_hex: capture.data_hex(),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
];
|
||||||
@@ -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<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the embedded keystore blob. Returns KIA/Star Line entries and VAG raw bytes.
|
||||||
|
pub fn parse_blob(blob: &[u8]) -> Option<ParsedKeystore> {
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
mod app;
|
mod app;
|
||||||
mod capture;
|
mod capture;
|
||||||
mod export;
|
mod export;
|
||||||
|
mod keystore;
|
||||||
mod protocols;
|
mod protocols;
|
||||||
mod radio;
|
mod radio;
|
||||||
mod storage;
|
mod storage;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
//! AUT64 block cipher implementation
|
//! 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
|
//! AUT64 algorithm: 12 rounds, 8-byte block/key size.
|
||||||
//! Based on: Reference AUT64 implementation
|
|
||||||
//! See: https://www.usenix.org/system/files/conference/usenixsecurity16/sec16_paper_garcia.pdf
|
//! See: https://www.usenix.org/system/files/conference/usenixsecurity16/sec16_paper_garcia.pdf
|
||||||
|
|
||||||
pub const AUT64_NUM_ROUNDS: usize = 12;
|
pub const AUT64_NUM_ROUNDS: usize = 12;
|
||||||
@@ -265,7 +265,7 @@ pub fn aut64_pack(src: &Aut64Key) -> [u8; AUT64_KEY_STRUCT_PACKED_SIZE] {
|
|||||||
dest
|
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)]
|
#[allow(dead_code)]
|
||||||
pub fn aut64_unpack(src: &[u8]) -> Aut64Key {
|
pub fn aut64_unpack(src: &[u8]) -> Aut64Key {
|
||||||
let mut dest = Aut64Key::default();
|
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;
|
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() {
|
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) {
|
for i in 0..(dest.sbox.len() / 2) {
|
||||||
|
|||||||
+12
-5
@@ -1,4 +1,11 @@
|
|||||||
//! Common utilities for protocol implementations.
|
//! 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
|
/// Decoded signal information
|
||||||
#[derive(Debug, Clone)]
|
#[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
|
/// # Arguments
|
||||||
/// * `data` - Data bytes to calculate CRC over
|
/// * `data` - Data bytes to calculate CRC over
|
||||||
/// * `poly` - CRC polynomial
|
/// * `poly` - CRC polynomial (e.g. 0x7F for Kia)
|
||||||
/// * `init` - Initial CRC value
|
/// * `init` - Initial CRC value (e.g. 0x00)
|
||||||
pub fn crc8(data: &[u8], poly: u8, init: u8) -> u8 {
|
pub fn crc8(data: &[u8], poly: u8, init: u8) -> u8 {
|
||||||
let mut crc = init;
|
let mut crc = init;
|
||||||
for &byte in data {
|
for &byte in data {
|
||||||
@@ -55,12 +62,12 @@ pub fn crc8(data: &[u8], poly: u8, init: u8) -> u8 {
|
|||||||
crc
|
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 {
|
pub fn crc8_kia(data: &[u8]) -> u8 {
|
||||||
crc8(data, 0x7F, 0x00)
|
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]
|
#[inline]
|
||||||
pub fn add_bit(data: &mut u64, count: &mut usize, bit: bool) {
|
pub fn add_bit(data: &mut u64, count: &mut usize, bit: bool) {
|
||||||
*data = (*data << 1) | (bit as u64);
|
*data = (*data << 1) | (bit as u64);
|
||||||
|
|||||||
+72
-52
@@ -1,11 +1,12 @@
|
|||||||
//! Fiat V0 protocol decoder/encoder
|
//! 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:
|
//! Protocol characteristics:
|
||||||
//! - Differential Manchester encoding: 200/400µs timing
|
//! - Differential Manchester encoding: 200/400µs timing
|
||||||
//! - 64-bit data (cnt:32 | serial:32) + 6-bit button
|
//! - 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 super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||||
use crate::duration_diff;
|
use crate::duration_diff;
|
||||||
@@ -16,21 +17,21 @@ const TE_LONG: u32 = 400;
|
|||||||
const TE_DELTA: u32 = 100;
|
const TE_DELTA: u32 = 100;
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
const MIN_COUNT_BIT: usize = 64;
|
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 GAP_US: u32 = 800;
|
||||||
const TOTAL_BURSTS: u8 = 3;
|
const TOTAL_BURSTS: u8 = 3;
|
||||||
const INTER_BURST_GAP: u32 = 25000;
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
enum ManchesterState {
|
enum ManchesterState {
|
||||||
Mid0,
|
Mid0 = 0,
|
||||||
Mid1,
|
Mid1 = 1,
|
||||||
Start0,
|
Start0 = 2,
|
||||||
Start1,
|
Start1 = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decoder states
|
/// Decoder states (matches protopirate's FiatV0DecoderStep)
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
enum DecoderStep {
|
enum DecoderStep {
|
||||||
Reset,
|
Reset,
|
||||||
@@ -68,37 +69,46 @@ impl FiatV0Decoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Manchester advance - returns decoded bit or None
|
/// Manchester state machine (same as Ford V0 / Flipper manchester_decoder).
|
||||||
fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option<bool> {
|
/// Event: 0=ShortLow, 1=ShortHigh, 2=LongLow, 3=LongHigh.
|
||||||
let event = match (is_short, is_high) {
|
fn manchester_advance(&mut self, event: u8) -> Option<bool> {
|
||||||
(true, true) => 0,
|
let (new_state, emit) = match (self.manchester_state, event) {
|
||||||
(true, false) => 1,
|
(ManchesterState::Mid0, 0) => (ManchesterState::Mid0, false),
|
||||||
(false, true) => 2,
|
(ManchesterState::Mid0, 1) => (ManchesterState::Start1, true),
|
||||||
(false, false) => 3,
|
(ManchesterState::Mid0, 2) => (ManchesterState::Mid0, false),
|
||||||
};
|
(ManchesterState::Mid0, 3) => (ManchesterState::Mid1, true),
|
||||||
|
|
||||||
let (new_state, output) = match (self.manchester_state, event) {
|
(ManchesterState::Mid1, 0) => (ManchesterState::Start0, true),
|
||||||
(ManchesterState::Mid0, 0) | (ManchesterState::Mid1, 0) => {
|
(ManchesterState::Mid1, 1) => (ManchesterState::Mid1, false),
|
||||||
(ManchesterState::Start1, None)
|
(ManchesterState::Mid1, 2) => (ManchesterState::Mid0, true),
|
||||||
}
|
(ManchesterState::Mid1, 3) => (ManchesterState::Mid1, false),
|
||||||
(ManchesterState::Mid0, 1) | (ManchesterState::Mid1, 1) => {
|
|
||||||
(ManchesterState::Start0, None)
|
(ManchesterState::Start0, 0) => (ManchesterState::Mid0, false),
|
||||||
}
|
(ManchesterState::Start0, 1) => (ManchesterState::Mid0, false),
|
||||||
(ManchesterState::Start1, 1) => (ManchesterState::Mid1, Some(true)),
|
(ManchesterState::Start0, 2) => (ManchesterState::Mid0, false),
|
||||||
(ManchesterState::Start1, 3) => (ManchesterState::Start0, Some(true)),
|
(ManchesterState::Start0, 3) => (ManchesterState::Mid1, false),
|
||||||
(ManchesterState::Start0, 0) => (ManchesterState::Mid0, Some(false)),
|
|
||||||
(ManchesterState::Start0, 2) => (ManchesterState::Start1, Some(false)),
|
(ManchesterState::Start1, 0) => (ManchesterState::Mid0, false),
|
||||||
_ => (ManchesterState::Mid1, None),
|
(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;
|
self.manchester_state = new_state;
|
||||||
output
|
if emit {
|
||||||
|
Some((event & 1) == 1)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn manchester_reset(&mut self) {
|
fn manchester_reset(&mut self) {
|
||||||
self.manchester_state = ManchesterState::Mid1;
|
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) {
|
fn add_manchester_bit(&mut self, bit: bool) {
|
||||||
let new_bit = if bit { 1u32 } else { 0u32 };
|
let new_bit = if bit { 1u32 } else { 0u32 };
|
||||||
let carry = (self.data_low >> 31) & 1;
|
let carry = (self.data_low >> 31) & 1;
|
||||||
@@ -162,6 +172,7 @@ impl ProtocolDecoder for FiatV0Decoder {
|
|||||||
|
|
||||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||||
match self.step {
|
match self.step {
|
||||||
|
// Reset: wait for short HIGH (matches reference)
|
||||||
DecoderStep::Reset => {
|
DecoderStep::Reset => {
|
||||||
if !level {
|
if !level {
|
||||||
return None;
|
return None;
|
||||||
@@ -177,35 +188,49 @@ impl ProtocolDecoder for FiatV0Decoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Preamble: only process LOW pulses (reference: if(level) return). Count short LOWs; gap = 800µs LOW.
|
||||||
DecoderStep::Preamble => {
|
DecoderStep::Preamble => {
|
||||||
// Count short pulses in preamble, look for gap
|
if level {
|
||||||
if duration_diff!(duration, TE_SHORT) < TE_DELTA {
|
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.preamble_count += 1;
|
||||||
self.te_last = duration;
|
self.te_last = duration;
|
||||||
} else if self.preamble_count >= PREAMBLE_PAIRS {
|
} else {
|
||||||
// Check for gap
|
if self.preamble_count >= PREAMBLE_PAIRS && gap_ok {
|
||||||
if duration_diff!(duration, GAP_US) < TE_DELTA {
|
|
||||||
self.step = DecoderStep::Data;
|
self.step = DecoderStep::Data;
|
||||||
self.preamble_count = 0;
|
self.preamble_count = 0;
|
||||||
self.data_low = 0;
|
self.data_low = 0;
|
||||||
self.data_high = 0;
|
self.data_high = 0;
|
||||||
self.bit_count = 0;
|
self.bit_count = 0;
|
||||||
self.te_last = duration;
|
self.te_last = duration;
|
||||||
return None;
|
|
||||||
} else {
|
} else {
|
||||||
self.step = DecoderStep::Reset;
|
self.step = DecoderStep::Reset;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
self.step = DecoderStep::Reset;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Data: Manchester events — short first, then long (matches reference)
|
||||||
DecoderStep::Data => {
|
DecoderStep::Data => {
|
||||||
let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA;
|
let short_diff = duration_diff!(duration, TE_SHORT);
|
||||||
let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA;
|
let long_diff = duration_diff!(duration, TE_LONG);
|
||||||
|
|
||||||
if is_short || is_long {
|
let event = if short_diff < TE_DELTA {
|
||||||
if let Some(bit) = self.manchester_advance(is_short, level) {
|
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);
|
self.add_manchester_bit(bit);
|
||||||
|
|
||||||
if self.bit_count > 0x46 {
|
if self.bit_count > 0x46 {
|
||||||
@@ -218,10 +243,6 @@ impl ProtocolDecoder for FiatV0Decoder {
|
|||||||
return Some(result);
|
return Some(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if duration > TE_LONG * 3 {
|
|
||||||
// End of signal
|
|
||||||
self.step = DecoderStep::Reset;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.te_last = duration;
|
self.te_last = duration;
|
||||||
}
|
}
|
||||||
@@ -249,14 +270,13 @@ impl ProtocolDecoder for FiatV0Decoder {
|
|||||||
signal.push(LevelDuration::new(false, INTER_BURST_GAP));
|
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 {
|
for i in 0..PREAMBLE_PAIRS {
|
||||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||||
if i < PREAMBLE_PAIRS - 1 {
|
signal.push(LevelDuration::new(
|
||||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
false,
|
||||||
} else {
|
if i == PREAMBLE_PAIRS - 1 { GAP_US } else { TE_SHORT },
|
||||||
signal.push(LevelDuration::new(false, GAP_US));
|
));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// First bit (bit 63)
|
// First bit (bit 63)
|
||||||
|
|||||||
+13
-17
@@ -1,6 +1,7 @@
|
|||||||
//! Ford V0 protocol decoder/encoder
|
//! 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:
|
//! Protocol characteristics:
|
||||||
//! - Manchester encoding: 250/500µs timing
|
//! - Manchester encoding: 250/500µs timing
|
||||||
@@ -8,6 +9,9 @@
|
|||||||
//! - Matrix-based CRC in GF(2)
|
//! - Matrix-based CRC in GF(2)
|
||||||
//! - BS (byte swap) magic calculation
|
//! - BS (byte swap) magic calculation
|
||||||
//! - 6 bursts, 4 preamble pairs, 3500µs gap
|
//! - 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 super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||||
use crate::radio::demodulator::LevelDuration;
|
use crate::radio::demodulator::LevelDuration;
|
||||||
@@ -551,18 +555,16 @@ impl ProtocolDecoder for FordV0Decoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── Step 3: PreambleCheck — count preamble pairs or transition to gap ───
|
// ─── Step 3: PreambleCheck — count preamble pairs or transition to gap ───
|
||||||
|
// Order matches protopirate ford_v0.c: check LONG first, then SHORT.
|
||||||
DecoderStep::PreambleCheck => {
|
DecoderStep::PreambleCheck => {
|
||||||
if level {
|
if level {
|
||||||
let short_diff = duration_diff!(duration, TE_SHORT);
|
if duration_diff!(duration, TE_LONG) < TE_DELTA {
|
||||||
let long_diff = duration_diff!(duration, TE_LONG);
|
// Long HIGH: another preamble pair
|
||||||
|
|
||||||
if long_diff < TE_DELTA && long_diff <= short_diff {
|
|
||||||
// Long HIGH (closer to TE_LONG): another preamble pair
|
|
||||||
self.header_count += 1;
|
self.header_count += 1;
|
||||||
self.te_last = duration;
|
self.te_last = duration;
|
||||||
self.step = DecoderStep::Preamble;
|
self.step = DecoderStep::Preamble;
|
||||||
} else if short_diff < TE_DELTA {
|
} else if duration_diff!(duration, TE_SHORT) < TE_DELTA {
|
||||||
// Short HIGH (closer to TE_SHORT): end of preamble, transition to gap
|
// Short HIGH: end of preamble, transition to gap
|
||||||
self.step = DecoderStep::Gap;
|
self.step = DecoderStep::Gap;
|
||||||
} else {
|
} else {
|
||||||
self.step = DecoderStep::Reset;
|
self.step = DecoderStep::Reset;
|
||||||
@@ -585,18 +587,12 @@ impl ProtocolDecoder for FordV0Decoder {
|
|||||||
|
|
||||||
// ─── Step 5: Data — Manchester decode 80 bits ───
|
// ─── Step 5: Data — Manchester decode 80 bits ───
|
||||||
DecoderStep::Data => {
|
DecoderStep::Data => {
|
||||||
// Map level+duration to Manchester event using NEAREST-MATCH.
|
// Map level+duration to Manchester event. Order matches protopirate ford_v0.c:
|
||||||
// With TE_DELTA=200, SHORT(250) and LONG(500) ranges overlap at 300–450µs.
|
// check SHORT first, then LONG (so when both within TE_DELTA, short wins).
|
||||||
// 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.
|
|
||||||
let short_diff = duration_diff!(duration, TE_SHORT);
|
let short_diff = duration_diff!(duration, TE_SHORT);
|
||||||
let long_diff = duration_diff!(duration, TE_LONG);
|
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
|
if level { 0 } else { 1 } // ShortLow / ShortHigh
|
||||||
} else if long_diff < TE_DELTA {
|
} else if long_diff < TE_DELTA {
|
||||||
if level { 2 } else { 3 } // LongLow / LongHigh
|
if level { 2 } else { 3 } // LongLow / LongHigh
|
||||||
|
|||||||
@@ -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.
|
//! 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;
|
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 {
|
pub fn keeloq_decrypt(data: u32, key: u64) -> u32 {
|
||||||
let mut block = data;
|
let mut x = data;
|
||||||
let mut tkey = key;
|
for r in 0..528u32 {
|
||||||
|
let key_bit = ((key >> ((15 - r) & 63)) & 1) as u32;
|
||||||
for _ in 0..528 {
|
let new_lsb = bit(x, 31) ^ bit(x, 15) ^ key_bit
|
||||||
let lutkey = ((block >> 0) & 1)
|
^ bit(KEELOQ_NLF, g5(x, 0, 8, 19, 25, 30));
|
||||||
| ((block >> 7) & 2)
|
x = (x << 1) ^ new_lsb;
|
||||||
| ((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);
|
|
||||||
}
|
}
|
||||||
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 {
|
pub fn keeloq_encrypt(data: u32, key: u64) -> u32 {
|
||||||
let mut block = data;
|
let mut x = data;
|
||||||
let mut tkey = key;
|
for r in 0..528u32 {
|
||||||
|
let key_bit = ((key >> (r & 63)) & 1) as u32;
|
||||||
for _ in 0..528 {
|
let new_msb = bit(x, 0) ^ bit(x, 16) ^ key_bit
|
||||||
let lutkey = ((block >> 1) & 1)
|
^ bit(KEELOQ_NLF, g5(x, 1, 9, 20, 26, 31));
|
||||||
| ((block >> 8) & 2)
|
x = (x >> 1) ^ (new_msb << 31);
|
||||||
| ((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);
|
|
||||||
}
|
}
|
||||||
block
|
x
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Normal learning key derivation
|
/// Normal learning key derivation (matches subghz_protocol_keeloq_common_normal_learning).
|
||||||
/// Derives a 64-bit key from a 32-bit fix code and a 64-bit manufacturer key
|
/// @param data - serial number (28-bit, upper bits ignored)
|
||||||
pub fn keeloq_normal_learning(fix: u32, manufacturer_key: u64) -> u64 {
|
/// @param key - manufacturer key (64-bit)
|
||||||
let serial_low = fix & 0xFFFF;
|
/// @return derived key for this serial (64-bit)
|
||||||
let serial_high = (fix >> 16) & 0xFFFF;
|
pub fn keeloq_normal_learning(data: u32, key: u64) -> u64 {
|
||||||
|
let data = data & 0x0FFFFFFF;
|
||||||
let key_low = keeloq_decrypt(serial_low as u32 | 0x20000000, manufacturer_key);
|
let k1 = keeloq_decrypt(data | 0x20000000, key);
|
||||||
let key_high = keeloq_decrypt(serial_high as u32 | 0x60000000, manufacturer_key);
|
let k2 = keeloq_decrypt(data | 0x60000000, key);
|
||||||
|
((k2 as u64) << 32) | (k1 as u64)
|
||||||
((key_high as u64) << 32) | (key_low as u64)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reverse the bits in a 64-bit key (for protocols that store data MSB-first)
|
/// Reverse the bits in a 64-bit key (for protocols that store data MSB-first)
|
||||||
|
|||||||
+29
-19
@@ -1,16 +1,8 @@
|
|||||||
//! Key management module for protocol encryption/decryption
|
//! Key management module for protocol encryption/decryption
|
||||||
//!
|
//!
|
||||||
//! Ported from protopirate's keys.c
|
//! 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,
|
||||||
//! Manages manufacturer keys used by various protocols:
|
//! matching the standard encrypted + VAG raw keystore data.
|
||||||
//! - 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.
|
|
||||||
|
|
||||||
use super::aut64::{self, Aut64Key, AUT64_KEY_STRUCT_PACKED_SIZE};
|
use super::aut64::{self, Aut64Key, AUT64_KEY_STRUCT_PACKED_SIZE};
|
||||||
use configparser::ini::Ini;
|
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 KIA_KEY4: u32 = 13; // kia_v5_key
|
||||||
const STAR_LINE_KEY: u32 = 20; // star_line_mf_key
|
const STAR_LINE_KEY: u32 = 20; // star_line_mf_key
|
||||||
|
|
||||||
/// Maximum number of VAG AUT64 keys
|
/// Maximum number of VAG AUT64 keys (embedded blob has 64 bytes = 4 keys)
|
||||||
const VAG_KEYS_COUNT: usize = 3;
|
const MAX_VAG_KEYS: usize = 4;
|
||||||
|
|
||||||
/// Global key store - thread-safe access to loaded keys
|
/// Global key store - thread-safe access to loaded keys
|
||||||
pub struct KeyStore {
|
pub struct KeyStore {
|
||||||
@@ -81,22 +73,20 @@ impl KeyStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load VAG AUT64 keys from raw binary data
|
/// Load VAG AUT64 keys from raw binary data (16 bytes per key; up to MAX_VAG_KEYS)
|
||||||
/// The data should contain packed AUT64 key structures (16 bytes each)
|
|
||||||
pub fn load_vag_keys_from_data(&mut self, data: &[u8]) {
|
pub fn load_vag_keys_from_data(&mut self, data: &[u8]) {
|
||||||
if self.vag_keys_loaded {
|
if self.vag_keys_loaded {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.vag_keys.clear();
|
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;
|
let offset = i * AUT64_KEY_STRUCT_PACKED_SIZE;
|
||||||
if offset + AUT64_KEY_STRUCT_PACKED_SIZE > data.len() {
|
if offset + AUT64_KEY_STRUCT_PACKED_SIZE > data.len() {
|
||||||
error!("VAG key data too short for key {}", i);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let key = aut64::aut64_unpack(&data[offset..offset + AUT64_KEY_STRUCT_PACKED_SIZE]);
|
let key = aut64::aut64_unpack(&data[offset..offset + AUT64_KEY_STRUCT_PACKED_SIZE]);
|
||||||
self.vag_keys.push(key);
|
self.vag_keys.push(key);
|
||||||
}
|
}
|
||||||
@@ -179,7 +169,7 @@ pub fn get_keystore_mut() -> std::sync::RwLockWriteGuard<'static, KeyStore> {
|
|||||||
global_keystore().write().unwrap()
|
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)]) {
|
pub fn load_keys(kia_entries: &[(u32, u64)]) {
|
||||||
let mut store = get_keystore_mut();
|
let mut store = get_keystore_mut();
|
||||||
store.load_kia_keys(kia_entries);
|
store.load_kia_keys(kia_entries);
|
||||||
@@ -191,6 +181,26 @@ pub fn load_vag_keys(path: &str) {
|
|||||||
store.load_vag_keys_from_file(path);
|
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/
|
// Keystore file loading from ~/.config/KAT/keystore/
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|||||||
+18
-12
@@ -1,13 +1,13 @@
|
|||||||
//! Kia V0 protocol decoder
|
//! Kia V0 protocol decoder/encoder
|
||||||
//!
|
//!
|
||||||
//! Ported from protopirate's kia_v0.c
|
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v0.c`.
|
||||||
|
//! Decode/encode logic (CRC8, preamble/sync, field layout) matches reference.
|
||||||
//!
|
//!
|
||||||
//! Protocol characteristics:
|
//! Protocol characteristics:
|
||||||
//! - PWM encoding: short pulse (250µs) = 0, long pulse (500µs) = 1
|
//! - PWM encoding: short pulse (250µs) = 0, long pulse (500µs) = 1
|
||||||
//! - 61 bits total
|
//! - 61 bits total (1 sync bit + 60 data bits)
|
||||||
//! - Preamble: alternating short pulses
|
//! - Preamble: alternating short pulses; sync: long-long pattern
|
||||||
//! - Sync: long-long pattern
|
//! - Data: 60 bits (4-bit prefix + 16-bit counter + 28-bit serial + 4-bit button + 8-bit CRC)
|
||||||
//! - Data: 59 bits (4-bit prefix + 16-bit counter + 28-bit serial + 4-bit button + 8-bit CRC)
|
|
||||||
|
|
||||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||||
use super::common::{crc8_kia, add_bit};
|
use super::common::{crc8_kia, add_bit};
|
||||||
@@ -19,7 +19,7 @@ const TE_LONG: u32 = 500;
|
|||||||
const TE_DELTA: u32 = 100;
|
const TE_DELTA: u32 = 100;
|
||||||
const MIN_COUNT_BIT: usize = 61;
|
const MIN_COUNT_BIT: usize = 61;
|
||||||
|
|
||||||
/// Decoder states
|
/// Decoder states (matches protopirate's KiaV0DecoderStep)
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
enum DecoderStep {
|
enum DecoderStep {
|
||||||
Reset,
|
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 {
|
fn calculate_crc(data: u64) -> u8 {
|
||||||
let crc_data = [
|
let crc_data = [
|
||||||
((data >> 48) & 0xFF) as u8,
|
((data >> 48) & 0xFF) as u8,
|
||||||
@@ -236,13 +236,13 @@ impl ProtocolDecoder for KiaV0Decoder {
|
|||||||
signal.push(LevelDuration::new(is_high, TE_SHORT));
|
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(true, TE_LONG));
|
||||||
signal.push(LevelDuration::new(false, TE_LONG));
|
signal.push(LevelDuration::new(false, TE_LONG));
|
||||||
|
|
||||||
// Data: 59 bits (MSB first)
|
// Data: 60 bits MSB first (bits 59..0)
|
||||||
for bit_num in 0..59 {
|
for bit_num in 0..60 {
|
||||||
let bit_mask = 1u64 << (58 - bit_num);
|
let bit_mask = 1u64 << (59 - bit_num);
|
||||||
let bit = (data & bit_mask) != 0;
|
let bit = (data & bit_mask) != 0;
|
||||||
let duration = if bit { TE_LONG } else { TE_SHORT };
|
let duration = if bit { TE_LONG } else { TE_SHORT };
|
||||||
|
|
||||||
@@ -257,3 +257,9 @@ impl ProtocolDecoder for KiaV0Decoder {
|
|||||||
Some(signal)
|
Some(signal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for KiaV0Decoder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+19
-12
@@ -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:
|
//! Protocol characteristics:
|
||||||
//! - Manchester encoding: 800/1600µs timing
|
//! - Manchester encoding: 800/1600µs timing
|
||||||
//! - 57 bits total
|
//! - 57 bits total (32 serial + 8 button + 12 counter + 4 CRC)
|
||||||
//! - Long preamble of ~90 pulses
|
//! - Long preamble of ~90 long pairs
|
||||||
//! - CRC4 checksum
|
//! - CRC4 checksum with offset rules (cnt_high 0 / >= 6)
|
||||||
|
|
||||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||||
use crate::radio::demodulator::LevelDuration;
|
use crate::radio::demodulator::LevelDuration;
|
||||||
@@ -17,7 +18,7 @@ const TE_LONG: u32 = 1600;
|
|||||||
const TE_DELTA: u32 = 200;
|
const TE_DELTA: u32 = 200;
|
||||||
const MIN_COUNT_BIT: usize = 57;
|
const MIN_COUNT_BIT: usize = 57;
|
||||||
|
|
||||||
/// Manchester states
|
/// Manchester decoder states (matches protopirate kia_v1 Manchester state machine)
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
enum ManchesterState {
|
enum ManchesterState {
|
||||||
Mid0,
|
Mid0,
|
||||||
@@ -26,7 +27,7 @@ enum ManchesterState {
|
|||||||
Start1,
|
Start1,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decoder states
|
/// Decoder states (matches protopirate's KiaV1DecoderStep)
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
enum DecoderStep {
|
enum DecoderStep {
|
||||||
Reset,
|
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 {
|
fn crc4(bytes: &[u8], offset: u8) -> u8 {
|
||||||
let mut crc: u8 = 0;
|
let mut crc: u8 = 0;
|
||||||
for &byte in bytes {
|
for &byte in bytes {
|
||||||
@@ -97,7 +98,7 @@ impl KiaV1Decoder {
|
|||||||
fn parse_data(&self) -> DecodedSignal {
|
fn parse_data(&self) -> DecodedSignal {
|
||||||
let data = self.decode_data;
|
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 serial = (data >> 24) as u32;
|
||||||
let button = ((data >> 16) & 0xFF) as u8;
|
let button = ((data >> 16) & 0xFF) as u8;
|
||||||
let cnt_low = ((data >> 8) & 0xFF) as u16;
|
let cnt_low = ((data >> 8) & 0xFF) as u16;
|
||||||
@@ -263,7 +264,7 @@ impl ProtocolDecoder for KiaV1Decoder {
|
|||||||
|
|
||||||
let mut signal = Vec::with_capacity(600);
|
let mut signal = Vec::with_capacity(600);
|
||||||
|
|
||||||
// Generate 3 bursts
|
// Generate 3 bursts (matches protopirate kia_v1 encode)
|
||||||
for burst in 0..3 {
|
for burst in 0..3 {
|
||||||
if burst > 0 {
|
if burst > 0 {
|
||||||
signal.push(LevelDuration::new(false, 25000));
|
signal.push(LevelDuration::new(false, 25000));
|
||||||
@@ -275,10 +276,10 @@ impl ProtocolDecoder for KiaV1Decoder {
|
|||||||
signal.push(LevelDuration::new(true, TE_LONG));
|
signal.push(LevelDuration::new(true, TE_LONG));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Short gap
|
// Short gap before data
|
||||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
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() {
|
for bit_num in (1..MIN_COUNT_BIT).rev() {
|
||||||
let bit = ((data >> (bit_num - 1)) & 1) == 1;
|
let bit = ((data >> (bit_num - 1)) & 1) == 1;
|
||||||
if bit {
|
if bit {
|
||||||
@@ -294,3 +295,9 @@ impl ProtocolDecoder for KiaV1Decoder {
|
|||||||
Some(signal)
|
Some(signal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for KiaV1Decoder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+20
-13
@@ -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:
|
//! Protocol characteristics:
|
||||||
//! - Manchester encoding: 500/1000µs timing
|
//! - Manchester encoding: 500/1000µs timing
|
||||||
//! - 53 bits total
|
//! - 53 bits total (32 serial + 4 button + 12 counter + 4 CRC, plus start bit)
|
||||||
//! - Long preamble of 252+ pairs
|
//! - Long preamble of 252 long pairs
|
||||||
//! - CRC4 checksum
|
//! - CRC4 checksum (XOR nibbles + offset 1)
|
||||||
|
|
||||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||||
use crate::radio::demodulator::LevelDuration;
|
use crate::radio::demodulator::LevelDuration;
|
||||||
@@ -17,7 +18,7 @@ const TE_LONG: u32 = 1000;
|
|||||||
const TE_DELTA: u32 = 150;
|
const TE_DELTA: u32 = 150;
|
||||||
const MIN_COUNT_BIT: usize = 53;
|
const MIN_COUNT_BIT: usize = 53;
|
||||||
|
|
||||||
/// Manchester states
|
/// Manchester decoder states (matches protopirate kia_v2 Manchester state machine)
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
enum ManchesterState {
|
enum ManchesterState {
|
||||||
Mid0,
|
Mid0,
|
||||||
@@ -26,7 +27,7 @@ enum ManchesterState {
|
|||||||
Start1,
|
Start1,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decoder states
|
/// Decoder states (matches protopirate's KiaV2DecoderStep)
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
enum DecoderStep {
|
enum DecoderStep {
|
||||||
Reset,
|
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 {
|
fn calculate_crc(data: u64) -> u8 {
|
||||||
let serial = ((data >> 20) & 0xFFFFFFFF) as u32;
|
let serial = ((data >> 20) & 0xFFFFFFFF) as u32;
|
||||||
let u_var4 = (data & 0xFFFFFFFF) as u32;
|
let u_var4 = (data & 0xFFFFFFFF) as u32;
|
||||||
@@ -105,10 +106,10 @@ impl KiaV2Decoder {
|
|||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse decoded data
|
/// Parse decoded data (field layout matches kia_v2.c)
|
||||||
fn parse_data(&self) -> DecodedSignal {
|
fn parse_data(&self) -> DecodedSignal {
|
||||||
let data = self.decode_data;
|
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 serial = ((data >> 20) & 0xFFFFFFFF) as u32;
|
||||||
let button = ((data >> 16) & 0x0F) as u8;
|
let button = ((data >> 16) & 0x0F) as u8;
|
||||||
|
|
||||||
@@ -246,7 +247,7 @@ impl ProtocolDecoder for KiaV2Decoder {
|
|||||||
|
|
||||||
let mut signal = Vec::with_capacity(700);
|
let mut signal = Vec::with_capacity(700);
|
||||||
|
|
||||||
// Generate 2 bursts
|
// Generate 2 bursts (matches protopirate kia_v2 encode)
|
||||||
for _burst in 0..2 {
|
for _burst in 0..2 {
|
||||||
// Preamble: 252 long pairs
|
// Preamble: 252 long pairs
|
||||||
for _ in 0..252 {
|
for _ in 0..252 {
|
||||||
@@ -254,10 +255,10 @@ impl ProtocolDecoder for KiaV2Decoder {
|
|||||||
signal.push(LevelDuration::new(true, TE_LONG));
|
signal.push(LevelDuration::new(true, TE_LONG));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Short gap
|
// Short gap before data
|
||||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
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() {
|
for bit_num in (1..MIN_COUNT_BIT).rev() {
|
||||||
let bit = ((new_data >> (bit_num - 1)) & 1) == 1;
|
let bit = ((new_data >> (bit_num - 1)) & 1) == 1;
|
||||||
if bit {
|
if bit {
|
||||||
@@ -273,3 +274,9 @@ impl ProtocolDecoder for KiaV2Decoder {
|
|||||||
Some(signal)
|
Some(signal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for KiaV2Decoder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+25
-60
@@ -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:
|
//! Protocol characteristics:
|
||||||
//! - PWM encoding: 400/800µs timing
|
//! - PWM encoding: 400/800µs (short=0, long=1)
|
||||||
//! - 68 bits total
|
//! - 68 bits total (8 bytes encrypted + 4 bits CRC)
|
||||||
//! - Short preamble of 16 pairs
|
//! - Short preamble of 16 pairs; sync 1200µs (V4: long HIGH, V3: long LOW)
|
||||||
//! - KeeLoq encryption (requires manufacturer key)
|
//! - KeeLoq encryption (KIA manufacturer key); V3/V4 differ only in sync polarity
|
||||||
//! - V3 and V4 differ only in sync polarity
|
|
||||||
|
|
||||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||||
|
use super::keeloq_common::{keeloq_decrypt, keeloq_encrypt};
|
||||||
use super::keys;
|
use super::keys;
|
||||||
use crate::radio::demodulator::LevelDuration;
|
use crate::radio::demodulator::LevelDuration;
|
||||||
use crate::duration_diff;
|
use crate::duration_diff;
|
||||||
@@ -23,7 +24,7 @@ const INTER_BURST_GAP_US: u32 = 10000;
|
|||||||
const PREAMBLE_PAIRS: usize = 16;
|
const PREAMBLE_PAIRS: usize = 16;
|
||||||
const TOTAL_BURSTS: usize = 3;
|
const TOTAL_BURSTS: usize = 3;
|
||||||
|
|
||||||
/// Decoder states
|
/// Decoder states (matches protopirate's KiaV3V4DecoderStep)
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
enum DecoderStep {
|
enum DecoderStep {
|
||||||
Reset,
|
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 {
|
fn reverse8(byte: u8) -> u8 {
|
||||||
let mut byte = byte;
|
let mut byte = byte;
|
||||||
byte = (byte & 0xF0) >> 4 | (byte & 0x0F) << 4;
|
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 {
|
fn calculate_crc(bytes: &[u8]) -> u8 {
|
||||||
let mut crc: u8 = 0;
|
let mut crc: u8 = 0;
|
||||||
for &byte in bytes.iter().take(8) {
|
for &byte in bytes.iter().take(8) {
|
||||||
@@ -85,61 +86,18 @@ impl KiaV3V4Decoder {
|
|||||||
crc & 0x0F
|
crc & 0x0F
|
||||||
}
|
}
|
||||||
|
|
||||||
/// KeeLoq decrypt
|
/// KIA manufacturer key from keystore (type 10)
|
||||||
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)
|
|
||||||
fn get_mf_key() -> u64 {
|
fn get_mf_key() -> u64 {
|
||||||
keys::get_keystore().get_kia_mf_key()
|
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<DecodedSignal> {
|
fn process_buffer(&self) -> Option<DecodedSignal> {
|
||||||
if self.raw_bit_count < 68 {
|
if self.raw_bit_count < 68 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut b = self.raw_bits;
|
let mut b = self.raw_bits;
|
||||||
|
|
||||||
// V3 sync means data is inverted
|
// V3 sync means data is inverted
|
||||||
if self.is_v3_sync {
|
if self.is_v3_sync {
|
||||||
let num_bytes = ((self.raw_bit_count + 7) / 8) as usize;
|
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 our_serial_lsb = (serial & 0xFF) as u8;
|
||||||
|
|
||||||
let mf_key = Self::get_mf_key();
|
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_btn = ((decrypted >> 28) & 0x0F) as u8;
|
||||||
let dec_serial_lsb = ((decrypted >> 16) & 0xFF) as u8;
|
let dec_serial_lsb = ((decrypted >> 16) & 0xFF) as u8;
|
||||||
|
|
||||||
@@ -313,7 +271,7 @@ impl ProtocolDecoder for KiaV3V4Decoder {
|
|||||||
(((button & 0x0F) as u32) << 28);
|
(((button & 0x0F) as u32) << 28);
|
||||||
|
|
||||||
let mf_key = Self::get_mf_key();
|
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
|
// Build raw bytes
|
||||||
let mut raw_bytes = [0u8; 9];
|
let mut raw_bytes = [0u8; 9];
|
||||||
@@ -343,18 +301,19 @@ impl ProtocolDecoder for KiaV3V4Decoder {
|
|||||||
|
|
||||||
let mut signal = Vec::with_capacity(600);
|
let mut signal = Vec::with_capacity(600);
|
||||||
|
|
||||||
|
// 3 bursts with 10ms gap (matches protopirate kia_v3_v4 encode)
|
||||||
for burst in 0..TOTAL_BURSTS {
|
for burst in 0..TOTAL_BURSTS {
|
||||||
if burst > 0 {
|
if burst > 0 {
|
||||||
signal.push(LevelDuration::new(false, INTER_BURST_GAP_US));
|
signal.push(LevelDuration::new(false, INTER_BURST_GAP_US));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preamble
|
// Preamble: 16 short pairs
|
||||||
for _ in 0..PREAMBLE_PAIRS {
|
for _ in 0..PREAMBLE_PAIRS {
|
||||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync pulse
|
// Sync: V4 = long HIGH, V3 = long LOW
|
||||||
if version == 0 {
|
if version == 0 {
|
||||||
// V4: long HIGH, short LOW
|
// V4: long HIGH, short LOW
|
||||||
signal.push(LevelDuration::new(true, SYNC_DURATION));
|
signal.push(LevelDuration::new(true, SYNC_DURATION));
|
||||||
@@ -365,7 +324,7 @@ impl ProtocolDecoder for KiaV3V4Decoder {
|
|||||||
signal.push(LevelDuration::new(false, SYNC_DURATION));
|
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 {
|
for byte_idx in 0..9 {
|
||||||
let bits_in_byte = if byte_idx == 8 { 4 } else { 8 };
|
let bits_in_byte = if byte_idx == 8 { 4 } else { 8 };
|
||||||
for bit_idx in (8 - bits_in_byte..8).rev() {
|
for bit_idx in (8 - bits_in_byte..8).rev() {
|
||||||
@@ -384,3 +343,9 @@ impl ProtocolDecoder for KiaV3V4Decoder {
|
|||||||
Some(signal)
|
Some(signal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for KiaV3V4Decoder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+22
-15
@@ -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:
|
//! Protocol characteristics:
|
||||||
//! - Manchester encoding: 400/800µs timing
|
//! - Manchester encoding: 400/800µs (opposite polarity to V1/V2: level ? ShortHigh : ShortLow)
|
||||||
//! - 64 bits total (+3 bit CRC)
|
//! - 64 data bits + 3-bit CRC (67 bits on air)
|
||||||
//! - Preamble of ~40+ short pairs
|
//! - Preamble of ~40+ short/long pairs; then 64-bit key then 3-bit CRC
|
||||||
//! - Custom "mixer" encryption
|
//! - Custom "mixer" decryption with KIA V5 manufacturer key
|
||||||
//! - Decode-only (no encoder)
|
|
||||||
|
|
||||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||||
use super::keys;
|
use super::keys;
|
||||||
@@ -19,7 +20,7 @@ const TE_LONG: u32 = 800;
|
|||||||
const TE_DELTA: u32 = 150;
|
const TE_DELTA: u32 = 150;
|
||||||
const MIN_COUNT_BIT: usize = 64;
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
enum ManchesterState {
|
enum ManchesterState {
|
||||||
Mid0,
|
Mid0,
|
||||||
@@ -28,7 +29,7 @@ enum ManchesterState {
|
|||||||
Start1,
|
Start1,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decoder states
|
/// Decoder states (matches protopirate's KiaV5DecoderStep)
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
enum DecoderStep {
|
enum DecoderStep {
|
||||||
Reset,
|
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 {
|
fn get_v5_key() -> u64 {
|
||||||
keys::get_keystore().get_kia_v5_key()
|
keys::get_keystore().get_kia_v5_key()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Custom mixer decryption
|
/// Mixer decryption (matches kia_v5.c custom cipher)
|
||||||
fn mixer_decode(encrypted: u32) -> u16 {
|
fn mixer_decode(encrypted: u32) -> u16 {
|
||||||
let mut s0 = (encrypted & 0xFF) as u8;
|
let mut s0 = (encrypted & 0xFF) as u8;
|
||||||
let mut s1 = ((encrypted >> 8) & 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
|
((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 {
|
fn compute_yek(key: u64) -> u64 {
|
||||||
let mut yek: u64 = 0;
|
let mut yek: u64 = 0;
|
||||||
for i in 0..8 {
|
for i in 0..8 {
|
||||||
@@ -180,7 +181,7 @@ impl KiaV5Decoder {
|
|||||||
output
|
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<DecodedSignal> {
|
fn parse_data(&self) -> Option<DecodedSignal> {
|
||||||
if self.bit_count < MIN_COUNT_BIT as u8 {
|
if self.bit_count < MIN_COUNT_BIT as u8 {
|
||||||
return None;
|
return None;
|
||||||
@@ -188,7 +189,7 @@ impl KiaV5Decoder {
|
|||||||
|
|
||||||
let key = self.saved_key;
|
let key = self.saved_key;
|
||||||
let yek = Self::compute_yek(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 serial = ((yek >> 32) & 0x0FFFFFFF) as u32;
|
||||||
let button = ((yek >> 60) & 0x0F) as u8;
|
let button = ((yek >> 60) & 0x0F) as u8;
|
||||||
let encrypted = (yek & 0xFFFFFFFF) as u32;
|
let encrypted = (yek & 0xFFFFFFFF) as u32;
|
||||||
@@ -316,6 +317,12 @@ impl ProtocolDecoder for KiaV5Decoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
|
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
|
||||||
None // V5 doesn't support encoding
|
None // V5 decode-only in protopirate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for KiaV5Decoder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-17
@@ -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:
|
//! Protocol characteristics:
|
||||||
//! - Manchester encoding: 200/400µs timing
|
//! - Manchester encoding: 200/400µs (level convention inverted vs Flipper; see manchester_advance)
|
||||||
//! - 144 bits total (split into 3 parts)
|
//! - 144 bits total: part1 (64) + part2 (64) + part3 (16), each part inverted on store
|
||||||
//! - Long preamble of 600+ pairs
|
//! - Long preamble of 601 pairs; sync bits 1,1,0,1 then data
|
||||||
//! - AES-128 encryption
|
//! - AES-128 decryption with key derived from KIA V6 A/B keystores (types 11/12) and XOR masks
|
||||||
//! - Decode-only (no encoder)
|
|
||||||
|
|
||||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||||
use super::keys;
|
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];
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
enum ManchesterState {
|
enum ManchesterState {
|
||||||
Mid0,
|
Mid0,
|
||||||
@@ -74,7 +75,7 @@ enum ManchesterState {
|
|||||||
Start1,
|
Start1,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decoder states
|
/// Decoder states (matches protopirate's KiaV6DecoderStep)
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
enum DecoderStep {
|
enum DecoderStep {
|
||||||
Reset,
|
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 {
|
fn get_keystore_a() -> u64 {
|
||||||
keys::get_keystore().get_kia_v6_keystore_a()
|
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 {
|
fn get_keystore_b() -> u64 {
|
||||||
keys::get_keystore().get_kia_v6_keystore_b()
|
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 {
|
fn crc8(data: &[u8], init: u8, polynomial: u8) -> u8 {
|
||||||
let mut crc = init;
|
let mut crc = init;
|
||||||
for &byte in data {
|
for &byte in data {
|
||||||
@@ -274,7 +275,7 @@ impl KiaV6Decoder {
|
|||||||
*data = state;
|
*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] {
|
fn get_aes_key() -> [u8; 16] {
|
||||||
let keystore_a = Self::get_keystore_a();
|
let keystore_a = Self::get_keystore_a();
|
||||||
let keystore_a_hi = ((keystore_a >> 32) & 0xFFFFFFFF) as u32;
|
let keystore_a_hi = ((keystore_a >> 32) & 0xFFFFFFFF) as u32;
|
||||||
@@ -305,7 +306,7 @@ impl KiaV6Decoder {
|
|||||||
aes_key
|
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)> {
|
fn decrypt(&self) -> Option<(u32, u8, u32, bool)> {
|
||||||
let mut encrypted_data = [0u8; 16];
|
let mut encrypted_data = [0u8; 16];
|
||||||
|
|
||||||
@@ -380,9 +381,8 @@ impl KiaV6Decoder {
|
|||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add initial sync bits
|
/// Add initial sync bits (1,1,0,1 — matches kia_v6.c)
|
||||||
fn add_sync_bits(&mut self) {
|
fn add_sync_bits(&mut self) {
|
||||||
// Add 1, 1, 0, 1 as initial bits
|
|
||||||
for bit in [true, true, false, true] {
|
for bit in [true, true, false, true] {
|
||||||
let carry = self.data_part1_low >> 31;
|
let carry = self.data_part1_low >> 31;
|
||||||
self.data_part1_low = (self.data_part1_low << 1) | (bit as u32);
|
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<Vec<LevelDuration>> {
|
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
|
||||||
None
|
None // V6 decode-only in protopirate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for KiaV6Decoder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
//! Protocol decoders and encoders for various keyfob systems.
|
//! Protocol decoders and encoders for various keyfob systems.
|
||||||
//!
|
//!
|
||||||
//! This module implements decoders for various keyfob protocols, ported from
|
//! Protocols are aligned with the ProtoPirate reference (`REFERENCES/ProtoPirate/protocols/`).
|
||||||
//! protopirate. Each protocol processes level+duration pairs from the demodulator.
|
//! Each decoder processes level+duration pairs from the demodulator and optionally supports
|
||||||
|
//! encoding (replay). Shared pieces: [common], [keeloq_common], [keys], [aut64].
|
||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
pub mod keeloq_common;
|
pub mod keeloq_common;
|
||||||
|
|||||||
+25
-23
@@ -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:
|
//! Protocol characteristics:
|
||||||
//! - Manchester encoding: 125/250µs bit timing, 250/500µs symbol timing
|
//! - Manchester encoding: 250/500µs symbol (125/250µs sub-symbol for preamble)
|
||||||
//! - 128 bits total (key1: 64 bits, key2: 16 bits, validation: 48 bits)
|
//! - 128 bits total: key1 (64) + validation (16) + key2/rest (48); decode uses key1 + 16-bit validation
|
||||||
//! - TEA and XOR encryption schemes
|
//! - TEA decrypt/encrypt with fixed key schedules; mode 0x23 adds XOR layer
|
||||||
//! - Two modes: 0x23 and 0x36
|
//! - Two modes: seed 0x23 (TEA + XOR), seed 0xF3/0x36 (TEA, BF2 key schedule)
|
||||||
|
|
||||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||||
use crate::duration_diff;
|
use crate::duration_diff;
|
||||||
@@ -35,7 +36,7 @@ const BF1_KEY_SCHEDULE: [u32; 4] = [0x4A434915, 0xD6743C2B, 0x1F29D308, 0xE6B79A
|
|||||||
// Brute-force constants for mode 0x36
|
// Brute-force constants for mode 0x36
|
||||||
const BF2_KEY_SCHEDULE: [u32; 4] = [0x4039C240, 0xEDA92CAB, 0x4306C02A, 0x02192A04];
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
enum ManchesterState {
|
enum ManchesterState {
|
||||||
Mid0,
|
Mid0,
|
||||||
@@ -44,16 +45,12 @@ enum ManchesterState {
|
|||||||
Start1,
|
Start1,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decoder states
|
/// Decoder states (matches protopirate's PsaDecoderState)
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
enum DecoderState {
|
enum DecoderState {
|
||||||
/// Waiting for first edge
|
|
||||||
WaitEdge,
|
WaitEdge,
|
||||||
/// Counting preamble pattern
|
|
||||||
CountPattern,
|
CountPattern,
|
||||||
/// Decoding Manchester data
|
|
||||||
DecodeManchester,
|
DecodeManchester,
|
||||||
/// Found end of data
|
|
||||||
End,
|
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<bool> {
|
fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option<bool> {
|
||||||
let event = match (is_short, is_high) {
|
let event = match (is_short, is_high) {
|
||||||
(true, true) => 0,
|
(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]) {
|
fn tea_decrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) {
|
||||||
let mut sum = TEA_DELTA.wrapping_mul(TEA_ROUNDS);
|
let mut sum = TEA_DELTA.wrapping_mul(TEA_ROUNDS);
|
||||||
for _ in 0..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]) {
|
fn tea_encrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) {
|
||||||
let mut sum: u32 = 0;
|
let mut sum: u32 = 0;
|
||||||
for _ in 0..TEA_ROUNDS {
|
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]) {
|
fn xor_decrypt(buffer: &mut [u8]) {
|
||||||
let e6 = buffer[8];
|
let e6 = buffer[8];
|
||||||
let e7 = buffer[9];
|
let e7 = buffer[9];
|
||||||
@@ -198,8 +195,9 @@ impl PsaDecoder {
|
|||||||
buffer[7] = e5 ^ e6 ^ e7;
|
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)> {
|
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;
|
let seed_byte = (self.key1_high >> 24) as u8;
|
||||||
|
|
||||||
if seed_byte >= 0x23 && seed_byte < 0x24 {
|
if seed_byte >= 0x23 && seed_byte < 0x24 {
|
||||||
@@ -250,8 +248,9 @@ impl PsaDecoder {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build DecodedSignal from key1 + validation; decrypt yields serial/button/counter (matches psa.c)
|
||||||
fn parse_data(&self) -> DecodedSignal {
|
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);
|
let data = ((self.key1_high as u64) << 32) | (self.key1_low as u64);
|
||||||
|
|
||||||
if let Some((serial, btn, counter, _crc, _mode)) = self.try_decrypt() {
|
if let Some((serial, btn, counter, _crc, _mode)) = self.try_decrypt() {
|
||||||
@@ -445,20 +444,17 @@ impl ProtocolDecoder for PsaDecoder {
|
|||||||
let key1_low = v1;
|
let key1_low = v1;
|
||||||
let validation = ((buffer[8] as u16) << 8) | (buffer[9] as u16);
|
let validation = ((buffer[8] as u16) << 8) | (buffer[9] as u16);
|
||||||
|
|
||||||
// Build signal
|
|
||||||
let mut signal = Vec::with_capacity(512);
|
let mut signal = Vec::with_capacity(512);
|
||||||
|
|
||||||
// Preamble: alternating 125µs pulses
|
// Preamble + sync (matches protopirate psa encode)
|
||||||
for _ in 0..70 {
|
for _ in 0..70 {
|
||||||
signal.push(LevelDuration::new(true, TE_SHORT_125));
|
signal.push(LevelDuration::new(true, TE_SHORT_125));
|
||||||
signal.push(LevelDuration::new(false, TE_SHORT_125));
|
signal.push(LevelDuration::new(false, TE_SHORT_125));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync
|
|
||||||
signal.push(LevelDuration::new(true, TE_LONG_250));
|
signal.push(LevelDuration::new(true, TE_LONG_250));
|
||||||
signal.push(LevelDuration::new(false, 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);
|
let key1 = ((key1_high as u64) << 32) | (key1_low as u64);
|
||||||
for bit in (0..64).rev() {
|
for bit in (0..64).rev() {
|
||||||
if (key1 >> bit) & 1 == 1 {
|
if (key1 >> bit) & 1 == 1 {
|
||||||
@@ -487,3 +483,9 @@ impl ProtocolDecoder for PsaDecoder {
|
|||||||
Some(signal)
|
Some(signal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for PsaDecoder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+17
-10
@@ -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:
|
//! Protocol characteristics:
|
||||||
//! - PWM encoding: 750/1100µs timing
|
//! - PWM encoding: 750µs = 0, 1100µs = 1; preamble uses 2× short then alternating
|
||||||
//! - Variable bit count (35, 51, 57, 63, 64, 81, 82)
|
//! - Variable bit count (35, 51, 57, 63, 64, 81, 82); only 51-bit format parsed for serial/button/counter
|
||||||
//! - Decode-only (no encoder)
|
|
||||||
//!
|
//!
|
||||||
//! References:
|
//! References: https://phreakerclub.com/72
|
||||||
//! - https://phreakerclub.com/72
|
|
||||||
|
|
||||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||||
use crate::duration_diff;
|
use crate::duration_diff;
|
||||||
@@ -19,7 +19,7 @@ const TE_LONG: u32 = 1100;
|
|||||||
const TE_DELTA: u32 = 160;
|
const TE_DELTA: u32 = 160;
|
||||||
const MIN_COUNT_BIT: usize = 35;
|
const MIN_COUNT_BIT: usize = 35;
|
||||||
|
|
||||||
/// Decoder states
|
/// Decoder states (matches protopirate's ScherKhanDecoderStep)
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
enum DecoderStep {
|
enum DecoderStep {
|
||||||
Reset,
|
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 {
|
fn parse_data(data: u64, bit_count: usize) -> DecodedSignal {
|
||||||
let (serial, btn, cnt) = match bit_count {
|
let (serial, btn, cnt) = match bit_count {
|
||||||
51 => {
|
51 => {
|
||||||
// MAGIC CODE, Dynamic
|
// 51-bit "MAGIC CODE" / Dynamic format: serial(28) | button(4) | counter(16) — matches reference
|
||||||
let serial =
|
let serial =
|
||||||
((data >> 24) & 0xFFFFFF0) as u32 | ((data >> 20) & 0x0F) as u32;
|
((data >> 24) & 0xFFFFFF0) as u32 | ((data >> 20) & 0x0F) as u32;
|
||||||
let btn = ((data >> 24) & 0x0F) as u8;
|
let btn = ((data >> 24) & 0x0F) as u8;
|
||||||
@@ -196,6 +197,12 @@ impl ProtocolDecoder for ScherKhanDecoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
|
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
|
||||||
None
|
None // Scher-Khan decode-only in protopirate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ScherKhanDecoder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
//! Star Line protocol decoder/encoder
|
//! 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:
|
//! Protocol characteristics:
|
||||||
//! - PWM encoding: 250/500µs timing
|
//! - PWM encoding: 250µs = 0, 500µs = 1
|
||||||
//! - 64 bits total
|
//! - 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
|
//! - 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::keeloq_common::{keeloq_decrypt, keeloq_encrypt, keeloq_normal_learning, reverse_key};
|
||||||
use super::keys;
|
use super::keys;
|
||||||
@@ -20,7 +21,7 @@ const TE_DELTA: u32 = 120;
|
|||||||
const MIN_COUNT_BIT: usize = 64;
|
const MIN_COUNT_BIT: usize = 64;
|
||||||
const HEADER_DURATION: u32 = 1000; // te_long * 2
|
const HEADER_DURATION: u32 = 1000; // te_long * 2
|
||||||
|
|
||||||
/// Decoder states
|
/// Decoder states (matches protopirate's StarLineDecoderStep)
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
enum DecoderStep {
|
enum DecoderStep {
|
||||||
Reset,
|
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 {
|
fn get_mf_key() -> u64 {
|
||||||
keys::get_keystore().get_star_line_mf_key()
|
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 {
|
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 reversed = reverse_key(data, MIN_COUNT_BIT);
|
||||||
let key_fix = (reversed >> 32) as u32;
|
let key_fix = (reversed >> 32) as u32;
|
||||||
let key_hop = (reversed & 0xFFFFFFFF) as u32;
|
let key_hop = (reversed & 0xFFFFFFFF) as u32;
|
||||||
@@ -246,13 +248,13 @@ impl ProtocolDecoder for StarLineDecoder {
|
|||||||
|
|
||||||
let mut signal = Vec::with_capacity(256);
|
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 {
|
for _ in 0..6 {
|
||||||
signal.push(LevelDuration::new(true, HEADER_DURATION));
|
signal.push(LevelDuration::new(true, HEADER_DURATION));
|
||||||
signal.push(LevelDuration::new(false, 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() {
|
for bit in (0..64).rev() {
|
||||||
if (data >> bit) & 1 == 1 {
|
if (data >> bit) & 1 == 1 {
|
||||||
// Bit 1: LONG HIGH + LONG LOW
|
// Bit 1: LONG HIGH + LONG LOW
|
||||||
@@ -268,3 +270,9 @@ impl ProtocolDecoder for StarLineDecoder {
|
|||||||
Some(signal)
|
Some(signal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for StarLineDecoder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+20
-32
@@ -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:
|
//! Protocol characteristics:
|
||||||
//! - PWM encoding: short HIGH (800µs) = 1, long HIGH (1600µs) = 0
|
//! - PWM encoding: 800µs HIGH = 1, 1600µs HIGH = 0; LOW is 800µs after each bit
|
||||||
//! - 64 bits total
|
//! - 64 bits total (8 bytes MSB first: button(4)+serial(24)+counter-related)
|
||||||
//! - Long preamble of 1600µs pulses
|
//! - Preamble: 79 full 1600µs pairs + 80th HIGH only; then gap 2800µs, sync 2800µs HIGH + 1600µs LOW
|
||||||
//! - Gap and sync pattern
|
//! - Complex counter encoding (decode_counter) from bytes 4–7
|
||||||
//! - Complex counter encoding
|
|
||||||
|
|
||||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||||
use crate::radio::demodulator::LevelDuration;
|
use crate::radio::demodulator::LevelDuration;
|
||||||
@@ -22,7 +22,7 @@ const MIN_COUNT_BIT: usize = 64;
|
|||||||
const GAP_US: u32 = 2800;
|
const GAP_US: u32 = 2800;
|
||||||
const SYNC_US: u32 = 2800;
|
const SYNC_US: u32 = 2800;
|
||||||
|
|
||||||
/// Decoder states
|
/// Decoder states (matches protopirate's SubaruDecoderStep)
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
enum DecoderStep {
|
enum DecoderStep {
|
||||||
Reset,
|
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 {
|
fn decode_counter(kb: &[u8; 8]) -> u16 {
|
||||||
let mut lo: u8 = 0;
|
let mut lo: u8 = 0;
|
||||||
if (kb[4] & 0x40) == 0 { lo |= 0x01; }
|
if (kb[4] & 0x40) == 0 { lo |= 0x01; }
|
||||||
@@ -115,9 +115,7 @@ impl SubaruDecoder {
|
|||||||
((hi as u16) << 8) | (lo as u16)
|
((hi as u16) << 8) | (lo as u16)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a level+duration to the signal, merging with the previous entry
|
/// Append level+duration, merging with previous if same level (prevents HackRF from merging consecutive same-level pulses)
|
||||||
/// if it has the same level. This prevents consecutive same-level pulses
|
|
||||||
/// which would silently merge during HackRF transmission.
|
|
||||||
fn add_level(signal: &mut Vec<LevelDuration>, level: bool, duration: u32) {
|
fn add_level(signal: &mut Vec<LevelDuration>, level: bool, duration: u32) {
|
||||||
if let Some(last) = signal.last_mut() {
|
if let Some(last) = signal.last_mut() {
|
||||||
if last.level == level {
|
if last.level == level {
|
||||||
@@ -128,15 +126,13 @@ impl SubaruDecoder {
|
|||||||
signal.push(LevelDuration::new(level, duration));
|
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<DecodedSignal> {
|
fn process_data(&self) -> Option<DecodedSignal> {
|
||||||
if self.bit_count < 64 {
|
if self.bit_count < 64 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let b = &self.data;
|
let b = &self.data;
|
||||||
|
|
||||||
// Build 64-bit key
|
|
||||||
let key = ((b[0] as u64) << 56) | ((b[1] as u64) << 48) |
|
let key = ((b[0] as u64) << 56) | ((b[1] as u64) << 48) |
|
||||||
((b[2] as u64) << 40) | ((b[3] as u64) << 32) |
|
((b[2] as u64) << 40) | ((b[3] as u64) << 32) |
|
||||||
((b[4] as u64) << 24) | ((b[5] as u64) << 16) |
|
((b[4] as u64) << 24) | ((b[5] as u64) << 16) |
|
||||||
@@ -297,37 +293,24 @@ impl ProtocolDecoder for SubaruDecoder {
|
|||||||
let key = decoded.data;
|
let key = decoded.data;
|
||||||
let mut signal = Vec::with_capacity(512);
|
let mut signal = Vec::with_capacity(512);
|
||||||
|
|
||||||
// Generate 3 bursts.
|
// 3 bursts; add_level() merges same-level pulses for correct HackRF timing (matches protopirate subaru encode)
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
for burst in 0..3 {
|
for burst in 0..3 {
|
||||||
if burst > 0 {
|
if burst > 0 {
|
||||||
// Inter-burst silence
|
|
||||||
Self::add_level(&mut signal, false, 25000);
|
Self::add_level(&mut signal, false, 25000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preamble: 79 full pairs + 80th HIGH only.
|
// Preamble: 79 full 1600µs pairs + 80th HIGH only; gap replaces 80th LOW
|
||||||
// The gap LOW replaces the 80th preamble LOW.
|
|
||||||
for i in 0..80 {
|
for i in 0..80 {
|
||||||
Self::add_level(&mut signal, true, TE_LONG);
|
Self::add_level(&mut signal, true, TE_LONG);
|
||||||
if i < 79 {
|
if i < 79 {
|
||||||
Self::add_level(&mut signal, false, TE_LONG);
|
Self::add_level(&mut signal, false, TE_LONG);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gap (replaces the 80th preamble LOW)
|
|
||||||
Self::add_level(&mut signal, false, GAP_US);
|
Self::add_level(&mut signal, false, GAP_US);
|
||||||
|
|
||||||
// Sync
|
|
||||||
Self::add_level(&mut signal, true, SYNC_US);
|
Self::add_level(&mut signal, true, SYNC_US);
|
||||||
Self::add_level(&mut signal, false, TE_LONG);
|
Self::add_level(&mut signal, false, TE_LONG);
|
||||||
|
|
||||||
// Data: 64 bits (MSB first)
|
// Data: 64 bits MSB first; short HIGH = 1, long HIGH = 0; LOW = 800µs after each
|
||||||
// Short HIGH = 1, Long HIGH = 0
|
|
||||||
for bit in (0..64).rev() {
|
for bit in (0..64).rev() {
|
||||||
if (key >> bit) & 1 == 1 {
|
if (key >> bit) & 1 == 1 {
|
||||||
Self::add_level(&mut signal, true, TE_SHORT);
|
Self::add_level(&mut signal, true, TE_SHORT);
|
||||||
@@ -337,10 +320,15 @@ impl ProtocolDecoder for SubaruDecoder {
|
|||||||
Self::add_level(&mut signal, false, TE_SHORT);
|
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);
|
Self::add_level(&mut signal, false, TE_LONG * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(signal)
|
Some(signal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for SubaruDecoder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+17
-12
@@ -1,12 +1,13 @@
|
|||||||
//! Suzuki protocol decoder/encoder
|
//! 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:
|
//! Protocol characteristics:
|
||||||
//! - PWM encoding: 250/500µs timing
|
//! - PWM encoding: 250µs HIGH = 0, 500µs HIGH = 1; LOW 250µs after each bit
|
||||||
//! - 64 bits total
|
//! - 64 bits total; preamble: 300+ short LOW pulses then long HIGH starts data
|
||||||
//! - 350 preamble pairs
|
//! - 350 preamble pairs (SHORT HIGH / SHORT LOW); 2000µs gap at end
|
||||||
//! - 2000µs gap between transmissions
|
//! - 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 super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||||
use crate::duration_diff;
|
use crate::duration_diff;
|
||||||
@@ -20,7 +21,7 @@ const PREAMBLE_COUNT: u16 = 350;
|
|||||||
const GAP_TIME: u32 = 2000;
|
const GAP_TIME: u32 = 2000;
|
||||||
const GAP_DELTA: u32 = 399;
|
const GAP_DELTA: u32 = 399;
|
||||||
|
|
||||||
/// Decoder states
|
/// Decoder states (matches protopirate's SuzukiDecoderStep)
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
enum DecoderStep {
|
enum DecoderStep {
|
||||||
Reset,
|
Reset,
|
||||||
@@ -53,10 +54,11 @@ impl SuzukiDecoder {
|
|||||||
self.decode_count_bit += 1;
|
self.decode_count_bit += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse 64-bit data (matches suzuki.c: serial, btn, cnt layout)
|
||||||
fn parse_data(data: u64) -> DecodedSignal {
|
fn parse_data(data: u64) -> DecodedSignal {
|
||||||
let data_high = (data >> 32) as u32;
|
let data_high = (data >> 32) as u32;
|
||||||
let data_low = data 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 serial = ((data_high & 0xFFF) << 16) | (data_low >> 16);
|
||||||
let btn = ((data_low >> 12) & 0xF) as u8;
|
let btn = ((data_low >> 12) & 0xF) as u8;
|
||||||
let cnt = ((data_high << 4) >> 16) as u16;
|
let cnt = ((data_high << 4) >> 16) as u16;
|
||||||
@@ -178,14 +180,12 @@ impl ProtocolDecoder for SuzukiDecoder {
|
|||||||
|
|
||||||
let mut signal = Vec::with_capacity(1024);
|
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 {
|
for _ in 0..PREAMBLE_COUNT {
|
||||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||||
}
|
}
|
||||||
|
// Data: 64 bits MSB first; SHORT HIGH = 0, LONG HIGH = 1; LOW = 250µs after each
|
||||||
// Data: 64 bits, MSB first
|
|
||||||
// SHORT HIGH (~250µs) = 0, LONG HIGH (~500µs) = 1
|
|
||||||
for bit in (0..64).rev() {
|
for bit in (0..64).rev() {
|
||||||
if (data >> bit) & 1 == 1 {
|
if (data >> bit) & 1 == 1 {
|
||||||
signal.push(LevelDuration::new(true, TE_LONG));
|
signal.push(LevelDuration::new(true, TE_LONG));
|
||||||
@@ -195,9 +195,14 @@ impl ProtocolDecoder for SuzukiDecoder {
|
|||||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||||
}
|
}
|
||||||
|
|
||||||
// End gap
|
|
||||||
signal.push(LevelDuration::new(false, GAP_TIME));
|
signal.push(LevelDuration::new(false, GAP_TIME));
|
||||||
|
|
||||||
Some(signal)
|
Some(signal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for SuzukiDecoder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+27
-23
@@ -1,17 +1,15 @@
|
|||||||
//! VAG (VW/Audi/Seat/Skoda) protocol decoder/encoder
|
//! 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:
|
//! Protocol characteristics:
|
||||||
//! - Manchester encoding
|
//! - Manchester encoding; 80 bits (key1 64 + key2 16)
|
||||||
//! - Type 1/2: 300/600µs timing (te_short=300), 80 bits, AUT64 or TEA encryption
|
//! - Type 1/2: 300/600µs, prefix 0x2F3F (AUT64) / 0x2F1C (TEA), dispatch 0x2A/0x1C/0x46
|
||||||
//! - Type 3/4: 500/1000µs timing (te_short=500), 80 bits, AUT64 encryption
|
//! - Type 3/4: 500/1000µs, preamble 41+ pairs, sync 1000µs + 3×750µs, dispatch 0x2B/0x1D/0x47
|
||||||
//! - 433.92 MHz and 434.42 MHz
|
//! - End-of-data gap 6000µs (accept within 4000µs); keys from keystore (VAG raw 64 bytes)
|
||||||
//! - 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)
|
|
||||||
|
|
||||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||||
use super::aut64;
|
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_DELTA: u32 = 0x9E3779B9;
|
||||||
const TEA_ROUNDS: usize = 32;
|
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];
|
static TEA_KEY_SCHEDULE: [u32; 4] = [0x0B46502D, 0x5E253718, 0x2BF93A19, 0x622C1206];
|
||||||
|
|
||||||
/// Manchester states
|
/// Manchester states
|
||||||
@@ -56,17 +54,17 @@ enum ManchesterEvent {
|
|||||||
Reset,
|
Reset,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decoder states
|
/// Decoder states (matches protopirate's VAGDecoderStep)
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
enum DecoderStep {
|
enum DecoderStep {
|
||||||
Reset,
|
Reset,
|
||||||
Preamble1, // Type 1/2: ~300µs preamble
|
Preamble1,
|
||||||
Data1, // Type 1/2: Manchester data
|
Data1,
|
||||||
Preamble2, // Type 3/4: ~500µs preamble
|
Preamble2,
|
||||||
Sync2A, // Type 3/4: sync pattern step A
|
Sync2A,
|
||||||
Sync2B, // Type 3/4: sync pattern step B (750µs)
|
Sync2B,
|
||||||
Sync2C, // Type 3/4: sync pattern step C (750µs)
|
Sync2C,
|
||||||
Data2, // Type 3/4: Manchester data
|
Data2,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// VAG sub-type
|
/// VAG sub-type
|
||||||
@@ -182,7 +180,7 @@ impl VagDecoder {
|
|||||||
self.bit_count += 1;
|
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]) {
|
fn tea_decrypt(v0: &mut u32, v1: &mut u32, key_schedule: &[u32; 4]) {
|
||||||
let mut sum = TEA_DELTA.wrapping_mul(TEA_ROUNDS as u32);
|
let mut sum = TEA_DELTA.wrapping_mul(TEA_ROUNDS as u32);
|
||||||
for _ in 0..TEA_ROUNDS {
|
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]) {
|
fn tea_encrypt(v0: &mut u32, v1: &mut u32, key_schedule: &[u32; 4]) {
|
||||||
let mut sum: u32 = 0;
|
let mut sum: u32 = 0;
|
||||||
for _ in 0..TEA_ROUNDS {
|
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) {
|
fn parse_data(&mut self) {
|
||||||
self.decrypted = false;
|
self.decrypted = false;
|
||||||
self.serial = 0;
|
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 {
|
if !level {
|
||||||
let gap_diff = if duration > 6000 {
|
let gap_diff = if duration > 6000 {
|
||||||
duration - 6000
|
duration - 6000
|
||||||
@@ -1145,3 +1143,9 @@ impl ProtocolDecoder for VagDecoder {
|
|||||||
self.encode_signal(decoded)
|
self.encode_signal(decoded)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for VagDecoder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
//! AM/OOK demodulator for extracting level+duration pairs from raw IQ samples.
|
//! 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
|
//! 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
|
//! that can be processed by protocol decoders, similar to how the Flipper Zero SubGHz
|
||||||
//! system works.
|
//! system works.
|
||||||
|
|||||||
+17
-1
@@ -29,6 +29,8 @@ pub struct Config {
|
|||||||
pub export_directory: PathBuf,
|
pub export_directory: PathBuf,
|
||||||
/// Maximum captures to keep in memory during a session
|
/// Maximum captures to keep in memory during a session
|
||||||
pub max_captures: usize,
|
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]
|
// [radio]
|
||||||
/// Default frequency in Hz
|
/// Default frequency in Hz
|
||||||
@@ -54,6 +56,7 @@ impl Config {
|
|||||||
Self {
|
Self {
|
||||||
export_directory: config_dir.join("exports"),
|
export_directory: config_dir.join("exports"),
|
||||||
max_captures: 100,
|
max_captures: 100,
|
||||||
|
research_mode: false,
|
||||||
default_frequency: 433_920_000,
|
default_frequency: 433_920_000,
|
||||||
default_lna_gain: 24,
|
default_lna_gain: 24,
|
||||||
default_vga_gain: 20,
|
default_vga_gain: 20,
|
||||||
@@ -83,6 +86,12 @@ impl Config {
|
|||||||
.map(|v| v as usize)
|
.map(|v| v as usize)
|
||||||
.unwrap_or(defaults.max_captures);
|
.unwrap_or(defaults.max_captures);
|
||||||
|
|
||||||
|
let research_mode = ini
|
||||||
|
.getbool("general", "research_mode")
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or(defaults.research_mode);
|
||||||
|
|
||||||
let default_frequency = ini
|
let default_frequency = ini
|
||||||
.getuint("radio", "default_frequency")
|
.getuint("radio", "default_frequency")
|
||||||
.ok()
|
.ok()
|
||||||
@@ -123,6 +132,7 @@ impl Config {
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
export_directory,
|
export_directory,
|
||||||
max_captures,
|
max_captures,
|
||||||
|
research_mode,
|
||||||
default_frequency,
|
default_frequency,
|
||||||
default_lna_gain,
|
default_lna_gain,
|
||||||
default_vga_gain,
|
default_vga_gain,
|
||||||
@@ -154,6 +164,10 @@ export_directory = {export_dir}
|
|||||||
; signals (.fob / .sub) survive in the exports folder.
|
; signals (.fob / .sub) survive in the exports folder.
|
||||||
max_captures = {max_captures}
|
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]
|
[radio]
|
||||||
; Default receive frequency in Hz ({freq_mhz:.2} MHz)
|
; Default receive frequency in Hz ({freq_mhz:.2} MHz)
|
||||||
; Common keyfob frequencies: 315000000, 433920000, 868350000
|
; Common keyfob frequencies: 315000000, 433920000, 868350000
|
||||||
@@ -179,6 +193,7 @@ include_raw_pairs = {raw_pairs}
|
|||||||
path = path.display(),
|
path = path.display(),
|
||||||
export_dir = export_str,
|
export_dir = export_str,
|
||||||
max_captures = self.max_captures,
|
max_captures = self.max_captures,
|
||||||
|
research_mode = self.research_mode,
|
||||||
freq_mhz = freq_mhz,
|
freq_mhz = freq_mhz,
|
||||||
frequency = self.default_frequency,
|
frequency = self.default_frequency,
|
||||||
lna = self.default_lna_gain,
|
lna = self.default_lna_gain,
|
||||||
@@ -342,7 +357,8 @@ impl Storage {
|
|||||||
&self.config.export_directory
|
&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 {
|
pub fn keystore_dir(&self) -> PathBuf {
|
||||||
self.config_dir.join("keystore")
|
self.config_dir.join("keystore")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,12 +158,14 @@ fn render_detail_panel(frame: &mut Frame, area: Rect, app: &App) {
|
|||||||
Span::styled(make, value_style),
|
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![
|
left_lines.push(Line::from(vec![
|
||||||
Span::styled(" Freq: ", label_style),
|
Span::styled(" Freq: ", label_style),
|
||||||
Span::styled(capture.frequency_mhz(), value_style),
|
Span::styled(capture.frequency_mhz(), value_style),
|
||||||
Span::styled(" Mod: ", label_style),
|
Span::styled(" Mod: ", label_style),
|
||||||
Span::styled(capture.modulation().to_string(), value_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(" Enc: ", label_style),
|
||||||
Span::styled(capture.encryption_type(), value_style),
|
Span::styled(capture.encryption_type(), value_style),
|
||||||
]));
|
]));
|
||||||
|
|||||||
Reference in New Issue
Block a user