minor updates
This commit is contained in:
Generated
+1
-1
@@ -402,7 +402,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kat"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"atty",
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
|
||||
## Overview
|
||||
|
||||
When no known protocol decodes a captured signal, KAT tries to decode it as **KeeLoq** using every manufacturer key in the embedded keystore. All decryption is done via the **keeloq_common** helper (no protocol-specific decrypt code). Two air formats are supported:
|
||||
When no known protocol decodes a captured signal, KAT tries to decode it as **KeeLoq** using **every** manufacturer key in the embedded keystore, **regardless of frequency**. All decryption is done via the **keeloq_common** helper (no protocol-specific decrypt code). Two air formats are tried (in order):
|
||||
|
||||
1. **Kia V3/V4 format** — 68 bits, 400/800 µs PWM, 315 or 433.92 MHz
|
||||
2. **Star Line format** — 64 bits, 250/500 µs PWM, 433.92 MHz
|
||||
1. **Kia V3/V4 format** — 68 bits, 400/800 µs PWM
|
||||
2. **Star Line format** — 64 bits, 250/500 µs PWM
|
||||
|
||||
Bit collection reuses the existing Kia V3/V4 and Star Line state machines; decryption and validation use only **keeloq_common** (`keeloq_decrypt`, `keeloq_normal_learning`, `reverse_key`, `reverse8`).
|
||||
Bit collection reuses the existing Kia V3/V4 and Star Line state machines; decryption and validation use only **keeloq_common** (`keeloq_decrypt`, `keeloq_normal_learning`, `reverse_key`, `reverse8`). Each key is tried in **both byte orders** (as stored in the keystore, and byte-swapped) so that either big-endian or little-endian key sources can match.
|
||||
|
||||
## When it runs
|
||||
|
||||
@@ -26,7 +26,7 @@ On first successful decrypt with a given key, the capture is shown in the list w
|
||||
|
||||
## Keystore
|
||||
|
||||
Uses **all** KeeLoq manufacturer keys from the embedded blob (types 0, 1, 2, 10, 20). Key names come from `keystore::KEY_ENTRY_NAMES` and are used only for the displayed protocol label.
|
||||
Uses **all** KeeLoq manufacturer keys from the embedded blob (types 0, 1, 2, 10, 20). Key names come from `keystore::KEY_ENTRY_NAMES` and are used only for the displayed protocol label. Keys are stored in the blob as 8 bytes little-endian; the resulting u64 matches reference/Pandora hex (MSB-first notation).
|
||||
|
||||
## Encoding
|
||||
|
||||
|
||||
+11
-2
@@ -21,6 +21,8 @@ pub enum InputMode {
|
||||
SettingsSelect,
|
||||
/// Editing a radio setting value
|
||||
SettingsEdit,
|
||||
/// Startup warning: HackRF not detected (dismiss to continue)
|
||||
HackRfNotDetected,
|
||||
/// Startup prompt: found .fob files, import? (y/n)
|
||||
StartupImport,
|
||||
/// Export: editing filename (before format-specific steps)
|
||||
@@ -279,7 +281,12 @@ impl App {
|
||||
}
|
||||
};
|
||||
|
||||
let radio_state = if hackrf.is_some() {
|
||||
// HackRF controller always returns Ok; when no device is present it has is_available() == false
|
||||
let hackrf_detected = hackrf
|
||||
.as_ref()
|
||||
.map_or(false, |h| h.is_available());
|
||||
|
||||
let radio_state = if hackrf_detected {
|
||||
RadioState::Idle
|
||||
} else {
|
||||
RadioState::Disconnected
|
||||
@@ -299,7 +306,9 @@ impl App {
|
||||
// Recursively scan import directory for .fob and .sub at startup (separate from export dir)
|
||||
let pending_fob_files =
|
||||
crate::export::scan_import_files_recursive(storage.import_dir());
|
||||
let initial_mode = if !pending_fob_files.is_empty() {
|
||||
let initial_mode = if !hackrf_detected {
|
||||
InputMode::HackRfNotDetected
|
||||
} else if !pending_fob_files.is_empty() {
|
||||
tracing::info!(
|
||||
"Found {} importable file(s) in import dir (recursive)",
|
||||
pending_fob_files.len()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Embedded keystore blob (standard encrypted + VAG raw).
|
||||
//! Data is stored as binary to avoid plain-text keys in config.
|
||||
//! Format: "KATK" magic, n_entries (u16 LE), then per entry: type_id (u32 LE), key (u64 LE), then "VAG " + 64 bytes.
|
||||
//! Key bytes are little-endian (LSB first); the resulting u64 matches reference hex (MSB-first notation).
|
||||
//! All manufacture keys from the encrypted keystore list are included for current and future protocols.
|
||||
//! Type 20 (Star Line) is also included so KeyStore.star_line_mf_key is populated from the same key as SL_A2-A4.
|
||||
|
||||
|
||||
@@ -33,6 +33,10 @@ pub struct ParsedKeystore {
|
||||
}
|
||||
|
||||
/// Parse the embedded keystore blob. Returns KIA/Star Line entries and VAG raw bytes.
|
||||
///
|
||||
/// Keys are stored as 8 bytes **little-endian** per entry; we load with `u64::from_le_bytes`.
|
||||
/// The resulting u64 value matches the reference/Pandora hex (e.g. KIA = 0xA8F5DFFC8DAA5CDB),
|
||||
/// which is typically written MSB-first in docs; KeeLoq uses this value as a 64-bit key.
|
||||
pub fn parse_blob(blob: &[u8]) -> Option<ParsedKeystore> {
|
||||
if blob.len() < 4 || &blob[0..4] != MAGIC {
|
||||
return None;
|
||||
|
||||
@@ -252,6 +252,15 @@ fn run_app<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, app: &mut A
|
||||
_ => {}
|
||||
},
|
||||
|
||||
// Startup: HackRF not detected — any key to dismiss
|
||||
InputMode::HackRfNotDetected => {
|
||||
app.input_mode = if app.pending_fob_files.is_empty() {
|
||||
InputMode::Normal
|
||||
} else {
|
||||
InputMode::StartupImport
|
||||
};
|
||||
}
|
||||
|
||||
// Startup: found .fob files, y/n to import
|
||||
InputMode::StartupImport => match key.code {
|
||||
KeyCode::Char('y') | KeyCode::Char('Y') => {
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
//! - **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.
|
||||
//! (delegated to the format-specific collectors) and tries each key in both byte orders
|
||||
//! (as stored, and byte-swapped) in case the source used big- or little-endian.
|
||||
|
||||
use super::common::DecodedSignal;
|
||||
use super::keeloq_common::{keeloq_decrypt, keeloq_normal_learning, reverse_key, reverse8};
|
||||
@@ -19,23 +20,19 @@ 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).
|
||||
/// Tries both Kia V3/V4 format (68-bit, 400/800µs) and Star Line format (64-bit, 250/500µs)
|
||||
/// regardless of frequency; each key is tried in both byte orders (LE and BE) until one validates.
|
||||
/// Returns `("Keeloq (keystore name)", decoded)` on first successful decrypt.
|
||||
pub fn try_decode(
|
||||
pairs: &[LevelDuration],
|
||||
frequency: u32,
|
||||
_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 {
|
||||
// Kia V3/V4 format: 400/800µs, 68 bits (try both polarities, all keys)
|
||||
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) {
|
||||
@@ -43,10 +40,8 @@ pub fn try_decode(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Star Line format: 250/500µs, 433 MHz
|
||||
if frequency.abs_diff(433_920_000) < (433_920_000 / 50) {
|
||||
// Star Line format: 250/500µs, 64 bits (try both polarities, all keys)
|
||||
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) {
|
||||
@@ -54,7 +49,6 @@ pub fn try_decode(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
@@ -91,10 +85,11 @@ fn try_kia_v3_v4_format(
|
||||
| (b[7] as u64);
|
||||
|
||||
for (name, mf_key) in keys {
|
||||
if *mf_key == 0 {
|
||||
for key in [*mf_key, mf_key.swap_bytes()] {
|
||||
if key == 0 {
|
||||
continue;
|
||||
}
|
||||
let decrypted = keeloq_decrypt(encrypted, *mf_key);
|
||||
let decrypted = keeloq_decrypt(encrypted, 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 {
|
||||
@@ -114,6 +109,7 @@ fn try_kia_v3_v4_format(
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@@ -131,11 +127,12 @@ fn try_star_line_format(
|
||||
let serial_lsb = (serial & 0xFF) as u8;
|
||||
|
||||
for (name, mf_key) in keys {
|
||||
if *mf_key == 0 {
|
||||
for key in [*mf_key, mf_key.swap_bytes()] {
|
||||
if key == 0 {
|
||||
continue;
|
||||
}
|
||||
// Simple learning
|
||||
let decrypt = keeloq_decrypt(key_hop, *mf_key);
|
||||
let decrypt = keeloq_decrypt(key_hop, 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 {
|
||||
@@ -155,7 +152,7 @@ fn try_star_line_format(
|
||||
));
|
||||
}
|
||||
// Normal learning
|
||||
let man_key = keeloq_normal_learning(key_fix, *mf_key);
|
||||
let man_key = keeloq_normal_learning(key_fix, 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;
|
||||
@@ -176,5 +173,6 @@ fn try_star_line_format(
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@@ -38,6 +38,11 @@ pub fn render_command_line(frame: &mut Frame, area: Rect, app: &App) {
|
||||
"EDIT",
|
||||
Style::default().fg(Color::Green),
|
||||
),
|
||||
InputMode::HackRfNotDetected => (
|
||||
String::new(),
|
||||
"WARNING",
|
||||
Style::default().fg(Color::Red),
|
||||
),
|
||||
InputMode::StartupImport => (
|
||||
String::new(),
|
||||
"IMPORT",
|
||||
|
||||
@@ -77,6 +77,10 @@ pub fn draw_ui(frame: &mut Frame, app: &App) {
|
||||
render_settings_dropdown(frame, app);
|
||||
}
|
||||
|
||||
if app.input_mode == InputMode::HackRfNotDetected {
|
||||
render_hackrf_not_detected(frame, app);
|
||||
}
|
||||
|
||||
if app.input_mode == InputMode::StartupImport {
|
||||
render_startup_import_prompt(frame, app);
|
||||
}
|
||||
@@ -160,6 +164,7 @@ fn render_help_bar(frame: &mut Frame, area: Rect, app: &App) {
|
||||
InputMode::SignalMenu => "Up/Down: Navigate | Enter: Select | Esc: Close",
|
||||
InputMode::SettingsSelect => "Left/Right: Select | Tab: Cycle | Enter: Edit | Esc: Back",
|
||||
InputMode::SettingsEdit => "Up/Down: Change Value | Enter: Apply | Esc: Cancel",
|
||||
InputMode::HackRfNotDetected => "Press any key to continue",
|
||||
InputMode::StartupImport => "y: Import | n: Skip",
|
||||
InputMode::ExportFilename => {
|
||||
match app.export_format {
|
||||
@@ -191,6 +196,46 @@ fn centered_rect(width: u16, height: u16, area: Rect) -> Rect {
|
||||
Rect::new(x, y, width.min(area.width), height.min(area.height))
|
||||
}
|
||||
|
||||
/// Render the HackRF not detected warning (red box at startup)
|
||||
fn render_hackrf_not_detected(frame: &mut Frame, _app: &App) {
|
||||
let area = frame.area();
|
||||
let popup = centered_rect(52, 7, area);
|
||||
|
||||
frame.render_widget(Clear, popup);
|
||||
|
||||
let red = Color::Red;
|
||||
let text = vec![
|
||||
Line::from(""),
|
||||
Line::from(Span::styled(
|
||||
" Your HackRF was not detected.",
|
||||
Style::default().fg(red).add_modifier(Modifier::BOLD),
|
||||
)),
|
||||
Line::from(""),
|
||||
Line::from(Span::styled(
|
||||
" Connect a HackRF One and restart, or continue in demo mode.",
|
||||
Style::default().fg(red),
|
||||
)),
|
||||
Line::from(""),
|
||||
Line::from(Span::styled(
|
||||
" Press any key to continue.",
|
||||
Style::default().fg(Color::White),
|
||||
)),
|
||||
];
|
||||
|
||||
let block = Block::default()
|
||||
.title(" Warning ")
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(red))
|
||||
.style(Style::default().fg(red));
|
||||
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(block)
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: true });
|
||||
|
||||
frame.render_widget(paragraph, popup);
|
||||
}
|
||||
|
||||
/// Render the startup import prompt overlay
|
||||
fn render_startup_import_prompt(frame: &mut Frame, app: &App) {
|
||||
let count = app.pending_fob_files.len();
|
||||
|
||||
Reference in New Issue
Block a user