diff --git a/src/app.rs b/src/app.rs index bef4a43..64dae7e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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 diff --git a/src/protocols/common.rs b/src/protocols/common.rs index 6bb5765..959386d 100644 --- a/src/protocols/common.rs +++ b/src/protocols/common.rs @@ -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, + /// Optional protocol display name (e.g. "KeeLoq (DoorHan)"). When set, used as the protocol name for this decode. + pub protocol_display_name: Option, } impl DecodedSignal { @@ -46,6 +48,7 @@ impl DecodedSignal { data_count_bit: bit_count, encoder_capable: false, extra: None, + protocol_display_name: None, } } } diff --git a/src/protocols/fiat_v0.rs b/src/protocols/fiat_v0.rs index d7ac8d5..53a3c69 100644 --- a/src/protocols/fiat_v0.rs +++ b/src/protocols/fiat_v0.rs @@ -117,6 +117,7 @@ impl FiatV0Decoder { data_count_bit: 71, encoder_capable: true, extra: None, + protocol_display_name: None, } } } diff --git a/src/protocols/ford_v0.rs b/src/protocols/ford_v0.rs index 7798835..45900be 100644 --- a/src/protocols/ford_v0.rs +++ b/src/protocols/ford_v0.rs @@ -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; diff --git a/src/protocols/keeloq.rs b/src/protocols/keeloq.rs new file mode 100644 index 0000000..10e62d1 --- /dev/null +++ b/src/protocols/keeloq.rs @@ -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)> { + 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 { + 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> { + 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) + } +} diff --git a/src/protocols/keeloq_generic.rs b/src/protocols/keeloq_generic.rs index 3e33224..2af03d8 100644 --- a/src/protocols/keeloq_generic.rs +++ b/src/protocols/keeloq_generic.rs @@ -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, }, )); } diff --git a/src/protocols/kia_v0.rs b/src/protocols/kia_v0.rs index 01d2726..6bb4dc8 100644 --- a/src/protocols/kia_v0.rs +++ b/src/protocols/kia_v0.rs @@ -85,6 +85,7 @@ impl KiaV0Decoder { data_count_bit: MIN_COUNT_BIT, encoder_capable: true, extra: None, + protocol_display_name: None, } } } diff --git a/src/protocols/kia_v1.rs b/src/protocols/kia_v1.rs index 754a0e8..7300d0c 100644 --- a/src/protocols/kia_v1.rs +++ b/src/protocols/kia_v1.rs @@ -134,6 +134,7 @@ impl KiaV1Decoder { data_count_bit: MIN_COUNT_BIT, encoder_capable: true, extra: None, + protocol_display_name: None, } } } diff --git a/src/protocols/kia_v2.rs b/src/protocols/kia_v2.rs index a036263..6339025 100644 --- a/src/protocols/kia_v2.rs +++ b/src/protocols/kia_v2.rs @@ -129,6 +129,7 @@ impl KiaV2Decoder { data_count_bit: MIN_COUNT_BIT, encoder_capable: true, extra: None, + protocol_display_name: None, } } } diff --git a/src/protocols/kia_v3_v4.rs b/src/protocols/kia_v3_v4.rs index 433601d..c0e8313 100644 --- a/src/protocols/kia_v3_v4.rs +++ b/src/protocols/kia_v3_v4.rs @@ -155,6 +155,7 @@ impl KiaV3V4Decoder { data_count_bit: MIN_COUNT_BIT, encoder_capable: true, extra: None, + protocol_display_name: None, }) } } diff --git a/src/protocols/kia_v5.rs b/src/protocols/kia_v5.rs index cc481bb..207893b 100644 --- a/src/protocols/kia_v5.rs +++ b/src/protocols/kia_v5.rs @@ -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, }) } } diff --git a/src/protocols/kia_v6.rs b/src/protocols/kia_v6.rs index 3411fff..10bf416 100644 --- a/src/protocols/kia_v6.rs +++ b/src/protocols/kia_v6.rs @@ -539,6 +539,7 @@ impl ProtocolDecoder for KiaV6Decoder { data_count_bit: MIN_COUNT_BIT, encoder_capable: false, extra: None, + protocol_display_name: None, }); } diff --git a/src/protocols/mod.rs b/src/protocols/mod.rs index 8e32b12..0a7b7a4 100644 --- a/src/protocols/mod.rs +++ b/src/protocols/mod.rs @@ -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()) } diff --git a/src/protocols/psa.rs b/src/protocols/psa.rs index 57cd77b..483d8d1 100644 --- a/src/protocols/psa.rs +++ b/src/protocols/psa.rs @@ -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, } } } diff --git a/src/protocols/scher_khan.rs b/src/protocols/scher_khan.rs index 849733a..482218b 100644 --- a/src/protocols/scher_khan.rs +++ b/src/protocols/scher_khan.rs @@ -71,6 +71,7 @@ impl ScherKhanDecoder { data_count_bit: bit_count, encoder_capable: false, extra: None, + protocol_display_name: None, } } } diff --git a/src/protocols/star_line.rs b/src/protocols/star_line.rs index 138f13a..7f89fc6 100644 --- a/src/protocols/star_line.rs +++ b/src/protocols/star_line.rs @@ -102,6 +102,7 @@ impl StarLineDecoder { data_count_bit: MIN_COUNT_BIT, encoder_capable: true, extra: None, + protocol_display_name: None, } } } diff --git a/src/protocols/subaru.rs b/src/protocols/subaru.rs index 55d0a57..f702e21 100644 --- a/src/protocols/subaru.rs +++ b/src/protocols/subaru.rs @@ -151,6 +151,7 @@ impl SubaruDecoder { data_count_bit: 64, encoder_capable: true, extra: None, + protocol_display_name: None, }) } } diff --git a/src/protocols/suzuki.rs b/src/protocols/suzuki.rs index f213c2c..34023ce 100644 --- a/src/protocols/suzuki.rs +++ b/src/protocols/suzuki.rs @@ -72,6 +72,7 @@ impl SuzukiDecoder { data_count_bit: MIN_COUNT_BIT, encoder_capable: true, extra: None, + protocol_display_name: None, } } } diff --git a/src/protocols/vag.rs b/src/protocols/vag.rs index 64fac6e..79da54d 100644 --- a/src/protocols/vag.rs +++ b/src/protocols/vag.rs @@ -795,6 +795,7 @@ impl VagDecoder { data_count_bit: self.data_count_bit, encoder_capable: self.decrypted, extra, + protocol_display_name: None, } } } diff --git a/src/ui/captures_list.rs b/src/ui/captures_list.rs index de602cc..45d3bf8 100644 --- a/src/ui/captures_list.rs +++ b/src/ui/captures_list.rs @@ -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 ];