Added a generic keeloq decode against manufacure keys
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
//! Generic KeeLoq fallback decoder.
|
||||
//!
|
||||
//! When no known protocol decodes a signal, we try to decode it as KeeLoq using every
|
||||
//! manufacturer key in the keystore. We support two common air formats (bit layout + timing):
|
||||
//! - **Kia V3/V4 format**: 68 bits, 400/800µs PWM, 315/433 MHz
|
||||
//! - **Star Line format**: 64 bits, 250/500µs PWM, 433 MHz
|
||||
//!
|
||||
//! All decryption is done via [keeloq_common]; this module only orchestrates bit collection
|
||||
//! (delegated to the format-specific collectors) and tries each key.
|
||||
|
||||
use super::common::DecodedSignal;
|
||||
use super::keeloq_common::{keeloq_decrypt, keeloq_normal_learning, reverse_key, reverse8};
|
||||
use super::kia_v3_v4;
|
||||
use super::star_line;
|
||||
use crate::keystore;
|
||||
use crate::radio::demodulator::LevelDuration;
|
||||
|
||||
const KIA_V3_V4_BITS: usize = 68;
|
||||
const STAR_LINE_BITS: usize = 64;
|
||||
|
||||
/// Try to decode an unknown signal as KeeLoq using every keystore manufacturer key.
|
||||
/// Tries Kia V3/V4 format first (315/433 MHz), then Star Line format (433 MHz).
|
||||
/// Returns `("Keeloq (keystore name)", decoded)` on first successful decrypt.
|
||||
pub fn try_decode(
|
||||
pairs: &[LevelDuration],
|
||||
frequency: u32,
|
||||
) -> Option<(String, DecodedSignal)> {
|
||||
let keys = keystore::keeloq_mf_keys_with_names();
|
||||
if keys.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Kia V3/V4 format: 400/800µs, 315/433 MHz
|
||||
let kia_freq_ok = [315_000_000u32, 433_920_000].iter().any(|&f| {
|
||||
let diff = if f > frequency { f - frequency } else { frequency - f };
|
||||
diff < (f / 50)
|
||||
});
|
||||
if kia_freq_ok {
|
||||
for invert in [false, true] {
|
||||
if let Some((buf, is_v3)) = kia_v3_v4::collect_kia_v3_v4_bits(pairs, invert) {
|
||||
if let Some((name, decoded)) = try_kia_v3_v4_format(&buf, is_v3, &keys) {
|
||||
return Some((format!("Keeloq ({})", name), decoded));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Star Line format: 250/500µs, 433 MHz
|
||||
if frequency.abs_diff(433_920_000) < (433_920_000 / 50) {
|
||||
for invert in [false, true] {
|
||||
if let Some(data) = star_line::collect_star_line_bits(pairs, invert) {
|
||||
if let Some((name, decoded)) = try_star_line_format(data, &keys) {
|
||||
return Some((format!("Keeloq ({})", name), decoded));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Try Kia V3/V4 68-bit format with each key. Uses [keeloq_common::keeloq_decrypt] for validation.
|
||||
fn try_kia_v3_v4_format(
|
||||
raw_bits: &[u8; 9],
|
||||
is_v3_sync: bool,
|
||||
keys: &[(String, u64)],
|
||||
) -> Option<(String, DecodedSignal)> {
|
||||
let mut b = *raw_bits;
|
||||
if is_v3_sync {
|
||||
for i in 0..9 {
|
||||
b[i] = !b[i];
|
||||
}
|
||||
}
|
||||
let encrypted = ((reverse8(b[3]) as u32) << 24)
|
||||
| ((reverse8(b[2]) as u32) << 16)
|
||||
| ((reverse8(b[1]) as u32) << 8)
|
||||
| (reverse8(b[0]) as u32);
|
||||
let serial = ((reverse8(b[7] & 0xF0) as u32) << 24)
|
||||
| ((reverse8(b[6]) as u32) << 16)
|
||||
| ((reverse8(b[5]) as u32) << 8)
|
||||
| (reverse8(b[4]) as u32);
|
||||
let button = (reverse8(b[7]) & 0xF0) >> 4;
|
||||
let our_serial_lsb = (serial & 0xFF) as u8;
|
||||
let key_data = ((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)
|
||||
| ((b[6] as u64) << 8)
|
||||
| (b[7] as u64);
|
||||
|
||||
for (name, mf_key) in keys {
|
||||
if *mf_key == 0 {
|
||||
continue;
|
||||
}
|
||||
let decrypted = keeloq_decrypt(encrypted, *mf_key);
|
||||
let dec_btn = ((decrypted >> 28) & 0x0F) as u8;
|
||||
let dec_serial_lsb = ((decrypted >> 16) & 0xFF) as u8;
|
||||
if dec_btn == button && dec_serial_lsb == our_serial_lsb {
|
||||
let counter = (decrypted & 0xFFFF) as u16;
|
||||
return Some((
|
||||
name.clone(),
|
||||
DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(button),
|
||||
counter: Some(counter),
|
||||
crc_valid: true,
|
||||
data: key_data,
|
||||
data_count_bit: KIA_V3_V4_BITS,
|
||||
encoder_capable: true,
|
||||
extra: None,
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Try Star Line 64-bit format with each key. Uses [keeloq_common::keeloq_decrypt] and
|
||||
/// [keeloq_common::keeloq_normal_learning] for validation.
|
||||
fn try_star_line_format(
|
||||
data: u64,
|
||||
keys: &[(String, u64)],
|
||||
) -> Option<(String, DecodedSignal)> {
|
||||
let reversed = reverse_key(data, STAR_LINE_BITS);
|
||||
let key_fix = (reversed >> 32) as u32;
|
||||
let key_hop = (reversed & 0xFFFFFFFF) as u32;
|
||||
let serial = key_fix & 0x00FFFFFF;
|
||||
let btn = (key_fix >> 24) as u8;
|
||||
let serial_lsb = (serial & 0xFF) as u8;
|
||||
|
||||
for (name, mf_key) in keys {
|
||||
if *mf_key == 0 {
|
||||
continue;
|
||||
}
|
||||
// Simple learning
|
||||
let decrypt = keeloq_decrypt(key_hop, *mf_key);
|
||||
let dec_btn = (decrypt >> 24) as u8;
|
||||
let dec_serial_lsb = ((decrypt >> 16) & 0xFF) as u8;
|
||||
if dec_btn == btn && dec_serial_lsb == serial_lsb {
|
||||
let counter = (decrypt & 0xFFFF) as u16;
|
||||
return Some((
|
||||
name.clone(),
|
||||
DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(btn),
|
||||
counter: Some(counter),
|
||||
crc_valid: true,
|
||||
data,
|
||||
data_count_bit: STAR_LINE_BITS,
|
||||
encoder_capable: true,
|
||||
extra: None,
|
||||
},
|
||||
));
|
||||
}
|
||||
// Normal learning
|
||||
let man_key = keeloq_normal_learning(key_fix, *mf_key);
|
||||
let decrypt = keeloq_decrypt(key_hop, man_key);
|
||||
let dec_btn = (decrypt >> 24) as u8;
|
||||
let dec_serial_lsb = ((decrypt >> 16) & 0xFF) as u8;
|
||||
if dec_btn == btn && dec_serial_lsb == serial_lsb {
|
||||
let counter = (decrypt & 0xFFFF) as u16;
|
||||
return Some((
|
||||
name.clone(),
|
||||
DecodedSignal {
|
||||
serial: Some(serial),
|
||||
button: Some(btn),
|
||||
counter: Some(counter),
|
||||
crc_valid: true,
|
||||
data,
|
||||
data_count_bit: STAR_LINE_BITS,
|
||||
encoder_capable: true,
|
||||
extra: None,
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -159,6 +159,102 @@ impl KiaV3V4Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect 68-bit Kia V3/V4 payload from level+duration pairs (for keeloq_generic fallback).
|
||||
/// Returns (raw_bits[0..9], is_v3_sync) when a valid 68-bit burst is found.
|
||||
pub fn collect_kia_v3_v4_bits(
|
||||
pairs: &[LevelDuration],
|
||||
invert_level: bool,
|
||||
) -> Option<([u8; 9], bool)> {
|
||||
let mut step = DecoderStep::Reset;
|
||||
let mut te_last = 0u32;
|
||||
let mut header_count = 0u16;
|
||||
let mut raw_bits = [0u8; 32];
|
||||
let mut raw_bit_count = 0u16;
|
||||
let mut is_v3_sync = false;
|
||||
|
||||
for pair in pairs {
|
||||
let level = if invert_level { !pair.level } else { pair.level };
|
||||
let duration = pair.duration_us;
|
||||
let is_short = duration_diff!(duration, TE_SHORT) < TE_DELTA;
|
||||
let is_long = duration_diff!(duration, TE_LONG) < TE_DELTA;
|
||||
let is_sync = duration > 1000 && duration < 1500;
|
||||
let is_very_long = duration > 1500;
|
||||
|
||||
match step {
|
||||
DecoderStep::Reset => {
|
||||
if level && is_short {
|
||||
step = DecoderStep::CheckPreamble;
|
||||
te_last = duration;
|
||||
header_count = 1;
|
||||
}
|
||||
}
|
||||
DecoderStep::CheckPreamble => {
|
||||
if level {
|
||||
if is_short {
|
||||
te_last = duration;
|
||||
} else if is_sync && header_count >= 8 {
|
||||
step = DecoderStep::CollectRawBits;
|
||||
raw_bit_count = 0;
|
||||
is_v3_sync = false;
|
||||
raw_bits = [0; 32];
|
||||
} else {
|
||||
step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
if is_sync && header_count >= 8 {
|
||||
step = DecoderStep::CollectRawBits;
|
||||
raw_bit_count = 0;
|
||||
is_v3_sync = true;
|
||||
raw_bits = [0; 32];
|
||||
} else if is_short && duration_diff!(te_last, TE_SHORT) < TE_DELTA {
|
||||
header_count += 1;
|
||||
} else if is_very_long {
|
||||
step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
}
|
||||
DecoderStep::CollectRawBits => {
|
||||
if level {
|
||||
if is_sync || is_very_long {
|
||||
if raw_bit_count >= 68 {
|
||||
let mut out = [0u8; 9];
|
||||
out.copy_from_slice(&raw_bits[0..9]);
|
||||
return Some((out, is_v3_sync));
|
||||
}
|
||||
step = DecoderStep::Reset;
|
||||
} else if is_short {
|
||||
if raw_bit_count < 256 {
|
||||
let byte_idx = (raw_bit_count / 8) as usize;
|
||||
let bit_idx = 7 - (raw_bit_count % 8);
|
||||
raw_bits[byte_idx] &= !(1 << bit_idx);
|
||||
raw_bit_count += 1;
|
||||
}
|
||||
} else if is_long {
|
||||
if raw_bit_count < 256 {
|
||||
let byte_idx = (raw_bit_count / 8) as usize;
|
||||
let bit_idx = 7 - (raw_bit_count % 8);
|
||||
raw_bits[byte_idx] |= 1 << bit_idx;
|
||||
raw_bit_count += 1;
|
||||
}
|
||||
} else {
|
||||
step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
if is_sync || is_very_long {
|
||||
if raw_bit_count >= 68 {
|
||||
let mut out = [0u8; 9];
|
||||
out.copy_from_slice(&raw_bits[0..9]);
|
||||
return Some((out, is_v3_sync));
|
||||
}
|
||||
step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for KiaV3V4Decoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Kia V3/V4"
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
mod common;
|
||||
pub mod keeloq_common;
|
||||
mod keeloq_generic;
|
||||
#[allow(dead_code)]
|
||||
pub mod aut64;
|
||||
#[allow(dead_code)]
|
||||
@@ -117,7 +118,11 @@ impl ProtocolRegistry {
|
||||
return Some(result);
|
||||
}
|
||||
// Try inverted polarity (capture LOW = RF HIGH)
|
||||
self.process_signal_inner(pairs, frequency, true)
|
||||
if let Some(result) = self.process_signal_inner(pairs, frequency, true) {
|
||||
return Some(result);
|
||||
}
|
||||
// No known protocol: try KeeLoq generic (uses keeloq_common with every keystore key)
|
||||
keeloq_generic::try_decode(pairs, frequency)
|
||||
}
|
||||
|
||||
/// ProtoPirate-style streaming decode: feed the whole stream, on each decode record and reset decoders, continue.
|
||||
|
||||
@@ -106,6 +106,97 @@ impl StarLineDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect 64-bit Star Line payload from level+duration pairs (for keeloq_generic fallback).
|
||||
pub fn collect_star_line_bits(
|
||||
pairs: &[LevelDuration],
|
||||
invert_level: bool,
|
||||
) -> Option<u64> {
|
||||
let mut step = DecoderStep::Reset;
|
||||
let mut header_count = 0u16;
|
||||
let mut decode_data = 0u64;
|
||||
let mut decode_count_bit = 0usize;
|
||||
let mut te_last = 0u32;
|
||||
|
||||
for pair in pairs {
|
||||
let level = if invert_level { !pair.level } else { pair.level };
|
||||
let duration = pair.duration_us;
|
||||
|
||||
match step {
|
||||
DecoderStep::Reset => {
|
||||
if level {
|
||||
if duration_diff!(duration, HEADER_DURATION) < TE_DELTA * 2 {
|
||||
step = DecoderStep::CheckPreamble;
|
||||
header_count += 1;
|
||||
} else if header_count > 4 {
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
te_last = duration;
|
||||
step = DecoderStep::CheckDuration;
|
||||
}
|
||||
} else {
|
||||
header_count = 0;
|
||||
}
|
||||
}
|
||||
DecoderStep::CheckPreamble => {
|
||||
if !level && duration_diff!(duration, HEADER_DURATION) < TE_DELTA * 2 {
|
||||
step = DecoderStep::Reset;
|
||||
} else {
|
||||
header_count = 0;
|
||||
step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
DecoderStep::SaveDuration => {
|
||||
if level {
|
||||
if duration >= (TE_LONG + TE_DELTA) {
|
||||
step = DecoderStep::Reset;
|
||||
if decode_count_bit >= MIN_COUNT_BIT && decode_count_bit <= MIN_COUNT_BIT + 2 {
|
||||
return Some(decode_data);
|
||||
}
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
header_count = 0;
|
||||
} else {
|
||||
te_last = duration;
|
||||
step = DecoderStep::CheckDuration;
|
||||
}
|
||||
} else {
|
||||
step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
DecoderStep::CheckDuration => {
|
||||
if !level {
|
||||
if duration_diff!(te_last, TE_SHORT) < TE_DELTA
|
||||
&& duration_diff!(duration, TE_SHORT) < TE_DELTA
|
||||
{
|
||||
if decode_count_bit < MIN_COUNT_BIT {
|
||||
decode_data = (decode_data << 1) | 0;
|
||||
decode_count_bit += 1;
|
||||
} else {
|
||||
decode_count_bit += 1;
|
||||
}
|
||||
step = DecoderStep::SaveDuration;
|
||||
} else if duration_diff!(te_last, TE_LONG) < TE_DELTA
|
||||
&& duration_diff!(duration, TE_LONG) < TE_DELTA
|
||||
{
|
||||
if decode_count_bit < MIN_COUNT_BIT {
|
||||
decode_data = (decode_data << 1) | 1;
|
||||
decode_count_bit += 1;
|
||||
} else {
|
||||
decode_count_bit += 1;
|
||||
}
|
||||
step = DecoderStep::SaveDuration;
|
||||
} else {
|
||||
step = DecoderStep::Reset;
|
||||
}
|
||||
} else {
|
||||
step = DecoderStep::Reset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
impl ProtocolDecoder for StarLineDecoder {
|
||||
fn name(&self) -> &'static str {
|
||||
"Star Line"
|
||||
|
||||
Reference in New Issue
Block a user