Added Keeloq

This commit is contained in:
KaraZajac
2026-02-17 19:47:57 -05:00
parent 7c02fc7483
commit 0cffd71a87
20 changed files with 397 additions and 6 deletions
+1
View File
@@ -767,6 +767,7 @@ impl App {
data_count_bit: capture.data_count_bit,
encoder_capable: true,
extra: capture.data_extra,
protocol_display_name: None,
};
// Generate the signal with the new button
+3
View File
@@ -32,6 +32,8 @@ pub struct DecodedSignal {
pub encoder_capable: bool,
/// Protocol-specific extra data for encoding (e.g. VAG: vag_type + key_idx)
pub extra: Option<u64>,
/// Optional protocol display name (e.g. "KeeLoq (DoorHan)"). When set, used as the protocol name for this decode.
pub protocol_display_name: Option<String>,
}
impl DecodedSignal {
@@ -46,6 +48,7 @@ impl DecodedSignal {
data_count_bit: bit_count,
encoder_capable: false,
extra: None,
protocol_display_name: None,
}
}
}
+1
View File
@@ -117,6 +117,7 @@ impl FiatV0Decoder {
data_count_bit: 71,
encoder_capable: true,
extra: None,
protocol_display_name: None,
}
}
}
+1
View File
@@ -617,6 +617,7 @@ impl ProtocolDecoder for FordV0Decoder {
data_count_bit: MIN_COUNT_BIT,
encoder_capable: true,
extra: None,
protocol_display_name: None,
};
self.data_low = 0;
+354
View File
@@ -0,0 +1,354 @@
//! KeeLoq protocol decoder and encoder (unleashed format).
//!
//! Timing and state machine match Flipper Unleashed:
//! `REFERENCES/unleashed-firmware/lib/subghz/protocols/keeloq.c`
//! - te_short=400µs, te_long=800µs, te_delta=140µs, 64 data bits.
//! - Preamble: HIGH pulses ~400µs; when LOW and header_count>2 and LOW ~4000µs, start data.
//! - Data: short HIGH + long LOW = 1, long HIGH + short LOW = 0. End when LOW ≥ 940µs.
//! Decryption tries all keystore keys with simple, normal, secure, magic_xor_type1,
//! magic_serial type1/2/3 (and both key byte orders). Encoder uses simple learning
//! and the key stored in DecodedSignal::extra (set when decoded).
use super::common::DecodedSignal;
use super::keeloq_common::{
keeloq_decrypt, keeloq_encrypt, keeloq_magic_serial_type1_learning,
keeloq_magic_serial_type2_learning, keeloq_magic_serial_type3_learning,
keeloq_magic_xor_type1_learning, keeloq_normal_learning, keeloq_secure_learning,
reverse_key,
};
use crate::keystore;
use crate::radio::demodulator::LevelDuration;
const TE_SHORT: u32 = 400;
const TE_LONG: u32 = 800;
const TE_DELTA: u32 = 140;
const MIN_COUNT_BIT: usize = 64;
fn duration_diff(a: u32, b: u32) -> u32 {
if a < b {
b - a
} else {
a - b
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Step {
Reset,
CheckPreamble,
SaveDuration,
CheckDuration,
}
/// Try to decrypt (fix, hop) with all keystore keys and learning types.
/// Returns (manufacture_name, serial, counter, button, key_for_encode) on success.
/// key_for_encode is Some(mf_key) when a keystore key was used, None for AN-Motors/HCS101.
fn try_keeloq_decrypt(
fix: u32,
hop: u32,
seed: u32,
) -> Option<(String, u32, u16, u8, Option<u64>)> {
let end_serial = (fix & 0xFF) as u8;
let btn = (fix >> 28) as u8;
fn check_decrypt(decrypt: u32, btn: u8, end_serial: u8) -> bool {
(decrypt >> 28) as u8 == btn
&& (((decrypt >> 16) & 0xFF) as u8 == end_serial || ((decrypt >> 16) & 0xFF) == 0)
}
let keys = keystore::keeloq_mf_keys_with_names();
for (name, mf_key) in keys {
for key in [mf_key, mf_key.swap_bytes()] {
if key == 0 {
continue;
}
// Simple
let decrypt = keeloq_decrypt(hop, key);
if check_decrypt(decrypt, btn, end_serial) {
let cnt = (decrypt & 0xFFFF) as u16;
let serial = fix & 0x0FFFFFFF;
return Some((name, serial, cnt, btn, Some(mf_key)));
}
// Normal
let man = keeloq_normal_learning(fix, key);
let decrypt = keeloq_decrypt(hop, man);
if check_decrypt(decrypt, btn, end_serial) {
let cnt = (decrypt & 0xFFFF) as u16;
let serial = fix & 0x0FFFFFFF;
return Some((name, serial, cnt, btn, Some(mf_key)));
}
// Secure (seed 0 and seed = fix for BFT-style)
for s in [0u32, seed] {
let man = keeloq_secure_learning(fix, s, key);
let decrypt = keeloq_decrypt(hop, man);
if check_decrypt(decrypt, btn, end_serial) {
let cnt = (decrypt & 0xFFFF) as u16;
let serial = fix & 0x0FFFFFFF;
return Some((name, serial, cnt, btn, Some(mf_key)));
}
}
// Magic XOR type1
let man = keeloq_magic_xor_type1_learning(fix, key);
let decrypt = keeloq_decrypt(hop, man);
if check_decrypt(decrypt, btn, end_serial) {
let cnt = (decrypt & 0xFFFF) as u16;
let serial = fix & 0x0FFFFFFF;
return Some((name, serial, cnt, btn, Some(mf_key)));
}
// Magic serial type 1/2/3
for man in [
keeloq_magic_serial_type1_learning(fix, key),
keeloq_magic_serial_type2_learning(fix, key),
keeloq_magic_serial_type3_learning(fix & 0xFFFFFF, key),
] {
let decrypt = keeloq_decrypt(hop, man);
if check_decrypt(decrypt, btn, end_serial) {
let cnt = (decrypt & 0xFFFF) as u16;
let serial = fix & 0x0FFFFFFF;
return Some((name, serial, cnt, btn, Some(mf_key)));
}
}
}
}
// AN-Motors / HCS101 special cases (no decrypt, no key for encode)
if (hop >> 24) == ((hop >> 16) & 0xFF)
&& (fix >> 28) == ((hop >> 12) & 0x0F)
&& (hop & 0xFFF) == 0x404
{
return Some((
"AN-Motors".to_string(),
fix & 0x0FFFFFFF,
(hop >> 16) as u16,
btn,
None,
));
}
if (hop & 0xFFF) == 0 && (fix >> 28) == ((hop >> 12) & 0x0F) {
return Some((
"HCS101".to_string(),
fix & 0x0FFFFFFF,
(hop >> 16) as u16,
btn,
None,
));
}
None
}
pub struct KeeloqDecoder {
step: Step,
header_count: u16,
te_last: u32,
decode_data: u64,
decode_count_bit: usize,
/// For secure learning when we don't have a prior decode (use fix as seed hint)
seed: u32,
}
impl KeeloqDecoder {
pub fn new() -> Self {
Self {
step: Step::Reset,
header_count: 0,
te_last: 0,
decode_data: 0,
decode_count_bit: 0,
seed: 0,
}
}
fn add_bit(&mut self, bit: u8) {
self.decode_data = (self.decode_data << 1) | (bit as u64);
self.decode_count_bit += 1;
}
fn reset_to(&mut self, step: Step) {
self.step = step;
if step == Step::Reset {
self.header_count = 0;
}
}
}
impl Default for KeeloqDecoder {
fn default() -> Self {
Self::new()
}
}
impl super::ProtocolDecoder for KeeloqDecoder {
fn name(&self) -> &'static str {
"KeeLoq"
}
fn timing(&self) -> super::ProtocolTiming {
super::ProtocolTiming {
te_short: TE_SHORT,
te_long: TE_LONG,
te_delta: TE_DELTA,
min_count_bit: MIN_COUNT_BIT,
}
}
fn supported_frequencies(&self) -> &[u32] {
&[315_000_000, 433_920_000, 868_350_000]
}
fn reset(&mut self) {
self.step = Step::Reset;
self.header_count = 0;
self.te_last = 0;
self.decode_data = 0;
self.decode_count_bit = 0;
}
fn feed(&mut self, level: bool, duration_us: u32) -> Option<DecodedSignal> {
match self.step {
Step::Reset => {
if level && duration_diff(duration_us, TE_SHORT) < TE_DELTA {
self.step = Step::CheckPreamble;
self.header_count = self.header_count.saturating_add(1);
}
}
Step::CheckPreamble => {
if !level && duration_diff(duration_us, TE_SHORT) < TE_DELTA {
self.step = Step::Reset;
return None;
}
if self.header_count > 2
&& duration_diff(duration_us, TE_SHORT * 10) < TE_DELTA * 10
{
self.step = Step::SaveDuration;
self.decode_data = 0;
self.decode_count_bit = 0;
} else {
self.step = Step::Reset;
self.header_count = 0;
}
}
Step::SaveDuration => {
if level {
self.te_last = duration_us;
self.step = Step::CheckDuration;
}
}
Step::CheckDuration => {
if !level {
let end_threshold = TE_SHORT * 2 + TE_DELTA;
if duration_us >= end_threshold {
self.step = Step::Reset;
if self.decode_count_bit >= MIN_COUNT_BIT
&& self.decode_count_bit <= MIN_COUNT_BIT + 2
{
let raw_data = self.decode_data;
let reversed = reverse_key(raw_data, MIN_COUNT_BIT);
let key_fix = (reversed >> 32) as u32;
let key_hop = (reversed & 0xFFFFFFFF) as u32;
if let Some((_mf_name, serial, cnt, btn, key_for_encode)) =
try_keeloq_decrypt(key_fix, key_hop, self.seed)
{
if self.seed == 0 {
self.seed = key_fix & 0x0FFFFFFF;
}
self.decode_data = 0;
self.decode_count_bit = 0;
self.header_count = 0;
return Some(DecodedSignal {
serial: Some(serial),
button: Some(btn),
counter: Some(cnt),
crc_valid: true,
data: raw_data,
data_count_bit: MIN_COUNT_BIT,
encoder_capable: true,
extra: key_for_encode,
protocol_display_name: Some(format!("KeeLoq ({})", _mf_name)),
});
}
}
self.decode_data = 0;
self.decode_count_bit = 0;
self.header_count = 0;
return None;
}
// Bit 1: te_last short, duration long
if duration_diff(self.te_last, TE_SHORT) < TE_DELTA
&& duration_diff(duration_us, TE_LONG) < TE_DELTA * 2
{
if self.decode_count_bit < MIN_COUNT_BIT {
self.add_bit(1);
} else {
self.decode_count_bit += 1;
}
self.step = Step::SaveDuration;
return None;
}
// Bit 0: te_last long, duration short
if duration_diff(self.te_last, TE_LONG) < TE_DELTA * 2
&& duration_diff(duration_us, TE_SHORT) < TE_DELTA
{
if self.decode_count_bit < MIN_COUNT_BIT {
self.add_bit(0);
} else {
self.decode_count_bit += 1;
}
self.step = Step::SaveDuration;
return None;
}
self.reset_to(Step::Reset);
} else {
self.reset_to(Step::Reset);
}
}
}
None
}
fn supports_encoding(&self) -> bool {
true
}
fn encode(&self, decoded: &DecodedSignal, button: u8) -> Option<Vec<LevelDuration>> {
let serial = decoded.serial?;
let counter = decoded.counter.unwrap_or(0).wrapping_add(1);
let key = decoded.extra;
let fix = ((button as u32) << 28) | (serial & 0x0FFFFFFF);
let plaintext = ((button as u32) << 28)
| ((serial & 0x3FF) << 16)
| (counter as u32);
let hop = if let Some(k) = key {
keeloq_encrypt(plaintext, k)
} else {
let reversed = reverse_key(decoded.data, MIN_COUNT_BIT);
(reversed & 0xFFFFFFFF) as u32
};
let yek = ((fix as u64) << 32) | (hop as u64);
let data = reverse_key(yek, MIN_COUNT_BIT);
let mut signal = Vec::with_capacity(256);
for _ in 0..11 {
signal.push(LevelDuration::new(true, TE_SHORT));
signal.push(LevelDuration::new(false, TE_SHORT));
}
signal.push(LevelDuration::new(true, TE_SHORT));
signal.push(LevelDuration::new(false, TE_SHORT * 10));
for i in (0..MIN_COUNT_BIT).rev() {
if (data >> i) & 1 == 1 {
signal.push(LevelDuration::new(true, TE_SHORT));
signal.push(LevelDuration::new(false, TE_LONG));
} else {
signal.push(LevelDuration::new(true, TE_LONG));
signal.push(LevelDuration::new(false, TE_SHORT));
}
}
signal.push(LevelDuration::new(true, TE_SHORT));
signal.push(LevelDuration::new(false, TE_LONG));
signal.push(LevelDuration::new(true, TE_SHORT));
signal.push(LevelDuration::new(false, TE_SHORT * 40));
Some(signal)
}
}
+3
View File
@@ -105,6 +105,7 @@ fn try_kia_v3_v4_format(
data_count_bit: KIA_V3_V4_BITS,
encoder_capable: true,
extra: None,
protocol_display_name: None,
},
));
}
@@ -148,6 +149,7 @@ fn try_star_line_format(
data_count_bit: STAR_LINE_BITS,
encoder_capable: true,
extra: None,
protocol_display_name: None,
},
));
}
@@ -169,6 +171,7 @@ fn try_star_line_format(
data_count_bit: STAR_LINE_BITS,
encoder_capable: true,
extra: None,
protocol_display_name: None,
},
));
}
+1
View File
@@ -85,6 +85,7 @@ impl KiaV0Decoder {
data_count_bit: MIN_COUNT_BIT,
encoder_capable: true,
extra: None,
protocol_display_name: None,
}
}
}
+1
View File
@@ -134,6 +134,7 @@ impl KiaV1Decoder {
data_count_bit: MIN_COUNT_BIT,
encoder_capable: true,
extra: None,
protocol_display_name: None,
}
}
}
+1
View File
@@ -129,6 +129,7 @@ impl KiaV2Decoder {
data_count_bit: MIN_COUNT_BIT,
encoder_capable: true,
extra: None,
protocol_display_name: None,
}
}
}
+1
View File
@@ -155,6 +155,7 @@ impl KiaV3V4Decoder {
data_count_bit: MIN_COUNT_BIT,
encoder_capable: true,
extra: None,
protocol_display_name: None,
})
}
}
+1
View File
@@ -199,6 +199,7 @@ impl KiaV5Decoder {
data_count_bit: MIN_COUNT_BIT,
encoder_capable: false, // V5 is decode-only
extra: None,
protocol_display_name: None,
})
}
}
+1
View File
@@ -539,6 +539,7 @@ impl ProtocolDecoder for KiaV6Decoder {
data_count_bit: MIN_COUNT_BIT,
encoder_capable: false,
extra: None,
protocol_display_name: None,
});
}
+18 -3
View File
@@ -11,6 +11,7 @@
mod common;
pub mod keeloq_common;
mod keeloq;
mod keeloq_generic;
#[allow(dead_code)]
pub mod aut64;
@@ -102,6 +103,7 @@ impl ProtocolRegistry {
Box::new(suzuki::SuzukiDecoder::new()),
Box::new(scher_khan::ScherKhanDecoder::new()),
Box::new(star_line::StarLineDecoder::new()),
Box::new(keeloq::KeeloqDecoder::new()),
Box::new(psa::PsaDecoder::new()),
];
@@ -171,7 +173,11 @@ impl ProtocolRegistry {
continue;
}
if let Some(decoded) = decoder.feed(level, duration_us) {
hit = Some((decoder.name().to_string(), decoded));
let name = decoded
.protocol_display_name
.as_deref()
.unwrap_or_else(|| decoder.name());
hit = Some((name.to_string(), decoded));
break;
}
}
@@ -220,7 +226,11 @@ impl ProtocolRegistry {
}
if let Some(decoded) = decoder.feed(level, duration_us) {
return Some((decoder.name().to_string(), decoded));
let name = decoded
.protocol_display_name
.as_deref()
.unwrap_or_else(|| decoder.name());
return Some((name.to_string(), decoded));
}
}
}
@@ -246,9 +256,14 @@ impl ProtocolRegistry {
/// Get a decoder by name
pub fn get(&self, name: &str) -> Option<&dyn ProtocolDecoder> {
let lookup = if name.starts_with("KeeLoq") {
"KeeLoq"
} else {
name
};
self.decoders
.iter()
.find(|d| d.name().eq_ignore_ascii_case(name))
.find(|d| d.name().eq_ignore_ascii_case(lookup))
.map(|d| d.as_ref())
}
+2
View File
@@ -263,6 +263,7 @@ impl PsaDecoder {
data_count_bit: MIN_COUNT_BIT,
encoder_capable: true,
extra: None,
protocol_display_name: None,
}
} else {
DecodedSignal {
@@ -274,6 +275,7 @@ impl PsaDecoder {
data_count_bit: MIN_COUNT_BIT,
encoder_capable: false,
extra: None,
protocol_display_name: None,
}
}
}
+1
View File
@@ -71,6 +71,7 @@ impl ScherKhanDecoder {
data_count_bit: bit_count,
encoder_capable: false,
extra: None,
protocol_display_name: None,
}
}
}
+1
View File
@@ -102,6 +102,7 @@ impl StarLineDecoder {
data_count_bit: MIN_COUNT_BIT,
encoder_capable: true,
extra: None,
protocol_display_name: None,
}
}
}
+1
View File
@@ -151,6 +151,7 @@ impl SubaruDecoder {
data_count_bit: 64,
encoder_capable: true,
extra: None,
protocol_display_name: None,
})
}
}
+1
View File
@@ -72,6 +72,7 @@ impl SuzukiDecoder {
data_count_bit: MIN_COUNT_BIT,
encoder_capable: true,
extra: None,
protocol_display_name: None,
}
}
}
+1
View File
@@ -795,6 +795,7 @@ impl VagDecoder {
data_count_bit: self.data_count_bit,
encoder_capable: self.decrypted,
extra,
protocol_display_name: None,
}
}
}
+3 -3
View File
@@ -44,7 +44,7 @@ pub fn render_captures_list(frame: &mut Frame, area: Rect, app: &App) {
/// Render the compact signal table
fn render_table(frame: &mut Frame, area: Rect, app: &App) {
let header_cells = [
"ID", "Time", "Protocol", "Freq", "Serial", "Btn", "Cnt", "Mod", "CRC", "Status",
"ID", "Time", "Protocol", "Freq", "Serial", "Btn", "Cnt", "Modulation", "CRC", "Status",
]
.iter()
.map(|h| Cell::from(*h).style(Style::default().add_modifier(Modifier::BOLD)));
@@ -99,12 +99,12 @@ fn render_table(frame: &mut Frame, area: Rect, app: &App) {
let widths = [
Constraint::Length(4), // ID
Constraint::Length(9), // Time
Constraint::Length(10), // Protocol
Constraint::Length(24), // Protocol (e.g. KeeLoq (DoorHan))
Constraint::Length(11), // Freq
Constraint::Length(9), // Serial
Constraint::Length(6), // Btn
Constraint::Length(6), // Cnt
Constraint::Length(7), // Mod
Constraint::Length(12), // Modulation
Constraint::Length(5), // CRC
Constraint::Length(10), // Status
];