Protocol updates
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
//! AUT64 block cipher implementation
|
||||
//!
|
||||
//! Ported from protopirate's aut64.c
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/aut64.c`.
|
||||
//! Encrypt/decrypt, pack/unpack, and all tables match the reference.
|
||||
//!
|
||||
//! AUT64 algorithm: 12 rounds, 8-byte block/key size
|
||||
//! Based on: Reference AUT64 implementation
|
||||
//! AUT64 algorithm: 12 rounds, 8-byte block/key size.
|
||||
//! See: https://www.usenix.org/system/files/conference/usenixsecurity16/sec16_paper_garcia.pdf
|
||||
|
||||
pub const AUT64_NUM_ROUNDS: usize = 12;
|
||||
@@ -265,7 +265,7 @@ pub fn aut64_pack(src: &Aut64Key) -> [u8; AUT64_KEY_STRUCT_PACKED_SIZE] {
|
||||
dest
|
||||
}
|
||||
|
||||
/// Unpack a 16-byte array into an AUT64 key structure
|
||||
/// Unpack a 16-byte array into an AUT64 key structure (matches aut64_unpack in reference)
|
||||
#[allow(dead_code)]
|
||||
pub fn aut64_unpack(src: &[u8]) -> Aut64Key {
|
||||
let mut dest = Aut64Key::default();
|
||||
@@ -276,9 +276,10 @@ pub fn aut64_unpack(src: &[u8]) -> Aut64Key {
|
||||
dest.key[i * 2 + 1] = src[i + 1] & 0xF;
|
||||
}
|
||||
|
||||
let pbox: u32 = ((src[5] as u32) << 16) | ((src[6] as u32) << 8) | src[7] as u32;
|
||||
let mut pbox: u32 = (u32::from(src[5]) << 16) | (u32::from(src[6]) << 8) | u32::from(src[7]);
|
||||
for i in (0..dest.pbox.len()).rev() {
|
||||
dest.pbox[i] = ((pbox >> ((dest.pbox.len() - 1 - i) * 3)) & 0x7) as u8;
|
||||
dest.pbox[i] = (pbox & 0x7) as u8;
|
||||
pbox >>= 3;
|
||||
}
|
||||
|
||||
for i in 0..(dest.sbox.len() / 2) {
|
||||
|
||||
+13
-6
@@ -1,4 +1,11 @@
|
||||
//! Common utilities for protocol implementations.
|
||||
//!
|
||||
//! The ProtoPirate reference has `REFERENCES/ProtoPirate/protocols/protocols_common.c`, which
|
||||
//! only provides Flipper preset name mapping (`protopirate_get_short_preset_name`). KAT does not
|
||||
//! use that; this module holds shared types and helpers used by multiple protocol decoders.
|
||||
//! Where applicable, algorithms match the reference: e.g. `crc8_kia` matches `kia_crc8` in
|
||||
//! kia_v0.c (polynomial 0x7F, init 0x00); `add_bit` matches the common shift-left-and-append
|
||||
//! pattern used in the reference decoders.
|
||||
|
||||
/// Decoded signal information
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -34,12 +41,12 @@ impl DecodedSignal {
|
||||
}
|
||||
}
|
||||
|
||||
/// CRC8 calculation with custom polynomial
|
||||
///
|
||||
/// CRC8 calculation with custom polynomial (MSB-first, shift-left style).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `data` - Data bytes to calculate CRC over
|
||||
/// * `poly` - CRC polynomial
|
||||
/// * `init` - Initial CRC value
|
||||
/// * `poly` - CRC polynomial (e.g. 0x7F for Kia)
|
||||
/// * `init` - Initial CRC value (e.g. 0x00)
|
||||
pub fn crc8(data: &[u8], poly: u8, init: u8) -> u8 {
|
||||
let mut crc = init;
|
||||
for &byte in data {
|
||||
@@ -55,12 +62,12 @@ pub fn crc8(data: &[u8], poly: u8, init: u8) -> u8 {
|
||||
crc
|
||||
}
|
||||
|
||||
/// CRC8 for Kia protocol (polynomial 0x7F, init 0x00)
|
||||
/// CRC8 for Kia protocol (matches kia_v0.c kia_crc8: polynomial 0x7F, init 0x00)
|
||||
pub fn crc8_kia(data: &[u8]) -> u8 {
|
||||
crc8(data, 0x7F, 0x00)
|
||||
}
|
||||
|
||||
/// Add a bit to the decoder's data accumulator
|
||||
/// Add a bit to the decoder's data accumulator (shift-left, LSB last; matches reference add_bit pattern)
|
||||
#[inline]
|
||||
pub fn add_bit(data: &mut u64, count: &mut usize, bit: bool) {
|
||||
*data = (*data << 1) | (bit as u64);
|
||||
|
||||
+82
-62
@@ -1,11 +1,12 @@
|
||||
//! Fiat V0 protocol decoder/encoder
|
||||
//!
|
||||
//! Ported from protopirate's fiat_v0.c
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/fiat_v0.c` (Flipper).
|
||||
//! Decode/encode logic (preamble, gap, Manchester, data/btn extraction, upload waveform) matches reference.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - Differential Manchester encoding: 200/400µs timing
|
||||
//! - 64-bit data (cnt:32 | serial:32) + 6-bit button
|
||||
//! - 150 preamble pairs, 800µs gap, 3 bursts
|
||||
//! - 150 preamble pairs (count LOW pulses), 800µs gap, 3 bursts
|
||||
|
||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||
use crate::duration_diff;
|
||||
@@ -16,21 +17,21 @@ const TE_LONG: u32 = 400;
|
||||
const TE_DELTA: u32 = 100;
|
||||
#[allow(dead_code)]
|
||||
const MIN_COUNT_BIT: usize = 64;
|
||||
const PREAMBLE_PAIRS: u16 = 150;
|
||||
const PREAMBLE_PAIRS: u16 = 150; // 0x96 in reference
|
||||
const GAP_US: u32 = 800;
|
||||
const TOTAL_BURSTS: u8 = 3;
|
||||
const INTER_BURST_GAP: u32 = 25000;
|
||||
|
||||
/// Manchester decoder states
|
||||
/// Manchester state machine states (matches Flipper's manchester_decoder.h, same as Ford V0)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ManchesterState {
|
||||
Mid0,
|
||||
Mid1,
|
||||
Start0,
|
||||
Start1,
|
||||
Mid0 = 0,
|
||||
Mid1 = 1,
|
||||
Start0 = 2,
|
||||
Start1 = 3,
|
||||
}
|
||||
|
||||
/// Decoder states
|
||||
/// Decoder states (matches protopirate's FiatV0DecoderStep)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
@@ -68,37 +69,46 @@ impl FiatV0Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Manchester advance - returns decoded bit or None
|
||||
fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option<bool> {
|
||||
let event = match (is_short, is_high) {
|
||||
(true, true) => 0,
|
||||
(true, false) => 1,
|
||||
(false, true) => 2,
|
||||
(false, false) => 3,
|
||||
};
|
||||
/// Manchester state machine (same as Ford V0 / Flipper manchester_decoder).
|
||||
/// Event: 0=ShortLow, 1=ShortHigh, 2=LongLow, 3=LongHigh.
|
||||
fn manchester_advance(&mut self, event: u8) -> Option<bool> {
|
||||
let (new_state, emit) = match (self.manchester_state, event) {
|
||||
(ManchesterState::Mid0, 0) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Mid0, 1) => (ManchesterState::Start1, true),
|
||||
(ManchesterState::Mid0, 2) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Mid0, 3) => (ManchesterState::Mid1, true),
|
||||
|
||||
let (new_state, output) = match (self.manchester_state, event) {
|
||||
(ManchesterState::Mid0, 0) | (ManchesterState::Mid1, 0) => {
|
||||
(ManchesterState::Start1, None)
|
||||
}
|
||||
(ManchesterState::Mid0, 1) | (ManchesterState::Mid1, 1) => {
|
||||
(ManchesterState::Start0, None)
|
||||
}
|
||||
(ManchesterState::Start1, 1) => (ManchesterState::Mid1, Some(true)),
|
||||
(ManchesterState::Start1, 3) => (ManchesterState::Start0, Some(true)),
|
||||
(ManchesterState::Start0, 0) => (ManchesterState::Mid0, Some(false)),
|
||||
(ManchesterState::Start0, 2) => (ManchesterState::Start1, Some(false)),
|
||||
_ => (ManchesterState::Mid1, None),
|
||||
(ManchesterState::Mid1, 0) => (ManchesterState::Start0, true),
|
||||
(ManchesterState::Mid1, 1) => (ManchesterState::Mid1, false),
|
||||
(ManchesterState::Mid1, 2) => (ManchesterState::Mid0, true),
|
||||
(ManchesterState::Mid1, 3) => (ManchesterState::Mid1, false),
|
||||
|
||||
(ManchesterState::Start0, 0) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start0, 1) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start0, 2) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start0, 3) => (ManchesterState::Mid1, false),
|
||||
|
||||
(ManchesterState::Start1, 0) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start1, 1) => (ManchesterState::Mid1, false),
|
||||
(ManchesterState::Start1, 2) => (ManchesterState::Mid0, false),
|
||||
(ManchesterState::Start1, 3) => (ManchesterState::Mid1, false),
|
||||
|
||||
_ => (ManchesterState::Mid1, false),
|
||||
};
|
||||
|
||||
self.manchester_state = new_state;
|
||||
output
|
||||
if emit {
|
||||
Some((event & 1) == 1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn manchester_reset(&mut self) {
|
||||
self.manchester_state = ManchesterState::Mid1;
|
||||
}
|
||||
|
||||
/// Add bit to accumulator; at 64 bits extract serial/cnt and clear data (bit_count unchanged in reference).
|
||||
fn add_manchester_bit(&mut self, bit: bool) {
|
||||
let new_bit = if bit { 1u32 } else { 0u32 };
|
||||
let carry = (self.data_low >> 31) & 1;
|
||||
@@ -162,6 +172,7 @@ impl ProtocolDecoder for FiatV0Decoder {
|
||||
|
||||
fn feed(&mut self, level: bool, duration: u32) -> Option<DecodedSignal> {
|
||||
match self.step {
|
||||
// Reset: wait for short HIGH (matches reference)
|
||||
DecoderStep::Reset => {
|
||||
if !level {
|
||||
return None;
|
||||
@@ -177,50 +188,60 @@ impl ProtocolDecoder for FiatV0Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
// Preamble: only process LOW pulses (reference: if(level) return). Count short LOWs; gap = 800µs LOW.
|
||||
DecoderStep::Preamble => {
|
||||
// Count short pulses in preamble, look for gap
|
||||
if duration_diff!(duration, TE_SHORT) < TE_DELTA {
|
||||
if level {
|
||||
return None;
|
||||
}
|
||||
let short_ok = duration_diff!(duration, TE_SHORT) < TE_DELTA;
|
||||
let gap_ok = duration_diff!(duration, GAP_US) < TE_DELTA;
|
||||
|
||||
if short_ok {
|
||||
self.preamble_count += 1;
|
||||
self.te_last = duration;
|
||||
} else if self.preamble_count >= PREAMBLE_PAIRS {
|
||||
// Check for gap
|
||||
if duration_diff!(duration, GAP_US) < TE_DELTA {
|
||||
} else {
|
||||
if self.preamble_count >= PREAMBLE_PAIRS && gap_ok {
|
||||
self.step = DecoderStep::Data;
|
||||
self.preamble_count = 0;
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
self.bit_count = 0;
|
||||
self.te_last = duration;
|
||||
return None;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
|
||||
// Data: Manchester events — short first, then long (matches reference)
|
||||
DecoderStep::Data => {
|
||||
let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA;
|
||||
let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA;
|
||||
let short_diff = duration_diff!(duration, TE_SHORT);
|
||||
let long_diff = duration_diff!(duration, TE_LONG);
|
||||
|
||||
if is_short || is_long {
|
||||
if let Some(bit) = self.manchester_advance(is_short, level) {
|
||||
self.add_manchester_bit(bit);
|
||||
|
||||
if self.bit_count > 0x46 {
|
||||
self.btn = ((self.data_low << 1) | 1) as u8;
|
||||
let result = self.parse_data();
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
self.bit_count = 0;
|
||||
self.step = DecoderStep::Reset;
|
||||
return Some(result);
|
||||
}
|
||||
let event = if short_diff < TE_DELTA {
|
||||
if level { 0 } else { 1 }
|
||||
} else if long_diff < TE_DELTA {
|
||||
if level { 2 } else { 3 }
|
||||
} else {
|
||||
self.te_last = duration;
|
||||
if duration > TE_LONG * 3 {
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
return None;
|
||||
};
|
||||
|
||||
if let Some(bit) = self.manchester_advance(event) {
|
||||
self.add_manchester_bit(bit);
|
||||
|
||||
if self.bit_count > 0x46 {
|
||||
self.btn = ((self.data_low << 1) | 1) as u8;
|
||||
let result = self.parse_data();
|
||||
self.data_low = 0;
|
||||
self.data_high = 0;
|
||||
self.bit_count = 0;
|
||||
self.step = DecoderStep::Reset;
|
||||
return Some(result);
|
||||
}
|
||||
} else if duration > TE_LONG * 3 {
|
||||
// End of signal
|
||||
self.step = DecoderStep::Reset;
|
||||
}
|
||||
|
||||
self.te_last = duration;
|
||||
@@ -249,14 +270,13 @@ impl ProtocolDecoder for FiatV0Decoder {
|
||||
signal.push(LevelDuration::new(false, INTER_BURST_GAP));
|
||||
}
|
||||
|
||||
// Preamble
|
||||
// Preamble: 150 HIGH-LOW pairs; last LOW is gap (matches reference get_upload)
|
||||
for i in 0..PREAMBLE_PAIRS {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
if i < PREAMBLE_PAIRS - 1 {
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
} else {
|
||||
signal.push(LevelDuration::new(false, GAP_US));
|
||||
}
|
||||
signal.push(LevelDuration::new(
|
||||
false,
|
||||
if i == PREAMBLE_PAIRS - 1 { GAP_US } else { TE_SHORT },
|
||||
));
|
||||
}
|
||||
|
||||
// First bit (bit 63)
|
||||
|
||||
+13
-17
@@ -1,6 +1,7 @@
|
||||
//! Ford V0 protocol decoder/encoder
|
||||
//!
|
||||
//! Ported from protopirate's ford_v0.c
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/ford_v0.c` (Flipper).
|
||||
//! Decode/encode logic (CRC, BS, decode_ford_v0, encode_ford_v0, upload waveform) matches reference.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - Manchester encoding: 250/500µs timing
|
||||
@@ -8,6 +9,9 @@
|
||||
//! - Matrix-based CRC in GF(2)
|
||||
//! - BS (byte swap) magic calculation
|
||||
//! - 6 bursts, 4 preamble pairs, 3500µs gap
|
||||
//!
|
||||
//! HackRF-specific: `TE_DELTA` (200µs) and `GAP_TOLERANCE` (1500µs) are wider than reference
|
||||
//! (100µs / 250µs) for software demodulator tolerance.
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
@@ -551,18 +555,16 @@ impl ProtocolDecoder for FordV0Decoder {
|
||||
}
|
||||
|
||||
// ─── Step 3: PreambleCheck — count preamble pairs or transition to gap ───
|
||||
// Order matches protopirate ford_v0.c: check LONG first, then SHORT.
|
||||
DecoderStep::PreambleCheck => {
|
||||
if level {
|
||||
let short_diff = duration_diff!(duration, TE_SHORT);
|
||||
let long_diff = duration_diff!(duration, TE_LONG);
|
||||
|
||||
if long_diff < TE_DELTA && long_diff <= short_diff {
|
||||
// Long HIGH (closer to TE_LONG): another preamble pair
|
||||
if duration_diff!(duration, TE_LONG) < TE_DELTA {
|
||||
// Long HIGH: another preamble pair
|
||||
self.header_count += 1;
|
||||
self.te_last = duration;
|
||||
self.step = DecoderStep::Preamble;
|
||||
} else if short_diff < TE_DELTA {
|
||||
// Short HIGH (closer to TE_SHORT): end of preamble, transition to gap
|
||||
} else if duration_diff!(duration, TE_SHORT) < TE_DELTA {
|
||||
// Short HIGH: end of preamble, transition to gap
|
||||
self.step = DecoderStep::Gap;
|
||||
} else {
|
||||
self.step = DecoderStep::Reset;
|
||||
@@ -585,18 +587,12 @@ impl ProtocolDecoder for FordV0Decoder {
|
||||
|
||||
// ─── Step 5: Data — Manchester decode 80 bits ───
|
||||
DecoderStep::Data => {
|
||||
// Map level+duration to Manchester event using NEAREST-MATCH.
|
||||
// With TE_DELTA=200, SHORT(250) and LONG(500) ranges overlap at 300–450µs.
|
||||
// First-match would always pick SHORT for overlapping durations, causing
|
||||
// bit errors and CRC failure. Nearest-match picks the closer timing.
|
||||
//
|
||||
// Tie-break favors LONG (strict < for short_diff) because asymmetric
|
||||
// demodulation compresses LOWs towards the midpoint (375µs) — these are
|
||||
// actually LONG pulses that got shortened by threshold bias.
|
||||
// Map level+duration to Manchester event. Order matches protopirate ford_v0.c:
|
||||
// check SHORT first, then LONG (so when both within TE_DELTA, short wins).
|
||||
let short_diff = duration_diff!(duration, TE_SHORT);
|
||||
let long_diff = duration_diff!(duration, TE_LONG);
|
||||
|
||||
let event = if short_diff < TE_DELTA && short_diff < long_diff {
|
||||
let event = if short_diff < TE_DELTA {
|
||||
if level { 0 } else { 1 } // ShortLow / ShortHigh
|
||||
} else if long_diff < TE_DELTA {
|
||||
if level { 2 } else { 3 } // LongLow / LongHigh
|
||||
|
||||
@@ -1,63 +1,57 @@
|
||||
//! KeeLoq common encryption/decryption routines
|
||||
//! KeeLoq common encryption/decryption and learning routines
|
||||
//!
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/keeloq_common.c`.
|
||||
//! Shared by Kia V3/V4, Star Line, and other KeeLoq-based protocols.
|
||||
//! Based on the NLF (Non-Linear Feedback) function with constant 0x3A5C742E.
|
||||
//! NLF (Non-Linear Feedback) constant 0x3A5C742E per reference.
|
||||
|
||||
/// The KeeLoq NLF constant
|
||||
/// The KeeLoq NLF constant (KEELOQ_NLF in reference)
|
||||
const KEELOQ_NLF: u32 = 0x3A5C742E;
|
||||
|
||||
/// KeeLoq decrypt: 528 rounds of the KeeLoq cipher (decrypt direction)
|
||||
#[inline]
|
||||
fn bit(x: u32, n: u32) -> u32 {
|
||||
(x >> n) & 1
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn g5(x: u32, a: u32, b: u32, c: u32, d: u32, e: u32) -> u32 {
|
||||
bit(x, a) | (bit(x, b) << 1) | (bit(x, c) << 2) | (bit(x, d) << 3) | (bit(x, e) << 4)
|
||||
}
|
||||
|
||||
/// KeeLoq decrypt: 528 rounds (matches subghz_protocol_keeloq_common_decrypt).
|
||||
/// Key bit for round r is key[(15 - r) & 63]. NLF index g5(x, 0, 8, 19, 25, 30).
|
||||
pub fn keeloq_decrypt(data: u32, key: u64) -> u32 {
|
||||
let mut block = data;
|
||||
let mut tkey = key;
|
||||
|
||||
for _ in 0..528 {
|
||||
let lutkey = ((block >> 0) & 1)
|
||||
| ((block >> 7) & 2)
|
||||
| ((block >> 17) & 4)
|
||||
| ((block >> 22) & 8)
|
||||
| ((block >> 26) & 16);
|
||||
let lsb = ((block >> 31)
|
||||
^ ((block >> 15) & 1)
|
||||
^ ((KEELOQ_NLF >> lutkey) & 1)
|
||||
^ (((tkey >> 15) & 1) as u32)) as u32;
|
||||
block = ((block & 0x7FFFFFFF) << 1) | lsb;
|
||||
tkey = ((tkey & 0x7FFFFFFFFFFFFFFF) << 1) | (tkey >> 63);
|
||||
let mut x = data;
|
||||
for r in 0..528u32 {
|
||||
let key_bit = ((key >> ((15 - r) & 63)) & 1) as u32;
|
||||
let new_lsb = bit(x, 31) ^ bit(x, 15) ^ key_bit
|
||||
^ bit(KEELOQ_NLF, g5(x, 0, 8, 19, 25, 30));
|
||||
x = (x << 1) ^ new_lsb;
|
||||
}
|
||||
block
|
||||
x
|
||||
}
|
||||
|
||||
/// KeeLoq encrypt: 528 rounds of the KeeLoq cipher (encrypt direction)
|
||||
/// KeeLoq encrypt: 528 rounds (matches subghz_protocol_keeloq_common_encrypt).
|
||||
/// Key bit for round r is key[r & 63]. NLF index g5(x, 1, 9, 20, 26, 31).
|
||||
pub fn keeloq_encrypt(data: u32, key: u64) -> u32 {
|
||||
let mut block = data;
|
||||
let mut tkey = key;
|
||||
|
||||
for _ in 0..528 {
|
||||
let lutkey = ((block >> 1) & 1)
|
||||
| ((block >> 8) & 2)
|
||||
| ((block >> 18) & 4)
|
||||
| ((block >> 23) & 8)
|
||||
| ((block >> 27) & 16);
|
||||
let msb = ((block >> 0)
|
||||
^ ((block >> 16) & 1)
|
||||
^ ((KEELOQ_NLF >> lutkey) & 1)
|
||||
^ (((tkey >> 0) & 1) as u32)) as u32;
|
||||
block = ((block >> 1) & 0x7FFFFFFF) | (msb << 31);
|
||||
tkey = ((tkey >> 1) & 0x7FFFFFFFFFFFFFFF) | ((tkey & 1) << 63);
|
||||
let mut x = data;
|
||||
for r in 0..528u32 {
|
||||
let key_bit = ((key >> (r & 63)) & 1) as u32;
|
||||
let new_msb = bit(x, 0) ^ bit(x, 16) ^ key_bit
|
||||
^ bit(KEELOQ_NLF, g5(x, 1, 9, 20, 26, 31));
|
||||
x = (x >> 1) ^ (new_msb << 31);
|
||||
}
|
||||
block
|
||||
x
|
||||
}
|
||||
|
||||
/// Normal learning key derivation
|
||||
/// Derives a 64-bit key from a 32-bit fix code and a 64-bit manufacturer key
|
||||
pub fn keeloq_normal_learning(fix: u32, manufacturer_key: u64) -> u64 {
|
||||
let serial_low = fix & 0xFFFF;
|
||||
let serial_high = (fix >> 16) & 0xFFFF;
|
||||
|
||||
let key_low = keeloq_decrypt(serial_low as u32 | 0x20000000, manufacturer_key);
|
||||
let key_high = keeloq_decrypt(serial_high as u32 | 0x60000000, manufacturer_key);
|
||||
|
||||
((key_high as u64) << 32) | (key_low as u64)
|
||||
/// Normal learning key derivation (matches subghz_protocol_keeloq_common_normal_learning).
|
||||
/// @param data - serial number (28-bit, upper bits ignored)
|
||||
/// @param key - manufacturer key (64-bit)
|
||||
/// @return derived key for this serial (64-bit)
|
||||
pub fn keeloq_normal_learning(data: u32, key: u64) -> u64 {
|
||||
let data = data & 0x0FFFFFFF;
|
||||
let k1 = keeloq_decrypt(data | 0x20000000, key);
|
||||
let k2 = keeloq_decrypt(data | 0x60000000, key);
|
||||
((k2 as u64) << 32) | (k1 as u64)
|
||||
}
|
||||
|
||||
/// Reverse the bits in a 64-bit key (for protocols that store data MSB-first)
|
||||
|
||||
+29
-19
@@ -1,16 +1,8 @@
|
||||
//! Key management module for protocol encryption/decryption
|
||||
//!
|
||||
//! Ported from protopirate's keys.c
|
||||
//!
|
||||
//! Manages manufacturer keys used by various protocols:
|
||||
//! - KIA V3/V4: kia_mf_key (manufacturer key for KeeLoq)
|
||||
//! - KIA V5: kia_v5_key (custom mixer cipher key)
|
||||
//! - KIA V6: kia_v6_a_key, kia_v6_b_key (AES-128 XOR mask keys)
|
||||
//! - Star Line: star_line_mf_key (manufacturer key for KeeLoq)
|
||||
//! - VAG: AUT64 keys loaded from keystore files
|
||||
//!
|
||||
//! Keys are loaded from `~/.config/KAT/keystore/keystore.ini` at startup.
|
||||
//! See [`load_keystore_from_dir`] for the file format.
|
||||
//! Aligned with ProtoPirate's keys.c (KIA_KEY1..4, get_kia_mf_key, etc.).
|
||||
//! Keys are loaded from the embedded keystore blob in `crate::keystore` at startup,
|
||||
//! matching the standard encrypted + VAG raw keystore data.
|
||||
|
||||
use super::aut64::{self, Aut64Key, AUT64_KEY_STRUCT_PACKED_SIZE};
|
||||
use configparser::ini::Ini;
|
||||
@@ -25,8 +17,8 @@ const KIA_KEY3: u32 = 12; // kia_v6_b_key
|
||||
const KIA_KEY4: u32 = 13; // kia_v5_key
|
||||
const STAR_LINE_KEY: u32 = 20; // star_line_mf_key
|
||||
|
||||
/// Maximum number of VAG AUT64 keys
|
||||
const VAG_KEYS_COUNT: usize = 3;
|
||||
/// Maximum number of VAG AUT64 keys (embedded blob has 64 bytes = 4 keys)
|
||||
const MAX_VAG_KEYS: usize = 4;
|
||||
|
||||
/// Global key store - thread-safe access to loaded keys
|
||||
pub struct KeyStore {
|
||||
@@ -81,22 +73,20 @@ impl KeyStore {
|
||||
}
|
||||
}
|
||||
|
||||
/// Load VAG AUT64 keys from raw binary data
|
||||
/// The data should contain packed AUT64 key structures (16 bytes each)
|
||||
/// Load VAG AUT64 keys from raw binary data (16 bytes per key; up to MAX_VAG_KEYS)
|
||||
pub fn load_vag_keys_from_data(&mut self, data: &[u8]) {
|
||||
if self.vag_keys_loaded {
|
||||
return;
|
||||
}
|
||||
|
||||
self.vag_keys.clear();
|
||||
let n = (data.len() / AUT64_KEY_STRUCT_PACKED_SIZE).min(MAX_VAG_KEYS);
|
||||
|
||||
for i in 0..VAG_KEYS_COUNT {
|
||||
for i in 0..n {
|
||||
let offset = i * AUT64_KEY_STRUCT_PACKED_SIZE;
|
||||
if offset + AUT64_KEY_STRUCT_PACKED_SIZE > data.len() {
|
||||
error!("VAG key data too short for key {}", i);
|
||||
break;
|
||||
}
|
||||
|
||||
let key = aut64::aut64_unpack(&data[offset..offset + AUT64_KEY_STRUCT_PACKED_SIZE]);
|
||||
self.vag_keys.push(key);
|
||||
}
|
||||
@@ -179,7 +169,7 @@ pub fn get_keystore_mut() -> std::sync::RwLockWriteGuard<'static, KeyStore> {
|
||||
global_keystore().write().unwrap()
|
||||
}
|
||||
|
||||
/// Initialize the global keystore with KIA keys
|
||||
/// Initialize the global keystore with KIA keys (matches protopirate_keys_load pattern)
|
||||
pub fn load_keys(kia_entries: &[(u32, u64)]) {
|
||||
let mut store = get_keystore_mut();
|
||||
store.load_kia_keys(kia_entries);
|
||||
@@ -191,6 +181,26 @@ pub fn load_vag_keys(path: &str) {
|
||||
store.load_vag_keys_from_file(path);
|
||||
}
|
||||
|
||||
/// Load the global keystore from the embedded blob (src/keystore/embedded.rs).
|
||||
/// Matches ProtoPirate loading from encrypted + VAG keystore; keys are compiled in.
|
||||
pub fn load_keystore_from_embedded() {
|
||||
let blob = crate::keystore::embedded_blob();
|
||||
let Some(parsed) = crate::keystore::parse_blob(blob) else {
|
||||
error!("Failed to parse embedded keystore blob");
|
||||
return;
|
||||
};
|
||||
let mut store = get_keystore_mut();
|
||||
store.load_kia_keys(&parsed.entries);
|
||||
if !parsed.vag_bytes.is_empty() {
|
||||
store.load_vag_keys_from_data(&parsed.vag_bytes);
|
||||
}
|
||||
info!(
|
||||
"Keystore loaded from embedded blob ({} entries, {} VAG keys)",
|
||||
parsed.entries.len(),
|
||||
store.vag_keys.len()
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Keystore file loading from ~/.config/KAT/keystore/
|
||||
// =============================================================================
|
||||
|
||||
+19
-13
@@ -1,13 +1,13 @@
|
||||
//! Kia V0 protocol decoder
|
||||
//! Kia V0 protocol decoder/encoder
|
||||
//!
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v0.c`.
|
||||
//! Decode/encode logic (CRC8, preamble/sync, field layout) matches reference.
|
||||
//!
|
||||
//! Ported from protopirate's kia_v0.c
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - PWM encoding: short pulse (250µs) = 0, long pulse (500µs) = 1
|
||||
//! - 61 bits total
|
||||
//! - Preamble: alternating short pulses
|
||||
//! - Sync: long-long pattern
|
||||
//! - Data: 59 bits (4-bit prefix + 16-bit counter + 28-bit serial + 4-bit button + 8-bit CRC)
|
||||
//! - 61 bits total (1 sync bit + 60 data bits)
|
||||
//! - Preamble: alternating short pulses; sync: long-long pattern
|
||||
//! - Data: 60 bits (4-bit prefix + 16-bit counter + 28-bit serial + 4-bit button + 8-bit CRC)
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use super::common::{crc8_kia, add_bit};
|
||||
@@ -19,7 +19,7 @@ const TE_LONG: u32 = 500;
|
||||
const TE_DELTA: u32 = 100;
|
||||
const MIN_COUNT_BIT: usize = 61;
|
||||
|
||||
/// Decoder states
|
||||
/// Decoder states (matches protopirate's KiaV0DecoderStep)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
@@ -48,7 +48,7 @@ impl KiaV0Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate CRC for Kia data packet
|
||||
/// CRC8 for Kia data packet (matches kia_v0.c kia_crc8: polynomial 0x7F, init 0x00)
|
||||
fn calculate_crc(data: u64) -> u8 {
|
||||
let crc_data = [
|
||||
((data >> 48) & 0xFF) as u8,
|
||||
@@ -236,13 +236,13 @@ impl ProtocolDecoder for KiaV0Decoder {
|
||||
signal.push(LevelDuration::new(is_high, TE_SHORT));
|
||||
}
|
||||
|
||||
// Sync: long-long
|
||||
// Sync: long-long (matches protopirate kia_v0 encode)
|
||||
signal.push(LevelDuration::new(true, TE_LONG));
|
||||
signal.push(LevelDuration::new(false, TE_LONG));
|
||||
|
||||
// Data: 59 bits (MSB first)
|
||||
for bit_num in 0..59 {
|
||||
let bit_mask = 1u64 << (58 - bit_num);
|
||||
// Data: 60 bits MSB first (bits 59..0)
|
||||
for bit_num in 0..60 {
|
||||
let bit_mask = 1u64 << (59 - bit_num);
|
||||
let bit = (data & bit_mask) != 0;
|
||||
let duration = if bit { TE_LONG } else { TE_SHORT };
|
||||
|
||||
@@ -257,3 +257,9 @@ impl ProtocolDecoder for KiaV0Decoder {
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for KiaV0Decoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
+19
-12
@@ -1,12 +1,13 @@
|
||||
//! Kia V1 protocol decoder
|
||||
//! Kia V1 protocol decoder/encoder
|
||||
//!
|
||||
//! Ported from protopirate's kia_v1.c
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v1.c`.
|
||||
//! Decode/encode logic (Manchester, CRC4, preamble, field layout) matches reference.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - Manchester encoding: 800/1600µs timing
|
||||
//! - 57 bits total
|
||||
//! - Long preamble of ~90 pulses
|
||||
//! - CRC4 checksum
|
||||
//! - 57 bits total (32 serial + 8 button + 12 counter + 4 CRC)
|
||||
//! - Long preamble of ~90 long pairs
|
||||
//! - CRC4 checksum with offset rules (cnt_high 0 / >= 6)
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
@@ -17,7 +18,7 @@ const TE_LONG: u32 = 1600;
|
||||
const TE_DELTA: u32 = 200;
|
||||
const MIN_COUNT_BIT: usize = 57;
|
||||
|
||||
/// Manchester states
|
||||
/// Manchester decoder states (matches protopirate kia_v1 Manchester state machine)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ManchesterState {
|
||||
Mid0,
|
||||
@@ -26,7 +27,7 @@ enum ManchesterState {
|
||||
Start1,
|
||||
}
|
||||
|
||||
/// Decoder states
|
||||
/// Decoder states (matches protopirate's KiaV1DecoderStep)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
@@ -56,7 +57,7 @@ impl KiaV1Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// CRC4 calculation for Kia V1
|
||||
/// CRC4 for Kia V1 (matches kia_v1.c: XOR nibbles + offset)
|
||||
fn crc4(bytes: &[u8], offset: u8) -> u8 {
|
||||
let mut crc: u8 = 0;
|
||||
for &byte in bytes {
|
||||
@@ -97,7 +98,7 @@ impl KiaV1Decoder {
|
||||
fn parse_data(&self) -> DecodedSignal {
|
||||
let data = self.decode_data;
|
||||
|
||||
// Extract fields per kia_v1.c
|
||||
// Field layout matches kia_v1.c: serial(32) | button(8) | cnt_low(8) | cnt_high(4) | crc(4)
|
||||
let serial = (data >> 24) as u32;
|
||||
let button = ((data >> 16) & 0xFF) as u8;
|
||||
let cnt_low = ((data >> 8) & 0xFF) as u16;
|
||||
@@ -263,7 +264,7 @@ impl ProtocolDecoder for KiaV1Decoder {
|
||||
|
||||
let mut signal = Vec::with_capacity(600);
|
||||
|
||||
// Generate 3 bursts
|
||||
// Generate 3 bursts (matches protopirate kia_v1 encode)
|
||||
for burst in 0..3 {
|
||||
if burst > 0 {
|
||||
signal.push(LevelDuration::new(false, 25000));
|
||||
@@ -275,10 +276,10 @@ impl ProtocolDecoder for KiaV1Decoder {
|
||||
signal.push(LevelDuration::new(true, TE_LONG));
|
||||
}
|
||||
|
||||
// Short gap
|
||||
// Short gap before data
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
|
||||
// Data: Manchester encoded, MSB first
|
||||
// Data: 57 bits Manchester encoded, MSB first
|
||||
for bit_num in (1..MIN_COUNT_BIT).rev() {
|
||||
let bit = ((data >> (bit_num - 1)) & 1) == 1;
|
||||
if bit {
|
||||
@@ -294,3 +295,9 @@ impl ProtocolDecoder for KiaV1Decoder {
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for KiaV1Decoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
+20
-13
@@ -1,12 +1,13 @@
|
||||
//! Kia V2 protocol decoder
|
||||
//! Kia V2 protocol decoder/encoder
|
||||
//!
|
||||
//! Ported from protopirate's kia_v2.c
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v2.c`.
|
||||
//! Decode/encode logic (Manchester, CRC4, preamble, byte-swapped counter) matches reference.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - Manchester encoding: 500/1000µs timing
|
||||
//! - 53 bits total
|
||||
//! - Long preamble of 252+ pairs
|
||||
//! - CRC4 checksum
|
||||
//! - 53 bits total (32 serial + 4 button + 12 counter + 4 CRC, plus start bit)
|
||||
//! - Long preamble of 252 long pairs
|
||||
//! - CRC4 checksum (XOR nibbles + offset 1)
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
@@ -17,7 +18,7 @@ const TE_LONG: u32 = 1000;
|
||||
const TE_DELTA: u32 = 150;
|
||||
const MIN_COUNT_BIT: usize = 53;
|
||||
|
||||
/// Manchester states
|
||||
/// Manchester decoder states (matches protopirate kia_v2 Manchester state machine)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ManchesterState {
|
||||
Mid0,
|
||||
@@ -26,7 +27,7 @@ enum ManchesterState {
|
||||
Start1,
|
||||
}
|
||||
|
||||
/// Decoder states
|
||||
/// Decoder states (matches protopirate's KiaV2DecoderStep)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
@@ -56,7 +57,7 @@ impl KiaV2Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate CRC for Kia V2
|
||||
/// CRC4 for Kia V2 (matches kia_v2.c: 6-byte permuted input, XOR nibbles, offset 1)
|
||||
fn calculate_crc(data: u64) -> u8 {
|
||||
let serial = ((data >> 20) & 0xFFFFFFFF) as u32;
|
||||
let u_var4 = (data & 0xFFFFFFFF) as u32;
|
||||
@@ -105,10 +106,10 @@ impl KiaV2Decoder {
|
||||
output
|
||||
}
|
||||
|
||||
/// Parse decoded data
|
||||
/// Parse decoded data (field layout matches kia_v2.c)
|
||||
fn parse_data(&self) -> DecodedSignal {
|
||||
let data = self.decode_data;
|
||||
|
||||
// serial(32) | button(4) | counter_swapped(12) | crc(4); counter byte-swapped in stream
|
||||
let serial = ((data >> 20) & 0xFFFFFFFF) as u32;
|
||||
let button = ((data >> 16) & 0x0F) as u8;
|
||||
|
||||
@@ -246,7 +247,7 @@ impl ProtocolDecoder for KiaV2Decoder {
|
||||
|
||||
let mut signal = Vec::with_capacity(700);
|
||||
|
||||
// Generate 2 bursts
|
||||
// Generate 2 bursts (matches protopirate kia_v2 encode)
|
||||
for _burst in 0..2 {
|
||||
// Preamble: 252 long pairs
|
||||
for _ in 0..252 {
|
||||
@@ -254,10 +255,10 @@ impl ProtocolDecoder for KiaV2Decoder {
|
||||
signal.push(LevelDuration::new(true, TE_LONG));
|
||||
}
|
||||
|
||||
// Short gap
|
||||
// Short gap before data
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
|
||||
// Data: Manchester encoded, MSB first
|
||||
// Data: 53 bits Manchester encoded, MSB first
|
||||
for bit_num in (1..MIN_COUNT_BIT).rev() {
|
||||
let bit = ((new_data >> (bit_num - 1)) & 1) == 1;
|
||||
if bit {
|
||||
@@ -273,3 +274,9 @@ impl ProtocolDecoder for KiaV2Decoder {
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for KiaV2Decoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
+25
-60
@@ -1,15 +1,16 @@
|
||||
//! Kia V3/V4 protocol decoder
|
||||
//! Kia V3/V4 protocol decoder/encoder
|
||||
//!
|
||||
//! Ported from protopirate's kia_v3_v4.c
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v3_v4.c`.
|
||||
//! Decode/encode logic (PWM, preamble, sync polarity, KeeLoq, CRC4, byte order) matches reference.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - PWM encoding: 400/800µs timing
|
||||
//! - 68 bits total
|
||||
//! - Short preamble of 16 pairs
|
||||
//! - KeeLoq encryption (requires manufacturer key)
|
||||
//! - V3 and V4 differ only in sync polarity
|
||||
//! - PWM encoding: 400/800µs (short=0, long=1)
|
||||
//! - 68 bits total (8 bytes encrypted + 4 bits CRC)
|
||||
//! - Short preamble of 16 pairs; sync 1200µs (V4: long HIGH, V3: long LOW)
|
||||
//! - KeeLoq encryption (KIA manufacturer key); V3/V4 differ only in sync polarity
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use super::keeloq_common::{keeloq_decrypt, keeloq_encrypt};
|
||||
use super::keys;
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
use crate::duration_diff;
|
||||
@@ -23,7 +24,7 @@ const INTER_BURST_GAP_US: u32 = 10000;
|
||||
const PREAMBLE_PAIRS: usize = 16;
|
||||
const TOTAL_BURSTS: usize = 3;
|
||||
|
||||
/// Decoder states
|
||||
/// Decoder states (matches protopirate's KiaV3V4DecoderStep)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
@@ -53,7 +54,7 @@ impl KiaV3V4Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Reverse bits in a byte
|
||||
/// Reverse bits in a byte (matches kia_v3_v4.c byte order for KeeLoq payload)
|
||||
fn reverse8(byte: u8) -> u8 {
|
||||
let mut byte = byte;
|
||||
byte = (byte & 0xF0) >> 4 | (byte & 0x0F) << 4;
|
||||
@@ -76,7 +77,7 @@ impl KiaV3V4Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// CRC4 calculation
|
||||
/// CRC4 for Kia V3/V4 (matches kia_v3_v4.c: XOR nibbles over first 8 bytes)
|
||||
fn calculate_crc(bytes: &[u8]) -> u8 {
|
||||
let mut crc: u8 = 0;
|
||||
for &byte in bytes.iter().take(8) {
|
||||
@@ -85,61 +86,18 @@ impl KiaV3V4Decoder {
|
||||
crc & 0x0F
|
||||
}
|
||||
|
||||
/// KeeLoq decrypt
|
||||
fn keeloq_decrypt(data: u32, key: u64) -> u32 {
|
||||
let mut block = data;
|
||||
let mut tkey = key;
|
||||
|
||||
for _ in 0..528 {
|
||||
let lutkey = ((block >> 0) & 1) |
|
||||
((block >> 7) & 2) |
|
||||
((block >> 17) & 4) |
|
||||
((block >> 22) & 8) |
|
||||
((block >> 26) & 16);
|
||||
let lsb = ((block >> 31) ^
|
||||
((block >> 15) & 1) ^
|
||||
((0x3A5C742E_u32 >> lutkey) & 1) ^
|
||||
(((tkey >> 15) & 1) as u32)) as u32;
|
||||
block = ((block & 0x7FFFFFFF) << 1) | lsb;
|
||||
tkey = ((tkey & 0x7FFFFFFFFFFFFFFF) << 1) | (tkey >> 63);
|
||||
}
|
||||
block
|
||||
}
|
||||
|
||||
/// KeeLoq encrypt
|
||||
fn keeloq_encrypt(data: u32, key: u64) -> u32 {
|
||||
let mut block = data;
|
||||
let mut tkey = key;
|
||||
|
||||
for _ in 0..528 {
|
||||
let lutkey = ((block >> 1) & 1) |
|
||||
((block >> 8) & 2) |
|
||||
((block >> 18) & 4) |
|
||||
((block >> 23) & 8) |
|
||||
((block >> 27) & 16);
|
||||
let msb = ((block >> 0) ^
|
||||
((block >> 16) & 1) ^
|
||||
((0x3A5C742E_u32 >> lutkey) & 1) ^
|
||||
(((tkey >> 0) & 1) as u32)) as u32;
|
||||
block = ((block >> 1) & 0x7FFFFFFF) | (msb << 31);
|
||||
tkey = ((tkey >> 1) & 0x7FFFFFFFFFFFFFFF) | ((tkey & 1) << 63);
|
||||
}
|
||||
block
|
||||
}
|
||||
|
||||
/// Get manufacturer key (placeholder - in real use, this would be loaded from config)
|
||||
/// KIA manufacturer key from keystore (type 10)
|
||||
fn get_mf_key() -> u64 {
|
||||
keys::get_keystore().get_kia_mf_key()
|
||||
}
|
||||
|
||||
/// Process the collected buffer and validate
|
||||
/// Process collected 68 bits: decrypt KeeLoq block, validate button/serial (matches kia_v3_v4.c)
|
||||
fn process_buffer(&self) -> Option<DecodedSignal> {
|
||||
if self.raw_bit_count < 68 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut b = self.raw_bits;
|
||||
|
||||
// V3 sync means data is inverted
|
||||
if self.is_v3_sync {
|
||||
let num_bytes = ((self.raw_bit_count + 7) / 8) as usize;
|
||||
@@ -164,7 +122,7 @@ impl KiaV3V4Decoder {
|
||||
let our_serial_lsb = (serial & 0xFF) as u8;
|
||||
|
||||
let mf_key = Self::get_mf_key();
|
||||
let decrypted = Self::keeloq_decrypt(encrypted, mf_key);
|
||||
let decrypted = keeloq_decrypt(encrypted, mf_key);
|
||||
let dec_btn = ((decrypted >> 28) & 0x0F) as u8;
|
||||
let dec_serial_lsb = ((decrypted >> 16) & 0xFF) as u8;
|
||||
|
||||
@@ -313,7 +271,7 @@ impl ProtocolDecoder for KiaV3V4Decoder {
|
||||
(((button & 0x0F) as u32) << 28);
|
||||
|
||||
let mf_key = Self::get_mf_key();
|
||||
let encrypted = Self::keeloq_encrypt(plaintext, mf_key);
|
||||
let encrypted = keeloq_encrypt(plaintext, mf_key);
|
||||
|
||||
// Build raw bytes
|
||||
let mut raw_bytes = [0u8; 9];
|
||||
@@ -343,18 +301,19 @@ impl ProtocolDecoder for KiaV3V4Decoder {
|
||||
|
||||
let mut signal = Vec::with_capacity(600);
|
||||
|
||||
// 3 bursts with 10ms gap (matches protopirate kia_v3_v4 encode)
|
||||
for burst in 0..TOTAL_BURSTS {
|
||||
if burst > 0 {
|
||||
signal.push(LevelDuration::new(false, INTER_BURST_GAP_US));
|
||||
}
|
||||
|
||||
// Preamble
|
||||
// Preamble: 16 short pairs
|
||||
for _ in 0..PREAMBLE_PAIRS {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
}
|
||||
|
||||
// Sync pulse
|
||||
// Sync: V4 = long HIGH, V3 = long LOW
|
||||
if version == 0 {
|
||||
// V4: long HIGH, short LOW
|
||||
signal.push(LevelDuration::new(true, SYNC_DURATION));
|
||||
@@ -365,7 +324,7 @@ impl ProtocolDecoder for KiaV3V4Decoder {
|
||||
signal.push(LevelDuration::new(false, SYNC_DURATION));
|
||||
}
|
||||
|
||||
// Data bits
|
||||
// Data: 68 bits PWM (8 bytes + 4-bit CRC), MSB first per byte
|
||||
for byte_idx in 0..9 {
|
||||
let bits_in_byte = if byte_idx == 8 { 4 } else { 8 };
|
||||
for bit_idx in (8 - bits_in_byte..8).rev() {
|
||||
@@ -384,3 +343,9 @@ impl ProtocolDecoder for KiaV3V4Decoder {
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for KiaV3V4Decoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
+22
-15
@@ -1,13 +1,14 @@
|
||||
//! Kia V5 protocol decoder
|
||||
//! Kia V5 protocol decoder (decode-only)
|
||||
//!
|
||||
//! Ported from protopirate's kia_v5.c
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v5.c`.
|
||||
//! Decode logic (Manchester polarity, mixer decrypt, YEK, preamble) matches reference.
|
||||
//! No encoder in protopirate.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - Manchester encoding: 400/800µs timing
|
||||
//! - 64 bits total (+3 bit CRC)
|
||||
//! - Preamble of ~40+ short pairs
|
||||
//! - Custom "mixer" encryption
|
||||
//! - Decode-only (no encoder)
|
||||
//! - Manchester encoding: 400/800µs (opposite polarity to V1/V2: level ? ShortHigh : ShortLow)
|
||||
//! - 64 data bits + 3-bit CRC (67 bits on air)
|
||||
//! - Preamble of ~40+ short/long pairs; then 64-bit key then 3-bit CRC
|
||||
//! - Custom "mixer" decryption with KIA V5 manufacturer key
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use super::keys;
|
||||
@@ -19,7 +20,7 @@ const TE_LONG: u32 = 800;
|
||||
const TE_DELTA: u32 = 150;
|
||||
const MIN_COUNT_BIT: usize = 64;
|
||||
|
||||
/// Manchester states
|
||||
/// Manchester decoder states (V5 uses opposite polarity to V1/V2; see manchester_advance)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ManchesterState {
|
||||
Mid0,
|
||||
@@ -28,7 +29,7 @@ enum ManchesterState {
|
||||
Start1,
|
||||
}
|
||||
|
||||
/// Decoder states
|
||||
/// Decoder states (matches protopirate's KiaV5DecoderStep)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
@@ -60,12 +61,12 @@ impl KiaV5Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get encryption key from global keystore
|
||||
/// KIA V5 manufacturer key from keystore (type 13)
|
||||
fn get_v5_key() -> u64 {
|
||||
keys::get_keystore().get_kia_v5_key()
|
||||
}
|
||||
|
||||
/// Custom mixer decryption
|
||||
/// Mixer decryption (matches kia_v5.c custom cipher)
|
||||
fn mixer_decode(encrypted: u32) -> u16 {
|
||||
let mut s0 = (encrypted & 0xFF) as u8;
|
||||
let mut s1 = ((encrypted >> 8) & 0xFF) as u8;
|
||||
@@ -128,7 +129,7 @@ impl KiaV5Decoder {
|
||||
((s0 as u16) + ((s1 as u16) << 8)) & 0xFFFF
|
||||
}
|
||||
|
||||
/// Reverse bits in 64-bit value
|
||||
/// YEK: reverse bit order per byte (matches kia_v5.c for key derivation)
|
||||
fn compute_yek(key: u64) -> u64 {
|
||||
let mut yek: u64 = 0;
|
||||
for i in 0..8 {
|
||||
@@ -180,7 +181,7 @@ impl KiaV5Decoder {
|
||||
output
|
||||
}
|
||||
|
||||
/// Parse decoded data
|
||||
/// Parse 64-bit key: YEK then serial/button from high bits, mixer_decode for counter (matches kia_v5.c)
|
||||
fn parse_data(&self) -> Option<DecodedSignal> {
|
||||
if self.bit_count < MIN_COUNT_BIT as u8 {
|
||||
return None;
|
||||
@@ -188,7 +189,7 @@ impl KiaV5Decoder {
|
||||
|
||||
let key = self.saved_key;
|
||||
let yek = Self::compute_yek(key);
|
||||
|
||||
// serial(28) + button(4) in high 32 bits; low 32 bits encrypted counter
|
||||
let serial = ((yek >> 32) & 0x0FFFFFFF) as u32;
|
||||
let button = ((yek >> 60) & 0x0F) as u8;
|
||||
let encrypted = (yek & 0xFFFFFFFF) as u32;
|
||||
@@ -316,6 +317,12 @@ impl ProtocolDecoder for KiaV5Decoder {
|
||||
}
|
||||
|
||||
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
|
||||
None // V5 doesn't support encoding
|
||||
None // V5 decode-only in protopirate
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for KiaV5Decoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
+23
-17
@@ -1,13 +1,14 @@
|
||||
//! Kia V6 protocol decoder
|
||||
//! Kia V6 protocol decoder (decode-only)
|
||||
//!
|
||||
//! Ported from protopirate's kia_v6.c
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/kia_v6.c`.
|
||||
//! Decode logic (Manchester level mapping, 3-part 144-bit frame, AES-128, CRC8, keystore XOR) matches reference.
|
||||
//! No encoder in protopirate.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - Manchester encoding: 200/400µs timing
|
||||
//! - 144 bits total (split into 3 parts)
|
||||
//! - Long preamble of 600+ pairs
|
||||
//! - AES-128 encryption
|
||||
//! - Decode-only (no encoder)
|
||||
//! - Manchester encoding: 200/400µs (level convention inverted vs Flipper; see manchester_advance)
|
||||
//! - 144 bits total: part1 (64) + part2 (64) + part3 (16), each part inverted on store
|
||||
//! - Long preamble of 601 pairs; sync bits 1,1,0,1 then data
|
||||
//! - AES-128 decryption with key derived from KIA V6 A/B keystores (types 11/12) and XOR masks
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use super::keys;
|
||||
@@ -65,7 +66,7 @@ const AES_SBOX_INV: [u8; 256] = [
|
||||
|
||||
const AES_RCON: [u8; 10] = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36];
|
||||
|
||||
/// Manchester states
|
||||
/// Manchester decoder states (event mapping 0/2/4/6 matches protopirate kia_v6 level convention)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ManchesterState {
|
||||
Mid0,
|
||||
@@ -74,7 +75,7 @@ enum ManchesterState {
|
||||
Start1,
|
||||
}
|
||||
|
||||
/// Decoder states
|
||||
/// Decoder states (matches protopirate's KiaV6DecoderStep)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
@@ -119,17 +120,17 @@ impl KiaV6Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get keystore A from global keystore
|
||||
/// KIA V6 keystore A from keystore (type 11)
|
||||
fn get_keystore_a() -> u64 {
|
||||
keys::get_keystore().get_kia_v6_keystore_a()
|
||||
}
|
||||
|
||||
/// Get keystore B from global keystore
|
||||
/// KIA V6 keystore B from keystore (type 12)
|
||||
fn get_keystore_b() -> u64 {
|
||||
keys::get_keystore().get_kia_v6_keystore_b()
|
||||
}
|
||||
|
||||
/// CRC8 calculation
|
||||
/// CRC8 for V6 (matches kia_v6.c: init 0xFF, polynomial 0x07, over first 15 bytes)
|
||||
fn crc8(data: &[u8], init: u8, polynomial: u8) -> u8 {
|
||||
let mut crc = init;
|
||||
for &byte in data {
|
||||
@@ -274,7 +275,7 @@ impl KiaV6Decoder {
|
||||
*data = state;
|
||||
}
|
||||
|
||||
/// Get AES key from keystores
|
||||
/// AES-128 key from V6 keystores A+B with XOR_MASK_LOW/HIGH (matches kia_v6.c)
|
||||
fn get_aes_key() -> [u8; 16] {
|
||||
let keystore_a = Self::get_keystore_a();
|
||||
let keystore_a_hi = ((keystore_a >> 32) & 0xFFFFFFFF) as u32;
|
||||
@@ -305,7 +306,7 @@ impl KiaV6Decoder {
|
||||
aes_key
|
||||
}
|
||||
|
||||
/// Decrypt the stored data
|
||||
/// Decrypt 16-byte block: byte layout matches kia_v6.c; AES-128 then CRC8 check
|
||||
fn decrypt(&self) -> Option<(u32, u8, u32, bool)> {
|
||||
let mut encrypted_data = [0u8; 16];
|
||||
|
||||
@@ -380,9 +381,8 @@ impl KiaV6Decoder {
|
||||
output
|
||||
}
|
||||
|
||||
/// Add initial sync bits
|
||||
/// Add initial sync bits (1,1,0,1 — matches kia_v6.c)
|
||||
fn add_sync_bits(&mut self) {
|
||||
// Add 1, 1, 0, 1 as initial bits
|
||||
for bit in [true, true, false, true] {
|
||||
let carry = self.data_part1_low >> 31;
|
||||
self.data_part1_low = (self.data_part1_low << 1) | (bit as u32);
|
||||
@@ -554,6 +554,12 @@ impl ProtocolDecoder for KiaV6Decoder {
|
||||
}
|
||||
|
||||
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
|
||||
None
|
||||
None // V6 decode-only in protopirate
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for KiaV6Decoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
//! Protocol decoders and encoders for various keyfob systems.
|
||||
//!
|
||||
//! This module implements decoders for various keyfob protocols, ported from
|
||||
//! protopirate. Each protocol processes level+duration pairs from the demodulator.
|
||||
//! Protocols are aligned with the ProtoPirate reference (`REFERENCES/ProtoPirate/protocols/`).
|
||||
//! Each decoder processes level+duration pairs from the demodulator and optionally supports
|
||||
//! encoding (replay). Shared pieces: [common], [keeloq_common], [keys], [aut64].
|
||||
|
||||
mod common;
|
||||
pub mod keeloq_common;
|
||||
|
||||
+25
-23
@@ -1,12 +1,13 @@
|
||||
//! PSA (Peugeot/Citroen) protocol decoder/encoder
|
||||
//! PSA (Peugeot/Citroën) protocol decoder/encoder
|
||||
//!
|
||||
//! Ported from protopirate's psa.c
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/psa.c`.
|
||||
//! Decode/encode logic (Manchester, preamble, TEA, XOR, mode 0x23/0x36) matches reference.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - Manchester encoding: 125/250µs bit timing, 250/500µs symbol timing
|
||||
//! - 128 bits total (key1: 64 bits, key2: 16 bits, validation: 48 bits)
|
||||
//! - TEA and XOR encryption schemes
|
||||
//! - Two modes: 0x23 and 0x36
|
||||
//! - Manchester encoding: 250/500µs symbol (125/250µs sub-symbol for preamble)
|
||||
//! - 128 bits total: key1 (64) + validation (16) + key2/rest (48); decode uses key1 + 16-bit validation
|
||||
//! - TEA decrypt/encrypt with fixed key schedules; mode 0x23 adds XOR layer
|
||||
//! - Two modes: seed 0x23 (TEA + XOR), seed 0xF3/0x36 (TEA, BF2 key schedule)
|
||||
|
||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||
use crate::duration_diff;
|
||||
@@ -35,7 +36,7 @@ const BF1_KEY_SCHEDULE: [u32; 4] = [0x4A434915, 0xD6743C2B, 0x1F29D308, 0xE6B79A
|
||||
// Brute-force constants for mode 0x36
|
||||
const BF2_KEY_SCHEDULE: [u32; 4] = [0x4039C240, 0xEDA92CAB, 0x4306C02A, 0x02192A04];
|
||||
|
||||
/// Manchester decoder states
|
||||
/// Manchester decoder states (matches protopirate psa.c Manchester state machine)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ManchesterState {
|
||||
Mid0,
|
||||
@@ -44,16 +45,12 @@ enum ManchesterState {
|
||||
Start1,
|
||||
}
|
||||
|
||||
/// Decoder states
|
||||
/// Decoder states (matches protopirate's PsaDecoderState)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderState {
|
||||
/// Waiting for first edge
|
||||
WaitEdge,
|
||||
/// Counting preamble pattern
|
||||
CountPattern,
|
||||
/// Decoding Manchester data
|
||||
DecodeManchester,
|
||||
/// Found end of data
|
||||
End,
|
||||
}
|
||||
|
||||
@@ -94,7 +91,7 @@ impl PsaDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Manchester advance
|
||||
/// Manchester state machine (matches psa.c event mapping)
|
||||
fn manchester_advance(&mut self, is_short: bool, is_high: bool) -> Option<bool> {
|
||||
let event = match (is_short, is_high) {
|
||||
(true, true) => 0,
|
||||
@@ -143,7 +140,7 @@ impl PsaDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// TEA decrypt
|
||||
/// TEA decrypt (matches psa.c / standard TEA)
|
||||
fn tea_decrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) {
|
||||
let mut sum = TEA_DELTA.wrapping_mul(TEA_ROUNDS);
|
||||
for _ in 0..TEA_ROUNDS {
|
||||
@@ -161,7 +158,7 @@ impl PsaDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// TEA encrypt
|
||||
/// TEA encrypt (matches psa.c / standard TEA)
|
||||
fn tea_encrypt(v0: &mut u32, v1: &mut u32, key: &[u32; 4]) {
|
||||
let mut sum: u32 = 0;
|
||||
for _ in 0..TEA_ROUNDS {
|
||||
@@ -179,7 +176,7 @@ impl PsaDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// XOR decrypt (mode 0x23)
|
||||
/// XOR decrypt for mode 0x23 (matches psa.c)
|
||||
fn xor_decrypt(buffer: &mut [u8]) {
|
||||
let e6 = buffer[8];
|
||||
let e7 = buffer[9];
|
||||
@@ -198,8 +195,9 @@ impl PsaDecoder {
|
||||
buffer[7] = e5 ^ e6 ^ e7;
|
||||
}
|
||||
|
||||
/// Decrypt key1 + validation: mode 0x23 (TEA+XOR) or 0x36 (TEA, BF2) — matches psa.c
|
||||
fn try_decrypt(&self) -> Option<(u32, u8, u32, u16, u8)> {
|
||||
// Try mode 0x23 first
|
||||
// Try mode 0x23 first (seed byte 0x23)
|
||||
let seed_byte = (self.key1_high >> 24) as u8;
|
||||
|
||||
if seed_byte >= 0x23 && seed_byte < 0x24 {
|
||||
@@ -250,8 +248,9 @@ impl PsaDecoder {
|
||||
None
|
||||
}
|
||||
|
||||
/// Build DecodedSignal from key1 + validation; decrypt yields serial/button/counter (matches psa.c)
|
||||
fn parse_data(&self) -> DecodedSignal {
|
||||
// Combine into 128-bit data (store lower 64)
|
||||
// Store key1 as 64-bit data for display/replay
|
||||
let data = ((self.key1_high as u64) << 32) | (self.key1_low as u64);
|
||||
|
||||
if let Some((serial, btn, counter, _crc, _mode)) = self.try_decrypt() {
|
||||
@@ -445,20 +444,17 @@ impl ProtocolDecoder for PsaDecoder {
|
||||
let key1_low = v1;
|
||||
let validation = ((buffer[8] as u16) << 8) | (buffer[9] as u16);
|
||||
|
||||
// Build signal
|
||||
let mut signal = Vec::with_capacity(512);
|
||||
|
||||
// Preamble: alternating 125µs pulses
|
||||
// Preamble + sync (matches protopirate psa encode)
|
||||
for _ in 0..70 {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT_125));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT_125));
|
||||
}
|
||||
|
||||
// Sync
|
||||
signal.push(LevelDuration::new(true, TE_LONG_250));
|
||||
signal.push(LevelDuration::new(false, TE_LONG_250));
|
||||
|
||||
// Key1: 64 bits Manchester encoded
|
||||
// Key1: 64 bits Manchester, then validation 16 bits
|
||||
let key1 = ((key1_high as u64) << 32) | (key1_low as u64);
|
||||
for bit in (0..64).rev() {
|
||||
if (key1 >> bit) & 1 == 1 {
|
||||
@@ -487,3 +483,9 @@ impl ProtocolDecoder for PsaDecoder {
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PsaDecoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
+17
-10
@@ -1,14 +1,14 @@
|
||||
//! Scher-Khan protocol decoder
|
||||
//! Scher-Khan protocol decoder (decode-only)
|
||||
//!
|
||||
//! Ported from protopirate's scher_khan.c
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/scher_khan.c`.
|
||||
//! Decode logic (PWM preamble/sync, short=0/long=1, variable bit count) matches reference.
|
||||
//! No encoder in protopirate.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - PWM encoding: 750/1100µs timing
|
||||
//! - Variable bit count (35, 51, 57, 63, 64, 81, 82)
|
||||
//! - Decode-only (no encoder)
|
||||
//! - PWM encoding: 750µs = 0, 1100µs = 1; preamble uses 2× short then alternating
|
||||
//! - Variable bit count (35, 51, 57, 63, 64, 81, 82); only 51-bit format parsed for serial/button/counter
|
||||
//!
|
||||
//! References:
|
||||
//! - https://phreakerclub.com/72
|
||||
//! References: https://phreakerclub.com/72
|
||||
|
||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||
use crate::duration_diff;
|
||||
@@ -19,7 +19,7 @@ const TE_LONG: u32 = 1100;
|
||||
const TE_DELTA: u32 = 160;
|
||||
const MIN_COUNT_BIT: usize = 35;
|
||||
|
||||
/// Decoder states
|
||||
/// Decoder states (matches protopirate's ScherKhanDecoderStep)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
@@ -48,10 +48,11 @@ impl ScherKhanDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse payload by bit count; 51-bit format yields serial/button/counter (matches scher_khan.c)
|
||||
fn parse_data(data: u64, bit_count: usize) -> DecodedSignal {
|
||||
let (serial, btn, cnt) = match bit_count {
|
||||
51 => {
|
||||
// MAGIC CODE, Dynamic
|
||||
// 51-bit "MAGIC CODE" / Dynamic format: serial(28) | button(4) | counter(16) — matches reference
|
||||
let serial =
|
||||
((data >> 24) & 0xFFFFFF0) as u32 | ((data >> 20) & 0x0F) as u32;
|
||||
let btn = ((data >> 24) & 0x0F) as u8;
|
||||
@@ -196,6 +197,12 @@ impl ProtocolDecoder for ScherKhanDecoder {
|
||||
}
|
||||
|
||||
fn encode(&self, _decoded: &DecodedSignal, _button: u8) -> Option<Vec<LevelDuration>> {
|
||||
None
|
||||
None // Scher-Khan decode-only in protopirate
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ScherKhanDecoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
//! Star Line protocol decoder/encoder
|
||||
//!
|
||||
//! Ported from protopirate's star_line.c
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/star_line.c`.
|
||||
//! Decode/encode logic (PWM header, fix/hop split, KeeLoq simple/normal learning) matches reference.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - PWM encoding: 250/500µs timing
|
||||
//! - 64 bits total
|
||||
//! - PWM encoding: 250µs = 0, 500µs = 1
|
||||
//! - 64 bits total: key_fix (32) + key_hop (32), sent MSB-first (reversed on air)
|
||||
//! - Header: 6 pairs of 1000µs HIGH + 1000µs LOW
|
||||
//! - KeeLoq encryption (requires manufacturer key)
|
||||
//! - KeeLoq: fix = serial(24) + button(8); hop encrypted with MF key or normal-learning derived key
|
||||
|
||||
use super::keeloq_common::{keeloq_decrypt, keeloq_encrypt, keeloq_normal_learning, reverse_key};
|
||||
use super::keys;
|
||||
@@ -20,7 +21,7 @@ const TE_DELTA: u32 = 120;
|
||||
const MIN_COUNT_BIT: usize = 64;
|
||||
const HEADER_DURATION: u32 = 1000; // te_long * 2
|
||||
|
||||
/// Decoder states
|
||||
/// Decoder states (matches protopirate's StarLineDecoderStep)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
@@ -49,13 +50,14 @@ impl StarLineDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get manufacturer key from global keystore
|
||||
/// Star Line manufacturer key from keystore (type 20)
|
||||
fn get_mf_key() -> u64 {
|
||||
keys::get_keystore().get_star_line_mf_key()
|
||||
}
|
||||
|
||||
/// Parse 64-bit payload: reverse_key then fix(32)/hop(32); KeeLoq decrypt (simple then normal learning) — matches star_line.c
|
||||
fn parse_data(data: u64) -> DecodedSignal {
|
||||
// Data is stored MSB-first in the air, reverse to get fix|hop
|
||||
// Data is MSB-first on air; reverse to get fix|hop
|
||||
let reversed = reverse_key(data, MIN_COUNT_BIT);
|
||||
let key_fix = (reversed >> 32) as u32;
|
||||
let key_hop = (reversed & 0xFFFFFFFF) as u32;
|
||||
@@ -246,13 +248,13 @@ impl ProtocolDecoder for StarLineDecoder {
|
||||
|
||||
let mut signal = Vec::with_capacity(256);
|
||||
|
||||
// Header: 6 pairs of LONG*2 HIGH + LONG*2 LOW
|
||||
// Header: 6 pairs 1000µs HIGH + 1000µs LOW (matches protopirate star_line encode)
|
||||
for _ in 0..6 {
|
||||
signal.push(LevelDuration::new(true, HEADER_DURATION));
|
||||
signal.push(LevelDuration::new(false, HEADER_DURATION));
|
||||
}
|
||||
|
||||
// Data: 64 bits, MSB first
|
||||
// Data: 64 bits PWM, MSB first (1=long, 0=short)
|
||||
for bit in (0..64).rev() {
|
||||
if (data >> bit) & 1 == 1 {
|
||||
// Bit 1: LONG HIGH + LONG LOW
|
||||
@@ -268,3 +270,9 @@ impl ProtocolDecoder for StarLineDecoder {
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for StarLineDecoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
+20
-32
@@ -1,13 +1,13 @@
|
||||
//! Subaru protocol decoder
|
||||
//! Subaru protocol decoder/encoder
|
||||
//!
|
||||
//! Ported from protopirate's subaru.c
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/subaru.c`.
|
||||
//! Decode/encode logic (PWM preamble/gap/sync, short=1/long=0, counter decode) matches reference.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - PWM encoding: short HIGH (800µs) = 1, long HIGH (1600µs) = 0
|
||||
//! - 64 bits total
|
||||
//! - Long preamble of 1600µs pulses
|
||||
//! - Gap and sync pattern
|
||||
//! - Complex counter encoding
|
||||
//! - PWM encoding: 800µs HIGH = 1, 1600µs HIGH = 0; LOW is 800µs after each bit
|
||||
//! - 64 bits total (8 bytes MSB first: button(4)+serial(24)+counter-related)
|
||||
//! - Preamble: 79 full 1600µs pairs + 80th HIGH only; then gap 2800µs, sync 2800µs HIGH + 1600µs LOW
|
||||
//! - Complex counter encoding (decode_counter) from bytes 4–7
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
@@ -22,7 +22,7 @@ const MIN_COUNT_BIT: usize = 64;
|
||||
const GAP_US: u32 = 2800;
|
||||
const SYNC_US: u32 = 2800;
|
||||
|
||||
/// Decoder states
|
||||
/// Decoder states (matches protopirate's SubaruDecoderStep)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
@@ -67,7 +67,7 @@ impl SubaruDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode the counter from the complex Subaru encoding
|
||||
/// Decode 16-bit counter from bytes 4–7 (matches subaru.c complex encoding)
|
||||
fn decode_counter(kb: &[u8; 8]) -> u16 {
|
||||
let mut lo: u8 = 0;
|
||||
if (kb[4] & 0x40) == 0 { lo |= 0x01; }
|
||||
@@ -115,9 +115,7 @@ impl SubaruDecoder {
|
||||
((hi as u16) << 8) | (lo as u16)
|
||||
}
|
||||
|
||||
/// Add a level+duration to the signal, merging with the previous entry
|
||||
/// if it has the same level. This prevents consecutive same-level pulses
|
||||
/// which would silently merge during HackRF transmission.
|
||||
/// Append level+duration, merging with previous if same level (prevents HackRF from merging consecutive same-level pulses)
|
||||
fn add_level(signal: &mut Vec<LevelDuration>, level: bool, duration: u32) {
|
||||
if let Some(last) = signal.last_mut() {
|
||||
if last.level == level {
|
||||
@@ -128,15 +126,13 @@ impl SubaruDecoder {
|
||||
signal.push(LevelDuration::new(level, duration));
|
||||
}
|
||||
|
||||
/// Process the decoded data
|
||||
/// Build DecodedSignal from 8-byte buffer: serial(bytes 1–3), button(byte0 low nibble), counter(decode_counter) — matches subaru.c
|
||||
fn process_data(&self) -> Option<DecodedSignal> {
|
||||
if self.bit_count < 64 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let b = &self.data;
|
||||
|
||||
// Build 64-bit key
|
||||
let key = ((b[0] as u64) << 56) | ((b[1] as u64) << 48) |
|
||||
((b[2] as u64) << 40) | ((b[3] as u64) << 32) |
|
||||
((b[4] as u64) << 24) | ((b[5] as u64) << 16) |
|
||||
@@ -297,37 +293,24 @@ impl ProtocolDecoder for SubaruDecoder {
|
||||
let key = decoded.data;
|
||||
let mut signal = Vec::with_capacity(512);
|
||||
|
||||
// Generate 3 bursts.
|
||||
//
|
||||
// IMPORTANT: Uses add_level() to merge adjacent same-level pulses.
|
||||
// The HackRF transmitter generates IQ samples sequentially from the
|
||||
// LevelDuration list, so consecutive same-level pairs silently merge
|
||||
// and corrupt timing. For example, the last preamble LOW (1600µs) +
|
||||
// gap LOW (2800µs) would become a single 4400µs LOW without merging.
|
||||
// 3 bursts; add_level() merges same-level pulses for correct HackRF timing (matches protopirate subaru encode)
|
||||
for burst in 0..3 {
|
||||
if burst > 0 {
|
||||
// Inter-burst silence
|
||||
Self::add_level(&mut signal, false, 25000);
|
||||
}
|
||||
|
||||
// Preamble: 79 full pairs + 80th HIGH only.
|
||||
// The gap LOW replaces the 80th preamble LOW.
|
||||
// Preamble: 79 full 1600µs pairs + 80th HIGH only; gap replaces 80th LOW
|
||||
for i in 0..80 {
|
||||
Self::add_level(&mut signal, true, TE_LONG);
|
||||
if i < 79 {
|
||||
Self::add_level(&mut signal, false, TE_LONG);
|
||||
}
|
||||
}
|
||||
|
||||
// Gap (replaces the 80th preamble LOW)
|
||||
Self::add_level(&mut signal, false, GAP_US);
|
||||
|
||||
// Sync
|
||||
Self::add_level(&mut signal, true, SYNC_US);
|
||||
Self::add_level(&mut signal, false, TE_LONG);
|
||||
|
||||
// Data: 64 bits (MSB first)
|
||||
// Short HIGH = 1, Long HIGH = 0
|
||||
// Data: 64 bits MSB first; short HIGH = 1, long HIGH = 0; LOW = 800µs after each
|
||||
for bit in (0..64).rev() {
|
||||
if (key >> bit) & 1 == 1 {
|
||||
Self::add_level(&mut signal, true, TE_SHORT);
|
||||
@@ -337,10 +320,15 @@ impl ProtocolDecoder for SubaruDecoder {
|
||||
Self::add_level(&mut signal, false, TE_SHORT);
|
||||
}
|
||||
|
||||
// End-of-burst gap (extends the last data LOW)
|
||||
Self::add_level(&mut signal, false, TE_LONG * 2);
|
||||
}
|
||||
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SubaruDecoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
+17
-12
@@ -1,12 +1,13 @@
|
||||
//! Suzuki protocol decoder/encoder
|
||||
//!
|
||||
//! Ported from protopirate's suzuki.c
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/suzuki.c`.
|
||||
//! Decode/encode logic (preamble count 350, gap 2000µs, short=0/long=1, field layout) matches reference.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - PWM encoding: 250/500µs timing
|
||||
//! - 64 bits total
|
||||
//! - 350 preamble pairs
|
||||
//! - 2000µs gap between transmissions
|
||||
//! - PWM encoding: 250µs HIGH = 0, 500µs HIGH = 1; LOW 250µs after each bit
|
||||
//! - 64 bits total; preamble: 300+ short LOW pulses then long HIGH starts data
|
||||
//! - 350 preamble pairs (SHORT HIGH / SHORT LOW); 2000µs gap at end
|
||||
//! - Field layout: serial = (data_high&0xFFF)<<16 | data_low>>16; btn = (data_low>>12)&0xF; cnt = (data_high<<4)>>16
|
||||
|
||||
use super::{DecodedSignal, ProtocolDecoder, ProtocolTiming};
|
||||
use crate::duration_diff;
|
||||
@@ -20,7 +21,7 @@ const PREAMBLE_COUNT: u16 = 350;
|
||||
const GAP_TIME: u32 = 2000;
|
||||
const GAP_DELTA: u32 = 399;
|
||||
|
||||
/// Decoder states
|
||||
/// Decoder states (matches protopirate's SuzukiDecoderStep)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
@@ -53,10 +54,11 @@ impl SuzukiDecoder {
|
||||
self.decode_count_bit += 1;
|
||||
}
|
||||
|
||||
/// Parse 64-bit data (matches suzuki.c: serial, btn, cnt layout)
|
||||
fn parse_data(data: u64) -> DecodedSignal {
|
||||
let data_high = (data >> 32) as u32;
|
||||
let data_low = data as u32;
|
||||
|
||||
// Reference: instance->generic.serial = ((data_high & 0xFFF) << 16) | (data_low >> 16); etc.
|
||||
let serial = ((data_high & 0xFFF) << 16) | (data_low >> 16);
|
||||
let btn = ((data_low >> 12) & 0xF) as u8;
|
||||
let cnt = ((data_high << 4) >> 16) as u16;
|
||||
@@ -178,14 +180,12 @@ impl ProtocolDecoder for SuzukiDecoder {
|
||||
|
||||
let mut signal = Vec::with_capacity(1024);
|
||||
|
||||
// Preamble: SHORT HIGH / SHORT LOW pairs
|
||||
// Preamble + data + gap (matches subghz_protocol_encoder_suzuki_get_upload in suzuki.c)
|
||||
for _ in 0..PREAMBLE_COUNT {
|
||||
signal.push(LevelDuration::new(true, TE_SHORT));
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
}
|
||||
|
||||
// Data: 64 bits, MSB first
|
||||
// SHORT HIGH (~250µs) = 0, LONG HIGH (~500µs) = 1
|
||||
// Data: 64 bits MSB first; SHORT HIGH = 0, LONG HIGH = 1; LOW = 250µs after each
|
||||
for bit in (0..64).rev() {
|
||||
if (data >> bit) & 1 == 1 {
|
||||
signal.push(LevelDuration::new(true, TE_LONG));
|
||||
@@ -195,9 +195,14 @@ impl ProtocolDecoder for SuzukiDecoder {
|
||||
signal.push(LevelDuration::new(false, TE_SHORT));
|
||||
}
|
||||
|
||||
// End gap
|
||||
signal.push(LevelDuration::new(false, GAP_TIME));
|
||||
|
||||
Some(signal)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SuzukiDecoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
+27
-23
@@ -1,17 +1,15 @@
|
||||
//! VAG (VW/Audi/Seat/Skoda) protocol decoder/encoder
|
||||
//!
|
||||
//! Ported from protopirate's vag.c
|
||||
//! Aligned with ProtoPirate reference: `REFERENCES/ProtoPirate/protocols/vag.c`.
|
||||
//! Decode/encode logic (steps, Type 1/2/3/4, AUT64/TEA, dispatch, gap 6000µs) matches reference.
|
||||
//! Intentional differences for HackRF: TE_DELTA 150 (ref 80), TE_DELTA_12 120 (ref 79);
|
||||
//! Preamble2/Sync2C accept shorter LOW pulses for asymmetric software demodulation.
|
||||
//!
|
||||
//! Protocol characteristics:
|
||||
//! - Manchester encoding
|
||||
//! - Type 1/2: 300/600µs timing (te_short=300), 80 bits, AUT64 or TEA encryption
|
||||
//! - Type 3/4: 500/1000µs timing (te_short=500), 80 bits, AUT64 encryption
|
||||
//! - 433.92 MHz and 434.42 MHz
|
||||
//! - Four sub-types:
|
||||
//! Type 1: AUT64 encrypted (300µs Manchester, dispatch 0x2A/0x1C/0x46)
|
||||
//! Type 2: TEA encrypted (300µs Manchester, dispatch 0x2A/0x1C/0x46)
|
||||
//! Type 3: AUT64 encrypted (500µs Manchester, auto-detect key)
|
||||
//! Type 4: AUT64 encrypted (500µs Manchester, key 2)
|
||||
//! - Manchester encoding; 80 bits (key1 64 + key2 16)
|
||||
//! - Type 1/2: 300/600µs, prefix 0x2F3F (AUT64) / 0x2F1C (TEA), dispatch 0x2A/0x1C/0x46
|
||||
//! - Type 3/4: 500/1000µs, preamble 41+ pairs, sync 1000µs + 3×750µs, dispatch 0x2B/0x1D/0x47
|
||||
//! - End-of-data gap 6000µs (accept within 4000µs); keys from keystore (VAG raw 64 bytes)
|
||||
|
||||
use super::{ProtocolDecoder, ProtocolTiming, DecodedSignal};
|
||||
use super::aut64;
|
||||
@@ -34,7 +32,7 @@ const TE_DELTA_12: u32 = 120; // Wider tolerance for HackRF (was 79)
|
||||
const TEA_DELTA: u32 = 0x9E3779B9;
|
||||
const TEA_ROUNDS: usize = 32;
|
||||
|
||||
/// TEA key schedule for VAG
|
||||
/// TEA key schedule for VAG (matches vag.c vag_tea_key_schedule)
|
||||
static TEA_KEY_SCHEDULE: [u32; 4] = [0x0B46502D, 0x5E253718, 0x2BF93A19, 0x622C1206];
|
||||
|
||||
/// Manchester states
|
||||
@@ -56,17 +54,17 @@ enum ManchesterEvent {
|
||||
Reset,
|
||||
}
|
||||
|
||||
/// Decoder states
|
||||
/// Decoder states (matches protopirate's VAGDecoderStep)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum DecoderStep {
|
||||
Reset,
|
||||
Preamble1, // Type 1/2: ~300µs preamble
|
||||
Data1, // Type 1/2: Manchester data
|
||||
Preamble2, // Type 3/4: ~500µs preamble
|
||||
Sync2A, // Type 3/4: sync pattern step A
|
||||
Sync2B, // Type 3/4: sync pattern step B (750µs)
|
||||
Sync2C, // Type 3/4: sync pattern step C (750µs)
|
||||
Data2, // Type 3/4: Manchester data
|
||||
Preamble1,
|
||||
Data1,
|
||||
Preamble2,
|
||||
Sync2A,
|
||||
Sync2B,
|
||||
Sync2C,
|
||||
Data2,
|
||||
}
|
||||
|
||||
/// VAG sub-type
|
||||
@@ -182,7 +180,7 @@ impl VagDecoder {
|
||||
self.bit_count += 1;
|
||||
}
|
||||
|
||||
/// TEA decrypt
|
||||
/// TEA decrypt (matches vag.c vag_tea_decrypt)
|
||||
fn tea_decrypt(v0: &mut u32, v1: &mut u32, key_schedule: &[u32; 4]) {
|
||||
let mut sum = TEA_DELTA.wrapping_mul(TEA_ROUNDS as u32);
|
||||
for _ in 0..TEA_ROUNDS {
|
||||
@@ -198,7 +196,7 @@ impl VagDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// TEA encrypt
|
||||
/// TEA encrypt (matches vag.c vag_tea_encrypt)
|
||||
fn tea_encrypt(v0: &mut u32, v1: &mut u32, key_schedule: &[u32; 4]) {
|
||||
let mut sum: u32 = 0;
|
||||
for _ in 0..TEA_ROUNDS {
|
||||
@@ -278,7 +276,7 @@ impl VagDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the collected data and attempt decryption
|
||||
/// Parse key1/key2 and decrypt by type (AUT64/TEA, dispatch); matches vag.c vag_parse_data
|
||||
fn parse_data(&mut self) {
|
||||
self.decrypted = false;
|
||||
self.serial = 0;
|
||||
@@ -950,7 +948,7 @@ impl ProtocolDecoder for VagDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for gap (end of data)
|
||||
// End-of-data gap: 6000µs, accept within 4000µs (matches vag.c check_gap1_data)
|
||||
if !level {
|
||||
let gap_diff = if duration > 6000 {
|
||||
duration - 6000
|
||||
@@ -1145,3 +1143,9 @@ impl ProtocolDecoder for VagDecoder {
|
||||
self.encode_signal(decoded)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VagDecoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user