Protocol updates
This commit is contained in:
+19
-18
@@ -243,10 +243,8 @@ impl App {
|
||||
pub fn new() -> Result<Self> {
|
||||
let storage = Storage::new()?;
|
||||
|
||||
// ── Load protocol encryption keys from keystore ──────────────────
|
||||
let keystore_dir = storage.keystore_dir();
|
||||
crate::protocols::keys::create_default_keystore(&keystore_dir);
|
||||
crate::protocols::keys::load_keystore_from_dir(&keystore_dir);
|
||||
// ── Load protocol encryption keys from embedded keystore ─────────
|
||||
crate::protocols::keys::load_keystore_from_embedded();
|
||||
|
||||
let protocols = ProtocolRegistry::new();
|
||||
let (radio_event_tx, radio_event_rx) = mpsc::channel();
|
||||
@@ -705,10 +703,6 @@ impl App {
|
||||
while let Ok(event) = self.radio_event_rx.try_recv() {
|
||||
match event {
|
||||
RadioEvent::SignalCaptured(mut capture) => {
|
||||
// Assign ID
|
||||
capture.id = self.next_capture_id;
|
||||
self.next_capture_id += 1;
|
||||
|
||||
// Convert stored pairs to the format protocols expect
|
||||
let pairs: Vec<crate::radio::LevelDuration> = capture.raw_pairs
|
||||
.iter()
|
||||
@@ -731,16 +725,23 @@ impl App {
|
||||
};
|
||||
}
|
||||
|
||||
// Captures are in-memory only — no auto-save to disk.
|
||||
// Use Export (.fob / .sub) to persist a signal.
|
||||
self.captures.push(capture);
|
||||
|
||||
// Auto-select and scroll to new capture
|
||||
let new_idx = self.captures.len() - 1;
|
||||
self.selected_capture = Some(new_idx);
|
||||
self.ensure_selection_visible();
|
||||
|
||||
self.status_message = Some("New signal captured".to_string());
|
||||
// When research_mode is off, only add successfully decoded signals.
|
||||
let show = self.storage.config.research_mode || capture.protocol.is_some();
|
||||
if show {
|
||||
capture.id = self.next_capture_id;
|
||||
self.next_capture_id += 1;
|
||||
// Captures are in-memory only — no auto-save to disk.
|
||||
// Use Export (.fob / .sub) to persist a signal.
|
||||
self.captures.push(capture);
|
||||
|
||||
// Auto-select and scroll to new capture
|
||||
let new_idx = self.captures.len() - 1;
|
||||
self.selected_capture = Some(new_idx);
|
||||
self.ensure_selection_visible();
|
||||
|
||||
self.status_message = Some("New signal captured".to_string());
|
||||
}
|
||||
// When research_mode is off and decode failed, the signal is dropped (not shown).
|
||||
}
|
||||
RadioEvent::Error(e) => {
|
||||
self.last_error = Some(e);
|
||||
|
||||
+48
-1
@@ -60,7 +60,7 @@ pub struct Capture {
|
||||
pub status: CaptureStatus,
|
||||
}
|
||||
|
||||
/// Modulation type used by protocol
|
||||
/// Modulation type used by protocol (encoding: PWM vs Manchester)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ModulationType {
|
||||
Unknown,
|
||||
@@ -81,6 +81,28 @@ impl std::fmt::Display for ModulationType {
|
||||
}
|
||||
}
|
||||
|
||||
/// RF modulation (carrier): AM/OOK vs FM/2FSK. From ProtoPirate SubGhzProtocolFlag_AM / _FM.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum RfModulation {
|
||||
AM,
|
||||
FM,
|
||||
/// Protocol used with both AM and FM (e.g. Kia V3/V4)
|
||||
Both,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RfModulation {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
RfModulation::AM => write!(f, "AM"),
|
||||
RfModulation::FM => write!(f, "FM"),
|
||||
RfModulation::Both => write!(f, "AM/FM"),
|
||||
RfModulation::Unknown => write!(f, "—"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Capture {
|
||||
/// Create a new capture from level+duration pairs
|
||||
pub fn from_pairs(id: u32, frequency: u32, pairs: Vec<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
|
||||
pub fn encryption_type(&self) -> &'static str {
|
||||
match self.protocol_name() {
|
||||
|
||||
@@ -34,6 +34,9 @@ pub struct FobSignalInfo {
|
||||
pub frequency: u32,
|
||||
pub frequency_mhz: String,
|
||||
pub modulation: String,
|
||||
/// RF carrier modulation: AM, FM, or AM/FM (from ProtoPirate). KAT receives AM only.
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub rf_modulation: Option<String>,
|
||||
pub encryption: String,
|
||||
pub data_bits: usize,
|
||||
pub data_hex: String,
|
||||
@@ -140,6 +143,10 @@ pub fn export_fob(
|
||||
frequency: capture.frequency,
|
||||
frequency_mhz: capture.frequency_mhz(),
|
||||
modulation: capture.modulation().to_string(),
|
||||
rf_modulation: match capture.rf_modulation() {
|
||||
crate::capture::RfModulation::Unknown => None,
|
||||
r => Some(r.to_string()),
|
||||
},
|
||||
encryption: capture.encryption_type().to_string(),
|
||||
data_bits: capture.data_count_bit,
|
||||
data_hex: capture.data_hex(),
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
//! Embedded keystore blob (standard encrypted + VAG raw).
|
||||
//! Data is stored as binary to avoid plain-text keys in config.
|
||||
//! Format: "KATK" magic, n_entries (u16 LE), then per entry: type_id (u32 LE), key (u64 LE), then "VAG " + 64 bytes.
|
||||
//! All manufacture keys from the encrypted keystore list are included for current and future protocols.
|
||||
//! Type 20 (Star Line) is also included so KeyStore.star_line_mf_key is populated from the same key as SL_A2-A4.
|
||||
|
||||
/// Blob: KATK + 30 entries (all manufacture keys + type 20 for Star Line) + VAG 64 bytes.
|
||||
#[rustfmt::skip]
|
||||
pub const KEYSTORE_BLOB: &[u8] = &[
|
||||
b'K', b'A', b'T', b'K',
|
||||
0x1E, 0x00, // 30 entries
|
||||
// type 10 KIA
|
||||
0x0A, 0x00, 0x00, 0x00, 0xDB, 0x5C, 0xAA, 0x8D, 0xFC, 0xDF, 0xF5, 0xA8,
|
||||
// type 11 KIAV6A
|
||||
0x0B, 0x00, 0x00, 0x00, 0xFC, 0x23, 0xAA, 0x80, 0xA8, 0x64, 0x86, 0x63,
|
||||
// type 12 KIAV6B
|
||||
0x0C, 0x00, 0x00, 0x00, 0xF4, 0x2B, 0xA2, 0x88, 0xA0, 0x6C, 0x8E, 0x6B,
|
||||
// type 13 KIAV5
|
||||
0x0D, 0x00, 0x00, 0x00, 0x30, 0x30, 0x45, 0x4B, 0x52, 0x46, 0x54, 0x53,
|
||||
// type 1 Alligator
|
||||
0x01, 0x00, 0x00, 0x00, 0x78, 0x55, 0x6C, 0x62, 0xB2, 0xD4, 0xF3, 0xEE,
|
||||
// type 2 Mongoose
|
||||
0x02, 0x00, 0x00, 0x00, 0xBA, 0xA0, 0xC1, 0xA7, 0xB2, 0xE7, 0xA5, 0xD5,
|
||||
// type 1 SL_A6-A9/Tomahawk_9010
|
||||
0x01, 0x00, 0x00, 0x00, 0x03, 0x12, 0x30, 0x08, 0x57, 0x04, 0x04, 0x55,
|
||||
// type 1 Pantera
|
||||
0x01, 0x00, 0x00, 0x00, 0x65, 0x6E, 0x79, 0x64, 0x4E, 0x47, 0x41, 0x4D,
|
||||
// type 1 SL_A2-A4
|
||||
0x01, 0x00, 0x00, 0x00, 0xDA, 0x78, 0xFE, 0xF8, 0x9B, 0xF8, 0xF7, 0x9B,
|
||||
// type 1 Cenmax_St-5
|
||||
0x01, 0x00, 0x00, 0x00, 0xFC, 0xED, 0xCF, 0xF7, 0xA8, 0xFB, 0xFB, 0xAA,
|
||||
// type 1 SL_B6,B9_dop
|
||||
0x01, 0x00, 0x00, 0x00, 0xFE, 0xDC, 0xBA, 0x2A, 0xB3, 0xD4, 0xE5, 0xF6,
|
||||
// type 1 Harpoon
|
||||
0x01, 0x00, 0x00, 0x00, 0x64, 0x08, 0x07, 0x64, 0x07, 0x01, 0x87, 0x25,
|
||||
// type 1 Tomahawk_TZ-9030
|
||||
0x01, 0x00, 0x00, 0x00, 0x25, 0x87, 0x01, 0x07, 0x64, 0x07, 0x08, 0x64,
|
||||
// type 1 Tomahawk_Z,X_3-5
|
||||
0x01, 0x00, 0x00, 0x00, 0xEF, 0xCD, 0xAB, 0x2A, 0x3B, 0x4D, 0x5E, 0x6F,
|
||||
// type 1 Cenmax_St-7
|
||||
0x01, 0x00, 0x00, 0x00, 0x2B, 0x75, 0xAC, 0x80, 0xB9, 0x46, 0xB4, 0x66,
|
||||
// type 1 Sheriff
|
||||
0x01, 0x00, 0x00, 0x00, 0x78, 0x94, 0x18, 0xA9, 0x94, 0x25, 0x33, 0x12,
|
||||
// type 1 Pantera_CLK
|
||||
0x01, 0x00, 0x00, 0x00, 0x14, 0x0A, 0x61, 0x82, 0x0A, 0x85, 0x30, 0x41,
|
||||
// type 1 Cenmax
|
||||
0x01, 0x00, 0x00, 0x00, 0x56, 0x34, 0x12, 0x90, 0x78, 0x56, 0x34, 0x12,
|
||||
// type 1 Alligator_S-275
|
||||
0x01, 0x00, 0x00, 0x00, 0xF9, 0x84, 0x62, 0x67, 0xB7, 0x5C, 0x36, 0x54,
|
||||
// type 2 Guard_RF-311A
|
||||
0x02, 0x00, 0x00, 0x00, 0x47, 0x88, 0x19, 0xEB, 0xC2, 0xDA, 0x61, 0x19,
|
||||
// type 1 Partisan_RX
|
||||
0x01, 0x00, 0x00, 0x00, 0x1E, 0xF1, 0x28, 0x70, 0xA1, 0x12, 0x3C, 0xAD,
|
||||
// type 1 APS-1100_APS-2550
|
||||
0x01, 0x00, 0x00, 0x00, 0x28, 0x47, 0x33, 0x56, 0x50, 0x2C, 0x73, 0x68,
|
||||
// type 1 Pantera_XS/Jaguar
|
||||
0x01, 0x00, 0x00, 0x00, 0x27, 0x46, 0x33, 0x56, 0x50, 0x2C, 0x73, 0x67,
|
||||
// type 0 Teco
|
||||
0x00, 0x00, 0x00, 0x00, 0xD7, 0xD1, 0x26, 0xF1, 0xE8, 0x26, 0x15, 0xB5,
|
||||
// type 0 Leopard
|
||||
0x00, 0x00, 0x00, 0x00, 0x46, 0x8A, 0xD8, 0xE7, 0x33, 0xDC, 0x67, 0x10,
|
||||
// type 0 Faraon
|
||||
0x00, 0x00, 0x00, 0x00, 0xF5, 0x87, 0x22, 0xB6, 0x38, 0x64, 0x32, 0x8A,
|
||||
// type 0 Reff
|
||||
0x00, 0x00, 0x00, 0x00, 0x8A, 0x41, 0xC2, 0x7D, 0x55, 0xFA, 0x12, 0x69,
|
||||
// type 0 ZX-730-750-1055
|
||||
0x00, 0x00, 0x00, 0x00, 0x96, 0x15, 0x35, 0x27, 0xAB, 0x7A, 0x1A, 0xB5,
|
||||
// type 20 Star Line (same key as SL_A2-A4, for KeyStore.star_line_mf_key)
|
||||
0x14, 0x00, 0x00, 0x00, 0xDA, 0x78, 0xFE, 0xF8, 0x9B, 0xF8, 0xF7, 0x9B,
|
||||
b'V', b'A', b'G', b' ',
|
||||
// VAG raw 64 bytes
|
||||
0x01, 0x37, 0x6C, 0x86, 0xAD, 0xAB, 0xCC, 0x43, 0x07, 0x4D, 0xE8, 0x59, 0xC1, 0x2F, 0x36, 0xAB,
|
||||
0x02, 0x37, 0x7C, 0x65, 0xCE, 0xDC, 0x42, 0xEA, 0xA4, 0x53, 0xE8, 0x61, 0xD9, 0xB7, 0x20, 0xFC,
|
||||
0x03, 0x8A, 0xA3, 0x7B, 0x1E, 0x56, 0x1F, 0x83, 0x84, 0xB6, 0x19, 0xC5, 0x2E, 0x0A, 0x3F, 0xD7,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
];
|
||||
@@ -0,0 +1,53 @@
|
||||
//! Embedded keystore: binary blob of protocol keys (KIA, Star Line, VAG).
|
||||
//! Matches ProtoPirate key types (keys.c: KIA_KEY1..4). Loaded at startup instead of keystore_dir.
|
||||
|
||||
mod embedded;
|
||||
|
||||
use std::convert::TryInto;
|
||||
|
||||
const MAGIC: &[u8; 4] = b"KATK";
|
||||
const VAG_TAG: &[u8; 4] = b"VAG ";
|
||||
const VAG_SIZE: usize = 64;
|
||||
const ENTRY_SIZE: usize = 4 + 8; // u32 type + u64 key
|
||||
|
||||
/// Parsed result from the embedded blob: (type_id, key) pairs and raw VAG bytes.
|
||||
pub struct ParsedKeystore {
|
||||
pub entries: Vec<(u32, u64)>,
|
||||
pub vag_bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Parse the embedded keystore blob. Returns KIA/Star Line entries and VAG raw bytes.
|
||||
pub fn parse_blob(blob: &[u8]) -> Option<ParsedKeystore> {
|
||||
if blob.len() < 4 || &blob[0..4] != MAGIC {
|
||||
return None;
|
||||
}
|
||||
let n = u16::from_le_bytes(blob[4..6].try_into().ok()?) as usize;
|
||||
let mut off = 6;
|
||||
let mut entries = Vec::with_capacity(n);
|
||||
for _ in 0..n {
|
||||
if off + ENTRY_SIZE > blob.len() {
|
||||
return None;
|
||||
}
|
||||
let ty = u32::from_le_bytes(blob[off..off + 4].try_into().ok()?);
|
||||
let key = u64::from_le_bytes(blob[off + 4..off + 12].try_into().ok()?);
|
||||
entries.push((ty, key));
|
||||
off += ENTRY_SIZE;
|
||||
}
|
||||
if off + 4 + VAG_SIZE > blob.len() || &blob[off..off + 4] != VAG_TAG {
|
||||
return Some(ParsedKeystore {
|
||||
entries,
|
||||
vag_bytes: Vec::new(),
|
||||
});
|
||||
}
|
||||
off += 4;
|
||||
let vag_bytes = blob[off..off + VAG_SIZE].to_vec();
|
||||
Some(ParsedKeystore {
|
||||
entries,
|
||||
vag_bytes,
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the embedded keystore blob for loading.
|
||||
pub fn embedded_blob() -> &'static [u8] {
|
||||
embedded::KEYSTORE_BLOB
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
mod app;
|
||||
mod capture;
|
||||
mod export;
|
||||
mod keystore;
|
||||
mod protocols;
|
||||
mod radio;
|
||||
mod storage;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! AUT64 block cipher implementation
|
||||
//!
|
||||
//! Ported from protopirate's aut64.c
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/aut64.c`.
|
||||
//! Encrypt/decrypt, pack/unpack, and all tables match the reference.
|
||||
//!
|
||||
//! AUT64 algorithm: 12 rounds, 8-byte block/key size
|
||||
//! Based on: Reference AUT64 implementation
|
||||
//! AUT64 algorithm: 12 rounds, 8-byte block/key size.
|
||||
//! See: https://www.usenix.org/system/files/conference/usenixsecurity16/sec16_paper_garcia.pdf
|
||||
|
||||
pub const AUT64_NUM_ROUNDS: usize = 12;
|
||||
@@ -265,7 +265,7 @@ pub fn aut64_pack(src: &Aut64Key) -> [u8; AUT64_KEY_STRUCT_PACKED_SIZE] {
|
||||
dest
|
||||
}
|
||||
|
||||
/// Unpack a 16-byte array into an AUT64 key structure
|
||||
/// Unpack a 16-byte array into an AUT64 key structure (matches aut64_unpack in reference)
|
||||
#[allow(dead_code)]
|
||||
pub fn aut64_unpack(src: &[u8]) -> Aut64Key {
|
||||
let mut dest = Aut64Key::default();
|
||||
@@ -276,9 +276,10 @@ pub fn aut64_unpack(src: &[u8]) -> Aut64Key {
|
||||
dest.key[i * 2 + 1] = src[i + 1] & 0xF;
|
||||
}
|
||||
|
||||
let pbox: u32 = ((src[5] as u32) << 16) | ((src[6] as u32) << 8) | src[7] as u32;
|
||||
let mut pbox: u32 = (u32::from(src[5]) << 16) | (u32::from(src[6]) << 8) | u32::from(src[7]);
|
||||
for i in (0..dest.pbox.len()).rev() {
|
||||
dest.pbox[i] = ((pbox >> ((dest.pbox.len() - 1 - i) * 3)) & 0x7) as u8;
|
||||
dest.pbox[i] = (pbox & 0x7) as u8;
|
||||
pbox >>= 3;
|
||||
}
|
||||
|
||||
for i in 0..(dest.sbox.len() / 2) {
|
||||
|
||||
+13
-6
@@ -1,4 +1,11 @@
|
||||
//! Common utilities for protocol implementations.
|
||||
//!
|
||||
//! The ProtoPirate reference has `REFERENCES/ProtoPirate/protocols/protocols_common.c`, which
|
||||
//! only provides Flipper preset name mapping (`protopirate_get_short_preset_name`). KAT does not
|
||||
//! use that; this module holds shared types and helpers used by multiple protocol decoders.
|
||||
//! Where applicable, algorithms match the reference: e.g. `crc8_kia` matches `kia_crc8` in
|
||||
//! kia_v0.c (polynomial 0x7F, init 0x00); `add_bit` matches the common shift-left-and-append
|
||||
//! pattern used in the reference decoders.
|
||||
|
||||
/// Decoded signal information
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -34,12 +41,12 @@ impl DecodedSignal {
|
||||
}
|
||||
}
|
||||
|
||||
/// CRC8 calculation with custom polynomial
|
||||
///
|
||||
/// CRC8 calculation with custom polynomial (MSB-first, shift-left style).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `data` - Data bytes to calculate CRC over
|
||||
/// * `poly` - CRC polynomial
|
||||
/// * `init` - Initial CRC value
|
||||
/// * `poly` - CRC polynomial (e.g. 0x7F for Kia)
|
||||
/// * `init` - Initial CRC value (e.g. 0x00)
|
||||
pub fn crc8(data: &[u8], poly: u8, init: u8) -> u8 {
|
||||
let mut crc = init;
|
||||
for &byte in data {
|
||||
@@ -55,12 +62,12 @@ pub fn crc8(data: &[u8], poly: u8, init: u8) -> u8 {
|
||||
crc
|
||||
}
|
||||
|
||||
/// CRC8 for Kia protocol (polynomial 0x7F, init 0x00)
|
||||
/// CRC8 for Kia protocol (matches kia_v0.c kia_crc8: polynomial 0x7F, init 0x00)
|
||||
pub fn crc8_kia(data: &[u8]) -> u8 {
|
||||
crc8(data, 0x7F, 0x00)
|
||||
}
|
||||
|
||||
/// Add a bit to the decoder's data accumulator
|
||||
/// Add a bit to the decoder's data accumulator (shift-left, LSB last; matches reference add_bit pattern)
|
||||
#[inline]
|
||||
pub fn add_bit(data: &mut u64, count: &mut usize, bit: bool) {
|
||||
*data = (*data << 1) | (bit as u64);
|
||||
|
||||
+82
-62
@@ -1,11 +1,12 @@
|
||||
//! Fiat V0 protocol decoder/encoder
|
||||
//!
|
||||
//! Ported from protopirate's fiat_v0.c
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/fiat_v0.c` (Flipper).
|
||||
//! Decode/encode logic (preamble, gap, Manchester, data/btn extraction, upload waveform) matches reference.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - Differential Manchester encoding: 200/400µs timing
|
||||
//! - 64-bit data (cnt:32 | serial:32) + 6-bit button
|
||||
//! - 150 preamble pairs, 800µs gap, 3 bursts
|
||||
//! - 150 preamble pairs (count LOW pulses), 800µs gap, 3 bursts
|
||||
|
||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||
use crate::duration_diff;
|
||||
@@ -16,21 +17,21 @@ const TE_LONG: u32 = 400;
|
||||
const TE_DELTA: u32 = 100;
|
||||
#[allow(dead_code)]
|
||||
const MIN_COUNT_BIT: usize = 64;
|
||||
const PREAMBLE_PAIRS: u16 = 150;
|
||||
const PREAMBLE_PAIRS: u16 = 150; // 0x96 in reference
|
||||
const GAP_US: u32 = 800;
|
||||
const TOTAL_BURSTS: u8 = 3;
|
||||
const INTER_BURST_GAP: u32 = 25000;
|
||||
|
||||
/// Manchester decoder states
|
||||
/// Manchester state machine states (matches Flipper's manchester_decoder.h, same as Ford V0)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ManchesterState {
|
||||
Mid0,
|
||||
Mid1,
|
||||
Start0,
|
||||
Start1,
|
||||
Mid0 = 0,
|
||||
Mid1 = 1,
|
||||
Start0 = 2,
|
||||
Start1 = 3,
|
||||
}
|
||||
|
||||
/// Decoder states
|
||||
/// Decoder states (matches protopirate's FiatV0DecoderStep)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
@@ -68,37 +69,46 @@ impl FiatV0Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Manchester advance - returns decoded bit or None
|
||||
fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option<bool> {
|
||||
let event = match (is_short, is_high) {
|
||||
(true, true) => 0,
|
||||
(true, false) => 1,
|
||||
(false, true) => 2,
|
||||
(false, false) => 3,
|
||||
};
|
||||
/// Manchester state machine (same as Ford V0 / Flipper manchester_decoder).
|
||||
/// Event: 0=ShortLow, 1=ShortHigh, 2=LongLow, 3=LongHigh.
|
||||
fn manchester_advance(&mut self, event: u8) -> Option<bool> {
|
||||
let (new_state, emit) = match (self.manchester_state, event) {
|
||||
(ManchesterState::Mid0, 0) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Mid0, 1) => (ManchesterState::Start1, true),
|
||||
(ManchesterState::Mid0, 2) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Mid0, 3) => (ManchesterState::Mid1, true),
|
||||
|
||||
let (new_state, output) = match (self.manchester_state, event) {
|
||||
(ManchesterState::Mid0, 0) | (ManchesterState::Mid1, 0) => {
|
||||
(ManchesterState::Start1, None)
|
||||
}
|
||||
(ManchesterState::Mid0, 1) | (ManchesterState::Mid1, 1) => {
|
||||
(ManchesterState::Start0, None)
|
||||
}
|
||||
(ManchesterState::Start1, 1) => (ManchesterState::Mid1, Some(true)),
|
||||
(ManchesterState::Start1, 3) => (ManchesterState::Start0, Some(true)),
|
||||
(ManchesterState::Start0, 0) => (ManchesterState::Mid0, Some(false)),
|
||||
(ManchesterState::Start0, 2) => (ManchesterState::Start1, Some(false)),
|
||||
_ => (ManchesterState::Mid1, None),
|
||||
(ManchesterState::Mid1, 0) => (ManchesterState::Start0, true),
|
||||
(ManchesterState::Mid1, 1) => (ManchesterState::Mid1, false),
|
||||
(ManchesterState::Mid1, 2) => (ManchesterState::Mid0, true),
|
||||
(ManchesterState::Mid1, 3) => (ManchesterState::Mid1, false),
|
||||
|
||||
(ManchesterState::Start0, 0) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start0, 1) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start0, 2) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start0, 3) => (ManchesterState::Mid1, false),
|
||||
|
||||
(ManchesterState::Start1, 0) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start1, 1) => (ManchesterState::Mid1, false),
|
||||
(ManchesterState::Start1, 2) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start1, 3) => (ManchesterState::Mid1, false),
|
||||
|
||||
_ => (ManchesterState::Mid1, false),
|
||||
};
|
||||
|
||||
self.manchester_state = new_state;
|
||||
output
|
||||
if emit {
|
||||
Some((event & 1) == 1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn manchester_reset(&mut self) {
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
}
|
||||
|
||||
/// Add bit to accumulator; at 64 bits extract serial/cnt and clear data (bit_count unchanged in reference).
|
||||
fn add_manchester_bit(&mut self, bit: bool) {
|
||||
let new_bit = if bit { 1u32 } else { 0u32 };
|
||||
let carry = (self.data_low >> 31) & 1;
|
||||
@@ -162,6 +172,7 @@ impl ProtocolDecoder for FiatV0Decoder {
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
match self.step {
|
||||
// Reset: wait for short HIGH (matches reference)
|
||||
DecoderStep::Reset => {
|
||||
if !level {
|
||||
return None;
|
||||
@@ -177,50 +188,60 @@ impl ProtocolDecoder for FiatV0Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
// Preamble: only process LOW pulses (reference: if(level) return). Count short LOWs; gap = 800µs LOW.
|
||||
DecoderStep::Preamble => {
|
||||
// Count short pulses in preamble, look for gap
|
||||
if duration_diff!(duration, TE_SHORT) < TE_DELTA {
|
||||
if level {
|
||||
return None;
|
||||
}
|
||||
let short_ok = duration_diff!(duration, TE_SHORT) < TE_DELTA;
|
||||
let gap_ok = duration_diff!(duration, GAP_US) < TE_DELTA;
|
||||
|
||||
if short_ok {
|
||||
self.preamble_count += 1;
|
||||
self.te_last = duration;
|
||||
} else if self.preamble_count >= PREAMBLE_PAIRS {
|
||||
// Check for gap
|
||||
if duration_diff!(duration, GAP_US) < TE_DELTA {
|
||||
} else {
|
||||
if self.preamble_count >= PREAMBLE_PAIRS && gap_ok {
|
||||
self.step = DecoderStep::Data;
|
||||
self.preamble_count = 0;
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
self.bit_count = 0;
|
||||
self.te_last = duration;
|
||||
return None;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
// Data: Manchester events — short first, then long (matches reference)
|
||||
DecoderStep::Data => {
|
||||
let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA;
|
||||
let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA;
|
||||
let short_diff = duration_diff!(duration, TE_SHORT);
|
||||
let long_diff = duration_diff!(duration, TE_LONG);
|
||||
|
||||
if is_short || is_long {
|
||||
if let Some(bit) = self.manchester_advance(is_short, level) {
|
||||
self.add_manchester_bit(bit);
|
||||
|
||||
if self.bit_count > 0x46 {
|
||||
self.btn = ((self.data_low << 1) | 1) as u8;
|
||||
let result = self.parse_data();
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
self.bit_count = 0;
|
||||
self.step = DecoderStep::Reset;
|
||||
return Some(result);
|
||||
}
|
||||
let event = if short_diff < TE_DELTA {
|
||||
if level { 0 } else { 1 }
|
||||
} else if long_diff < TE_DELTA {
|
||||
if level { 2 } else { 3 }
|
||||
} else {
|
||||
self.te_last = duration;
|
||||
if duration > TE_LONG * 3 {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
return None;
|
||||
};
|
||||
|
||||
if let Some(bit) = self.manchester_advance(event) {
|
||||
self.add_manchester_bit(bit);
|
||||
|
||||
if self.bit_count > 0x46 {
|
||||
self.btn = ((self.data_low << 1) | 1) as u8;
|
||||
let result = self.parse_data();
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
self.bit_count = 0;
|
||||
self.step = DecoderStep::Reset;
|
||||
return Some(result);
|
||||
}
|
||||
} else if duration > TE_LONG * 3 {
|
||||
// End of signal
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
|
||||
self.te_last = duration;
|
||||
@@ -249,14 +270,13 @@ impl ProtocolDecoder for FiatV0Decoder {
|
||||
signal.push(LevelDuration::new(false, INTER_BURST_GAP));
|
||||
}
|
||||
|
||||
// Preamble
|
||||
// Preamble: 150 HIGH-LOW pairs; last LOW is gap (matches reference get_upload)
|
||||
for i in 0..PREAMBLE_PAIRS {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
if i < PREAMBLE_PAIRS - 1 {
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
} else {
|
||||
signal.push(LevelDuration::new(false, GAP_US));
|
||||
}
|
||||
signal.push(LevelDuration::new(
|
||||
false,
|
||||
if i == PREAMBLE_PAIRS - 1 { GAP_US } else { TE_SHORT },
|
||||
));
|
||||
}
|
||||
|
||||
// First bit (bit 63)
|
||||
|
||||
+13
-17
@@ -1,6 +1,7 @@
|
||||
//! Ford V0 protocol decoder/encoder
|
||||
//!
|
||||
//! Ported from protopirate's ford_v0.c
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/ford_v0.c` (Flipper).
|
||||
//! Decode/encode logic (CRC, BS, decode_ford_v0, encode_ford_v0, upload waveform) matches reference.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - Manchester encoding: 250/500µs timing
|
||||
@@ -8,6 +9,9 @@
|
||||
//! - Matrix-based CRC in GF(2)
|
||||
//! - BS (byte swap) magic calculation
|
||||
//! - 6 bursts, 4 preamble pairs, 3500µs gap
|
||||
//!
|
||||
//! HackRF-specific: `TE_DELTA` (200µs) and `GAP_TOLERANCE` (1500µs) are wider than reference
|
||||
//! (100µs / 250µs) for software demodulator tolerance.
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
@@ -551,18 +555,16 @@ impl ProtocolDecoder for FordV0Decoder {
|
||||
}
|
||||
|
||||
// ─── Step 3: PreambleCheck — count preamble pairs or transition to gap ───
|
||||
// Order matches protopirate ford_v0.c: check LONG first, then SHORT.
|
||||
DecoderStep::PreambleCheck => {
|
||||
if level {
|
||||
let short_diff = duration_diff!(duration, TE_SHORT);
|
||||
let long_diff = duration_diff!(duration, TE_LONG);
|
||||
|
||||
if long_diff < TE_DELTA && long_diff <= short_diff {
|
||||
// Long HIGH (closer to TE_LONG): another preamble pair
|
||||
if duration_diff!(duration, TE_LONG) < TE_DELTA {
|
||||
// Long HIGH: another preamble pair
|
||||
self.header_count += 1;
|
||||
self.te_last = duration;
|
||||
self.step = DecoderStep::Preamble;
|
||||
} else if short_diff < TE_DELTA {
|
||||
// Short HIGH (closer to TE_SHORT): end of preamble, transition to gap
|
||||
} else if duration_diff!(duration, TE_SHORT) < TE_DELTA {
|
||||
// Short HIGH: end of preamble, transition to gap
|
||||
self.step = DecoderStep::Gap;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
@@ -585,18 +587,12 @@ impl ProtocolDecoder for FordV0Decoder {
|
||||
|
||||
// ─── Step 5: Data — Manchester decode 80 bits ───
|
||||
DecoderStep::Data => {
|
||||
// Map level+duration to Manchester event using NEAREST-MATCH.
|
||||
// With TE_DELTA=200, SHORT(250) and LONG(500) ranges overlap at 300–450µs.
|
||||
// First-match would always pick SHORT for overlapping durations, causing
|
||||
// bit errors and CRC failure. Nearest-match picks the closer timing.
|
||||
//
|
||||
// Tie-break favors LONG (strict < for short_diff) because asymmetric
|
||||
// demodulation compresses LOWs towards the midpoint (375µs) — these are
|
||||
// actually LONG pulses that got shortened by threshold bias.
|
||||
// Map level+duration to Manchester event. Order matches protopirate ford_v0.c:
|
||||
// check SHORT first, then LONG (so when both within TE_DELTA, short wins).
|
||||
let short_diff = duration_diff!(duration, TE_SHORT);
|
||||
let long_diff = duration_diff!(duration, TE_LONG);
|
||||
|
||||
let event = if short_diff < TE_DELTA && short_diff < long_diff {
|
||||
let event = if short_diff < TE_DELTA {
|
||||
if level { 0 } else { 1 } // ShortLow / ShortHigh
|
||||
} else if long_diff < TE_DELTA {
|
||||
if level { 2 } else { 3 } // LongLow / LongHigh
|
||||
|
||||
@@ -1,63 +1,57 @@
|
||||
//! KeeLoq common encryption/decryption routines
|
||||
//! KeeLoq common encryption/decryption and learning routines
|
||||
//!
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/keeloq_common.c`.
|
||||
//! Shared by Kia V3/V4, Star Line, and other KeeLoq-based protocols.
|
||||
//! Based on the NLF (Non-Linear Feedback) function with constant 0x3A5C742E.
|
||||
//! NLF (Non-Linear Feedback) constant 0x3A5C742E per reference.
|
||||
|
||||
/// The KeeLoq NLF constant
|
||||
/// The KeeLoq NLF constant (KEELOQ_NLF in reference)
|
||||
const KEELOQ_NLF: u32 = 0x3A5C742E;
|
||||
|
||||
/// KeeLoq decrypt: 528 rounds of the KeeLoq cipher (decrypt direction)
|
||||
#[inline]
|
||||
fn bit(x: u32, n: u32) -> u32 {
|
||||
(x >> n) & 1
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn g5(x: u32, a: u32, b: u32, c: u32, d: u32, e: u32) -> u32 {
|
||||
bit(x, a) | (bit(x, b) << 1) | (bit(x, c) << 2) | (bit(x, d) << 3) | (bit(x, e) << 4)
|
||||
}
|
||||
|
||||
/// KeeLoq decrypt: 528 rounds (matches subghz_protocol_keeloq_common_decrypt).
|
||||
/// Key bit for round r is key[(15 - r) & 63]. NLF index g5(x, 0, 8, 19, 25, 30).
|
||||
pub fn keeloq_decrypt(data: u32, key: u64) -> u32 {
|
||||
let mut block = data;
|
||||
let mut tkey = key;
|
||||
|
||||
for _ in 0..528 {
|
||||
let lutkey = ((block >> 0) & 1)
|
||||
| ((block >> 7) & 2)
|
||||
| ((block >> 17) & 4)
|
||||
| ((block >> 22) & 8)
|
||||
| ((block >> 26) & 16);
|
||||
let lsb = ((block >> 31)
|
||||
^ ((block >> 15) & 1)
|
||||
^ ((KEELOQ_NLF >> lutkey) & 1)
|
||||
^ (((tkey >> 15) & 1) as u32)) as u32;
|
||||
block = ((block & 0x7FFFFFFF) << 1) | lsb;
|
||||
tkey = ((tkey & 0x7FFFFFFFFFFFFFFF) << 1) | (tkey >> 63);
|
||||
let mut x = data;
|
||||
for r in 0..528u32 {
|
||||
let key_bit = ((key >> ((15 - r) & 63)) & 1) as u32;
|
||||
let new_lsb = bit(x, 31) ^ bit(x, 15) ^ key_bit
|
||||
^ bit(KEELOQ_NLF, g5(x, 0, 8, 19, 25, 30));
|
||||
x = (x << 1) ^ new_lsb;
|
||||
}
|
||||
block
|
||||
x
|
||||
}
|
||||
|
||||
/// KeeLoq encrypt: 528 rounds of the KeeLoq cipher (encrypt direction)
|
||||
/// KeeLoq encrypt: 528 rounds (matches subghz_protocol_keeloq_common_encrypt).
|
||||
/// Key bit for round r is key[r & 63]. NLF index g5(x, 1, 9, 20, 26, 31).
|
||||
pub fn keeloq_encrypt(data: u32, key: u64) -> u32 {
|
||||
let mut block = data;
|
||||
let mut tkey = key;
|
||||
|
||||
for _ in 0..528 {
|
||||
let lutkey = ((block >> 1) & 1)
|
||||
| ((block >> 8) & 2)
|
||||
| ((block >> 18) & 4)
|
||||
| ((block >> 23) & 8)
|
||||
| ((block >> 27) & 16);
|
||||
let msb = ((block >> 0)
|
||||
^ ((block >> 16) & 1)
|
||||
^ ((KEELOQ_NLF >> lutkey) & 1)
|
||||
^ (((tkey >> 0) & 1) as u32)) as u32;
|
||||
block = ((block >> 1) & 0x7FFFFFFF) | (msb << 31);
|
||||
tkey = ((tkey >> 1) & 0x7FFFFFFFFFFFFFFF) | ((tkey & 1) << 63);
|
||||
let mut x = data;
|
||||
for r in 0..528u32 {
|
||||
let key_bit = ((key >> (r & 63)) & 1) as u32;
|
||||
let new_msb = bit(x, 0) ^ bit(x, 16) ^ key_bit
|
||||
^ bit(KEELOQ_NLF, g5(x, 1, 9, 20, 26, 31));
|
||||
x = (x >> 1) ^ (new_msb << 31);
|
||||
}
|
||||
block
|
||||
x
|
||||
}
|
||||
|
||||
/// Normal learning key derivation
|
||||
/// Derives a 64-bit key from a 32-bit fix code and a 64-bit manufacturer key
|
||||
pub fn keeloq_normal_learning(fix: u32, manufacturer_key: u64) -> u64 {
|
||||
let serial_low = fix & 0xFFFF;
|
||||
let serial_high = (fix >> 16) & 0xFFFF;
|
||||
|
||||
let key_low = keeloq_decrypt(serial_low as u32 | 0x20000000, manufacturer_key);
|
||||
let key_high = keeloq_decrypt(serial_high as u32 | 0x60000000, manufacturer_key);
|
||||
|
||||
((key_high as u64) << 32) | (key_low as u64)
|
||||
/// Normal learning key derivation (matches subghz_protocol_keeloq_common_normal_learning).
|
||||
/// @param data - serial number (28-bit, upper bits ignored)
|
||||
/// @param key - manufacturer key (64-bit)
|
||||
/// @return derived key for this serial (64-bit)
|
||||
pub fn keeloq_normal_learning(data: u32, key: u64) -> u64 {
|
||||
let data = data & 0x0FFFFFFF;
|
||||
let k1 = keeloq_decrypt(data | 0x20000000, key);
|
||||
let k2 = keeloq_decrypt(data | 0x60000000, key);
|
||||
((k2 as u64) << 32) | (k1 as u64)
|
||||
}
|
||||
|
||||
/// Reverse the bits in a 64-bit key (for protocols that store data MSB-first)
|
||||
|
||||
+29
-19
@@ -1,16 +1,8 @@
|
||||
//! Key management module for protocol encryption/decryption
|
||||
//!
|
||||
//! Ported from protopirate's keys.c
|
||||
//!
|
||||
//! Manages manufacturer keys used by various protocols:
|
||||
//! - KIA V3/V4: kia_mf_key (manufacturer key for KeeLoq)
|
||||
//! - KIA V5: kia_v5_key (custom mixer cipher key)
|
||||
//! - KIA V6: kia_v6_a_key, kia_v6_b_key (AES-128 XOR mask keys)
|
||||
//! - Star Line: star_line_mf_key (manufacturer key for KeeLoq)
|
||||
//! - VAG: AUT64 keys loaded from keystore files
|
||||
//!
|
||||
//! Keys are loaded from `~/.config/KAT/keystore/keystore.ini` at startup.
|
||||
//! See [`load_keystore_from_dir`] for the file format.
|
||||
//! Aligned with ProtoPirate's keys.c (KIA_KEY1..4, get_kia_mf_key, etc.).
|
||||
//! Keys are loaded from the embedded keystore blob in `crate::keystore` at startup,
|
||||
//! matching the standard encrypted + VAG raw keystore data.
|
||||
|
||||
use super::aut64::{self, Aut64Key, AUT64_KEY_STRUCT_PACKED_SIZE};
|
||||
use configparser::ini::Ini;
|
||||
@@ -25,8 +17,8 @@ const KIA_KEY3: u32 = 12; // kia_v6_b_key
|
||||
const KIA_KEY4: u32 = 13; // kia_v5_key
|
||||
const STAR_LINE_KEY: u32 = 20; // star_line_mf_key
|
||||
|
||||
/// Maximum number of VAG AUT64 keys
|
||||
const VAG_KEYS_COUNT: usize = 3;
|
||||
/// Maximum number of VAG AUT64 keys (embedded blob has 64 bytes = 4 keys)
|
||||
const MAX_VAG_KEYS: usize = 4;
|
||||
|
||||
/// Global key store - thread-safe access to loaded keys
|
||||
pub struct KeyStore {
|
||||
@@ -81,22 +73,20 @@ impl KeyStore {
|
||||
}
|
||||
}
|
||||
|
||||
/// Load VAG AUT64 keys from raw binary data
|
||||
/// The data should contain packed AUT64 key structures (16 bytes each)
|
||||
/// Load VAG AUT64 keys from raw binary data (16 bytes per key; up to MAX_VAG_KEYS)
|
||||
pub fn load_vag_keys_from_data(&mut self, data: &[u8]) {
|
||||
if self.vag_keys_loaded {
|
||||
return;
|
||||
}
|
||||
|
||||
self.vag_keys.clear();
|
||||
let n = (data.len() / AUT64_KEY_STRUCT_PACKED_SIZE).min(MAX_VAG_KEYS);
|
||||
|
||||
for i in 0..VAG_KEYS_COUNT {
|
||||
for i in 0..n {
|
||||
let offset = i * AUT64_KEY_STRUCT_PACKED_SIZE;
|
||||
if offset + AUT64_KEY_STRUCT_PACKED_SIZE > data.len() {
|
||||
error!("VAG key data too short for key {}", i);
|
||||
break;
|
||||
}
|
||||
|
||||
let key = aut64::aut64_unpack(&data[offset..offset + AUT64_KEY_STRUCT_PACKED_SIZE]);
|
||||
self.vag_keys.push(key);
|
||||
}
|
||||
@@ -179,7 +169,7 @@ pub fn get_keystore_mut() -> std::sync::RwLockWriteGuard<'static, KeyStore> {
|
||||
global_keystore().write().unwrap()
|
||||
}
|
||||
|
||||
/// Initialize the global keystore with KIA keys
|
||||
/// Initialize the global keystore with KIA keys (matches protopirate_keys_load pattern)
|
||||
pub fn load_keys(kia_entries: &[(u32, u64)]) {
|
||||
let mut store = get_keystore_mut();
|
||||
store.load_kia_keys(kia_entries);
|
||||
@@ -191,6 +181,26 @@ pub fn load_vag_keys(path: &str) {
|
||||
store.load_vag_keys_from_file(path);
|
||||
}
|
||||
|
||||
/// Load the global keystore from the embedded blob (src/keystore/embedded.rs).
|
||||
/// Matches ProtoPirate loading from encrypted + VAG keystore; keys are compiled in.
|
||||
pub fn load_keystore_from_embedded() {
|
||||
let blob = crate::keystore::embedded_blob();
|
||||
let Some(parsed) = crate::keystore::parse_blob(blob) else {
|
||||
error!("Failed to parse embedded keystore blob");
|
||||
return;
|
||||
};
|
||||
let mut store = get_keystore_mut();
|
||||
store.load_kia_keys(&parsed.entries);
|
||||
if !parsed.vag_bytes.is_empty() {
|
||||
store.load_vag_keys_from_data(&parsed.vag_bytes);
|
||||
}
|
||||
info!(
|
||||
"Keystore loaded from embedded blob ({} entries, {} VAG keys)",
|
||||
parsed.entries.len(),
|
||||
store.vag_keys.len()
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Keystore file loading from ~/.config/KAT/keystore/
|
||||
// =============================================================================
|
||||
|
||||
+19
-13
@@ -1,13 +1,13 @@
|
||||
//! Kia V0 protocol decoder
|
||||
//! Kia V0 protocol decoder/encoder
|
||||
//!
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v0.c`.
|
||||
//! Decode/encode logic (CRC8, preamble/sync, field layout) matches reference.
|
||||
//!
|
||||
//! Ported from protopirate's kia_v0.c
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - PWM encoding: short pulse (250µs) = 0, long pulse (500µs) = 1
|
||||
//! - 61 bits total
|
||||
//! - Preamble: alternating short pulses
|
||||
//! - Sync: long-long pattern
|
||||
//! - Data: 59 bits (4-bit prefix + 16-bit counter + 28-bit serial + 4-bit button + 8-bit CRC)
|
||||
//! - 61 bits total (1 sync bit + 60 data bits)
|
||||
//! - Preamble: alternating short pulses; sync: long-long pattern
|
||||
//! - Data: 60 bits (4-bit prefix + 16-bit counter + 28-bit serial + 4-bit button + 8-bit CRC)
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use super::common::{crc8_kia, add_bit};
|
||||
@@ -19,7 +19,7 @@ const TE_LONG: u32 = 500;
|
||||
const TE_DELTA: u32 = 100;
|
||||
const MIN_COUNT_BIT: usize = 61;
|
||||
|
||||
/// Decoder states
|
||||
/// Decoder states (matches protopirate's KiaV0DecoderStep)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
@@ -48,7 +48,7 @@ impl KiaV0Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate CRC for Kia data packet
|
||||
/// CRC8 for Kia data packet (matches kia_v0.c kia_crc8: polynomial 0x7F, init 0x00)
|
||||
fn calculate_crc(data: u64) -> u8 {
|
||||
let crc_data = [
|
||||
((data >> 48) & 0xFF) as u8,
|
||||
@@ -236,13 +236,13 @@ impl ProtocolDecoder for KiaV0Decoder {
|
||||
signal.push(LevelDuration::new(is_high, TE_SHORT));
|
||||
}
|
||||
|
||||
// Sync: long-long
|
||||
// Sync: long-long (matches protopirate kia_v0 encode)
|
||||
signal.push(LevelDuration::new(true, TE_LONG));
|
||||
signal.push(LevelDuration::new(false, TE_LONG));
|
||||
|
||||
// Data: 59 bits (MSB first)
|
||||
for bit_num in 0..59 {
|
||||
let bit_mask = 1u64 << (58 - bit_num);
|
||||
// Data: 60 bits MSB first (bits 59..0)
|
||||
for bit_num in 0..60 {
|
||||
let bit_mask = 1u64 << (59 - bit_num);
|
||||
let bit = (data & bit_mask) != 0;
|
||||
let duration = if bit { TE_LONG } else { TE_SHORT };
|
||||
|
||||
@@ -257,3 +257,9 @@ impl ProtocolDecoder for KiaV0Decoder {
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for KiaV0Decoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
+19
-12
@@ -1,12 +1,13 @@
|
||||
//! Kia V1 protocol decoder
|
||||
//! Kia V1 protocol decoder/encoder
|
||||
//!
|
||||
//! Ported from protopirate's kia_v1.c
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v1.c`.
|
||||
//! Decode/encode logic (Manchester, CRC4, preamble, field layout) matches reference.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - Manchester encoding: 800/1600µs timing
|
||||
//! - 57 bits total
|
||||
//! - Long preamble of ~90 pulses
|
||||
//! - CRC4 checksum
|
||||
//! - 57 bits total (32 serial + 8 button + 12 counter + 4 CRC)
|
||||
//! - Long preamble of ~90 long pairs
|
||||
//! - CRC4 checksum with offset rules (cnt_high 0 / >= 6)
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
@@ -17,7 +18,7 @@ const TE_LONG: u32 = 1600;
|
||||
const TE_DELTA: u32 = 200;
|
||||
const MIN_COUNT_BIT: usize = 57;
|
||||
|
||||
/// Manchester states
|
||||
/// Manchester decoder states (matches protopirate kia_v1 Manchester state machine)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ManchesterState {
|
||||
Mid0,
|
||||
@@ -26,7 +27,7 @@ enum ManchesterState {
|
||||
Start1,
|
||||
}
|
||||
|
||||
/// Decoder states
|
||||
/// Decoder states (matches protopirate's KiaV1DecoderStep)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
@@ -56,7 +57,7 @@ impl KiaV1Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// CRC4 calculation for Kia V1
|
||||
/// CRC4 for Kia V1 (matches kia_v1.c: XOR nibbles + offset)
|
||||
fn crc4(bytes: &[u8], offset: u8) -> u8 {
|
||||
let mut crc: u8 = 0;
|
||||
for &byte in bytes {
|
||||
@@ -97,7 +98,7 @@ impl KiaV1Decoder {
|
||||
fn parse_data(&self) -> DecodedSignal {
|
||||
let data = self.decode_data;
|
||||
|
||||
// Extract fields per kia_v1.c
|
||||
// Field layout matches kia_v1.c: serial(32) | button(8) | cnt_low(8) | cnt_high(4) | crc(4)
|
||||
let serial = (data >> 24) as u32;
|
||||
let button = ((data >> 16) & 0xFF) as u8;
|
||||
let cnt_low = ((data >> 8) & 0xFF) as u16;
|
||||
@@ -263,7 +264,7 @@ impl ProtocolDecoder for KiaV1Decoder {
|
||||
|
||||
let mut signal = Vec::with_capacity(600);
|
||||
|
||||
// Generate 3 bursts
|
||||
// Generate 3 bursts (matches protopirate kia_v1 encode)
|
||||
for burst in 0..3 {
|
||||
if burst > 0 {
|
||||
signal.push(LevelDuration::new(false, 25000));
|
||||
@@ -275,10 +276,10 @@ impl ProtocolDecoder for KiaV1Decoder {
|
||||
signal.push(LevelDuration::new(true, TE_LONG));
|
||||
}
|
||||
|
||||
// Short gap
|
||||
// Short gap before data
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
|
||||
// Data: Manchester encoded, MSB first
|
||||
// Data: 57 bits Manchester encoded, MSB first
|
||||
for bit_num in (1..MIN_COUNT_BIT).rev() {
|
||||
let bit = ((data >> (bit_num - 1)) & 1) == 1;
|
||||
if bit {
|
||||
@@ -294,3 +295,9 @@ impl ProtocolDecoder for KiaV1Decoder {
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for KiaV1Decoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
+20
-13
@@ -1,12 +1,13 @@
|
||||
//! Kia V2 protocol decoder
|
||||
//! Kia V2 protocol decoder/encoder
|
||||
//!
|
||||
//! Ported from protopirate's kia_v2.c
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v2.c`.
|
||||
//! Decode/encode logic (Manchester, CRC4, preamble, byte-swapped counter) matches reference.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - Manchester encoding: 500/1000µs timing
|
||||
//! - 53 bits total
|
||||
//! - Long preamble of 252+ pairs
|
||||
//! - CRC4 checksum
|
||||
//! - 53 bits total (32 serial + 4 button + 12 counter + 4 CRC, plus start bit)
|
||||
//! - Long preamble of 252 long pairs
|
||||
//! - CRC4 checksum (XOR nibbles + offset 1)
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
@@ -17,7 +18,7 @@ const TE_LONG: u32 = 1000;
|
||||
const TE_DELTA: u32 = 150;
|
||||
const MIN_COUNT_BIT: usize = 53;
|
||||
|
||||
/// Manchester states
|
||||
/// Manchester decoder states (matches protopirate kia_v2 Manchester state machine)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ManchesterState {
|
||||
Mid0,
|
||||
@@ -26,7 +27,7 @@ enum ManchesterState {
|
||||
Start1,
|
||||
}
|
||||
|
||||
/// Decoder states
|
||||
/// Decoder states (matches protopirate's KiaV2DecoderStep)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
@@ -56,7 +57,7 @@ impl KiaV2Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate CRC for Kia V2
|
||||
/// CRC4 for Kia V2 (matches kia_v2.c: 6-byte permuted input, XOR nibbles, offset 1)
|
||||
fn calculate_crc(data: u64) -> u8 {
|
||||
let serial = ((data >> 20) & 0xFFFFFFFF) as u32;
|
||||
let u_var4 = (data & 0xFFFFFFFF) as u32;
|
||||
@@ -105,10 +106,10 @@ impl KiaV2Decoder {
|
||||
output
|
||||
}
|
||||
|
||||
/// Parse decoded data
|
||||
/// Parse decoded data (field layout matches kia_v2.c)
|
||||
fn parse_data(&self) -> DecodedSignal {
|
||||
let data = self.decode_data;
|
||||
|
||||
// serial(32) | button(4) | counter_swapped(12) | crc(4); counter byte-swapped in stream
|
||||
let serial = ((data >> 20) & 0xFFFFFFFF) as u32;
|
||||
let button = ((data >> 16) & 0x0F) as u8;
|
||||
|
||||
@@ -246,7 +247,7 @@ impl ProtocolDecoder for KiaV2Decoder {
|
||||
|
||||
let mut signal = Vec::with_capacity(700);
|
||||
|
||||
// Generate 2 bursts
|
||||
// Generate 2 bursts (matches protopirate kia_v2 encode)
|
||||
for _burst in 0..2 {
|
||||
// Preamble: 252 long pairs
|
||||
for _ in 0..252 {
|
||||
@@ -254,10 +255,10 @@ impl ProtocolDecoder for KiaV2Decoder {
|
||||
signal.push(LevelDuration::new(true, TE_LONG));
|
||||
}
|
||||
|
||||
// Short gap
|
||||
// Short gap before data
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
|
||||
// Data: Manchester encoded, MSB first
|
||||
// Data: 53 bits Manchester encoded, MSB first
|
||||
for bit_num in (1..MIN_COUNT_BIT).rev() {
|
||||
let bit = ((new_data >> (bit_num - 1)) & 1) == 1;
|
||||
if bit {
|
||||
@@ -273,3 +274,9 @@ impl ProtocolDecoder for KiaV2Decoder {
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for KiaV2Decoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
+25
-60
@@ -1,15 +1,16 @@
|
||||
//! Kia V3/V4 protocol decoder
|
||||
//! Kia V3/V4 protocol decoder/encoder
|
||||
//!
|
||||
//! Ported from protopirate's kia_v3_v4.c
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v3_v4.c`.
|
||||
//! Decode/encode logic (PWM, preamble, sync polarity, KeeLoq, CRC4, byte order) matches reference.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - PWM encoding: 400/800µs timing
|
||||
//! - 68 bits total
|
||||
//! - Short preamble of 16 pairs
|
||||
//! - KeeLoq encryption (requires manufacturer key)
|
||||
//! - V3 and V4 differ only in sync polarity
|
||||
//! - PWM encoding: 400/800µs (short=0, long=1)
|
||||
//! - 68 bits total (8 bytes encrypted + 4 bits CRC)
|
||||
//! - Short preamble of 16 pairs; sync 1200µs (V4: long HIGH, V3: long LOW)
|
||||
//! - KeeLoq encryption (KIA manufacturer key); V3/V4 differ only in sync polarity
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use super::keeloq_common::{keeloq_decrypt, keeloq_encrypt};
|
||||
use super::keys;
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
use crate::duration_diff;
|
||||
@@ -23,7 +24,7 @@ const INTER_BURST_GAP_US: u32 = 10000;
|
||||
const PREAMBLE_PAIRS: usize = 16;
|
||||
const TOTAL_BURSTS: usize = 3;
|
||||
|
||||
/// Decoder states
|
||||
/// Decoder states (matches protopirate's KiaV3V4DecoderStep)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
@@ -53,7 +54,7 @@ impl KiaV3V4Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Reverse bits in a byte
|
||||
/// Reverse bits in a byte (matches kia_v3_v4.c byte order for KeeLoq payload)
|
||||
fn reverse8(byte: u8) -> u8 {
|
||||
let mut byte = byte;
|
||||
byte = (byte & 0xF0) >> 4 | (byte & 0x0F) << 4;
|
||||
@@ -76,7 +77,7 @@ impl KiaV3V4Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// CRC4 calculation
|
||||
/// CRC4 for Kia V3/V4 (matches kia_v3_v4.c: XOR nibbles over first 8 bytes)
|
||||
fn calculate_crc(bytes: &[u8]) -> u8 {
|
||||
let mut crc: u8 = 0;
|
||||
for &byte in bytes.iter().take(8) {
|
||||
@@ -85,61 +86,18 @@ impl KiaV3V4Decoder {
|
||||
crc & 0x0F
|
||||
}
|
||||
|
||||
/// KeeLoq decrypt
|
||||
fn keeloq_decrypt(data: u32, key: u64) -> u32 {
|
||||
let mut block = data;
|
||||
let mut tkey = key;
|
||||
|
||||
for _ in 0..528 {
|
||||
let lutkey = ((block >> 0) & 1) |
|
||||
((block >> 7) & 2) |
|
||||
((block >> 17) & 4) |
|
||||
((block >> 22) & 8) |
|
||||
((block >> 26) & 16);
|
||||
let lsb = ((block >> 31) ^
|
||||
((block >> 15) & 1) ^
|
||||
((0x3A5C742E_u32 >> lutkey) & 1) ^
|
||||
(((tkey >> 15) & 1) as u32)) as u32;
|
||||
block = ((block & 0x7FFFFFFF) << 1) | lsb;
|
||||
tkey = ((tkey & 0x7FFFFFFFFFFFFFFF) << 1) | (tkey >> 63);
|
||||
}
|
||||
block
|
||||
}
|
||||
|
||||
/// KeeLoq encrypt
|
||||
fn keeloq_encrypt(data: u32, key: u64) -> u32 {
|
||||
let mut block = data;
|
||||
let mut tkey = key;
|
||||
|
||||
for _ in 0..528 {
|
||||
let lutkey = ((block >> 1) & 1) |
|
||||
((block >> 8) & 2) |
|
||||
((block >> 18) & 4) |
|
||||
((block >> 23) & 8) |
|
||||
((block >> 27) & 16);
|
||||
let msb = ((block >> 0) ^
|
||||
((block >> 16) & 1) ^
|
||||
((0x3A5C742E_u32 >> lutkey) & 1) ^
|
||||
(((tkey >> 0) & 1) as u32)) as u32;
|
||||
block = ((block >> 1) & 0x7FFFFFFF) | (msb << 31);
|
||||
tkey = ((tkey >> 1) & 0x7FFFFFFFFFFFFFFF) | ((tkey & 1) << 63);
|
||||
}
|
||||
block
|
||||
}
|
||||
|
||||
/// Get manufacturer key (placeholder - in real use, this would be loaded from config)
|
||||
/// KIA manufacturer key from keystore (type 10)
|
||||
fn get_mf_key() -> u64 {
|
||||
keys::get_keystore().get_kia_mf_key()
|
||||
}
|
||||
|
||||
/// Process the collected buffer and validate
|
||||
/// Process collected 68 bits: decrypt KeeLoq block, validate button/serial (matches kia_v3_v4.c)
|
||||
fn process_buffer(&self) -> Option<DecodedSignal> {
|
||||
if self.raw_bit_count < 68 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut b = self.raw_bits;
|
||||
|
||||
// V3 sync means data is inverted
|
||||
if self.is_v3_sync {
|
||||
let num_bytes = ((self.raw_bit_count + 7) / 8) as usize;
|
||||
@@ -164,7 +122,7 @@ impl KiaV3V4Decoder {
|
||||
let our_serial_lsb = (serial & 0xFF) as u8;
|
||||
|
||||
let mf_key = Self::get_mf_key();
|
||||
let decrypted = Self::keeloq_decrypt(encrypted, mf_key);
|
||||
let decrypted = keeloq_decrypt(encrypted, mf_key);
|
||||
let dec_btn = ((decrypted >> 28) & 0x0F) as u8;
|
||||
let dec_serial_lsb = ((decrypted >> 16) & 0xFF) as u8;
|
||||
|
||||
@@ -313,7 +271,7 @@ impl ProtocolDecoder for KiaV3V4Decoder {
|
||||
(((button & 0x0F) as u32) << 28);
|
||||
|
||||
let mf_key = Self::get_mf_key();
|
||||
let encrypted = Self::keeloq_encrypt(plaintext, mf_key);
|
||||
let encrypted = keeloq_encrypt(plaintext, mf_key);
|
||||
|
||||
// Build raw bytes
|
||||
let mut raw_bytes = [0u8; 9];
|
||||
@@ -343,18 +301,19 @@ impl ProtocolDecoder for KiaV3V4Decoder {
|
||||
|
||||
let mut signal = Vec::with_capacity(600);
|
||||
|
||||
// 3 bursts with 10ms gap (matches protopirate kia_v3_v4 encode)
|
||||
for burst in 0..TOTAL_BURSTS {
|
||||
if burst > 0 {
|
||||
signal.push(LevelDuration::new(false, INTER_BURST_GAP_US));
|
||||
}
|
||||
|
||||
// Preamble
|
||||
// Preamble: 16 short pairs
|
||||
for _ in 0..PREAMBLE_PAIRS {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
}
|
||||
|
||||
// Sync pulse
|
||||
// Sync: V4 = long HIGH, V3 = long LOW
|
||||
if version == 0 {
|
||||
// V4: long HIGH, short LOW
|
||||
signal.push(LevelDuration::new(true, SYNC_DURATION));
|
||||
@@ -365,7 +324,7 @@ impl ProtocolDecoder for KiaV3V4Decoder {
|
||||
signal.push(LevelDuration::new(false, SYNC_DURATION));
|
||||
}
|
||||
|
||||
// Data bits
|
||||
// Data: 68 bits PWM (8 bytes + 4-bit CRC), MSB first per byte
|
||||
for byte_idx in 0..9 {
|
||||
let bits_in_byte = if byte_idx == 8 { 4 } else { 8 };
|
||||
for bit_idx in (8 - bits_in_byte..8).rev() {
|
||||
@@ -384,3 +343,9 @@ impl ProtocolDecoder for KiaV3V4Decoder {
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for KiaV3V4Decoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
+22
-15
@@ -1,13 +1,14 @@
|
||||
//! Kia V5 protocol decoder
|
||||
//! Kia V5 protocol decoder (decode-only)
|
||||
//!
|
||||
//! Ported from protopirate's kia_v5.c
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v5.c`.
|
||||
//! Decode logic (Manchester polarity, mixer decrypt, YEK, preamble) matches reference.
|
||||
//! No encoder in protopirate.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - Manchester encoding: 400/800µs timing
|
||||
//! - 64 bits total (+3 bit CRC)
|
||||
//! - Preamble of ~40+ short pairs
|
||||
//! - Custom "mixer" encryption
|
||||
//! - Decode-only (no encoder)
|
||||
//! - Manchester encoding: 400/800µs (opposite polarity to V1/V2: level ? ShortHigh : ShortLow)
|
||||
//! - 64 data bits + 3-bit CRC (67 bits on air)
|
||||
//! - Preamble of ~40+ short/long pairs; then 64-bit key then 3-bit CRC
|
||||
//! - Custom "mixer" decryption with KIA V5 manufacturer key
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use super::keys;
|
||||
@@ -19,7 +20,7 @@ const TE_LONG: u32 = 800;
|
||||
const TE_DELTA: u32 = 150;
|
||||
const MIN_COUNT_BIT: usize = 64;
|
||||
|
||||
/// Manchester states
|
||||
/// Manchester decoder states (V5 uses opposite polarity to V1/V2; see manchester_advance)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ManchesterState {
|
||||
Mid0,
|
||||
@@ -28,7 +29,7 @@ enum ManchesterState {
|
||||
Start1,
|
||||
}
|
||||
|
||||
/// Decoder states
|
||||
/// Decoder states (matches protopirate's KiaV5DecoderStep)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
@@ -60,12 +61,12 @@ impl KiaV5Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get encryption key from global keystore
|
||||
/// KIA V5 manufacturer key from keystore (type 13)
|
||||
fn get_v5_key() -> u64 {
|
||||
keys::get_keystore().get_kia_v5_key()
|
||||
}
|
||||
|
||||
/// Custom mixer decryption
|
||||
/// Mixer decryption (matches kia_v5.c custom cipher)
|
||||
fn mixer_decode(encrypted: u32) -> u16 {
|
||||
let mut s0 = (encrypted & 0xFF) as u8;
|
||||
let mut s1 = ((encrypted >> 8) & 0xFF) as u8;
|
||||
@@ -128,7 +129,7 @@ impl KiaV5Decoder {
|
||||
((s0 as u16) + ((s1 as u16) << 8)) & 0xFFFF
|
||||
}
|
||||
|
||||
/// Reverse bits in 64-bit value
|
||||
/// YEK: reverse bit order per byte (matches kia_v5.c for key derivation)
|
||||
fn compute_yek(key: u64) -> u64 {
|
||||
let mut yek: u64 = 0;
|
||||
for i in 0..8 {
|
||||
@@ -180,7 +181,7 @@ impl KiaV5Decoder {
|
||||
output
|
||||
}
|
||||
|
||||
/// Parse decoded data
|
||||
/// Parse 64-bit key: YEK then serial/button from high bits, mixer_decode for counter (matches kia_v5.c)
|
||||
fn parse_data(&self) -> Option<DecodedSignal> {
|
||||
if self.bit_count < MIN_COUNT_BIT as u8 {
|
||||
return None;
|
||||
@@ -188,7 +189,7 @@ impl KiaV5Decoder {
|
||||
|
||||
let key = self.saved_key;
|
||||
let yek = Self::compute_yek(key);
|
||||
|
||||
// serial(28) + button(4) in high 32 bits; low 32 bits encrypted counter
|
||||
let serial = ((yek >> 32) & 0x0FFFFFFF) as u32;
|
||||
let button = ((yek >> 60) & 0x0F) as u8;
|
||||
let encrypted = (yek & 0xFFFFFFFF) as u32;
|
||||
@@ -316,6 +317,12 @@ impl ProtocolDecoder for KiaV5Decoder {
|
||||
}
|
||||
|
||||
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
|
||||
None // V5 doesn't support encoding
|
||||
None // V5 decode-only in protopirate
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for KiaV5Decoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
+23
-17
@@ -1,13 +1,14 @@
|
||||
//! Kia V6 protocol decoder
|
||||
//! Kia V6 protocol decoder (decode-only)
|
||||
//!
|
||||
//! Ported from protopirate's kia_v6.c
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v6.c`.
|
||||
//! Decode logic (Manchester level mapping, 3-part 144-bit frame, AES-128, CRC8, keystore XOR) matches reference.
|
||||
//! No encoder in protopirate.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - Manchester encoding: 200/400µs timing
|
||||
//! - 144 bits total (split into 3 parts)
|
||||
//! - Long preamble of 600+ pairs
|
||||
//! - AES-128 encryption
|
||||
//! - Decode-only (no encoder)
|
||||
//! - Manchester encoding: 200/400µs (level convention inverted vs Flipper; see manchester_advance)
|
||||
//! - 144 bits total: part1 (64) + part2 (64) + part3 (16), each part inverted on store
|
||||
//! - Long preamble of 601 pairs; sync bits 1,1,0,1 then data
|
||||
//! - AES-128 decryption with key derived from KIA V6 A/B keystores (types 11/12) and XOR masks
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use super::keys;
|
||||
@@ -65,7 +66,7 @@ const AES_SBOX_INV: [u8; 256] = [
|
||||
|
||||
const AES_RCON: [u8; 10] = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36];
|
||||
|
||||
/// Manchester states
|
||||
/// Manchester decoder states (event mapping 0/2/4/6 matches protopirate kia_v6 level convention)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ManchesterState {
|
||||
Mid0,
|
||||
@@ -74,7 +75,7 @@ enum ManchesterState {
|
||||
Start1,
|
||||
}
|
||||
|
||||
/// Decoder states
|
||||
/// Decoder states (matches protopirate's KiaV6DecoderStep)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
@@ -119,17 +120,17 @@ impl KiaV6Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get keystore A from global keystore
|
||||
/// KIA V6 keystore A from keystore (type 11)
|
||||
fn get_keystore_a() -> u64 {
|
||||
keys::get_keystore().get_kia_v6_keystore_a()
|
||||
}
|
||||
|
||||
/// Get keystore B from global keystore
|
||||
/// KIA V6 keystore B from keystore (type 12)
|
||||
fn get_keystore_b() -> u64 {
|
||||
keys::get_keystore().get_kia_v6_keystore_b()
|
||||
}
|
||||
|
||||
/// CRC8 calculation
|
||||
/// CRC8 for V6 (matches kia_v6.c: init 0xFF, polynomial 0x07, over first 15 bytes)
|
||||
fn crc8(data: &[u8], init: u8, polynomial: u8) -> u8 {
|
||||
let mut crc = init;
|
||||
for &byte in data {
|
||||
@@ -274,7 +275,7 @@ impl KiaV6Decoder {
|
||||
*data = state;
|
||||
}
|
||||
|
||||
/// Get AES key from keystores
|
||||
/// AES-128 key from V6 keystores A+B with XOR_MASK_LOW/HIGH (matches kia_v6.c)
|
||||
fn get_aes_key() -> [u8; 16] {
|
||||
let keystore_a = Self::get_keystore_a();
|
||||
let keystore_a_hi = ((keystore_a >> 32) & 0xFFFFFFFF) as u32;
|
||||
@@ -305,7 +306,7 @@ impl KiaV6Decoder {
|
||||
aes_key
|
||||
}
|
||||
|
||||
/// Decrypt the stored data
|
||||
/// Decrypt 16-byte block: byte layout matches kia_v6.c; AES-128 then CRC8 check
|
||||
fn decrypt(&self) -> Option<(u32, u8, u32, bool)> {
|
||||
let mut encrypted_data = [0u8; 16];
|
||||
|
||||
@@ -380,9 +381,8 @@ impl KiaV6Decoder {
|
||||
output
|
||||
}
|
||||
|
||||
/// Add initial sync bits
|
||||
/// Add initial sync bits (1,1,0,1 — matches kia_v6.c)
|
||||
fn add_sync_bits(&mut self) {
|
||||
// Add 1, 1, 0, 1 as initial bits
|
||||
for bit in [true, true, false, true] {
|
||||
let carry = self.data_part1_low >> 31;
|
||||
self.data_part1_low = (self.data_part1_low << 1) | (bit as u32);
|
||||
@@ -554,6 +554,12 @@ impl ProtocolDecoder for KiaV6Decoder {
|
||||
}
|
||||
|
||||
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
|
||||
None
|
||||
None // V6 decode-only in protopirate
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for KiaV6Decoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
//! Protocol decoders and encoders for various keyfob systems.
|
||||
//!
|
||||
//! This module implements decoders for various keyfob protocols, ported from
|
||||
//! protopirate. Each protocol processes level+duration pairs from the demodulator.
|
||||
//! Protocols are aligned with the ProtoPirate reference (`REFERENCES/ProtoPirate/protocols/`).
|
||||
//! Each decoder processes level+duration pairs from the demodulator and optionally supports
|
||||
//! encoding (replay). Shared pieces: [common], [keeloq_common], [keys], [aut64].
|
||||
|
||||
mod common;
|
||||
pub mod keeloq_common;
|
||||
|
||||
+25
-23
@@ -1,12 +1,13 @@
|
||||
//! PSA (Peugeot/Citroen) protocol decoder/encoder
|
||||
//! PSA (Peugeot/Citroën) protocol decoder/encoder
|
||||
//!
|
||||
//! Ported from protopirate's psa.c
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/psa.c`.
|
||||
//! Decode/encode logic (Manchester, preamble, TEA, XOR, mode 0x23/0x36) matches reference.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - Manchester encoding: 125/250µs bit timing, 250/500µs symbol timing
|
||||
//! - 128 bits total (key1: 64 bits, key2: 16 bits, validation: 48 bits)
|
||||
//! - TEA and XOR encryption schemes
|
||||
//! - Two modes: 0x23 and 0x36
|
||||
//! - Manchester encoding: 250/500µs symbol (125/250µs sub-symbol for preamble)
|
||||
//! - 128 bits total: key1 (64) + validation (16) + key2/rest (48); decode uses key1 + 16-bit validation
|
||||
//! - TEA decrypt/encrypt with fixed key schedules; mode 0x23 adds XOR layer
|
||||
//! - Two modes: seed 0x23 (TEA + XOR), seed 0xF3/0x36 (TEA, BF2 key schedule)
|
||||
|
||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||
use crate::duration_diff;
|
||||
@@ -35,7 +36,7 @@ const BF1_KEY_SCHEDULE: [u32; 4] = [0x4A434915, 0xD6743C2B, 0x1F29D308, 0xE6B79A
|
||||
// Brute-force constants for mode 0x36
|
||||
const BF2_KEY_SCHEDULE: [u32; 4] = [0x4039C240, 0xEDA92CAB, 0x4306C02A, 0x02192A04];
|
||||
|
||||
/// Manchester decoder states
|
||||
/// Manchester decoder states (matches protopirate psa.c Manchester state machine)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ManchesterState {
|
||||
Mid0,
|
||||
@@ -44,16 +45,12 @@ enum ManchesterState {
|
||||
Start1,
|
||||
}
|
||||
|
||||
/// Decoder states
|
||||
/// Decoder states (matches protopirate's PsaDecoderState)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderState {
|
||||
/// Waiting for first edge
|
||||
WaitEdge,
|
||||
/// Counting preamble pattern
|
||||
CountPattern,
|
||||
/// Decoding Manchester data
|
||||
DecodeManchester,
|
||||
/// Found end of data
|
||||
End,
|
||||
}
|
||||
|
||||
@@ -94,7 +91,7 @@ impl PsaDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Manchester advance
|
||||
/// Manchester state machine (matches psa.c event mapping)
|
||||
fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option<bool> {
|
||||
let event = match (is_short, is_high) {
|
||||
(true, true) => 0,
|
||||
@@ -143,7 +140,7 @@ impl PsaDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// TEA decrypt
|
||||
/// TEA decrypt (matches psa.c / standard TEA)
|
||||
fn tea_decrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) {
|
||||
let mut sum = TEA_DELTA.wrapping_mul(TEA_ROUNDS);
|
||||
for _ in 0..TEA_ROUNDS {
|
||||
@@ -161,7 +158,7 @@ impl PsaDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// TEA encrypt
|
||||
/// TEA encrypt (matches psa.c / standard TEA)
|
||||
fn tea_encrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) {
|
||||
let mut sum: u32 = 0;
|
||||
for _ in 0..TEA_ROUNDS {
|
||||
@@ -179,7 +176,7 @@ impl PsaDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// XOR decrypt (mode 0x23)
|
||||
/// XOR decrypt for mode 0x23 (matches psa.c)
|
||||
fn xor_decrypt(buffer: &mut [u8]) {
|
||||
let e6 = buffer[8];
|
||||
let e7 = buffer[9];
|
||||
@@ -198,8 +195,9 @@ impl PsaDecoder {
|
||||
buffer[7] = e5 ^ e6 ^ e7;
|
||||
}
|
||||
|
||||
/// Decrypt key1 + validation: mode 0x23 (TEA+XOR) or 0x36 (TEA, BF2) — matches psa.c
|
||||
fn try_decrypt(&self) -> Option<(u32, u8, u32, u16, u8)> {
|
||||
// Try mode 0x23 first
|
||||
// Try mode 0x23 first (seed byte 0x23)
|
||||
let seed_byte = (self.key1_high >> 24) as u8;
|
||||
|
||||
if seed_byte >= 0x23 && seed_byte < 0x24 {
|
||||
@@ -250,8 +248,9 @@ impl PsaDecoder {
|
||||
None
|
||||
}
|
||||
|
||||
/// Build DecodedSignal from key1 + validation; decrypt yields serial/button/counter (matches psa.c)
|
||||
fn parse_data(&self) -> DecodedSignal {
|
||||
// Combine into 128-bit data (store lower 64)
|
||||
// Store key1 as 64-bit data for display/replay
|
||||
let data = ((self.key1_high as u64) << 32) | (self.key1_low as u64);
|
||||
|
||||
if let Some((serial, btn, counter, _crc, _mode)) = self.try_decrypt() {
|
||||
@@ -445,20 +444,17 @@ impl ProtocolDecoder for PsaDecoder {
|
||||
let key1_low = v1;
|
||||
let validation = ((buffer[8] as u16) << 8) | (buffer[9] as u16);
|
||||
|
||||
// Build signal
|
||||
let mut signal = Vec::with_capacity(512);
|
||||
|
||||
// Preamble: alternating 125µs pulses
|
||||
// Preamble + sync (matches protopirate psa encode)
|
||||
for _ in 0..70 {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT_125));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT_125));
|
||||
}
|
||||
|
||||
// Sync
|
||||
signal.push(LevelDuration::new(true, TE_LONG_250));
|
||||
signal.push(LevelDuration::new(false, TE_LONG_250));
|
||||
|
||||
// Key1: 64 bits Manchester encoded
|
||||
// Key1: 64 bits Manchester, then validation 16 bits
|
||||
let key1 = ((key1_high as u64) << 32) | (key1_low as u64);
|
||||
for bit in (0..64).rev() {
|
||||
if (key1 >> bit) & 1 == 1 {
|
||||
@@ -487,3 +483,9 @@ impl ProtocolDecoder for PsaDecoder {
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PsaDecoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
+17
-10
@@ -1,14 +1,14 @@
|
||||
//! Scher-Khan protocol decoder
|
||||
//! Scher-Khan protocol decoder (decode-only)
|
||||
//!
|
||||
//! Ported from protopirate's scher_khan.c
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/scher_khan.c`.
|
||||
//! Decode logic (PWM preamble/sync, short=0/long=1, variable bit count) matches reference.
|
||||
//! No encoder in protopirate.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - PWM encoding: 750/1100µs timing
|
||||
//! - Variable bit count (35, 51, 57, 63, 64, 81, 82)
|
||||
//! - Decode-only (no encoder)
|
||||
//! - PWM encoding: 750µs = 0, 1100µs = 1; preamble uses 2× short then alternating
|
||||
//! - Variable bit count (35, 51, 57, 63, 64, 81, 82); only 51-bit format parsed for serial/button/counter
|
||||
//!
|
||||
//! References:
|
||||
//! - https://phreakerclub.com/72
|
||||
//! References: https://phreakerclub.com/72
|
||||
|
||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||
use crate::duration_diff;
|
||||
@@ -19,7 +19,7 @@ const TE_LONG: u32 = 1100;
|
||||
const TE_DELTA: u32 = 160;
|
||||
const MIN_COUNT_BIT: usize = 35;
|
||||
|
||||
/// Decoder states
|
||||
/// Decoder states (matches protopirate's ScherKhanDecoderStep)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
@@ -48,10 +48,11 @@ impl ScherKhanDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse payload by bit count; 51-bit format yields serial/button/counter (matches scher_khan.c)
|
||||
fn parse_data(data: u64, bit_count: usize) -> DecodedSignal {
|
||||
let (serial, btn, cnt) = match bit_count {
|
||||
51 => {
|
||||
// MAGIC CODE, Dynamic
|
||||
// 51-bit "MAGIC CODE" / Dynamic format: serial(28) | button(4) | counter(16) — matches reference
|
||||
let serial =
|
||||
((data >> 24) & 0xFFFFFF0) as u32 | ((data >> 20) & 0x0F) as u32;
|
||||
let btn = ((data >> 24) & 0x0F) as u8;
|
||||
@@ -196,6 +197,12 @@ impl ProtocolDecoder for ScherKhanDecoder {
|
||||
}
|
||||
|
||||
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
|
||||
None
|
||||
None // Scher-Khan decode-only in protopirate
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ScherKhanDecoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
//! Star Line protocol decoder/encoder
|
||||
//!
|
||||
//! Ported from protopirate's star_line.c
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/star_line.c`.
|
||||
//! Decode/encode logic (PWM header, fix/hop split, KeeLoq simple/normal learning) matches reference.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - PWM encoding: 250/500µs timing
|
||||
//! - 64 bits total
|
||||
//! - PWM encoding: 250µs = 0, 500µs = 1
|
||||
//! - 64 bits total: key_fix (32) + key_hop (32), sent MSB-first (reversed on air)
|
||||
//! - Header: 6 pairs of 1000µs HIGH + 1000µs LOW
|
||||
//! - KeeLoq encryption (requires manufacturer key)
|
||||
//! - KeeLoq: fix = serial(24) + button(8); hop encrypted with MF key or normal-learning derived key
|
||||
|
||||
use super::keeloq_common::{keeloq_decrypt, keeloq_encrypt, keeloq_normal_learning, reverse_key};
|
||||
use super::keys;
|
||||
@@ -20,7 +21,7 @@ const TE_DELTA: u32 = 120;
|
||||
const MIN_COUNT_BIT: usize = 64;
|
||||
const HEADER_DURATION: u32 = 1000; // te_long * 2
|
||||
|
||||
/// Decoder states
|
||||
/// Decoder states (matches protopirate's StarLineDecoderStep)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
@@ -49,13 +50,14 @@ impl StarLineDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get manufacturer key from global keystore
|
||||
/// Star Line manufacturer key from keystore (type 20)
|
||||
fn get_mf_key() -> u64 {
|
||||
keys::get_keystore().get_star_line_mf_key()
|
||||
}
|
||||
|
||||
/// Parse 64-bit payload: reverse_key then fix(32)/hop(32); KeeLoq decrypt (simple then normal learning) — matches star_line.c
|
||||
fn parse_data(data: u64) -> DecodedSignal {
|
||||
// Data is stored MSB-first in the air, reverse to get fix|hop
|
||||
// Data is MSB-first on air; reverse to get fix|hop
|
||||
let reversed = reverse_key(data, MIN_COUNT_BIT);
|
||||
let key_fix = (reversed >> 32) as u32;
|
||||
let key_hop = (reversed & 0xFFFFFFFF) as u32;
|
||||
@@ -246,13 +248,13 @@ impl ProtocolDecoder for StarLineDecoder {
|
||||
|
||||
let mut signal = Vec::with_capacity(256);
|
||||
|
||||
// Header: 6 pairs of LONG*2 HIGH + LONG*2 LOW
|
||||
// Header: 6 pairs 1000µs HIGH + 1000µs LOW (matches protopirate star_line encode)
|
||||
for _ in 0..6 {
|
||||
signal.push(LevelDuration::new(true, HEADER_DURATION));
|
||||
signal.push(LevelDuration::new(false, HEADER_DURATION));
|
||||
}
|
||||
|
||||
// Data: 64 bits, MSB first
|
||||
// Data: 64 bits PWM, MSB first (1=long, 0=short)
|
||||
for bit in (0..64).rev() {
|
||||
if (data >> bit) & 1 == 1 {
|
||||
// Bit 1: LONG HIGH + LONG LOW
|
||||
@@ -268,3 +270,9 @@ impl ProtocolDecoder for StarLineDecoder {
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for StarLineDecoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
+20
-32
@@ -1,13 +1,13 @@
|
||||
//! Subaru protocol decoder
|
||||
//! Subaru protocol decoder/encoder
|
||||
//!
|
||||
//! Ported from protopirate's subaru.c
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/subaru.c`.
|
||||
//! Decode/encode logic (PWM preamble/gap/sync, short=1/long=0, counter decode) matches reference.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - PWM encoding: short HIGH (800µs) = 1, long HIGH (1600µs) = 0
|
||||
//! - 64 bits total
|
||||
//! - Long preamble of 1600µs pulses
|
||||
//! - Gap and sync pattern
|
||||
//! - Complex counter encoding
|
||||
//! - PWM encoding: 800µs HIGH = 1, 1600µs HIGH = 0; LOW is 800µs after each bit
|
||||
//! - 64 bits total (8 bytes MSB first: button(4)+serial(24)+counter-related)
|
||||
//! - Preamble: 79 full 1600µs pairs + 80th HIGH only; then gap 2800µs, sync 2800µs HIGH + 1600µs LOW
|
||||
//! - Complex counter encoding (decode_counter) from bytes 4–7
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
@@ -22,7 +22,7 @@ const MIN_COUNT_BIT: usize = 64;
|
||||
const GAP_US: u32 = 2800;
|
||||
const SYNC_US: u32 = 2800;
|
||||
|
||||
/// Decoder states
|
||||
/// Decoder states (matches protopirate's SubaruDecoderStep)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
@@ -67,7 +67,7 @@ impl SubaruDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode the counter from the complex Subaru encoding
|
||||
/// Decode 16-bit counter from bytes 4–7 (matches subaru.c complex encoding)
|
||||
fn decode_counter(kb: &[u8; 8]) -> u16 {
|
||||
let mut lo: u8 = 0;
|
||||
if (kb[4] & 0x40) == 0 { lo |= 0x01; }
|
||||
@@ -115,9 +115,7 @@ impl SubaruDecoder {
|
||||
((hi as u16) << 8) | (lo as u16)
|
||||
}
|
||||
|
||||
/// Add a level+duration to the signal, merging with the previous entry
|
||||
/// if it has the same level. This prevents consecutive same-level pulses
|
||||
/// which would silently merge during HackRF transmission.
|
||||
/// Append level+duration, merging with previous if same level (prevents HackRF from merging consecutive same-level pulses)
|
||||
fn add_level(signal: &mut Vec<LevelDuration>, level: bool, duration: u32) {
|
||||
if let Some(last) = signal.last_mut() {
|
||||
if last.level == level {
|
||||
@@ -128,15 +126,13 @@ impl SubaruDecoder {
|
||||
signal.push(LevelDuration::new(level, duration));
|
||||
}
|
||||
|
||||
/// Process the decoded data
|
||||
/// Build DecodedSignal from 8-byte buffer: serial(bytes 1–3), button(byte0 low nibble), counter(decode_counter) — matches subaru.c
|
||||
fn process_data(&self) -> Option<DecodedSignal> {
|
||||
if self.bit_count < 64 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let b = &self.data;
|
||||
|
||||
// Build 64-bit key
|
||||
let key = ((b[0] as u64) << 56) | ((b[1] as u64) << 48) |
|
||||
((b[2] as u64) << 40) | ((b[3] as u64) << 32) |
|
||||
((b[4] as u64) << 24) | ((b[5] as u64) << 16) |
|
||||
@@ -297,37 +293,24 @@ impl ProtocolDecoder for SubaruDecoder {
|
||||
let key = decoded.data;
|
||||
let mut signal = Vec::with_capacity(512);
|
||||
|
||||
// Generate 3 bursts.
|
||||
//
|
||||
// IMPORTANT: Uses add_level() to merge adjacent same-level pulses.
|
||||
// The HackRF transmitter generates IQ samples sequentially from the
|
||||
// LevelDuration list, so consecutive same-level pairs silently merge
|
||||
// and corrupt timing. For example, the last preamble LOW (1600µs) +
|
||||
// gap LOW (2800µs) would become a single 4400µs LOW without merging.
|
||||
// 3 bursts; add_level() merges same-level pulses for correct HackRF timing (matches protopirate subaru encode)
|
||||
for burst in 0..3 {
|
||||
if burst > 0 {
|
||||
// Inter-burst silence
|
||||
Self::add_level(&mut signal, false, 25000);
|
||||
}
|
||||
|
||||
// Preamble: 79 full pairs + 80th HIGH only.
|
||||
// The gap LOW replaces the 80th preamble LOW.
|
||||
// Preamble: 79 full 1600µs pairs + 80th HIGH only; gap replaces 80th LOW
|
||||
for i in 0..80 {
|
||||
Self::add_level(&mut signal, true, TE_LONG);
|
||||
if i < 79 {
|
||||
Self::add_level(&mut signal, false, TE_LONG);
|
||||
}
|
||||
}
|
||||
|
||||
// Gap (replaces the 80th preamble LOW)
|
||||
Self::add_level(&mut signal, false, GAP_US);
|
||||
|
||||
// Sync
|
||||
Self::add_level(&mut signal, true, SYNC_US);
|
||||
Self::add_level(&mut signal, false, TE_LONG);
|
||||
|
||||
// Data: 64 bits (MSB first)
|
||||
// Short HIGH = 1, Long HIGH = 0
|
||||
// Data: 64 bits MSB first; short HIGH = 1, long HIGH = 0; LOW = 800µs after each
|
||||
for bit in (0..64).rev() {
|
||||
if (key >> bit) & 1 == 1 {
|
||||
Self::add_level(&mut signal, true, TE_SHORT);
|
||||
@@ -337,10 +320,15 @@ impl ProtocolDecoder for SubaruDecoder {
|
||||
Self::add_level(&mut signal, false, TE_SHORT);
|
||||
}
|
||||
|
||||
// End-of-burst gap (extends the last data LOW)
|
||||
Self::add_level(&mut signal, false, TE_LONG * 2);
|
||||
}
|
||||
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SubaruDecoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
+17
-12
@@ -1,12 +1,13 @@
|
||||
//! Suzuki protocol decoder/encoder
|
||||
//!
|
||||
//! Ported from protopirate's suzuki.c
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/suzuki.c`.
|
||||
//! Decode/encode logic (preamble count 350, gap 2000µs, short=0/long=1, field layout) matches reference.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - PWM encoding: 250/500µs timing
|
||||
//! - 64 bits total
|
||||
//! - 350 preamble pairs
|
||||
//! - 2000µs gap between transmissions
|
||||
//! - PWM encoding: 250µs HIGH = 0, 500µs HIGH = 1; LOW 250µs after each bit
|
||||
//! - 64 bits total; preamble: 300+ short LOW pulses then long HIGH starts data
|
||||
//! - 350 preamble pairs (SHORT HIGH / SHORT LOW); 2000µs gap at end
|
||||
//! - Field layout: serial = (data_high&0xFFF)<<16 | data_low>>16; btn = (data_low>>12)&0xF; cnt = (data_high<<4)>>16
|
||||
|
||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||
use crate::duration_diff;
|
||||
@@ -20,7 +21,7 @@ const PREAMBLE_COUNT: u16 = 350;
|
||||
const GAP_TIME: u32 = 2000;
|
||||
const GAP_DELTA: u32 = 399;
|
||||
|
||||
/// Decoder states
|
||||
/// Decoder states (matches protopirate's SuzukiDecoderStep)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
@@ -53,10 +54,11 @@ impl SuzukiDecoder {
|
||||
self.decode_count_bit += 1;
|
||||
}
|
||||
|
||||
/// Parse 64-bit data (matches suzuki.c: serial, btn, cnt layout)
|
||||
fn parse_data(data: u64) -> DecodedSignal {
|
||||
let data_high = (data >> 32) as u32;
|
||||
let data_low = data as u32;
|
||||
|
||||
// Reference: instance->generic.serial = ((data_high & 0xFFF) << 16) | (data_low >> 16); etc.
|
||||
let serial = ((data_high & 0xFFF) << 16) | (data_low >> 16);
|
||||
let btn = ((data_low >> 12) & 0xF) as u8;
|
||||
let cnt = ((data_high << 4) >> 16) as u16;
|
||||
@@ -178,14 +180,12 @@ impl ProtocolDecoder for SuzukiDecoder {
|
||||
|
||||
let mut signal = Vec::with_capacity(1024);
|
||||
|
||||
// Preamble: SHORT HIGH / SHORT LOW pairs
|
||||
// Preamble + data + gap (matches subghz_protocol_encoder_suzuki_get_upload in suzuki.c)
|
||||
for _ in 0..PREAMBLE_COUNT {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
}
|
||||
|
||||
// Data: 64 bits, MSB first
|
||||
// SHORT HIGH (~250µs) = 0, LONG HIGH (~500µs) = 1
|
||||
// Data: 64 bits MSB first; SHORT HIGH = 0, LONG HIGH = 1; LOW = 250µs after each
|
||||
for bit in (0..64).rev() {
|
||||
if (data >> bit) & 1 == 1 {
|
||||
signal.push(LevelDuration::new(true, TE_LONG));
|
||||
@@ -195,9 +195,14 @@ impl ProtocolDecoder for SuzukiDecoder {
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
}
|
||||
|
||||
// End gap
|
||||
signal.push(LevelDuration::new(false, GAP_TIME));
|
||||
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SuzukiDecoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
+27
-23
@@ -1,17 +1,15 @@
|
||||
//! VAG (VW/Audi/Seat/Skoda) protocol decoder/encoder
|
||||
//!
|
||||
//! Ported from protopirate's vag.c
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/vag.c`.
|
||||
//! Decode/encode logic (steps, Type 1/2/3/4, AUT64/TEA, dispatch, gap 6000µs) matches reference.
|
||||
//! Intentional differences for HackRF: TE_DELTA 150 (ref 80), TE_DELTA_12 120 (ref 79);
|
||||
//! Preamble2/Sync2C accept shorter LOW pulses for asymmetric software demodulation.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - Manchester encoding
|
||||
//! - Type 1/2: 300/600µs timing (te_short=300), 80 bits, AUT64 or TEA encryption
|
||||
//! - Type 3/4: 500/1000µs timing (te_short=500), 80 bits, AUT64 encryption
|
||||
//! - 433.92 MHz and 434.42 MHz
|
||||
//! - Four sub-types:
|
||||
//! Type 1: AUT64 encrypted (300µs Manchester, dispatch 0x2A/0x1C/0x46)
|
||||
//! Type 2: TEA encrypted (300µs Manchester, dispatch 0x2A/0x1C/0x46)
|
||||
//! Type 3: AUT64 encrypted (500µs Manchester, auto-detect key)
|
||||
//! Type 4: AUT64 encrypted (500µs Manchester, key 2)
|
||||
//! - Manchester encoding; 80 bits (key1 64 + key2 16)
|
||||
//! - Type 1/2: 300/600µs, prefix 0x2F3F (AUT64) / 0x2F1C (TEA), dispatch 0x2A/0x1C/0x46
|
||||
//! - Type 3/4: 500/1000µs, preamble 41+ pairs, sync 1000µs + 3×750µs, dispatch 0x2B/0x1D/0x47
|
||||
//! - End-of-data gap 6000µs (accept within 4000µs); keys from keystore (VAG raw 64 bytes)
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use super::aut64;
|
||||
@@ -34,7 +32,7 @@ const TE_DELTA_12: u32 = 120; // Wider tolerance for HackRF (was 79)
|
||||
const TEA_DELTA: u32 = 0x9E3779B9;
|
||||
const TEA_ROUNDS: usize = 32;
|
||||
|
||||
/// TEA key schedule for VAG
|
||||
/// TEA key schedule for VAG (matches vag.c vag_tea_key_schedule)
|
||||
static TEA_KEY_SCHEDULE: [u32; 4] = [0x0B46502D, 0x5E253718, 0x2BF93A19, 0x622C1206];
|
||||
|
||||
/// Manchester states
|
||||
@@ -56,17 +54,17 @@ enum ManchesterEvent {
|
||||
Reset,
|
||||
}
|
||||
|
||||
/// Decoder states
|
||||
/// Decoder states (matches protopirate's VAGDecoderStep)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
Preamble1, // Type 1/2: ~300µs preamble
|
||||
Data1, // Type 1/2: Manchester data
|
||||
Preamble2, // Type 3/4: ~500µs preamble
|
||||
Sync2A, // Type 3/4: sync pattern step A
|
||||
Sync2B, // Type 3/4: sync pattern step B (750µs)
|
||||
Sync2C, // Type 3/4: sync pattern step C (750µs)
|
||||
Data2, // Type 3/4: Manchester data
|
||||
Preamble1,
|
||||
Data1,
|
||||
Preamble2,
|
||||
Sync2A,
|
||||
Sync2B,
|
||||
Sync2C,
|
||||
Data2,
|
||||
}
|
||||
|
||||
/// VAG sub-type
|
||||
@@ -182,7 +180,7 @@ impl VagDecoder {
|
||||
self.bit_count += 1;
|
||||
}
|
||||
|
||||
/// TEA decrypt
|
||||
/// TEA decrypt (matches vag.c vag_tea_decrypt)
|
||||
fn tea_decrypt(v0: &mut u32, v1: &mut u32, key_schedule: &[u32; 4]) {
|
||||
let mut sum = TEA_DELTA.wrapping_mul(TEA_ROUNDS as u32);
|
||||
for _ in 0..TEA_ROUNDS {
|
||||
@@ -198,7 +196,7 @@ impl VagDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// TEA encrypt
|
||||
/// TEA encrypt (matches vag.c vag_tea_encrypt)
|
||||
fn tea_encrypt(v0: &mut u32, v1: &mut u32, key_schedule: &[u32; 4]) {
|
||||
let mut sum: u32 = 0;
|
||||
for _ in 0..TEA_ROUNDS {
|
||||
@@ -278,7 +276,7 @@ impl VagDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the collected data and attempt decryption
|
||||
/// Parse key1/key2 and decrypt by type (AUT64/TEA, dispatch); matches vag.c vag_parse_data
|
||||
fn parse_data(&mut self) {
|
||||
self.decrypted = false;
|
||||
self.serial = 0;
|
||||
@@ -950,7 +948,7 @@ impl ProtocolDecoder for VagDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for gap (end of data)
|
||||
// End-of-data gap: 6000µs, accept within 4000µs (matches vag.c check_gap1_data)
|
||||
if !level {
|
||||
let gap_diff = if duration > 6000 {
|
||||
duration - 6000
|
||||
@@ -1145,3 +1143,9 @@ impl ProtocolDecoder for VagDecoder {
|
||||
self.encode_signal(decoded)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VagDecoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
//! AM/OOK demodulator for extracting level+duration pairs from raw IQ samples.
|
||||
//!
|
||||
//! KAT uses **AM (envelope) detection only**; FM/2FSK is not demodulated. Protocols
|
||||
//! are tagged with AM/FM/Both (from ProtoPirate) for display and export. FM protocols
|
||||
//! may still decode when the received signal is strong enough to produce a usable
|
||||
//! envelope; proper FM support would require a separate demodulator path.
|
||||
//!
|
||||
//! This demodulator converts raw IQ samples into a stream of (level, duration_us) pairs
|
||||
//! that can be processed by protocol decoders, similar to how the Flipper Zero SubGHz
|
||||
//! system works.
|
||||
|
||||
+17
-1
@@ -29,6 +29,8 @@ pub struct Config {
|
||||
pub export_directory: PathBuf,
|
||||
/// Maximum captures to keep in memory during a session
|
||||
pub max_captures: usize,
|
||||
/// If off, only successfully decoded signals are added to the list. If on, unknown signals are also shown.
|
||||
pub research_mode: bool,
|
||||
|
||||
// [radio]
|
||||
/// Default frequency in Hz
|
||||
@@ -54,6 +56,7 @@ impl Config {
|
||||
Self {
|
||||
export_directory: config_dir.join("exports"),
|
||||
max_captures: 100,
|
||||
research_mode: false,
|
||||
default_frequency: 433_920_000,
|
||||
default_lna_gain: 24,
|
||||
default_vga_gain: 20,
|
||||
@@ -83,6 +86,12 @@ impl Config {
|
||||
.map(|v| v as usize)
|
||||
.unwrap_or(defaults.max_captures);
|
||||
|
||||
let research_mode = ini
|
||||
.getbool("general", "research_mode")
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or(defaults.research_mode);
|
||||
|
||||
let default_frequency = ini
|
||||
.getuint("radio", "default_frequency")
|
||||
.ok()
|
||||
@@ -123,6 +132,7 @@ impl Config {
|
||||
Ok(Self {
|
||||
export_directory,
|
||||
max_captures,
|
||||
research_mode,
|
||||
default_frequency,
|
||||
default_lna_gain,
|
||||
default_vga_gain,
|
||||
@@ -154,6 +164,10 @@ export_directory = {export_dir}
|
||||
; signals (.fob / .sub) survive in the exports folder.
|
||||
max_captures = {max_captures}
|
||||
|
||||
; When off, only successfully decoded signals appear in the list.
|
||||
; When on, unknown (unidentified) signals are also shown (research mode).
|
||||
research_mode = {research_mode}
|
||||
|
||||
[radio]
|
||||
; Default receive frequency in Hz ({freq_mhz:.2} MHz)
|
||||
; Common keyfob frequencies: 315000000, 433920000, 868350000
|
||||
@@ -179,6 +193,7 @@ include_raw_pairs = {raw_pairs}
|
||||
path = path.display(),
|
||||
export_dir = export_str,
|
||||
max_captures = self.max_captures,
|
||||
research_mode = self.research_mode,
|
||||
freq_mhz = freq_mhz,
|
||||
frequency = self.default_frequency,
|
||||
lna = self.default_lna_gain,
|
||||
@@ -342,7 +357,8 @@ impl Storage {
|
||||
&self.config.export_directory
|
||||
}
|
||||
|
||||
/// Get the keystore directory path (`~/.config/KAT/keystore`)
|
||||
/// Get the keystore directory path (`~/.config/KAT/keystore`). Kept for optional file-based override.
|
||||
#[allow(dead_code)]
|
||||
pub fn keystore_dir(&self) -> PathBuf {
|
||||
self.config_dir.join("keystore")
|
||||
}
|
||||
|
||||
@@ -158,12 +158,14 @@ fn render_detail_panel(frame: &mut Frame, area: Rect, app: &App) {
|
||||
Span::styled(make, value_style),
|
||||
]));
|
||||
|
||||
// Row 2: Frequency + Modulation + Encryption
|
||||
// Row 2: Frequency + Encoding (Mod) + RF (AM/FM) + Encryption
|
||||
left_lines.push(Line::from(vec![
|
||||
Span::styled(" Freq: ", label_style),
|
||||
Span::styled(capture.frequency_mhz(), value_style),
|
||||
Span::styled(" Mod: ", label_style),
|
||||
Span::styled(capture.modulation().to_string(), value_style),
|
||||
Span::styled(" RF: ", label_style),
|
||||
Span::styled(capture.rf_modulation().to_string(), value_style),
|
||||
Span::styled(" Enc: ", label_style),
|
||||
Span::styled(capture.encryption_type(), value_style),
|
||||
]));
|
||||
|
||||
Reference in New Issue
Block a user