From 284bf45198f252ccd772433323af3736579601cf Mon Sep 17 00:00:00 2001 From: leviathan Date: Sat, 7 Feb 2026 18:53:53 -0500 Subject: [PATCH] protocol updates --- Cargo.lock | 2 +- src/app.rs | 6 + src/protocols/ford_v0.rs | 743 +++++++++++++++++++++++++++---------- src/protocols/keys.rs | 183 +++++++++ src/protocols/kia_v3_v4.rs | 4 +- src/protocols/kia_v5.rs | 33 +- src/protocols/kia_v6.rs | 22 +- src/protocols/star_line.rs | 5 +- src/storage.rs | 22 +- 9 files changed, 793 insertions(+), 227 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9ec8190..ad564d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -402,7 +402,7 @@ dependencies = [ [[package]] name = "kat" -version = "0.2.0" +version = "1.0.0" dependencies = [ "anyhow", "atty", diff --git a/src/app.rs b/src/app.rs index 662d78b..b8e5d55 100644 --- a/src/app.rs +++ b/src/app.rs @@ -227,6 +227,12 @@ impl App { /// Create a new application instance pub fn new() -> Result { let storage = Storage::new()?; + + // ── Load protocol encryption keys from keystore ────────────────── + let keystore_dir = storage.keystore_dir(); + crate::protocols::keys::create_default_keystore(&keystore_dir); + crate::protocols::keys::load_keystore_from_dir(&keystore_dir); + let protocols = ProtocolRegistry::new(); let (radio_event_tx, radio_event_rx) = mpsc::channel(); diff --git a/src/protocols/ford_v0.rs b/src/protocols/ford_v0.rs index 4b3addd..be428a5 100644 --- a/src/protocols/ford_v0.rs +++ b/src/protocols/ford_v0.rs @@ -1,11 +1,13 @@ -//! Ford V0 protocol decoder +//! Ford V0 protocol decoder/encoder //! //! Ported from protopirate's ford_v0.c //! //! Protocol characteristics: //! - Manchester encoding: 250/500µs timing -//! - 64 bits total -//! - Matrix-based CRC +//! - 80 bits total (64-bit key1 + 16-bit key2) +//! - Matrix-based CRC in GF(2) +//! - BS (byte swap) magic calculation +//! - 6 bursts, 4 preamble pairs, 3500µs gap use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal}; use crate::radio::demodulator::LevelDuration; @@ -15,131 +17,474 @@ const TE_SHORT: u32 = 250; const TE_LONG: u32 = 500; const TE_DELTA: u32 = 100; const MIN_COUNT_BIT: usize = 64; +const TOTAL_BURSTS: u8 = 6; +const PREAMBLE_PAIRS: usize = 4; +const GAP_US: u32 = 3500; -/// Manchester decoder states +// CRC matrix for Ford V0 — GF(2) matrix multiplication +// Copied directly from protopirate's ford_v0.c +const CRC_MATRIX: [u8; 64] = [ + 0xDA, 0xB5, 0x55, 0x6A, 0xAA, 0xAA, 0xAA, 0xD5, + 0xB6, 0x6C, 0xCC, 0xD9, 0x99, 0x99, 0x99, 0xB3, + 0x71, 0xE3, 0xC3, 0xC7, 0x87, 0x87, 0x87, 0x8F, + 0x0F, 0xE0, 0x3F, 0xC0, 0x7F, 0x80, 0x7F, 0x80, + 0x00, 0x1F, 0xFF, 0xC0, 0x00, 0x7F, 0xFF, 0x80, + 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0x80, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, + 0x23, 0x12, 0x94, 0x84, 0x35, 0xF4, 0x55, 0x84, +]; + +/// Manchester state machine states (matches Flipper's manchester_decoder.h) #[derive(Debug, Clone, Copy, PartialEq)] enum ManchesterState { - Mid0, - Mid1, - Start0, - Start1, + Mid0 = 0, + Mid1 = 1, + Start0 = 2, + Start1 = 3, } -/// Decoder states +/// Decoder step states (matches protopirate's FordV0DecoderStep) #[derive(Debug, Clone, Copy, PartialEq)] enum DecoderStep { Reset, - CheckPreamble, - SaveDuration, - CheckDuration, + Preamble, + PreambleCheck, + Gap, + Data, } /// Ford V0 protocol decoder pub struct FordV0Decoder { step: DecoderStep, - te_last: u32, - header_count: u16, - decode_data: u64, - decode_count_bit: usize, manchester_state: ManchesterState, + decode_data: u64, + bit_count: u8, + header_count: u16, + te_last: u32, + // Decoded results (stored for encoding) + key1: u64, + key2: u16, + serial: u32, + button: u8, + counter: u32, + bs_magic: u8, } impl FordV0Decoder { pub fn new() -> Self { Self { step: DecoderStep::Reset, - te_last: 0, - header_count: 0, - decode_data: 0, - decode_count_bit: 0, manchester_state: ManchesterState::Mid1, + decode_data: 0, + bit_count: 0, + header_count: 0, + te_last: 0, + key1: 0, + key2: 0, + serial: 0, + button: 0, + counter: 0, + bs_magic: 0, } } - /// Manchester decode: advance state machine - fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option { - let event = match (is_short, is_high) { - (true, true) => 0, // Short High - (true, false) => 1, // Short Low - (false, true) => 2, // Long High - (false, false) => 3, // Long Low - }; + /// Add a bit to the accumulator. + /// After 64 bits, key1 is extracted and the accumulator resets for key2. + fn add_bit(&mut self, bit: bool) { + self.decode_data = (self.decode_data << 1) | (bit as u64); + self.bit_count += 1; + } - let (new_state, output) = match (self.manchester_state, event) { - // From Mid0 or Mid1 - (ManchesterState::Mid0, 0) | (ManchesterState::Mid1, 0) => - (ManchesterState::Start1, None), - (ManchesterState::Mid0, 1) | (ManchesterState::Mid1, 1) => - (ManchesterState::Start0, None), - - // From Start1 - (ManchesterState::Start1, 1) => (ManchesterState::Mid1, Some(true)), - (ManchesterState::Start1, 3) => (ManchesterState::Start0, Some(true)), - - // From Start0 - (ManchesterState::Start0, 0) => (ManchesterState::Mid0, Some(false)), - (ManchesterState::Start0, 2) => (ManchesterState::Start1, Some(false)), - - // Reset on invalid transitions - _ => (ManchesterState::Mid1, None), + /// Check if we've reached a data milestone (64 or 80 bits). + /// At 64 bits: extract key1 (inverted) and reset accumulator. + /// At 80 bits: extract key2 (inverted), decode fields, return true. + fn process_data(&mut self) -> bool { + if self.bit_count == 64 { + // First 64 bits → key1 (bit-inverted) + self.key1 = !self.decode_data; + self.decode_data = 0; + return false; + } + + if self.bit_count == 80 { + // Next 16 bits → key2 (bit-inverted) + let key2_raw = (self.decode_data & 0xFFFF) as u16; + self.key2 = !key2_raw; + + // Decode serial, button, counter, bs_magic from key1+key2 + let (serial, button, count, bs_magic) = + Self::decode_ford_v0(self.key1, self.key2); + self.serial = serial; + self.button = button; + self.counter = count; + self.bs_magic = bs_magic; + return true; + } + + false + } + + /// Manchester state machine (matches Flipper's manchester_advance exactly). + /// + /// Event values: + /// 0 = ShortLow (short HIGH pulse → transition to LOW) + /// 1 = ShortHigh (short LOW pulse → transition to HIGH) + /// 2 = LongLow (long HIGH pulse → transition to LOW) + /// 3 = LongHigh (long LOW pulse → transition to HIGH) + /// + /// Returns Some(bit) when a data bit is produced. + fn manchester_advance(&mut self, event: u8) -> Option { + let (new_state, emit) = match (self.manchester_state, event) { + // State Mid0: currently in middle of a 0-bit (signal is LOW) + (ManchesterState::Mid0, 0) => (ManchesterState::Mid0, false), // ShortLow: error, stay + (ManchesterState::Mid0, 1) => (ManchesterState::Start1, true), // ShortHigh: emit + (ManchesterState::Mid0, 2) => (ManchesterState::Mid0, false), // LongLow: error + (ManchesterState::Mid0, 3) => (ManchesterState::Mid1, true), // LongHigh: emit + + // State Mid1: currently in middle of a 1-bit (signal is HIGH) + (ManchesterState::Mid1, 0) => (ManchesterState::Start0, true), // ShortLow: emit + (ManchesterState::Mid1, 1) => (ManchesterState::Mid1, false), // ShortHigh: error, stay + (ManchesterState::Mid1, 2) => (ManchesterState::Mid0, true), // LongLow: emit + (ManchesterState::Mid1, 3) => (ManchesterState::Mid1, false), // LongHigh: error + + // State Start0: at start of a 0-bit (signal is HIGH, waiting for H→L) + (ManchesterState::Start0, 0) => (ManchesterState::Mid0, false), // ShortLow: complete 0 + (ManchesterState::Start0, 1) => (ManchesterState::Mid0, false), // error → reset + (ManchesterState::Start0, 2) => (ManchesterState::Mid0, false), // error + (ManchesterState::Start0, 3) => (ManchesterState::Mid1, false), // error + + // State Start1: at start of a 1-bit (signal is LOW, waiting for L→H) + (ManchesterState::Start1, 0) => (ManchesterState::Mid0, false), // error + (ManchesterState::Start1, 1) => (ManchesterState::Mid1, false), // ShortHigh: complete 1 + (ManchesterState::Start1, 2) => (ManchesterState::Mid0, false), // error + (ManchesterState::Start1, 3) => (ManchesterState::Mid1, false), // error + + _ => (ManchesterState::Mid1, false), }; self.manchester_state = new_state; - output + + if emit { + // Bit value: 1 for High events (1,3), 0 for Low events (0,2) + Some((event & 1) == 1) + } else { + None + } } - /// CRC matrix for Ford - const CRC_MATRIX: [[u8; 4]; 8] = [ - [0x0C, 0xBB, 0x51, 0x25], - [0x18, 0xB1, 0x62, 0xCB], - [0x30, 0xA7, 0x44, 0x57], - [0x60, 0x89, 0x88, 0xAE], - [0xC0, 0xD6, 0xD4, 0x97], - [0x25, 0x49, 0x6D, 0xE1], - [0x4A, 0x92, 0xDA, 0x03], - [0x94, 0xE1, 0x71, 0x06], - ]; + // ========================================================================= + // CRC functions + // ========================================================================= - /// Calculate Ford CRC - fn calculate_crc(data: u64) -> u8 { + /// Population count for a byte + fn popcount8(mut x: u8) -> u8 { + let mut count = 0u8; + while x != 0 { + count += x & 1; + x >>= 1; + } + count + } + + /// Calculate CRC using GF(2) matrix multiplication. + /// buf must have at least 9 bytes; CRC is computed over buf[1..=8]. + fn calculate_crc(buf: &[u8]) -> u8 { let mut crc = 0u8; - - for byte_idx in 0..7 { - let byte = ((data >> (56 - byte_idx * 8)) & 0xFF) as u8; - for bit in 0..8 { - if (byte >> (7 - bit)) & 1 == 1 { - crc ^= Self::CRC_MATRIX[bit][byte_idx % 4]; - } + for row in 0..8 { + let mut xor_sum = 0u8; + for col in 0..8 { + xor_sum ^= CRC_MATRIX[row * 8 + col] & buf[col + 1]; + } + let parity = Self::popcount8(xor_sum) & 1; + if parity != 0 { + crc |= 1 << row; } } - crc } - /// Parse decoded data - fn parse_data(data: u64) -> DecodedSignal { - // Ford V0 format: - // Bits 60-63: Prefix (0x5) - // Bits 32-59: Serial (28 bits) - // Bits 28-31: Button (4 bits) - // Bits 16-27: Counter (12 bits) - // Bits 8-15: Encrypted data - // Bits 0-7: CRC - - let serial = ((data >> 32) & 0x0FFFFFFF) as u32; - let button = ((data >> 28) & 0x0F) as u8; - let counter = ((data >> 16) & 0x0FFF) as u16; - let received_crc = (data & 0xFF) as u8; - let calculated_crc = Self::calculate_crc(data); + /// Calculate CRC for transmission (key1 bytes + BS byte, XOR 0x80) + fn calculate_crc_for_tx(key1: u64, bs: u8) -> u8 { + let mut buf = [0u8; 16]; + for i in 0..8 { + buf[i] = (key1 >> (56 - i * 8)) as u8; + } + buf[8] = bs; + Self::calculate_crc(&buf) ^ 0x80 + } - DecodedSignal { - serial: Some(serial), - button: Some(button), - counter: Some(counter), - crc_valid: received_crc == calculated_crc, - data, - data_count_bit: MIN_COUNT_BIT, - encoder_capable: true, + /// Verify CRC of received key1 + key2 + fn verify_crc(key1: u64, key2: u16) -> bool { + let mut buf = [0u8; 16]; + for i in 0..8 { + buf[i] = (key1 >> (56 - i * 8)) as u8; + } + buf[8] = (key2 >> 8) as u8; // BS byte + let calculated_crc = Self::calculate_crc(&buf); + let received_crc = (key2 as u8) ^ 0x80; + calculated_crc == received_crc + } + + // ========================================================================= + // BS calculation + // ========================================================================= + + /// Calculate BS = (count_low_byte + bs_magic + (button << 4)) with overflow handling + fn calculate_bs(count: u32, button: u8, bs_magic: u8) -> u8 { + let result: u16 = (count as u16 & 0xFF) + .wrapping_add(bs_magic as u16) + .wrapping_add((button as u16) << 4); + (result as u8).wrapping_sub(if result & 0xFF00 != 0 { 0x80 } else { 0 }) + } + + // ========================================================================= + // Decode function — extract serial/button/counter/bs_magic from key1+key2 + // Matches protopirate's decode_ford_v0() exactly + // ========================================================================= + + fn decode_ford_v0(key1: u64, key2: u16) -> (u32, u8, u32, u8) { + let mut buf = [0u8; 13]; + + // Extract key1 bytes (big-endian) + for i in 0..8 { + buf[i] = (key1 >> (56 - i * 8)) as u8; + } + // Extract key2 bytes + buf[8] = (key2 >> 8) as u8; + buf[9] = key2 as u8; + + // BS parity calculation + let bs = buf[8]; + let mut tmp = bs; + let mut parity = 0u8; + let parity_any: u8 = if tmp != 0 { 1 } else { 0 }; + while tmp != 0 { + parity ^= tmp & 1; + tmp >>= 1; + } + buf[11] = if parity_any != 0 { parity } else { 0 }; + + // XOR decryption based on parity bit + let (xor_byte, limit) = if buf[11] != 0 { + (buf[7], 7usize) + } else { + (buf[6], 6usize) + }; + + for idx in 1..limit { + buf[idx] ^= xor_byte; + } + + if buf[11] == 0 { + buf[7] ^= xor_byte; + } + + // Bit-interleave swap of buf[6] and buf[7] + let orig_b7 = buf[7]; + buf[7] = (orig_b7 & 0xAA) | (buf[6] & 0x55); + let mixed = (buf[6] & 0xAA) | (orig_b7 & 0x55); + buf[12] = mixed; + buf[6] = mixed; + + // Extract serial (stored little-endian in buf[1..5], convert to big-endian) + let serial_le = (buf[1] as u32) + | ((buf[2] as u32) << 8) + | ((buf[3] as u32) << 16) + | ((buf[4] as u32) << 24); + let serial = ((serial_le & 0xFF) << 24) + | (((serial_le >> 8) & 0xFF) << 16) + | (((serial_le >> 16) & 0xFF) << 8) + | ((serial_le >> 24) & 0xFF); + + // Extract button (high nibble of buf[5]) + let button = (buf[5] >> 4) & 0x0F; + + // Extract counter (20-bit) + let count = ((buf[5] as u32 & 0x0F) << 16) | ((buf[6] as u32) << 8) | (buf[7] as u32); + + // Calculate BS magic number for this fob + let bs_magic = bs + .wrapping_add(if bs & 0x80 != 0 { 0x80 } else { 0 }) + .wrapping_sub(button << 4) + .wrapping_sub(count as u8); + + (serial, button, count, bs_magic) + } + + // ========================================================================= + // Encode function — rebuild key1 from serial/button/counter/bs + // Matches protopirate's encode_ford_v0() exactly + // ========================================================================= + + fn encode_ford_v0( + header_byte: u8, + serial: u32, + button: u8, + count: u32, + bs: u8, + ) -> u64 { + let mut buf = [0u8; 8]; + + buf[0] = header_byte; + + // Serial in big-endian + buf[1] = (serial >> 24) as u8; + buf[2] = (serial >> 16) as u8; + buf[3] = (serial >> 8) as u8; + buf[4] = serial as u8; + + // Button + counter high nibble + buf[5] = ((button & 0x0F) << 4) | ((count >> 16) as u8 & 0x0F); + + let count_mid = (count >> 8) as u8; + let count_low = count as u8; + + // Bit-interleave: split even/odd bits between the two counter bytes + let post_xor_6 = (count_mid & 0xAA) | (count_low & 0x55); + let post_xor_7 = (count_low & 0xAA) | (count_mid & 0x55); + + // Calculate BS parity + let mut parity = 0u8; + let mut tmp = bs; + while tmp != 0 { + parity ^= tmp & 1; + tmp >>= 1; + } + let parity_bit = if bs != 0 { parity != 0 } else { false }; + + // XOR encryption based on parity (inverse of decode) + if parity_bit { + let xor_byte = post_xor_7; + buf[1] ^= xor_byte; + buf[2] ^= xor_byte; + buf[3] ^= xor_byte; + buf[4] ^= xor_byte; + buf[5] ^= xor_byte; + buf[6] = post_xor_6 ^ xor_byte; + buf[7] = post_xor_7; + } else { + let xor_byte = post_xor_6; + buf[1] ^= xor_byte; + buf[2] ^= xor_byte; + buf[3] ^= xor_byte; + buf[4] ^= xor_byte; + buf[5] ^= xor_byte; + buf[6] = post_xor_6; + buf[7] = post_xor_7 ^ xor_byte; + } + + // Pack into u64 + let mut key1 = 0u64; + for b in &buf { + key1 = (key1 << 8) | (*b as u64); + } + key1 + } + + // ========================================================================= + // Encoder signal builder — differential Manchester with ADD_LEVEL merging + // Matches protopirate's subghz_protocol_encoder_ford_v0_get_upload() + // ========================================================================= + + fn build_upload(key1: u64, key2: u16) -> Vec { + let mut signal = Vec::with_capacity(1024); + + // Transmitted data is bit-inverted + let tx_key1 = !key1; + let tx_key2 = !key2; + + for burst in 0..TOTAL_BURSTS { + // Preamble start: short HIGH + long LOW + Self::add_level(&mut signal, true, TE_SHORT); + Self::add_level(&mut signal, false, TE_LONG); + + // Preamble pairs: long HIGH + long LOW + for _ in 0..PREAMBLE_PAIRS { + Self::add_level(&mut signal, true, TE_LONG); + Self::add_level(&mut signal, false, TE_LONG); + } + + // End preamble: short HIGH + gap LOW + Self::add_level(&mut signal, true, TE_SHORT); + Self::add_level(&mut signal, false, GAP_US); + + // First data bit (bit 62 of tx_key1; bit 63 is implicit from gap) + let first_bit = ((tx_key1 >> 62) & 1) == 1; + if first_bit { + Self::add_level(&mut signal, true, TE_LONG); + } else { + Self::add_level(&mut signal, true, TE_SHORT); + Self::add_level(&mut signal, false, TE_LONG); + } + + let mut prev_bit = first_bit; + + // Encode remaining key1 bits (61 down to 0) — differential Manchester + for bit_pos in (0..62).rev() { + let curr_bit = ((tx_key1 >> bit_pos) & 1) == 1; + Self::encode_diff_bit(&mut signal, prev_bit, curr_bit); + prev_bit = curr_bit; + } + + // Encode key2 bits (15 down to 0) + for bit_pos in (0..16).rev() { + let curr_bit = ((tx_key2 >> bit_pos) & 1) == 1; + Self::encode_diff_bit(&mut signal, prev_bit, curr_bit); + prev_bit = curr_bit; + } + + // Inter-burst gap (except for last burst) + if burst < TOTAL_BURSTS - 1 { + Self::add_level(&mut signal, false, TE_LONG * 100); + } + } + + signal + } + + /// Encode one differential Manchester bit transition + fn encode_diff_bit(signal: &mut Vec, prev: bool, curr: bool) { + match (prev, curr) { + (false, false) => { + // 0→0: mid-bit transition only (HIGH short, LOW short) + Self::add_level(signal, true, TE_SHORT); + Self::add_level(signal, false, TE_SHORT); + } + (false, true) => { + // 0→1: transition at start (extends to LONG HIGH) + Self::add_level(signal, true, TE_LONG); + } + (true, false) => { + // 1→0: transition at start (extends to LONG LOW) + Self::add_level(signal, false, TE_LONG); + } + (true, true) => { + // 1→1: mid-bit transition only (LOW short, HIGH short) + Self::add_level(signal, false, TE_SHORT); + Self::add_level(signal, true, TE_SHORT); + } + } + } + + /// ADD_LEVEL equivalent: merge adjacent same-level pulses for efficiency. + /// This matches protopirate's ADD_LEVEL macro behavior. + fn add_level(signal: &mut Vec, level: bool, duration: u32) { + if let Some(last) = signal.last_mut() { + if last.level == level { + *last = LevelDuration::new(level, last.duration_us + duration); + return; + } + } + signal.push(LevelDuration::new(level, duration)); + } + + /// Get button name for Ford V0 + fn button_name(btn: u8) -> &'static str { + match btn { + 0x01 => "Lock", + 0x02 => "Unlock", + 0x04 => "Boot", + _ => "Unknown", } } } @@ -164,105 +509,116 @@ impl ProtocolDecoder for FordV0Decoder { fn reset(&mut self) { self.step = DecoderStep::Reset; - self.te_last = 0; - self.header_count = 0; - self.decode_data = 0; - self.decode_count_bit = 0; self.manchester_state = ManchesterState::Mid1; + self.decode_data = 0; + self.bit_count = 0; + self.header_count = 0; + self.te_last = 0; + self.key1 = 0; + self.key2 = 0; + self.serial = 0; + self.button = 0; + self.counter = 0; + self.bs_magic = 0; } fn feed(&mut self, level: bool, duration: u32) -> Option { - let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA; - let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA; - match self.step { + // ─── Step 1: Reset — wait for short HIGH pulse ─── DecoderStep::Reset => { - if level && is_short { - self.step = DecoderStep::CheckPreamble; - self.header_count = 1; + if level && duration_diff!(duration, TE_SHORT) < TE_DELTA { + self.decode_data = 0; + self.bit_count = 0; + self.header_count = 0; + self.step = DecoderStep::Preamble; + self.te_last = duration; + // Reset Manchester state machine self.manchester_state = ManchesterState::Mid1; } } - DecoderStep::CheckPreamble => { - if is_short { - self.header_count += 1; - if self.header_count > 20 && !level { - // Enough preamble, start looking for data - self.step = DecoderStep::SaveDuration; - self.decode_data = 0; - self.decode_count_bit = 0; - self.manchester_state = ManchesterState::Mid1; - } - } else if is_long { - if self.header_count > 10 { - self.step = DecoderStep::SaveDuration; - self.decode_data = 0; - self.decode_count_bit = 0; - self.manchester_state = ManchesterState::Mid1; - - // Process this long pulse - if let Some(bit) = self.manchester_advance(false, level) { - self.decode_data = (self.decode_data << 1) | (bit as u64); - self.decode_count_bit += 1; - } + // ─── Step 2: Preamble — wait for long LOW after short HIGH ─── + DecoderStep::Preamble => { + if !level { + if duration_diff!(duration, TE_LONG) < TE_DELTA { + self.te_last = duration; + self.step = DecoderStep::PreambleCheck; } else { self.step = DecoderStep::Reset; } - } else { + } + } + + // ─── Step 3: PreambleCheck — count preamble pairs or transition to gap ─── + DecoderStep::PreambleCheck => { + if level { + 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 duration_diff!(duration, TE_SHORT) < TE_DELTA { + // Short HIGH: end of preamble, transition to gap + self.step = DecoderStep::Gap; + } else { + self.step = DecoderStep::Reset; + } + } + } + + // ─── Step 4: Gap — wait for ~3500µs LOW gap ─── + DecoderStep::Gap => { + if !level && duration_diff!(duration, GAP_US) < 250 { + // Gap detected, start data collection + // First bit is implicitly 1 (matches protopirate) + self.decode_data = 1; + self.bit_count = 1; + self.step = DecoderStep::Data; + } else if !level && duration > GAP_US + 250 { self.step = DecoderStep::Reset; } } - DecoderStep::SaveDuration => { - self.te_last = duration; - self.step = DecoderStep::CheckDuration; - } + // ─── Step 5: Data — Manchester decode 80 bits ─── + DecoderStep::Data => { + // Map level+duration to Manchester event + // Flipper convention: level=true (HIGH) → Low event, level=false (LOW) → High event + let event = if duration_diff!(duration, TE_SHORT) < TE_DELTA { + if level { 0 } else { 1 } // ShortLow / ShortHigh + } else if duration_diff!(duration, TE_LONG) < TE_DELTA { + if level { 2 } else { 3 } // LongLow / LongHigh + } else { + self.step = DecoderStep::Reset; + return None; + }; - DecoderStep::CheckDuration => { - let last_short = duration_diff!(self.te_last, TE_SHORT) < TE_DELTA; - let last_long = duration_diff!(self.te_last, TE_LONG) < TE_DELTA; - - // Check for end of transmission - if duration > TE_LONG * 3 { - if self.decode_count_bit >= MIN_COUNT_BIT { - let result = Self::parse_data(self.decode_data); + // Advance Manchester state machine + if let Some(data_bit) = self.manchester_advance(event) { + self.add_bit(data_bit); + + if self.process_data() { + // 80 bits decoded — check CRC and return result + let crc_ok = Self::verify_crc(self.key1, self.key2); + let _btn_name = Self::button_name(self.button); + + let result = DecodedSignal { + serial: Some(self.serial), + button: Some(self.button), + counter: Some(self.counter as u16), + crc_valid: crc_ok, + data: self.key1, + data_count_bit: MIN_COUNT_BIT, + encoder_capable: true, + }; + + self.decode_data = 0; + self.bit_count = 0; self.step = DecoderStep::Reset; return Some(result); } - self.step = DecoderStep::Reset; - return None; } - // Manchester decode - if last_short { - if let Some(bit) = self.manchester_advance(true, !level) { - self.decode_data = (self.decode_data << 1) | (bit as u64); - self.decode_count_bit += 1; - } - } else if last_long { - if let Some(bit) = self.manchester_advance(false, !level) { - self.decode_data = (self.decode_data << 1) | (bit as u64); - self.decode_count_bit += 1; - } - } - - if is_short || is_long { - if let Some(bit) = self.manchester_advance(is_short, level) { - self.decode_data = (self.decode_data << 1) | (bit as u64); - self.decode_count_bit += 1; - } - self.step = DecoderStep::SaveDuration; - } else { - self.step = DecoderStep::Reset; - } - - // Check if we have enough bits - if self.decode_count_bit >= MIN_COUNT_BIT { - let result = Self::parse_data(self.decode_data); - self.step = DecoderStep::Reset; - return Some(result); - } + self.te_last = duration; } } @@ -275,46 +631,33 @@ impl ProtocolDecoder for FordV0Decoder { fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option> { let serial = decoded.serial?; - let counter = decoded.counter.unwrap_or(0); - // Build data packet - let mut data: u64 = 0; - data |= 0x5 << 60; // Prefix - data |= ((serial as u64) & 0x0FFFFFFF) << 32; - data |= ((button as u64) & 0x0F) << 28; - data |= ((counter as u64) & 0x0FFF) << 16; - data |= ((decoded.data >> 8) & 0xFF) << 8; // Keep encrypted byte - data |= Self::calculate_crc(data) as u64; + // Use the stored counter from last decode (or from decoded signal) + // Increment counter by 1 for the new transmission + let base_counter = if self.counter != 0 { + self.counter + } else { + decoded.counter.unwrap_or(0) as u32 + }; + let new_counter = base_counter.wrapping_add(1) & 0xFFFFF; // 20-bit counter - let mut signal = Vec::with_capacity(256); + // Use stored bs_magic (or default to 0x6F for backward compatibility) + let bs_magic = if self.bs_magic != 0 { self.bs_magic } else { 0x6F }; - // Preamble - for _ in 0..30 { - signal.push(LevelDuration::new(true, TE_SHORT)); - signal.push(LevelDuration::new(false, TE_SHORT)); - } + // Calculate BS for the new counter value + let bs = Self::calculate_bs(new_counter, button, bs_magic); - // Sync - signal.push(LevelDuration::new(true, TE_LONG)); - signal.push(LevelDuration::new(false, TE_LONG)); + // Extract header byte from the original key1 (first byte) + let header_byte = (decoded.data >> 56) as u8; - // Data: Manchester encoded, 64 bits MSB first - for bit_num in (0..64).rev() { - let bit = (data >> bit_num) & 1 == 1; - if bit { - // Manchester 1: low-high - signal.push(LevelDuration::new(false, TE_SHORT)); - signal.push(LevelDuration::new(true, TE_SHORT)); - } else { - // Manchester 0: high-low - signal.push(LevelDuration::new(true, TE_SHORT)); - signal.push(LevelDuration::new(false, TE_SHORT)); - } - } + // Encode new key1 from fields + let new_key1 = Self::encode_ford_v0(header_byte, serial, button, new_counter, bs); - // End - signal.push(LevelDuration::new(false, TE_LONG * 4)); + // Calculate CRC for key2 + let crc = Self::calculate_crc_for_tx(new_key1, bs); + let new_key2 = ((bs as u16) << 8) | (crc as u16); - Some(signal) + // Build the signal upload + Some(Self::build_upload(new_key1, new_key2)) } } diff --git a/src/protocols/keys.rs b/src/protocols/keys.rs index 7081b3e..f22f15e 100644 --- a/src/protocols/keys.rs +++ b/src/protocols/keys.rs @@ -6,9 +6,14 @@ //! - 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 configparser::ini::Ini; use std::path::Path; use std::sync::{OnceLock, RwLock}; use tracing::{info, warn, error}; @@ -18,6 +23,7 @@ const KIA_KEY1: u32 = 10; // kia_mf_key const KIA_KEY2: u32 = 11; // kia_v6_a_key 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; @@ -32,6 +38,8 @@ pub struct KeyStore { pub kia_v6_b_key: u64, /// KIA V5 mixer key pub kia_v5_key: u64, + /// Star Line manufacturer key (for KeeLoq) + pub star_line_mf_key: u64, /// VAG AUT64 keys pub vag_keys: Vec, /// Whether VAG keys have been loaded @@ -45,6 +53,7 @@ impl Default for KeyStore { kia_v6_a_key: 0, kia_v6_b_key: 0, kia_v5_key: 0, + star_line_mf_key: 0, vag_keys: Vec::new(), vag_keys_loaded: false, } @@ -66,6 +75,7 @@ impl KeyStore { KIA_KEY2 => self.kia_v6_a_key = key_value, KIA_KEY3 => self.kia_v6_b_key = key_value, KIA_KEY4 => self.kia_v5_key = key_value, + STAR_LINE_KEY => self.star_line_mf_key = key_value, _ => {} } } @@ -146,6 +156,11 @@ impl KeyStore { pub fn get_kia_v5_key(&self) -> u64 { self.kia_v5_key } + + /// Get the Star Line manufacturer key + pub fn get_star_line_mf_key(&self) -> u64 { + self.star_line_mf_key + } } /// Global singleton keystore @@ -175,3 +190,171 @@ pub fn load_vag_keys(path: &str) { let mut store = get_keystore_mut(); store.load_vag_keys_from_file(path); } + +// ============================================================================= +// Keystore file loading from ~/.config/KAT/keystore/ +// ============================================================================= + +/// Parse a hex string (with or without "0x" prefix) into a u64. +fn parse_hex_u64(s: &str) -> Option { + let s = s.trim(); + let s = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")).unwrap_or(s); + u64::from_str_radix(s, 16).ok() +} + +/// Default keystore.ini template content. +/// Written to disk on first run so users know which keys to configure. +const DEFAULT_KEYSTORE_INI: &str = r#"; KAT Keystore — Protocol Encryption Keys +; +; Place your protocol keys here. Key values should be in hexadecimal +; with a 0x prefix (e.g. 0x0123456789ABCDEF). +; +; Keys left at 0x0000000000000000 or omitted are treated as "not loaded" +; and the corresponding protocol will decode without decryption +; (serial/button still visible, but counter may not validate). +; +; This file corresponds to protopirate's keystore/encrypted and keystore/vag +; assets. Since KAT runs on a PC (not a Flipper), keys must be provided +; in plaintext here rather than in the Flipper's encrypted keystore format. + +[kia] +; KIA V3/V4: KeeLoq manufacturer key (used for hop-code decryption) +mf_key = 0x0000000000000000 + +; KIA V5: Custom mixer cipher key +v5_key = 0x0000000000000000 + +; KIA V6: AES-128 key components (XOR-masked before use) +v6_a_key = 0x0000000000000000 +v6_b_key = 0x0000000000000000 + +[star_line] +; Star Line: KeeLoq manufacturer key (used for hop-code decryption) +mf_key = 0x0000000000000000 + +[vag] +; VAG: Path to AUT64 binary key file (raw packed keys, 16 bytes each). +; Can be absolute or relative to the keystore directory. +; Leave empty or commented out if you don't have VAG keys. +; keys_file = vag.bin +"#; + +/// Write the default keystore.ini template if it doesn't exist yet. +pub fn create_default_keystore(keystore_dir: &Path) { + let ini_path = keystore_dir.join("keystore.ini"); + if ini_path.exists() { + return; + } + + match std::fs::write(&ini_path, DEFAULT_KEYSTORE_INI) { + Ok(_) => info!("Created default keystore template at {:?}", ini_path), + Err(e) => warn!("Could not create default keystore.ini: {}", e), + } +} + +/// Load all keys from the keystore directory. +/// +/// Reads `keystore.ini` from the given directory and populates the global +/// [`KeyStore`] with: +/// +/// - KIA V3/V4 manufacturer key (`[kia] mf_key`) +/// - KIA V5 mixer key (`[kia] v5_key`) +/// - KIA V6 AES keys (`[kia] v6_a_key`, `[kia] v6_b_key`) +/// - Star Line manufacturer key (`[star_line] mf_key`) +/// - VAG AUT64 keys from binary file (`[vag] keys_file`) +/// +/// Keys that are missing, zeroed, or unparseable are silently skipped. +pub fn load_keystore_from_dir(keystore_dir: &Path) { + let ini_path = keystore_dir.join("keystore.ini"); + + if !ini_path.exists() { + info!("No keystore.ini found at {:?} — keys not loaded", ini_path); + return; + } + + let mut ini = Ini::new(); + if let Err(e) = ini.load(ini_path.to_string_lossy().as_ref()) { + error!("Failed to parse keystore.ini: {}", e); + return; + } + + let mut store = get_keystore_mut(); + let mut loaded_count = 0u32; + + // ── KIA keys ────────────────────────────────────────────────────────── + if let Some(val) = ini.get("kia", "mf_key").and_then(|s| parse_hex_u64(&s)) { + if val != 0 { + store.kia_mf_key = val; + loaded_count += 1; + info!("Loaded KIA V3/V4 manufacturer key"); + } + } + + if let Some(val) = ini.get("kia", "v5_key").and_then(|s| parse_hex_u64(&s)) { + if val != 0 { + store.kia_v5_key = val; + loaded_count += 1; + info!("Loaded KIA V5 mixer key"); + } + } + + if let Some(val) = ini.get("kia", "v6_a_key").and_then(|s| parse_hex_u64(&s)) { + if val != 0 { + store.kia_v6_a_key = val; + loaded_count += 1; + info!("Loaded KIA V6 AES key A"); + } + } + + if let Some(val) = ini.get("kia", "v6_b_key").and_then(|s| parse_hex_u64(&s)) { + if val != 0 { + store.kia_v6_b_key = val; + loaded_count += 1; + info!("Loaded KIA V6 AES key B"); + } + } + + // ── Star Line keys ──────────────────────────────────────────────────── + if let Some(val) = ini.get("star_line", "mf_key").and_then(|s| parse_hex_u64(&s)) { + if val != 0 { + store.star_line_mf_key = val; + loaded_count += 1; + info!("Loaded Star Line manufacturer key"); + } + } + + // ── VAG AUT64 keys (binary file) ────────────────────────────────────── + if let Some(vag_file) = ini.get("vag", "keys_file") { + let vag_file = vag_file.trim().to_string(); + if !vag_file.is_empty() { + let vag_path = if Path::new(&vag_file).is_absolute() { + std::path::PathBuf::from(&vag_file) + } else { + keystore_dir.join(&vag_file) + }; + + if vag_path.exists() { + match std::fs::read(&vag_path) { + Ok(data) => { + store.load_vag_keys_from_data(&data); + if store.vag_keys_loaded { + loaded_count += store.vag_keys.len() as u32; + info!("Loaded {} VAG AUT64 keys from {:?}", store.vag_keys.len(), vag_path); + } + } + Err(e) => { + error!("Failed to read VAG key file {:?}: {}", vag_path, e); + } + } + } else { + warn!("VAG key file not found: {:?}", vag_path); + } + } + } + + if loaded_count > 0 { + info!("Keystore loaded: {} key(s) from {:?}", loaded_count, keystore_dir); + } else { + info!("Keystore loaded but no non-zero keys found. Edit keystore.ini to add your keys."); + } +} diff --git a/src/protocols/kia_v3_v4.rs b/src/protocols/kia_v3_v4.rs index 6d7e76f..a55575a 100644 --- a/src/protocols/kia_v3_v4.rs +++ b/src/protocols/kia_v3_v4.rs @@ -10,6 +10,7 @@ //! - V3 and V4 differ only in sync polarity use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal}; +use super::keys; use crate::radio::demodulator::LevelDuration; use crate::duration_diff; @@ -128,8 +129,7 @@ impl KiaV3V4Decoder { /// Get manufacturer key (placeholder - in real use, this would be loaded from config) fn get_mf_key() -> u64 { - // This is a placeholder - actual key should be loaded from secure storage - 0x0000000000000000 + keys::get_keystore().get_kia_mf_key() } /// Process the collected buffer and validate diff --git a/src/protocols/kia_v5.rs b/src/protocols/kia_v5.rs index a55882d..2dd2f7a 100644 --- a/src/protocols/kia_v5.rs +++ b/src/protocols/kia_v5.rs @@ -10,6 +10,7 @@ //! - Decode-only (no encoder) use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal}; +use super::keys; use crate::radio::demodulator::LevelDuration; use crate::duration_diff; @@ -59,9 +60,9 @@ impl KiaV5Decoder { } } - /// Get encryption key (placeholder) + /// Get encryption key from global keystore fn get_v5_key() -> u64 { - 0x0000000000000000 + keys::get_keystore().get_kia_v5_key() } /// Custom mixer decryption @@ -144,25 +145,33 @@ impl KiaV5Decoder { } /// Manchester state machine + /// NOTE: V5 uses OPPOSITE Manchester polarity from V1/V2. + /// In protopirate, V1/V2 use: level ? ShortLow : ShortHigh + /// But V5 uses: level ? ShortHigh : ShortLow + /// To achieve this inversion, we swap the is_high mapping but use V1's + /// standard state table, so that only ONE inversion occurs. fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option { + // Invert polarity: V5 uses opposite Manchester convention from V1/V2 + let is_high = !is_high; + let event = match (is_short, is_high) { - (true, true) => 0, // Short High - (true, false) => 1, // Short Low - (false, true) => 2, // Long High - (false, false) => 3, // Long Low + (true, false) => 0, // Short Low + (true, true) => 1, // Short High + (false, false) => 2, // Long Low + (false, true) => 3, // Long High }; let (new_state, output) = match (self.manchester_state, event) { - (ManchesterState::Mid0, 1) | (ManchesterState::Mid1, 1) => - (ManchesterState::Start0, None), (ManchesterState::Mid0, 0) | (ManchesterState::Mid1, 0) => + (ManchesterState::Start0, None), + (ManchesterState::Mid0, 1) | (ManchesterState::Mid1, 1) => (ManchesterState::Start1, None), - (ManchesterState::Start1, 1) => (ManchesterState::Mid1, Some(true)), - (ManchesterState::Start1, 3) => (ManchesterState::Start0, Some(true)), + (ManchesterState::Start1, 0) => (ManchesterState::Mid1, Some(true)), + (ManchesterState::Start1, 2) => (ManchesterState::Start0, Some(true)), - (ManchesterState::Start0, 0) => (ManchesterState::Mid0, Some(false)), - (ManchesterState::Start0, 2) => (ManchesterState::Start1, Some(false)), + (ManchesterState::Start0, 1) => (ManchesterState::Mid0, Some(false)), + (ManchesterState::Start0, 3) => (ManchesterState::Start1, Some(false)), _ => (ManchesterState::Mid1, None), }; diff --git a/src/protocols/kia_v6.rs b/src/protocols/kia_v6.rs index ad01f34..03e2485 100644 --- a/src/protocols/kia_v6.rs +++ b/src/protocols/kia_v6.rs @@ -10,6 +10,7 @@ //! - Decode-only (no encoder) use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal}; +use super::keys; use crate::radio::demodulator::LevelDuration; use crate::duration_diff; @@ -118,14 +119,14 @@ impl KiaV6Decoder { } } - /// Get keystore A (placeholder) + /// Get keystore A from global keystore fn get_keystore_a() -> u64 { - 0x0000000000000000 + keys::get_keystore().get_kia_v6_keystore_a() } - /// Get keystore B (placeholder) + /// Get keystore B from global keystore fn get_keystore_b() -> u64 { - 0x0000000000000000 + keys::get_keystore().get_kia_v6_keystore_b() } /// CRC8 calculation @@ -347,12 +348,17 @@ impl KiaV6Decoder { } /// Manchester state machine + /// NOTE: Due to opposite level conventions between Flipper and KAT, + /// KAT level=true corresponds to Flipper level=false (and vice versa). + /// For short pulses: protopirate uses (level & 0x7F) << 1, which gives 0/2. + /// For long pulses: protopirate uses level ? 6 : 4. + /// With the inverted convention, KAT's is_high=true maps to Flipper level=false. fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option { let event = match (is_short, is_high) { - (true, true) => 0, // Short High - (true, false) => 2, // Short Low - (false, true) => 6, // Long High - (false, false) => 4, // Long Low + (true, true) => 0, // Short High (KAT) → Flipper level=false → 0 + (true, false) => 2, // Short Low (KAT) → Flipper level=true → 2 + (false, true) => 4, // Long High (KAT) → Flipper level=false → 4 + (false, false) => 6, // Long Low (KAT) → Flipper level=true → 6 }; let (new_state, output) = match (self.manchester_state, event) { diff --git a/src/protocols/star_line.rs b/src/protocols/star_line.rs index 7590d01..85af52d 100644 --- a/src/protocols/star_line.rs +++ b/src/protocols/star_line.rs @@ -9,6 +9,7 @@ //! - KeeLoq encryption (requires manufacturer key) use super::keeloq_common::{keeloq_decrypt, keeloq_encrypt, keeloq_normal_learning, reverse_key}; +use super::keys; use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming}; use crate::duration_diff; use crate::radio::demodulator::LevelDuration; @@ -48,9 +49,9 @@ impl StarLineDecoder { } } - /// Get manufacturer key (placeholder) + /// Get manufacturer key from global keystore fn get_mf_key() -> u64 { - 0x0000000000000000 + keys::get_keystore().get_star_line_mf_key() } fn parse_data(data: u64) -> DecodedSignal { diff --git a/src/storage.rs b/src/storage.rs index 41cc9fc..43fb240 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -1,4 +1,4 @@ -//! Storage management for configuration and exports. +//! Storage management for configuration, exports, and keystores. //! //! All application data lives under `~/.config/KAT/`: //! @@ -6,6 +6,9 @@ //! ~/.config/KAT/ //! config.ini — User configuration //! exports/ — Exported .fob / .sub files +//! keystore/ — Protocol encryption keys +//! keystore.ini — Key definitions (hex values) +//! vag.bin — VAG AUT64 binary key file (optional) //! ``` //! //! Captures are **in-memory only** and are discarded when KAT exits. @@ -297,9 +300,19 @@ impl Storage { ); } - // ── 5. Log resolved paths ─────────────────────────────────────── + // ── 5. Ensure keystore directory exists ──────────────────────── + let keystore_dir = config_dir.join("keystore"); + if !keystore_dir.exists() { + fs::create_dir_all(&keystore_dir).with_context(|| { + format!("Failed to create keystore dir: {:?}", keystore_dir) + })?; + tracing::info!("Created keystore directory: {:?}", keystore_dir); + } + + // ── 6. Log resolved paths ─────────────────────────────────────── tracing::info!("Config dir: {:?}", config_dir); tracing::info!("Export dir: {:?}", config.export_directory); + tracing::info!("Keystore dir: {:?}", keystore_dir); Ok(Self { config_dir, @@ -328,4 +341,9 @@ impl Storage { pub fn export_dir(&self) -> &PathBuf { &self.config.export_directory } + + /// Get the keystore directory path (`~/.config/KAT/keystore`) + pub fn keystore_dir(&self) -> PathBuf { + self.config_dir.join("keystore") + } }