protocol updates
This commit is contained in:
Generated
+1
-1
@@ -402,7 +402,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kat"
|
name = "kat"
|
||||||
version = "0.2.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"atty",
|
"atty",
|
||||||
|
|||||||
@@ -227,6 +227,12 @@ impl App {
|
|||||||
/// Create a new application instance
|
/// Create a new application instance
|
||||||
pub fn new() -> Result<Self> {
|
pub fn new() -> Result<Self> {
|
||||||
let storage = Storage::new()?;
|
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 protocols = ProtocolRegistry::new();
|
||||||
let (radio_event_tx, radio_event_rx) = mpsc::channel();
|
let (radio_event_tx, radio_event_rx) = mpsc::channel();
|
||||||
|
|
||||||
|
|||||||
+538
-195
@@ -1,11 +1,13 @@
|
|||||||
//! Ford V0 protocol decoder
|
//! Ford V0 protocol decoder/encoder
|
||||||
//!
|
//!
|
||||||
//! Ported from protopirate's ford_v0.c
|
//! Ported from protopirate's ford_v0.c
|
||||||
//!
|
//!
|
||||||
//! Protocol characteristics:
|
//! Protocol characteristics:
|
||||||
//! - Manchester encoding: 250/500µs timing
|
//! - Manchester encoding: 250/500µs timing
|
||||||
//! - 64 bits total
|
//! - 80 bits total (64-bit key1 + 16-bit key2)
|
||||||
//! - Matrix-based CRC
|
//! - 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 super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||||
use crate::radio::demodulator::LevelDuration;
|
use crate::radio::demodulator::LevelDuration;
|
||||||
@@ -15,131 +17,474 @@ const TE_SHORT: u32 = 250;
|
|||||||
const TE_LONG: u32 = 500;
|
const TE_LONG: u32 = 500;
|
||||||
const TE_DELTA: u32 = 100;
|
const TE_DELTA: u32 = 100;
|
||||||
const MIN_COUNT_BIT: usize = 64;
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
enum ManchesterState {
|
enum ManchesterState {
|
||||||
Mid0,
|
Mid0 = 0,
|
||||||
Mid1,
|
Mid1 = 1,
|
||||||
Start0,
|
Start0 = 2,
|
||||||
Start1,
|
Start1 = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decoder states
|
/// Decoder step states (matches protopirate's FordV0DecoderStep)
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
enum DecoderStep {
|
enum DecoderStep {
|
||||||
Reset,
|
Reset,
|
||||||
CheckPreamble,
|
Preamble,
|
||||||
SaveDuration,
|
PreambleCheck,
|
||||||
CheckDuration,
|
Gap,
|
||||||
|
Data,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ford V0 protocol decoder
|
/// Ford V0 protocol decoder
|
||||||
pub struct FordV0Decoder {
|
pub struct FordV0Decoder {
|
||||||
step: DecoderStep,
|
step: DecoderStep,
|
||||||
te_last: u32,
|
|
||||||
header_count: u16,
|
|
||||||
decode_data: u64,
|
|
||||||
decode_count_bit: usize,
|
|
||||||
manchester_state: ManchesterState,
|
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 {
|
impl FordV0Decoder {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
step: DecoderStep::Reset,
|
step: DecoderStep::Reset,
|
||||||
te_last: 0,
|
|
||||||
header_count: 0,
|
|
||||||
decode_data: 0,
|
|
||||||
decode_count_bit: 0,
|
|
||||||
manchester_state: ManchesterState::Mid1,
|
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
|
/// Add a bit to the accumulator.
|
||||||
fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option<bool> {
|
/// After 64 bits, key1 is extracted and the accumulator resets for key2.
|
||||||
let event = match (is_short, is_high) {
|
fn add_bit(&mut self, bit: bool) {
|
||||||
(true, true) => 0, // Short High
|
self.decode_data = (self.decode_data << 1) | (bit as u64);
|
||||||
(true, false) => 1, // Short Low
|
self.bit_count += 1;
|
||||||
(false, true) => 2, // Long High
|
}
|
||||||
(false, false) => 3, // Long Low
|
|
||||||
};
|
|
||||||
|
|
||||||
let (new_state, output) = match (self.manchester_state, event) {
|
/// Check if we've reached a data milestone (64 or 80 bits).
|
||||||
// From Mid0 or Mid1
|
/// At 64 bits: extract key1 (inverted) and reset accumulator.
|
||||||
(ManchesterState::Mid0, 0) | (ManchesterState::Mid1, 0) =>
|
/// At 80 bits: extract key2 (inverted), decode fields, return true.
|
||||||
(ManchesterState::Start1, None),
|
fn process_data(&mut self) -> bool {
|
||||||
(ManchesterState::Mid0, 1) | (ManchesterState::Mid1, 1) =>
|
if self.bit_count == 64 {
|
||||||
(ManchesterState::Start0, None),
|
// First 64 bits → key1 (bit-inverted)
|
||||||
|
self.key1 = !self.decode_data;
|
||||||
|
self.decode_data = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// From Start1
|
if self.bit_count == 80 {
|
||||||
(ManchesterState::Start1, 1) => (ManchesterState::Mid1, Some(true)),
|
// Next 16 bits → key2 (bit-inverted)
|
||||||
(ManchesterState::Start1, 3) => (ManchesterState::Start0, Some(true)),
|
let key2_raw = (self.decode_data & 0xFFFF) as u16;
|
||||||
|
self.key2 = !key2_raw;
|
||||||
|
|
||||||
// From Start0
|
// Decode serial, button, counter, bs_magic from key1+key2
|
||||||
(ManchesterState::Start0, 0) => (ManchesterState::Mid0, Some(false)),
|
let (serial, button, count, bs_magic) =
|
||||||
(ManchesterState::Start0, 2) => (ManchesterState::Start1, Some(false)),
|
Self::decode_ford_v0(self.key1, self.key2);
|
||||||
|
self.serial = serial;
|
||||||
|
self.button = button;
|
||||||
|
self.counter = count;
|
||||||
|
self.bs_magic = bs_magic;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Reset on invalid transitions
|
false
|
||||||
_ => (ManchesterState::Mid1, None),
|
}
|
||||||
|
|
||||||
|
/// 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<bool> {
|
||||||
|
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;
|
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] = [
|
// CRC functions
|
||||||
[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],
|
|
||||||
];
|
|
||||||
|
|
||||||
/// Calculate Ford CRC
|
/// Population count for a byte
|
||||||
fn calculate_crc(data: u64) -> u8 {
|
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;
|
let mut crc = 0u8;
|
||||||
|
for row in 0..8 {
|
||||||
for byte_idx in 0..7 {
|
let mut xor_sum = 0u8;
|
||||||
let byte = ((data >> (56 - byte_idx * 8)) & 0xFF) as u8;
|
for col in 0..8 {
|
||||||
for bit in 0..8 {
|
xor_sum ^= CRC_MATRIX[row * 8 + col] & buf[col + 1];
|
||||||
if (byte >> (7 - bit)) & 1 == 1 {
|
}
|
||||||
crc ^= Self::CRC_MATRIX[bit][byte_idx % 4];
|
let parity = Self::popcount8(xor_sum) & 1;
|
||||||
}
|
if parity != 0 {
|
||||||
|
crc |= 1 << row;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
crc
|
crc
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse decoded data
|
/// Calculate CRC for transmission (key1 bytes + BS byte, XOR 0x80)
|
||||||
fn parse_data(data: u64) -> DecodedSignal {
|
fn calculate_crc_for_tx(key1: u64, bs: u8) -> u8 {
|
||||||
// Ford V0 format:
|
let mut buf = [0u8; 16];
|
||||||
// Bits 60-63: Prefix (0x5)
|
for i in 0..8 {
|
||||||
// Bits 32-59: Serial (28 bits)
|
buf[i] = (key1 >> (56 - i * 8)) as u8;
|
||||||
// Bits 28-31: Button (4 bits)
|
}
|
||||||
// Bits 16-27: Counter (12 bits)
|
buf[8] = bs;
|
||||||
// Bits 8-15: Encrypted data
|
Self::calculate_crc(&buf) ^ 0x80
|
||||||
// Bits 0-7: CRC
|
}
|
||||||
|
|
||||||
let serial = ((data >> 32) & 0x0FFFFFFF) as u32;
|
/// Verify CRC of received key1 + key2
|
||||||
let button = ((data >> 28) & 0x0F) as u8;
|
fn verify_crc(key1: u64, key2: u16) -> bool {
|
||||||
let counter = ((data >> 16) & 0x0FFF) as u16;
|
let mut buf = [0u8; 16];
|
||||||
let received_crc = (data & 0xFF) as u8;
|
for i in 0..8 {
|
||||||
let calculated_crc = Self::calculate_crc(data);
|
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
|
||||||
|
}
|
||||||
|
|
||||||
DecodedSignal {
|
// =========================================================================
|
||||||
serial: Some(serial),
|
// BS calculation
|
||||||
button: Some(button),
|
// =========================================================================
|
||||||
counter: Some(counter),
|
|
||||||
crc_valid: received_crc == calculated_crc,
|
/// Calculate BS = (count_low_byte + bs_magic + (button << 4)) with overflow handling
|
||||||
data,
|
fn calculate_bs(count: u32, button: u8, bs_magic: u8) -> u8 {
|
||||||
data_count_bit: MIN_COUNT_BIT,
|
let result: u16 = (count as u16 & 0xFF)
|
||||||
encoder_capable: true,
|
.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<LevelDuration> {
|
||||||
|
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<LevelDuration>, 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<LevelDuration>, 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) {
|
fn reset(&mut self) {
|
||||||
self.step = DecoderStep::Reset;
|
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.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<DecodedSignal> {
|
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||||
let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA;
|
|
||||||
let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA;
|
|
||||||
|
|
||||||
match self.step {
|
match self.step {
|
||||||
|
// ─── Step 1: Reset — wait for short HIGH pulse ───
|
||||||
DecoderStep::Reset => {
|
DecoderStep::Reset => {
|
||||||
if level && is_short {
|
if level && duration_diff!(duration, TE_SHORT) < TE_DELTA {
|
||||||
self.step = DecoderStep::CheckPreamble;
|
self.decode_data = 0;
|
||||||
self.header_count = 1;
|
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;
|
self.manchester_state = ManchesterState::Mid1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DecoderStep::CheckPreamble => {
|
// ─── Step 2: Preamble — wait for long LOW after short HIGH ───
|
||||||
if is_short {
|
DecoderStep::Preamble => {
|
||||||
self.header_count += 1;
|
if !level {
|
||||||
if self.header_count > 20 && !level {
|
if duration_diff!(duration, TE_LONG) < TE_DELTA {
|
||||||
// Enough preamble, start looking for data
|
self.te_last = duration;
|
||||||
self.step = DecoderStep::SaveDuration;
|
self.step = DecoderStep::PreambleCheck;
|
||||||
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;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
self.step = DecoderStep::Reset;
|
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;
|
self.step = DecoderStep::Reset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DecoderStep::SaveDuration => {
|
// ─── Step 5: Data — Manchester decode 80 bits ───
|
||||||
self.te_last = duration;
|
DecoderStep::Data => {
|
||||||
self.step = DecoderStep::CheckDuration;
|
// 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 => {
|
// Advance Manchester state machine
|
||||||
let last_short = duration_diff!(self.te_last, TE_SHORT) < TE_DELTA;
|
if let Some(data_bit) = self.manchester_advance(event) {
|
||||||
let last_long = duration_diff!(self.te_last, TE_LONG) < TE_DELTA;
|
self.add_bit(data_bit);
|
||||||
|
|
||||||
// Check for end of transmission
|
if self.process_data() {
|
||||||
if duration > TE_LONG * 3 {
|
// 80 bits decoded — check CRC and return result
|
||||||
if self.decode_count_bit >= MIN_COUNT_BIT {
|
let crc_ok = Self::verify_crc(self.key1, self.key2);
|
||||||
let result = Self::parse_data(self.decode_data);
|
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;
|
self.step = DecoderStep::Reset;
|
||||||
return Some(result);
|
return Some(result);
|
||||||
}
|
}
|
||||||
self.step = DecoderStep::Reset;
|
|
||||||
return None;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manchester decode
|
self.te_last = duration;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,46 +631,33 @@ impl ProtocolDecoder for FordV0Decoder {
|
|||||||
|
|
||||||
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
|
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
|
||||||
let serial = decoded.serial?;
|
let serial = decoded.serial?;
|
||||||
let counter = decoded.counter.unwrap_or(0);
|
|
||||||
|
|
||||||
// Build data packet
|
// Use the stored counter from last decode (or from decoded signal)
|
||||||
let mut data: u64 = 0;
|
// Increment counter by 1 for the new transmission
|
||||||
data |= 0x5 << 60; // Prefix
|
let base_counter = if self.counter != 0 {
|
||||||
data |= ((serial as u64) & 0x0FFFFFFF) << 32;
|
self.counter
|
||||||
data |= ((button as u64) & 0x0F) << 28;
|
} else {
|
||||||
data |= ((counter as u64) & 0x0FFF) << 16;
|
decoded.counter.unwrap_or(0) as u32
|
||||||
data |= ((decoded.data >> 8) & 0xFF) << 8; // Keep encrypted byte
|
};
|
||||||
data |= Self::calculate_crc(data) as u64;
|
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
|
// Calculate BS for the new counter value
|
||||||
for _ in 0..30 {
|
let bs = Self::calculate_bs(new_counter, button, bs_magic);
|
||||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
|
||||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync
|
// Extract header byte from the original key1 (first byte)
|
||||||
signal.push(LevelDuration::new(true, TE_LONG));
|
let header_byte = (decoded.data >> 56) as u8;
|
||||||
signal.push(LevelDuration::new(false, TE_LONG));
|
|
||||||
|
|
||||||
// Data: Manchester encoded, 64 bits MSB first
|
// Encode new key1 from fields
|
||||||
for bit_num in (0..64).rev() {
|
let new_key1 = Self::encode_ford_v0(header_byte, serial, button, new_counter, bs);
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// End
|
// Calculate CRC for key2
|
||||||
signal.push(LevelDuration::new(false, TE_LONG * 4));
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,14 @@
|
|||||||
//! - KIA V3/V4: kia_mf_key (manufacturer key for KeeLoq)
|
//! - KIA V3/V4: kia_mf_key (manufacturer key for KeeLoq)
|
||||||
//! - KIA V5: kia_v5_key (custom mixer cipher key)
|
//! - KIA V5: kia_v5_key (custom mixer cipher key)
|
||||||
//! - KIA V6: kia_v6_a_key, kia_v6_b_key (AES-128 XOR mask keys)
|
//! - 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
|
//! - VAG: AUT64 keys loaded from keystore files
|
||||||
|
//!
|
||||||
|
//! Keys are loaded from `~/.config/KAT/keystore/keystore.ini` at startup.
|
||||||
|
//! See [`load_keystore_from_dir`] for the file format.
|
||||||
|
|
||||||
use super::aut64::{self, Aut64Key, AUT64_KEY_STRUCT_PACKED_SIZE};
|
use super::aut64::{self, Aut64Key, AUT64_KEY_STRUCT_PACKED_SIZE};
|
||||||
|
use configparser::ini::Ini;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::{OnceLock, RwLock};
|
use std::sync::{OnceLock, RwLock};
|
||||||
use tracing::{info, warn, error};
|
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_KEY2: u32 = 11; // kia_v6_a_key
|
||||||
const KIA_KEY3: u32 = 12; // kia_v6_b_key
|
const KIA_KEY3: u32 = 12; // kia_v6_b_key
|
||||||
const KIA_KEY4: u32 = 13; // kia_v5_key
|
const KIA_KEY4: u32 = 13; // kia_v5_key
|
||||||
|
const STAR_LINE_KEY: u32 = 20; // star_line_mf_key
|
||||||
|
|
||||||
/// Maximum number of VAG AUT64 keys
|
/// Maximum number of VAG AUT64 keys
|
||||||
const VAG_KEYS_COUNT: usize = 3;
|
const VAG_KEYS_COUNT: usize = 3;
|
||||||
@@ -32,6 +38,8 @@ pub struct KeyStore {
|
|||||||
pub kia_v6_b_key: u64,
|
pub kia_v6_b_key: u64,
|
||||||
/// KIA V5 mixer key
|
/// KIA V5 mixer key
|
||||||
pub kia_v5_key: u64,
|
pub kia_v5_key: u64,
|
||||||
|
/// Star Line manufacturer key (for KeeLoq)
|
||||||
|
pub star_line_mf_key: u64,
|
||||||
/// VAG AUT64 keys
|
/// VAG AUT64 keys
|
||||||
pub vag_keys: Vec<Aut64Key>,
|
pub vag_keys: Vec<Aut64Key>,
|
||||||
/// Whether VAG keys have been loaded
|
/// Whether VAG keys have been loaded
|
||||||
@@ -45,6 +53,7 @@ impl Default for KeyStore {
|
|||||||
kia_v6_a_key: 0,
|
kia_v6_a_key: 0,
|
||||||
kia_v6_b_key: 0,
|
kia_v6_b_key: 0,
|
||||||
kia_v5_key: 0,
|
kia_v5_key: 0,
|
||||||
|
star_line_mf_key: 0,
|
||||||
vag_keys: Vec::new(),
|
vag_keys: Vec::new(),
|
||||||
vag_keys_loaded: false,
|
vag_keys_loaded: false,
|
||||||
}
|
}
|
||||||
@@ -66,6 +75,7 @@ impl KeyStore {
|
|||||||
KIA_KEY2 => self.kia_v6_a_key = key_value,
|
KIA_KEY2 => self.kia_v6_a_key = key_value,
|
||||||
KIA_KEY3 => self.kia_v6_b_key = key_value,
|
KIA_KEY3 => self.kia_v6_b_key = key_value,
|
||||||
KIA_KEY4 => self.kia_v5_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 {
|
pub fn get_kia_v5_key(&self) -> u64 {
|
||||||
self.kia_v5_key
|
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
|
/// Global singleton keystore
|
||||||
@@ -175,3 +190,171 @@ pub fn load_vag_keys(path: &str) {
|
|||||||
let mut store = get_keystore_mut();
|
let mut store = get_keystore_mut();
|
||||||
store.load_vag_keys_from_file(path);
|
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<u64> {
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
//! - V3 and V4 differ only in sync polarity
|
//! - V3 and V4 differ only in sync polarity
|
||||||
|
|
||||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||||
|
use super::keys;
|
||||||
use crate::radio::demodulator::LevelDuration;
|
use crate::radio::demodulator::LevelDuration;
|
||||||
use crate::duration_diff;
|
use crate::duration_diff;
|
||||||
|
|
||||||
@@ -128,8 +129,7 @@ impl KiaV3V4Decoder {
|
|||||||
|
|
||||||
/// Get manufacturer key (placeholder - in real use, this would be loaded from config)
|
/// Get manufacturer key (placeholder - in real use, this would be loaded from config)
|
||||||
fn get_mf_key() -> u64 {
|
fn get_mf_key() -> u64 {
|
||||||
// This is a placeholder - actual key should be loaded from secure storage
|
keys::get_keystore().get_kia_mf_key()
|
||||||
0x0000000000000000
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process the collected buffer and validate
|
/// Process the collected buffer and validate
|
||||||
|
|||||||
+21
-12
@@ -10,6 +10,7 @@
|
|||||||
//! - Decode-only (no encoder)
|
//! - Decode-only (no encoder)
|
||||||
|
|
||||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||||
|
use super::keys;
|
||||||
use crate::radio::demodulator::LevelDuration;
|
use crate::radio::demodulator::LevelDuration;
|
||||||
use crate::duration_diff;
|
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 {
|
fn get_v5_key() -> u64 {
|
||||||
0x0000000000000000
|
keys::get_keystore().get_kia_v5_key()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Custom mixer decryption
|
/// Custom mixer decryption
|
||||||
@@ -144,25 +145,33 @@ impl KiaV5Decoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Manchester state machine
|
/// 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<bool> {
|
fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option<bool> {
|
||||||
|
// Invert polarity: V5 uses opposite Manchester convention from V1/V2
|
||||||
|
let is_high = !is_high;
|
||||||
|
|
||||||
let event = match (is_short, is_high) {
|
let event = match (is_short, is_high) {
|
||||||
(true, true) => 0, // Short High
|
(true, false) => 0, // Short Low
|
||||||
(true, false) => 1, // Short Low
|
(true, true) => 1, // Short High
|
||||||
(false, true) => 2, // Long High
|
(false, false) => 2, // Long Low
|
||||||
(false, false) => 3, // Long Low
|
(false, true) => 3, // Long High
|
||||||
};
|
};
|
||||||
|
|
||||||
let (new_state, output) = match (self.manchester_state, event) {
|
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::Mid0, 0) | (ManchesterState::Mid1, 0) =>
|
||||||
|
(ManchesterState::Start0, None),
|
||||||
|
(ManchesterState::Mid0, 1) | (ManchesterState::Mid1, 1) =>
|
||||||
(ManchesterState::Start1, None),
|
(ManchesterState::Start1, None),
|
||||||
|
|
||||||
(ManchesterState::Start1, 1) => (ManchesterState::Mid1, Some(true)),
|
(ManchesterState::Start1, 0) => (ManchesterState::Mid1, Some(true)),
|
||||||
(ManchesterState::Start1, 3) => (ManchesterState::Start0, Some(true)),
|
(ManchesterState::Start1, 2) => (ManchesterState::Start0, Some(true)),
|
||||||
|
|
||||||
(ManchesterState::Start0, 0) => (ManchesterState::Mid0, Some(false)),
|
(ManchesterState::Start0, 1) => (ManchesterState::Mid0, Some(false)),
|
||||||
(ManchesterState::Start0, 2) => (ManchesterState::Start1, Some(false)),
|
(ManchesterState::Start0, 3) => (ManchesterState::Start1, Some(false)),
|
||||||
|
|
||||||
_ => (ManchesterState::Mid1, None),
|
_ => (ManchesterState::Mid1, None),
|
||||||
};
|
};
|
||||||
|
|||||||
+14
-8
@@ -10,6 +10,7 @@
|
|||||||
//! - Decode-only (no encoder)
|
//! - Decode-only (no encoder)
|
||||||
|
|
||||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||||
|
use super::keys;
|
||||||
use crate::radio::demodulator::LevelDuration;
|
use crate::radio::demodulator::LevelDuration;
|
||||||
use crate::duration_diff;
|
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 {
|
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 {
|
fn get_keystore_b() -> u64 {
|
||||||
0x0000000000000000
|
keys::get_keystore().get_kia_v6_keystore_b()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// CRC8 calculation
|
/// CRC8 calculation
|
||||||
@@ -347,12 +348,17 @@ impl KiaV6Decoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Manchester state machine
|
/// 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<bool> {
|
fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option<bool> {
|
||||||
let event = match (is_short, is_high) {
|
let event = match (is_short, is_high) {
|
||||||
(true, true) => 0, // Short High
|
(true, true) => 0, // Short High (KAT) → Flipper level=false → 0
|
||||||
(true, false) => 2, // Short Low
|
(true, false) => 2, // Short Low (KAT) → Flipper level=true → 2
|
||||||
(false, true) => 6, // Long High
|
(false, true) => 4, // Long High (KAT) → Flipper level=false → 4
|
||||||
(false, false) => 4, // Long Low
|
(false, false) => 6, // Long Low (KAT) → Flipper level=true → 6
|
||||||
};
|
};
|
||||||
|
|
||||||
let (new_state, output) = match (self.manchester_state, event) {
|
let (new_state, output) = match (self.manchester_state, event) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
//! - KeeLoq encryption (requires manufacturer key)
|
//! - KeeLoq encryption (requires manufacturer key)
|
||||||
|
|
||||||
use super::keeloq_common::{keeloq_decrypt, keeloq_encrypt, keeloq_normal_learning, reverse_key};
|
use super::keeloq_common::{keeloq_decrypt, keeloq_encrypt, keeloq_normal_learning, reverse_key};
|
||||||
|
use super::keys;
|
||||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||||
use crate::duration_diff;
|
use crate::duration_diff;
|
||||||
use crate::radio::demodulator::LevelDuration;
|
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 {
|
fn get_mf_key() -> u64 {
|
||||||
0x0000000000000000
|
keys::get_keystore().get_star_line_mf_key()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_data(data: u64) -> DecodedSignal {
|
fn parse_data(data: u64) -> DecodedSignal {
|
||||||
|
|||||||
+20
-2
@@ -1,4 +1,4 @@
|
|||||||
//! Storage management for configuration and exports.
|
//! Storage management for configuration, exports, and keystores.
|
||||||
//!
|
//!
|
||||||
//! All application data lives under `~/.config/KAT/`:
|
//! All application data lives under `~/.config/KAT/`:
|
||||||
//!
|
//!
|
||||||
@@ -6,6 +6,9 @@
|
|||||||
//! ~/.config/KAT/
|
//! ~/.config/KAT/
|
||||||
//! config.ini — User configuration
|
//! config.ini — User configuration
|
||||||
//! exports/ — Exported .fob / .sub files
|
//! 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.
|
//! 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!("Config dir: {:?}", config_dir);
|
||||||
tracing::info!("Export dir: {:?}", config.export_directory);
|
tracing::info!("Export dir: {:?}", config.export_directory);
|
||||||
|
tracing::info!("Keystore dir: {:?}", keystore_dir);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
config_dir,
|
config_dir,
|
||||||
@@ -328,4 +341,9 @@ impl Storage {
|
|||||||
pub fn export_dir(&self) -> &PathBuf {
|
pub fn export_dir(&self) -> &PathBuf {
|
||||||
&self.config.export_directory
|
&self.config.export_directory
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the keystore directory path (`~/.config/KAT/keystore`)
|
||||||
|
pub fn keystore_dir(&self) -> PathBuf {
|
||||||
|
self.config_dir.join("keystore")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user