keystore fix + looser vw timings.

This commit is contained in:
KaraZajac
2026-02-17 23:41:35 -05:00
parent 9dd5758973
commit 08baed9677
4 changed files with 24 additions and 238 deletions
+1 -1
View File
@@ -39,4 +39,4 @@ Supported; 3 bursts, 10 s inter-burst gap; preamble, sync, encrypted payload + C
## Keystore ## Keystore
Requires KIA manufacturer key (keystore type 10, `kia_mf_key`). Loaded from embedded blob or keystore.ini. Requires KIA manufacturer key (keystore type 10, `kia_mf_key`). Loaded from the embedded keystore (built from `REFERENCES/mf_keys.txt`).
+8 -203
View File
@@ -1,9 +1,11 @@
//! Key management module for protocol encryption/decryption //! Key management module for protocol encryption/decryption
//! //!
//! All keys are loaded from the **embedded keystore only** (no keystore.ini or file-based keys).
//! The blob is built from the master list `REFERENCES/mf_keys.txt` by
//! `scripts/build_keystore_from_mf_keys.py` and embedded in `crate::keystore::embedded`.
//!
//! Aligned with ProtoPirate's keys.c (KIA_KEY1..4, get_kia_mf_key, etc.). //! Aligned with ProtoPirate's keys.c (KIA_KEY1..4, get_kia_mf_key, etc.).
//! Keys are loaded from the embedded keystore blob in `crate::keystore` at startup //! At startup `load_keystore_from_embedded()` parses `keystore::embedded::KEYSTORE_BLOB` and populates:
//! via `load_keystore_from_embedded()`, which parses the blob from
//! `keystore::embedded::KEYSTORE_BLOB` and populates:
//! //!
//! - **KIA**: type 10 → kia_mf_key, 11 → kia_v6_a_key, 12 → kia_v6_b_key, 13 → kia_v5_key //! - **KIA**: type 10 → kia_mf_key, 11 → kia_v6_a_key, 12 → kia_v6_b_key, 13 → kia_v5_key
//! - **Star Line**: type 20 → star_line_mf_key //! - **Star Line**: type 20 → star_line_mf_key
@@ -13,10 +15,8 @@
//! KIA V5 uses `get_kia_v5_key()`, KIA V6 uses `get_kia_v6_keystore_a()` / `get_kia_v6_keystore_b()`. //! KIA V5 uses `get_kia_v5_key()`, KIA V6 uses `get_kia_v6_keystore_a()` / `get_kia_v6_keystore_b()`.
use super::aut64::{self, Aut64Key, AUT64_KEY_STRUCT_PACKED_SIZE}; use super::aut64::{self, Aut64Key, AUT64_KEY_STRUCT_PACKED_SIZE};
use configparser::ini::Ini;
use std::path::Path;
use std::sync::{OnceLock, RwLock}; use std::sync::{OnceLock, RwLock};
use tracing::{info, warn, error}; use tracing::{error, info};
/// Key type identifiers; must match type IDs in `keystore::embedded::KEYSTORE_BLOB`. /// Key type identifiers; must match type IDs in `keystore::embedded::KEYSTORE_BLOB`.
const KIA_KEY1: u32 = 10; // kia_mf_key (KIA V3/V4) const KIA_KEY1: u32 = 10; // kia_mf_key (KIA V3/V4)
@@ -81,7 +81,8 @@ impl KeyStore {
} }
} }
/// Load VAG AUT64 keys from raw binary data (16 bytes per key; up to MAX_VAG_KEYS) /// Load VAG AUT64 keys from raw binary data (16 bytes per key; up to MAX_VAG_KEYS).
/// Used only by the embedded keystore parser.
pub fn load_vag_keys_from_data(&mut self, data: &[u8]) { pub fn load_vag_keys_from_data(&mut self, data: &[u8]) {
if self.vag_keys_loaded { if self.vag_keys_loaded {
return; return;
@@ -103,28 +104,6 @@ impl KeyStore {
info!("Loaded {} VAG keys", self.vag_keys.len()); info!("Loaded {} VAG keys", self.vag_keys.len());
} }
/// Load VAG AUT64 keys from a file path
pub fn load_vag_keys_from_file(&mut self, path: &str) {
if self.vag_keys_loaded {
return;
}
let file_path = Path::new(path);
if !file_path.exists() {
warn!("VAG key file not found: {}", path);
return;
}
match std::fs::read(file_path) {
Ok(data) => {
self.load_vag_keys_from_data(&data);
}
Err(e) => {
error!("Failed to read VAG key file {}: {}", path, e);
}
}
}
/// Get a VAG AUT64 key by its internal index field /// Get a VAG AUT64 key by its internal index field
pub fn get_vag_key(&self, index: u8) -> Option<&Aut64Key> { pub fn get_vag_key(&self, index: u8) -> Option<&Aut64Key> {
self.vag_keys.iter().find(|k| k.index == index) self.vag_keys.iter().find(|k| k.index == index)
@@ -183,12 +162,6 @@ pub fn load_keys(kia_entries: &[(u32, u64)]) {
store.load_kia_keys(kia_entries); store.load_kia_keys(kia_entries);
} }
/// Initialize VAG keys from file
pub fn load_vag_keys(path: &str) {
let mut store = get_keystore_mut();
store.load_vag_keys_from_file(path);
}
/// Load the global keystore from the embedded blob (src/keystore/embedded.rs). /// Load the global keystore from the embedded blob (src/keystore/embedded.rs).
/// Populates KIA (V3/V4, V5, V6A, V6B), Star Line, and VAG AUT64 keys from the blob. /// Populates KIA (V3/V4, V5, V6A, V6B), Star Line, and VAG AUT64 keys from the blob.
/// VAG raw bytes are 64 bytes = 4 × 16-byte packed keys; each key's `index` is byte 0 (used by VAG lookup). /// VAG raw bytes are 64 bytes = 4 × 16-byte packed keys; each key's `index` is byte 0 (used by VAG lookup).
@@ -209,171 +182,3 @@ pub fn load_keystore_from_embedded() {
store.vag_keys.len() store.vag_keys.len()
); );
} }
// =============================================================================
// Keystore file loading from ~/.config/KAT/keystore/
// =============================================================================
/// Parse a hex string (with or without "0x" prefix) into a u64.
fn parse_hex_u64(s: &str) -> Option<u64> {
let s = s.trim();
let s = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")).unwrap_or(s);
u64::from_str_radix(s, 16).ok()
}
/// Default keystore.ini template content.
/// Written to disk on first run so users know which keys to configure.
const DEFAULT_KEYSTORE_INI: &str = r#"; KAT Keystore — Protocol Encryption Keys
;
; Place your protocol keys here. Key values should be in hexadecimal
; with a 0x prefix (e.g. 0x0123456789ABCDEF).
;
; Keys left at 0x0000000000000000 or omitted are treated as "not loaded"
; and the corresponding protocol will decode without decryption
; (serial/button still visible, but counter may not validate).
;
; This file corresponds to protopirate's keystore/encrypted and keystore/vag
; assets. Since KAT runs on a PC (not a Flipper), keys must be provided
; in plaintext here rather than in the Flipper's encrypted keystore format.
[kia]
; KIA V3/V4: KeeLoq manufacturer key (used for hop-code decryption)
mf_key = 0x0000000000000000
; KIA V5: Custom mixer cipher key
v5_key = 0x0000000000000000
; KIA V6: AES-128 key components (XOR-masked before use)
v6_a_key = 0x0000000000000000
v6_b_key = 0x0000000000000000
[star_line]
; Star Line: KeeLoq manufacturer key (used for hop-code decryption)
mf_key = 0x0000000000000000
[vag]
; VAG: Path to AUT64 binary key file (raw packed keys, 16 bytes each).
; Can be absolute or relative to the keystore directory.
; Leave empty or commented out if you don't have VAG keys.
; keys_file = vag.bin
"#;
/// Write the default keystore.ini template if it doesn't exist yet.
pub fn create_default_keystore(keystore_dir: &Path) {
let ini_path = keystore_dir.join("keystore.ini");
if ini_path.exists() {
return;
}
match std::fs::write(&ini_path, DEFAULT_KEYSTORE_INI) {
Ok(_) => info!("Created default keystore template at {:?}", ini_path),
Err(e) => warn!("Could not create default keystore.ini: {}", e),
}
}
/// Load all keys from the keystore directory.
///
/// Reads `keystore.ini` from the given directory and populates the global
/// [`KeyStore`] with:
///
/// - KIA V3/V4 manufacturer key (`[kia] mf_key`)
/// - KIA V5 mixer key (`[kia] v5_key`)
/// - KIA V6 AES keys (`[kia] v6_a_key`, `[kia] v6_b_key`)
/// - Star Line manufacturer key (`[star_line] mf_key`)
/// - VAG AUT64 keys from binary file (`[vag] keys_file`)
///
/// Keys that are missing, zeroed, or unparseable are silently skipped.
pub fn load_keystore_from_dir(keystore_dir: &Path) {
let ini_path = keystore_dir.join("keystore.ini");
if !ini_path.exists() {
info!("No keystore.ini found at {:?} — keys not loaded", ini_path);
return;
}
let mut ini = Ini::new();
if let Err(e) = ini.load(ini_path.to_string_lossy().as_ref()) {
error!("Failed to parse keystore.ini: {}", e);
return;
}
let mut store = get_keystore_mut();
let mut loaded_count = 0u32;
// ── KIA keys ──────────────────────────────────────────────────────────
if let Some(val) = ini.get("kia", "mf_key").and_then(|s| parse_hex_u64(&s)) {
if val != 0 {
store.kia_mf_key = val;
loaded_count += 1;
info!("Loaded KIA V3/V4 manufacturer key");
}
}
if let Some(val) = ini.get("kia", "v5_key").and_then(|s| parse_hex_u64(&s)) {
if val != 0 {
store.kia_v5_key = val;
loaded_count += 1;
info!("Loaded KIA V5 mixer key");
}
}
if let Some(val) = ini.get("kia", "v6_a_key").and_then(|s| parse_hex_u64(&s)) {
if val != 0 {
store.kia_v6_a_key = val;
loaded_count += 1;
info!("Loaded KIA V6 AES key A");
}
}
if let Some(val) = ini.get("kia", "v6_b_key").and_then(|s| parse_hex_u64(&s)) {
if val != 0 {
store.kia_v6_b_key = val;
loaded_count += 1;
info!("Loaded KIA V6 AES key B");
}
}
// ── Star Line keys ────────────────────────────────────────────────────
if let Some(val) = ini.get("star_line", "mf_key").and_then(|s| parse_hex_u64(&s)) {
if val != 0 {
store.star_line_mf_key = val;
loaded_count += 1;
info!("Loaded Star Line manufacturer key");
}
}
// ── VAG AUT64 keys (binary file) ──────────────────────────────────────
if let Some(vag_file) = ini.get("vag", "keys_file") {
let vag_file = vag_file.trim().to_string();
if !vag_file.is_empty() {
let vag_path = if Path::new(&vag_file).is_absolute() {
std::path::PathBuf::from(&vag_file)
} else {
keystore_dir.join(&vag_file)
};
if vag_path.exists() {
match std::fs::read(&vag_path) {
Ok(data) => {
store.load_vag_keys_from_data(&data);
if store.vag_keys_loaded {
loaded_count += store.vag_keys.len() as u32;
info!("Loaded {} VAG AUT64 keys from {:?}", store.vag_keys.len(), vag_path);
}
}
Err(e) => {
error!("Failed to read VAG key file {:?}: {}", vag_path, e);
}
}
} else {
warn!("VAG key file not found: {:?}", vag_path);
}
}
}
if loaded_count > 0 {
info!("Keystore loaded: {} key(s) from {:?}", loaded_count, keystore_dir);
} else {
info!("Keystore loaded but no non-zero keys found. Edit keystore.ini to add your keys.");
}
}
+14 -15
View File
@@ -5,10 +5,9 @@
//! parse (vag_parse_data, vag_aut64_decrypt, vag_tea_decrypt), dispatch (0x2A/0x1C/0x46 and //! parse (vag_parse_data, vag_aut64_decrypt, vag_tea_decrypt), dispatch (0x2A/0x1C/0x46 and
//! 0x2B/0x1D/0x47), and encoder (vag_encoder_build_type1/2/3_4) match the reference. //! 0x2B/0x1D/0x47), and encoder (vag_encoder_build_type1/2/3_4) match the reference.
//! //!
//! **Timing**: Reference uses VAG_TOL_300 (79) and VAG_TOL_500 (120). Reset/Preamble1 use 300±79/80; //! **Timing**: Slightly looser than reference for real-world decode rate. Reference uses 79/80/120;
//! Preamble1→Data1 gap 600µs ±79; Data1 short 300±79/80, long 600±79/80; end-of-data gap 6000µs //! we use 90 for reset/preamble/sync and 130 for Data2. Reset/Preamble1 300±90; gap 600±90;
//! (accept diff < 4000). Preamble2 count 500±80; Sync2A 500/1000µs ±79; Sync2B 750µs ±79; //! Data1 short 300±90, long 600±90; Preamble2 500±90; Sync2 500/1000/750±90; Data2 500±130, 1000±130.
//! Sync2C 750µs ±79; Data2 short 500±120 (380620µs), long 1000±120 (8801120µs).
//! //!
//! **Protocol**: Manchester, 80 bits (key1 64 + key2 16). Type 1/2: 300/600µs, prefix 0xAF3F/0xAF1C. //! **Protocol**: Manchester, 80 bits (key1 64 + key2 16). Type 1/2: 300/600µs, prefix 0xAF3F/0xAF1C.
//! Type 3/4: 500µs, 45 preamble pairs, sync 1000+500 then 3×750µs; key1/key2 not inverted. //! Type 3/4: 500µs, 45 preamble pairs, sync 1000+500 then 3×750µs; key1/key2 not inverted.
@@ -23,21 +22,21 @@ use crate::radio::demodulator::LevelDuration;
const TE_SHORT: u32 = 500; const TE_SHORT: u32 = 500;
const TE_LONG: u32 = 1000; const TE_LONG: u32 = 1000;
#[allow(dead_code)] #[allow(dead_code)]
const TE_DELTA: u32 = 80; // ref vag.c (Type 3/4); exposed via timing() const TE_DELTA: u32 = 100; // Type 3/4 tolerance; exposed via timing()
#[allow(dead_code)] #[allow(dead_code)]
const MIN_COUNT_BIT: usize = 80; const MIN_COUNT_BIT: usize = 80;
// Type 1/2 timing // Type 1/2 timing
const TE_SHORT_12: u32 = 300; const TE_SHORT_12: u32 = 300;
const TE_LONG_12: u32 = 600; const TE_LONG_12: u32 = 600;
const TE_DELTA_12: u32 = 80; // Preamble1/Data1 (ref vag.c 79/80) const TE_DELTA_12: u32 = 100; // Preamble1/Data1 (looser for real-world captures)
// Reference-aligned deltas (vag.c VAG_NEAR / VAG_TOL_300 79, VAG_TOL_500 120) // Looser than reference for better decode rate on real hardware
const REF_RESET_DELTA: u32 = 79; // Reset: 300±79, 500±79 for Preamble2 const REF_RESET_DELTA: u32 = 100; // Reset: 300±100, 500±100 for Preamble2
const REF_PREAMBLE_SYNC: u32 = 80; // Preamble2 counting: 500±80 const REF_PREAMBLE_SYNC: u32 = 100; // Preamble2 counting: 500±100
const REF_SYNC2_AB_DELTA: u32 = 79; // Sync2A/Sync2B: 500/1000/750±79 (ref VAG_NEAR(..., 79)) const REF_SYNC2_AB_DELTA: u32 = 100; // Sync2A/Sync2B: 500/1000/750±100
const REF_SYNC2C_DELTA: u32 = 79; // Sync2C: 750±79 const REF_SYNC2C_DELTA: u32 = 100; // Sync2C: 750±100
const REF_GAP1_DELTA: u32 = 79; // Preamble1→Data1 gap 600µs ±79 (ref check_gap1) const REF_GAP1_DELTA: u32 = 100; // Preamble1→Data1 gap 600µs ±100
// TEA constants // TEA constants
const TEA_DELTA: u32 = 0x9E3779B9; const TEA_DELTA: u32 = 0x9E3779B9;
@@ -1143,10 +1142,10 @@ impl ProtocolDecoder for VagDecoder {
} }
DecoderStep::Data2 => { DecoderStep::Data2 => {
// Matches vag.c: short 380-620µs, long 880-1120µs // Data2: short 500±140µs, long 1000±140µs
let event = if duration >= 380 && duration <= 620 { let event = if duration >= 360 && duration <= 640 {
Some(if level { ManchesterEvent::ShortLow } else { ManchesterEvent::ShortHigh }) Some(if level { ManchesterEvent::ShortLow } else { ManchesterEvent::ShortHigh })
} else if duration >= 880 && duration <= 1120 { } else if duration >= 860 && duration <= 1140 {
Some(if level { ManchesterEvent::LongLow } else { ManchesterEvent::LongHigh }) Some(if level { ManchesterEvent::LongLow } else { ManchesterEvent::LongHigh })
} else { } else {
None None
+1 -19
View File
@@ -7,9 +7,6 @@
//! config.ini — User configuration //! config.ini — User configuration
//! exports/ — Exported .fob / .sub files (save location) //! exports/ — Exported .fob / .sub files (save location)
//! import/ — Scanned at startup for .fob / .sub to import //! import/ — Scanned at startup for .fob / .sub to import
//! keystore/ — Protocol encryption keys
//! keystore.ini — Key definitions (hex values)
//! vag.bin — VAG AUT64 binary key file (optional)
//! ``` //! ```
//! //!
//! Captures are **in-memory only** and are discarded when KAT exits. //! Captures are **in-memory only** and are discarded when KAT exits.
@@ -345,20 +342,10 @@ impl Storage {
); );
} }
// ── 6. Ensure keystore directory exists ──────────────────────── // ── 6. Log resolved paths ───────────────────────────────────────
let keystore_dir = config_dir.join("keystore");
if !keystore_dir.exists() {
fs::create_dir_all(&keystore_dir).with_context(|| {
format!("Failed to create keystore dir: {:?}", keystore_dir)
})?;
tracing::info!("Created keystore directory: {:?}", keystore_dir);
}
// ── 7. Log resolved paths ───────────────────────────────────────
tracing::info!("Config dir: {:?}", config_dir); tracing::info!("Config dir: {:?}", config_dir);
tracing::info!("Export dir: {:?}", config.export_directory); tracing::info!("Export dir: {:?}", config.export_directory);
tracing::info!("Import dir: {:?}", config.import_directory); tracing::info!("Import dir: {:?}", config.import_directory);
tracing::info!("Keystore dir: {:?}", keystore_dir);
Ok(Self { Ok(Self {
config_dir, config_dir,
@@ -393,9 +380,4 @@ impl Storage {
&self.config.import_directory &self.config.import_directory
} }
/// Get the keystore directory path (`~/.config/KAT/keystore`). Kept for optional file-based override.
#[allow(dead_code)]
pub fn keystore_dir(&self) -> PathBuf {
self.config_dir.join("keystore")
}
} }