380 lines
13 KiB
Rust
380 lines
13 KiB
Rust
//! Key management module for protocol encryption/decryption
|
||
//!
|
||
//! 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
|
||
//! 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
|
||
//! - **Star Line**: type 20 → star_line_mf_key
|
||
//! - **VAG**: raw 64 bytes after "VAG " tag = 4 × 16-byte AUT64 packed keys (index in byte 0 of each)
|
||
//!
|
||
//! VAG protocol looks up keys by `get_vag_key((key_index + 1) as u8)` (index 1, 2, 3).
|
||
//! 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 configparser::ini::Ini;
|
||
use std::path::Path;
|
||
use std::sync::{OnceLock, RwLock};
|
||
use tracing::{info, warn, error};
|
||
|
||
/// 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_KEY2: u32 = 11; // kia_v6_a_key (KIA V6A)
|
||
const KIA_KEY3: u32 = 12; // kia_v6_b_key (KIA V6B)
|
||
const KIA_KEY4: u32 = 13; // kia_v5_key (KIA V5)
|
||
const STAR_LINE_KEY: u32 = 20; // star_line_mf_key
|
||
|
||
/// Maximum number of VAG AUT64 keys (embedded blob has 64 bytes = 4 keys)
|
||
const MAX_VAG_KEYS: usize = 4;
|
||
|
||
/// Global key store - thread-safe access to loaded keys
|
||
pub struct KeyStore {
|
||
/// KIA manufacturer key (for KeeLoq-based V3/V4)
|
||
pub kia_mf_key: u64,
|
||
/// KIA V6 AES key A
|
||
pub kia_v6_a_key: u64,
|
||
/// KIA V6 AES key B
|
||
pub kia_v6_b_key: u64,
|
||
/// KIA V5 mixer key
|
||
pub kia_v5_key: u64,
|
||
/// Star Line manufacturer key (for KeeLoq)
|
||
pub star_line_mf_key: u64,
|
||
/// VAG AUT64 keys
|
||
pub vag_keys: Vec<Aut64Key>,
|
||
/// Whether VAG keys have been loaded
|
||
pub vag_keys_loaded: bool,
|
||
}
|
||
|
||
impl Default for KeyStore {
|
||
fn default() -> Self {
|
||
Self {
|
||
kia_mf_key: 0,
|
||
kia_v6_a_key: 0,
|
||
kia_v6_b_key: 0,
|
||
kia_v5_key: 0,
|
||
star_line_mf_key: 0,
|
||
vag_keys: Vec::new(),
|
||
vag_keys_loaded: false,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl KeyStore {
|
||
/// Create a new empty key store
|
||
pub fn new() -> Self {
|
||
Self::default()
|
||
}
|
||
|
||
/// Load KIA keys from a key entries list
|
||
/// Each entry is (type_id, key_value)
|
||
pub fn load_kia_keys(&mut self, entries: &[(u32, u64)]) {
|
||
for &(key_type, key_value) in entries {
|
||
match key_type {
|
||
KIA_KEY1 => self.kia_mf_key = key_value,
|
||
KIA_KEY2 => self.kia_v6_a_key = key_value,
|
||
KIA_KEY3 => self.kia_v6_b_key = key_value,
|
||
KIA_KEY4 => self.kia_v5_key = key_value,
|
||
STAR_LINE_KEY => self.star_line_mf_key = key_value,
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Load VAG AUT64 keys from raw binary data (16 bytes per key; up to MAX_VAG_KEYS)
|
||
pub fn load_vag_keys_from_data(&mut self, data: &[u8]) {
|
||
if self.vag_keys_loaded {
|
||
return;
|
||
}
|
||
|
||
self.vag_keys.clear();
|
||
let n = (data.len() / AUT64_KEY_STRUCT_PACKED_SIZE).min(MAX_VAG_KEYS);
|
||
|
||
for i in 0..n {
|
||
let offset = i * AUT64_KEY_STRUCT_PACKED_SIZE;
|
||
if offset + AUT64_KEY_STRUCT_PACKED_SIZE > data.len() {
|
||
break;
|
||
}
|
||
let key = aut64::aut64_unpack(&data[offset..offset + AUT64_KEY_STRUCT_PACKED_SIZE]);
|
||
self.vag_keys.push(key);
|
||
}
|
||
|
||
self.vag_keys_loaded = true;
|
||
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
|
||
pub fn get_vag_key(&self, index: u8) -> Option<&Aut64Key> {
|
||
self.vag_keys.iter().find(|k| k.index == index)
|
||
}
|
||
|
||
/// Get a VAG AUT64 key by array position (0-based)
|
||
pub fn get_vag_key_by_position(&self, position: usize) -> Option<&Aut64Key> {
|
||
self.vag_keys.get(position)
|
||
}
|
||
|
||
/// Get the KIA manufacturer key
|
||
pub fn get_kia_mf_key(&self) -> u64 {
|
||
self.kia_mf_key
|
||
}
|
||
|
||
/// Get the KIA V6 AES key A
|
||
pub fn get_kia_v6_keystore_a(&self) -> u64 {
|
||
self.kia_v6_a_key
|
||
}
|
||
|
||
/// Get the KIA V6 AES key B
|
||
pub fn get_kia_v6_keystore_b(&self) -> u64 {
|
||
self.kia_v6_b_key
|
||
}
|
||
|
||
/// Get the KIA V5 mixer key
|
||
pub fn get_kia_v5_key(&self) -> u64 {
|
||
self.kia_v5_key
|
||
}
|
||
|
||
/// Get the Star Line manufacturer key
|
||
pub fn get_star_line_mf_key(&self) -> u64 {
|
||
self.star_line_mf_key
|
||
}
|
||
}
|
||
|
||
/// Global singleton keystore
|
||
fn global_keystore() -> &'static RwLock<KeyStore> {
|
||
static GLOBAL_KEYSTORE: OnceLock<RwLock<KeyStore>> = OnceLock::new();
|
||
GLOBAL_KEYSTORE.get_or_init(|| RwLock::new(KeyStore::new()))
|
||
}
|
||
|
||
/// Get a read reference to the global keystore
|
||
pub fn get_keystore() -> std::sync::RwLockReadGuard<'static, KeyStore> {
|
||
global_keystore().read().unwrap()
|
||
}
|
||
|
||
/// Get a write reference to the global keystore
|
||
pub fn get_keystore_mut() -> std::sync::RwLockWriteGuard<'static, KeyStore> {
|
||
global_keystore().write().unwrap()
|
||
}
|
||
|
||
/// Initialize the global keystore with KIA keys (matches protopirate_keys_load pattern)
|
||
pub fn load_keys(kia_entries: &[(u32, u64)]) {
|
||
let mut store = get_keystore_mut();
|
||
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).
|
||
/// 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).
|
||
pub fn load_keystore_from_embedded() {
|
||
let blob = crate::keystore::embedded_blob();
|
||
let Some(parsed) = crate::keystore::parse_blob(blob) else {
|
||
error!("Failed to parse embedded keystore blob");
|
||
return;
|
||
};
|
||
let mut store = get_keystore_mut();
|
||
store.load_kia_keys(&parsed.entries);
|
||
if !parsed.vag_bytes.is_empty() {
|
||
store.load_vag_keys_from_data(&parsed.vag_bytes);
|
||
}
|
||
info!(
|
||
"Keystore loaded from embedded blob ({} entries, {} VAG keys)",
|
||
parsed.entries.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.");
|
||
}
|
||
}
|