minor updates
This commit is contained in:
+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,39 +20,32 @@ 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 {
|
||||
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));
|
||||
}
|
||||
// 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) {
|
||||
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));
|
||||
}
|
||||
// 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) {
|
||||
return Some((format!("Keeloq ({})", name), decoded));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,27 +85,29 @@ fn try_kia_v3_v4_format(
|
||||
| (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,
|
||||
},
|
||||
));
|
||||
for key in [*mf_key, mf_key.swap_bytes()] {
|
||||
if key == 0 {
|
||||
continue;
|
||||
}
|
||||
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 {
|
||||
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
|
||||
@@ -131,49 +127,51 @@ fn try_star_line_format(
|
||||
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,
|
||||
},
|
||||
));
|
||||
for key in [*mf_key, mf_key.swap_bytes()] {
|
||||
if key == 0 {
|
||||
continue;
|
||||
}
|
||||
// Simple learning
|
||||
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 {
|
||||
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, 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
|
||||
|
||||
@@ -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