402 lines
15 KiB
Rust
402 lines
15 KiB
Rust
//! Storage management for configuration, exports, and keystores.
|
|
//!
|
|
//! All application data lives under `~/.config/KAT/`:
|
|
//!
|
|
//! ```text
|
|
//! ~/.config/KAT/
|
|
//! config.ini — User configuration
|
|
//! exports/ — Exported .fob / .sub files (save location)
|
|
//! 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.
|
|
//! Only explicitly exported signals (.fob / .sub) persist between runs.
|
|
|
|
use anyhow::{Context, Result};
|
|
use configparser::ini::Ini;
|
|
use std::fs;
|
|
use std::path::PathBuf;
|
|
|
|
// ─── Config ──────────────────────────────────────────────────────────────────
|
|
|
|
/// Application configuration loaded from `~/.config/KAT/config.ini`
|
|
#[derive(Debug, Clone)]
|
|
pub struct Config {
|
|
// [general]
|
|
/// Directory for exporting signals (.fob / .sub files)
|
|
pub export_directory: PathBuf,
|
|
/// Directory scanned at startup for .fob and .sub files to import (separate from export)
|
|
pub import_directory: PathBuf,
|
|
/// Maximum captures to keep in memory during a session
|
|
pub max_captures: usize,
|
|
/// If off, only successfully decoded signals are added to the list. If on, unknown signals are also shown.
|
|
pub research_mode: bool,
|
|
|
|
// [radio]
|
|
/// Default frequency in Hz
|
|
pub default_frequency: u32,
|
|
/// Default LNA gain (0-40 dB, 8 dB steps)
|
|
pub default_lna_gain: u32,
|
|
/// Default VGA gain (0-62 dB, 2 dB steps)
|
|
pub default_vga_gain: u32,
|
|
/// Default amplifier state
|
|
pub default_amp: bool,
|
|
|
|
// [export]
|
|
/// Default export format (fob or sub)
|
|
pub default_export_format: String,
|
|
/// Include raw level/duration pairs in exports
|
|
pub include_raw_pairs: bool,
|
|
}
|
|
|
|
impl Config {
|
|
/// Build the default config, using the given config_dir as the base.
|
|
/// This keeps everything under `~/.config/KAT/` by default.
|
|
fn default_for(config_dir: &PathBuf) -> Self {
|
|
Self {
|
|
export_directory: config_dir.join("exports"),
|
|
import_directory: config_dir.join("import"),
|
|
max_captures: 100,
|
|
research_mode: false,
|
|
default_frequency: 433_920_000,
|
|
default_lna_gain: 24,
|
|
default_vga_gain: 20,
|
|
default_amp: false,
|
|
default_export_format: "fob".to_string(),
|
|
include_raw_pairs: true,
|
|
}
|
|
}
|
|
|
|
/// Load config from an INI file, falling back to defaults for missing keys.
|
|
fn load_from_ini(path: &std::path::Path, config_dir: &PathBuf) -> Result<Self> {
|
|
let mut ini = Ini::new();
|
|
ini.load(path)
|
|
.map_err(|e| anyhow::anyhow!("Failed to load config: {}", e))?;
|
|
|
|
let defaults = Config::default_for(config_dir);
|
|
|
|
let export_directory = ini
|
|
.get("general", "export_directory")
|
|
.map(|s| expand_tilde(&s))
|
|
.unwrap_or(defaults.export_directory);
|
|
|
|
let import_directory = ini
|
|
.get("general", "import_directory")
|
|
.map(|s| expand_tilde(&s))
|
|
.unwrap_or(defaults.import_directory);
|
|
|
|
let max_captures = ini
|
|
.getuint("general", "max_captures")
|
|
.ok()
|
|
.flatten()
|
|
.map(|v| v as usize)
|
|
.unwrap_or(defaults.max_captures);
|
|
|
|
let research_mode = ini
|
|
.getbool("general", "research_mode")
|
|
.ok()
|
|
.flatten()
|
|
.unwrap_or(defaults.research_mode);
|
|
|
|
let default_frequency = ini
|
|
.getuint("radio", "default_frequency")
|
|
.ok()
|
|
.flatten()
|
|
.map(|v| v as u32)
|
|
.unwrap_or(defaults.default_frequency);
|
|
|
|
let default_lna_gain = ini
|
|
.getuint("radio", "default_lna_gain")
|
|
.ok()
|
|
.flatten()
|
|
.map(|v| v as u32)
|
|
.unwrap_or(defaults.default_lna_gain);
|
|
|
|
let default_vga_gain = ini
|
|
.getuint("radio", "default_vga_gain")
|
|
.ok()
|
|
.flatten()
|
|
.map(|v| v as u32)
|
|
.unwrap_or(defaults.default_vga_gain);
|
|
|
|
let default_amp = ini
|
|
.getbool("radio", "default_amp")
|
|
.ok()
|
|
.flatten()
|
|
.unwrap_or(defaults.default_amp);
|
|
|
|
let default_export_format = ini
|
|
.get("export", "default_format")
|
|
.unwrap_or(defaults.default_export_format);
|
|
|
|
let include_raw_pairs = ini
|
|
.getbool("export", "include_raw_pairs")
|
|
.ok()
|
|
.flatten()
|
|
.unwrap_or(defaults.include_raw_pairs);
|
|
|
|
Ok(Self {
|
|
export_directory,
|
|
import_directory,
|
|
max_captures,
|
|
research_mode,
|
|
default_frequency,
|
|
default_lna_gain,
|
|
default_vga_gain,
|
|
default_amp,
|
|
default_export_format,
|
|
include_raw_pairs,
|
|
})
|
|
}
|
|
|
|
/// Save config to an INI-style file with comments explaining each field.
|
|
fn save_to_ini(&self, path: &std::path::Path) -> Result<()> {
|
|
let export_str = self.export_directory.to_string_lossy();
|
|
let import_str = self.import_directory.to_string_lossy();
|
|
let freq_mhz = self.default_frequency as f64 / 1_000_000.0;
|
|
|
|
let content = format!(
|
|
r#"; KAT — Keyfob Analysis Toolkit configuration
|
|
; Location: {path}
|
|
;
|
|
; Edit this file to change default settings.
|
|
; Lines starting with ; or # are comments.
|
|
|
|
[general]
|
|
; Directory where .fob and .sub exports are saved.
|
|
; Supports ~ for home directory.
|
|
export_directory = {export_dir}
|
|
|
|
; Directory scanned at startup for .fob and .sub files to import (not used for saving).
|
|
; Supports ~ for home directory.
|
|
import_directory = {import_dir}
|
|
|
|
; Maximum number of captures to keep in memory per session.
|
|
; Captures are NOT persisted between runs — only exported
|
|
; signals (.fob / .sub) survive in the exports folder.
|
|
max_captures = {max_captures}
|
|
|
|
; When off, only successfully decoded signals appear in the list.
|
|
; When on, unknown (unidentified) signals are also shown (research mode).
|
|
research_mode = {research_mode}
|
|
|
|
[radio]
|
|
; Default receive frequency in Hz ({freq_mhz:.2} MHz)
|
|
; Common keyfob frequencies: 315000000, 433920000, 868350000
|
|
default_frequency = {frequency}
|
|
|
|
; Default LNA gain in dB (0, 8, 16, 24, 32, 40)
|
|
default_lna_gain = {lna}
|
|
|
|
; Default VGA gain in dB (0-62, even numbers)
|
|
default_vga_gain = {vga}
|
|
|
|
; Enable RF amplifier by default (true/false)
|
|
default_amp = {amp}
|
|
|
|
[export]
|
|
; Default export format: fob (JSON metadata) or sub (Flipper Zero)
|
|
default_format = {export_fmt}
|
|
|
|
; Include raw signal level/duration pairs in .fob exports.
|
|
; Enables signal replay but increases file size.
|
|
include_raw_pairs = {raw_pairs}
|
|
"#,
|
|
path = path.display(),
|
|
export_dir = export_str,
|
|
import_dir = import_str,
|
|
max_captures = self.max_captures,
|
|
research_mode = self.research_mode,
|
|
freq_mhz = freq_mhz,
|
|
frequency = self.default_frequency,
|
|
lna = self.default_lna_gain,
|
|
vga = self.default_vga_gain,
|
|
amp = self.default_amp,
|
|
export_fmt = self.default_export_format,
|
|
raw_pairs = self.include_raw_pairs,
|
|
);
|
|
|
|
fs::write(path, content)
|
|
.with_context(|| format!("Failed to write config to {:?}", path))?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Fallback Default (without knowing config_dir). Only used if something goes
|
|
/// very wrong and we need a Config without a Storage instance.
|
|
impl Default for Config {
|
|
fn default() -> Self {
|
|
let fallback = resolve_config_dir()
|
|
.unwrap_or_else(|| PathBuf::from(".").join("KAT"));
|
|
Config::default_for(&fallback)
|
|
}
|
|
}
|
|
|
|
/// Expand `~` at the start of a path to the user's home directory.
|
|
fn expand_tilde(s: &str) -> PathBuf {
|
|
if s.starts_with("~/") {
|
|
if let Some(home) = dirs::home_dir() {
|
|
return home.join(&s[2..]);
|
|
}
|
|
}
|
|
PathBuf::from(s)
|
|
}
|
|
|
|
/// Resolve the KAT config directory to `~/.config/KAT/` regardless of OS.
|
|
pub fn resolve_config_dir() -> Option<PathBuf> {
|
|
dirs::home_dir().map(|home| home.join(".config").join("KAT"))
|
|
}
|
|
|
|
// ─── Storage ─────────────────────────────────────────────────────────────────
|
|
|
|
/// Storage manager for configuration and exports.
|
|
///
|
|
/// On construction it ensures the directory tree exists:
|
|
///
|
|
/// ```text
|
|
/// ~/.config/KAT/
|
|
/// config.ini
|
|
/// exports/
|
|
/// ```
|
|
///
|
|
/// Captures are in-memory only — they are discarded on exit.
|
|
pub struct Storage {
|
|
/// Base config directory (~/.config/KAT)
|
|
config_dir: PathBuf,
|
|
/// Configuration
|
|
pub config: Config,
|
|
}
|
|
|
|
impl Storage {
|
|
/// Create a new storage manager.
|
|
///
|
|
/// 1. Resolves the config directory (`~/.config/KAT`).
|
|
/// 2. Creates it if missing.
|
|
/// 3. Loads `config.ini` if it exists, otherwise writes a default one.
|
|
/// 4. Creates the export directory if missing.
|
|
pub fn new() -> Result<Self> {
|
|
// ── 1. Resolve base path ─────────────────────────────────────────
|
|
let config_dir = resolve_config_dir()
|
|
.context("Could not determine home directory (is $HOME set?)")?;
|
|
|
|
let config_path = config_dir.join("config.ini");
|
|
|
|
// ── 2. Ensure directory tree exists ──────────────────────────────
|
|
if !config_dir.exists() {
|
|
fs::create_dir_all(&config_dir)
|
|
.with_context(|| format!("Failed to create config dir: {:?}", config_dir))?;
|
|
tracing::info!("Created config directory: {:?}", config_dir);
|
|
}
|
|
|
|
// ── 3. Load or create config.ini ─────────────────────────────────
|
|
let config = if config_path.exists() {
|
|
tracing::info!("Loading config from {:?}", config_path);
|
|
match Config::load_from_ini(&config_path, &config_dir) {
|
|
Ok(cfg) => cfg,
|
|
Err(e) => {
|
|
tracing::warn!(
|
|
"Failed to parse config.ini, using defaults: {}",
|
|
e
|
|
);
|
|
Config::default_for(&config_dir)
|
|
}
|
|
}
|
|
} else {
|
|
tracing::info!(
|
|
"No config.ini found — creating default at {:?}",
|
|
config_path
|
|
);
|
|
let config = Config::default_for(&config_dir);
|
|
if let Err(e) = config.save_to_ini(&config_path) {
|
|
tracing::warn!("Could not write default config.ini: {}", e);
|
|
}
|
|
config
|
|
};
|
|
|
|
// ── 4. Ensure export directory exists ────────────────────────────
|
|
if !config.export_directory.exists() {
|
|
fs::create_dir_all(&config.export_directory).with_context(|| {
|
|
format!(
|
|
"Failed to create export dir: {:?}",
|
|
config.export_directory
|
|
)
|
|
})?;
|
|
tracing::info!(
|
|
"Created export directory: {:?}",
|
|
config.export_directory
|
|
);
|
|
}
|
|
|
|
// ── 5. Ensure import directory exists ────────────────────────────
|
|
if !config.import_directory.exists() {
|
|
fs::create_dir_all(&config.import_directory).with_context(|| {
|
|
format!(
|
|
"Failed to create import dir: {:?}",
|
|
config.import_directory
|
|
)
|
|
})?;
|
|
tracing::info!(
|
|
"Created import directory: {:?}",
|
|
config.import_directory
|
|
);
|
|
}
|
|
|
|
// ── 6. Ensure keystore directory exists ────────────────────────
|
|
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!("Export dir: {:?}", config.export_directory);
|
|
tracing::info!("Import dir: {:?}", config.import_directory);
|
|
tracing::info!("Keystore dir: {:?}", keystore_dir);
|
|
|
|
Ok(Self {
|
|
config_dir,
|
|
config,
|
|
})
|
|
}
|
|
|
|
/// Save the current configuration back to `config.ini`.
|
|
#[allow(dead_code)]
|
|
pub fn save_config(&self) -> Result<()> {
|
|
let config_path = self.config_dir.join("config.ini");
|
|
self.config.save_to_ini(&config_path)?;
|
|
tracing::info!("Saved config to {:?}", config_path);
|
|
Ok(())
|
|
}
|
|
|
|
// ─── Path accessors ──────────────────────────────────────────────────
|
|
|
|
/// Get the config directory path (`~/.config/KAT`)
|
|
#[allow(dead_code)]
|
|
pub fn config_dir(&self) -> &PathBuf {
|
|
&self.config_dir
|
|
}
|
|
|
|
/// Get the export directory path (from config, default `~/.config/KAT/exports`)
|
|
pub fn export_dir(&self) -> &PathBuf {
|
|
&self.config.export_directory
|
|
}
|
|
|
|
/// Get the import directory path (from config, default `~/.config/KAT/import`)
|
|
pub fn import_dir(&self) -> &PathBuf {
|
|
&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")
|
|
}
|
|
}
|