Protocol updates

This commit is contained in:
leviathan
2026-02-11 21:59:30 -05:00
parent 0767850a6f
commit 9e2565c1fc
30 changed files with 772 additions and 507 deletions
Vendored
BIN
View File
Binary file not shown.
+103 -87
View File
@@ -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 V0V6, 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. KATs 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
View File
@@ -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
View File
@@ -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() {
+7
View File
@@ -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(),
+76
View File
@@ -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,
];
+53
View File
@@ -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
}
+1
View File
@@ -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;
+7 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 300450µ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
+41 -47
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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()
} }
} }
+3 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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()
} }
} }
+17 -9
View File
@@ -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
View File
@@ -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 47
//! - 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 47 (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 13), 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
View File
@@ -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
View File
@@ -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()
}
}
+5
View File
@@ -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
View File
@@ -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")
} }
+3 -1
View File
@@ -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),
])); ]));