diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 7db87ec..8a9a508 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -861,6 +861,15 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs" version = "6.0.0" @@ -2945,6 +2954,7 @@ dependencies = [ "anyhow", "byteorder", "ctap-hid-fido2", + "directories", "hex", "log", "log4rs", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2a0f369..d224dd5 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -32,8 +32,10 @@ thiserror = "2" # Makes custom error handling much easier anyhow = "1" # For easy error propagation ctap-hid-fido2 = "3.5" # For fido2 interface operations -log = "0.4" # Logging facade -log4rs = "1" # For logging to output (like stdout) +log = "0.4" # Logging facade +log4rs = "1" # For logging to output (like stdout) +# dirs = "6" +directories = "6" # For Applcation config/data dir handling [profile.dev] incremental = true # Compile your binary in smaller steps. @@ -43,5 +45,5 @@ codegen-units = 256 codegen-units = 1 # Allows LLVM to perform better optimization. lto = true # Enables link-time-optimizations however is not stable. opt-level = 3 # Prioritizes speed. Use `z` if you prefer small binary size. -# panic = "abort" # Higher performance by disabling panic handlers. -strip = true # Ensures debug symbols are removed. +# panic = "abort" # Higher performance by disabling panic handlers. +strip = true # Ensures debug symbols are removed. diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8dc3839..3d1e0d2 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,463 +1,25 @@ -use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; -use pcsc::{Context, Protocols, Scope, ShareMode}; -use serde::{Deserialize, Serialize}; -use std::io::Cursor; // use tauri::State; +mod types; mod fido; - -// --- Constants --- - -// The Rescue Application ID (AID) from src/rescue.c -const RESCUE_AID: &[u8] = &[0xA0, 0x58, 0x3F, 0xC1, 0x9B, 0x7E, 0x4F, 0x21]; - -// APDU Instructions -const INS_WRITE: u8 = 0x1C; -const INS_SECURE: u8 = 0x1D; -const INS_READ: u8 = 0x1E; - -// PHY Tags from src/fs/phy.h -const TAG_VIDPID: u8 = 0x00; -const TAG_LED_GPIO: u8 = 0x04; -const TAG_LED_BRIGHTNESS: u8 = 0x05; -const TAG_OPTS: u8 = 0x06; -const TAG_UP_BTN: u8 = 0x08; // Presence Button Timeout -const TAG_USB_PRODUCT: u8 = 0x09; -const TAG_CURVES: u8 = 0x0A; -const TAG_LED_DRIVER: u8 = 0x0C; - -// Bitmasks for TAG_OPTS -const OPT_LED_DIMMABLE: u16 = 0x02; -const OPT_DISABLE_POWER_RESET: u16 = 0x04; -const OPT_LED_STEADY: u16 = 0x08; - -// Bitmasks for TAG_CURVES -const CURVE_SECP256K1: u32 = 0x08; - -// --- Data Structures --- - -#[derive(Serialize)] -struct DeviceInfo { - serial: String, - flash_used: u32, - flash_total: u32, - firmware_version: String, -} - -#[derive(Serialize, Deserialize, Debug, Default)] -struct AppConfig { - vid: String, - pid: String, - product_name: String, - led_gpio: u8, - led_brightness: u8, - touch_timeout: u8, - #[serde(skip_serializing_if = "Option::is_none")] - led_driver: Option, - // New Options - led_dimmable: bool, - power_cycle_on_reset: bool, - led_steady: bool, - enable_secp256k1: bool, -} - -// Partial config for writing only changed values -#[derive(Deserialize, Debug)] -struct AppConfigInput { - vid: Option, - pid: Option, - product_name: Option, - led_gpio: Option, - led_brightness: Option, - touch_timeout: Option, - led_driver: Option, - led_dimmable: Option, - power_cycle_on_reset: Option, - led_steady: Option, - enable_secp256k1: Option, -} - -#[derive(Serialize)] -struct FullDeviceStatus { - info: DeviceInfo, - config: AppConfig, - secure_boot: bool, - secure_lock: bool, -} - -// Custom Error types -#[derive(Debug, thiserror::Error)] -enum AppError { - #[error("PCSC Error: {0}")] - Pcsc(#[from] pcsc::Error), - #[error("IO/Hex Error: {0}")] - Io(String), - #[error("Device Error: {0}")] - Device(String), -} - -// Allow error to be serialized to string for Tauri -impl serde::Serialize for AppError { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -// --- Helper Functions --- - -/// Connects to the first available reader and selects the Rescue Applet -fn connect_and_select() -> Result<(pcsc::Card, Vec), AppError> { - let ctx = Context::establish(Scope::User)?; - - // List readers - let mut readers_buf = [0; 2048]; - let mut readers = ctx.list_readers(&mut readers_buf)?; - - // Use the first reader found - let reader = readers - .next() - .ok_or_else(|| AppError::Device("No Smart Card Reader found.".into()))?; - - // Connect - let card = ctx.connect(reader, ShareMode::Shared, Protocols::ANY)?; - - // Select Applet APDU: 00 A4 04 04 [Len] [AID] - let mut apdu = vec![0x00, 0xA4, 0x04, 0x04, RESCUE_AID.len() as u8]; - apdu.extend_from_slice(RESCUE_AID); - - let mut rx_buf = [0; 256]; - let rx = card.transmit(&apdu, &mut rx_buf)?; - - // Check Success (0x90 0x00) - if !rx.ends_with(&[0x90, 0x00]) { - return Err(AppError::Device( - "Rescue Applet not found on device. Is it in FIDO mode?".into(), - )); - } - - Ok((card, rx.to_vec())) -} - -// --- Tauri Commands --- - -#[tauri::command] -fn read_device_details() -> Result { - let (card, select_resp) = connect_and_select()?; - - // 1. Parse Basic Info (Same as your get_device_info) - if select_resp.len() < 14 { - return Err(AppError::Device("Invalid select response".into())); - } - let version_major = select_resp[2]; - let version_minor = select_resp[3]; - let serial_str = hex::encode_upper(&select_resp[4..12]); - - // 2. Read Flash Info (APDU: 80 1E 02 00 00) - let mut rx_buf = [0; 256]; - let rx_flash = card.transmit(&[0x80, INS_READ, 0x02, 0x00, 0x00], &mut rx_buf)?; - if !rx_flash.ends_with(&[0x90, 0x00]) { - return Err(AppError::Device("Failed to read flash".into())); - } - - let mut rdr = Cursor::new(&rx_flash[..rx_flash.len() - 2]); - let _free = rdr.read_u32::().unwrap_or(0); - let used = rdr.read_u32::().unwrap_or(0); - let total = rdr.read_u32::().unwrap_or(0); - - // 3. Read Secure Boot Status (APDU: 80 1E 03 00 00) -> [Enabled(1), Locked(1), Key(1)...] - let rx_secure = card.transmit(&[0x80, INS_READ, 0x03, 0x00, 0x00], &mut rx_buf)?; - let (sb_enabled, sb_locked) = if rx_secure.ends_with(&[0x90, 0x00]) && rx_secure.len() >= 4 { - (rx_secure[0] != 0, rx_secure[1] != 0) - } else { - (false, false) - }; - - // 4. Read PHY Config (APDU: 80 1E 01 01 00) -> TLV Data - let rx_phy = card.transmit(&[0x80, INS_READ, 0x01, 0x01, 0x00], &mut rx_buf)?; - if !rx_phy.ends_with(&[0x90, 0x00]) { - return Err(AppError::Device("Failed to read config".into())); - } - - // Parse TLV - let mut config = AppConfig::default(); - let data = &rx_phy[..rx_phy.len() - 2]; - let mut i = 0; - while i < data.len() { - if i + 2 > data.len() { - break; - } - let tag = data[i]; - let len = data[i + 1] as usize; - i += 2; - if i + len > data.len() { - break; - } - let val = &data[i..i + len]; - - match tag { - TAG_VIDPID => { - if val.len() == 4 { - let vid = u16::from_be_bytes([val[0], val[1]]); - let pid = u16::from_be_bytes([val[2], val[3]]); - config.vid = format!("{:04X}", vid); - config.pid = format!("{:04X}", pid); - } - } - TAG_LED_GPIO => { - if !val.is_empty() { - config.led_gpio = val[0]; - } - } - TAG_LED_BRIGHTNESS => { - if !val.is_empty() { - config.led_brightness = val[0]; - } - } - TAG_UP_BTN => { - if !val.is_empty() { - config.touch_timeout = val[0]; - } - } - TAG_USB_PRODUCT => { - // Remove null terminator if present - let s = std::str::from_utf8(val) - .unwrap_or("") - .trim_matches(char::from(0)); - config.product_name = s.to_string(); - } - TAG_OPTS => { - if val.len() >= 2 { - let opts = u16::from_be_bytes([val[0], val[1]]); - - config.led_dimmable = (opts & OPT_LED_DIMMABLE) != 0; - config.power_cycle_on_reset = (opts & OPT_DISABLE_POWER_RESET) == 0; - config.led_steady = (opts & OPT_LED_STEADY) != 0; - } - } - TAG_CURVES => { - if val.len() >= 4 { - let curves = u32::from_be_bytes([val[0], val[1], val[2], val[3]]); - config.enable_secp256k1 = (curves & CURVE_SECP256K1) != 0; - } - } - TAG_LED_DRIVER => { - if !val.is_empty() { - config.led_driver = Some(val[0]); - } - } - _ => {} - } - i += len; - } - - Ok(FullDeviceStatus { - info: DeviceInfo { - serial: serial_str, - flash_used: used / 1024, - flash_total: total / 1024, - firmware_version: format!("{}.{}", version_major, version_minor), - }, - config, - secure_boot: sb_enabled, - secure_lock: sb_locked, - }) -} - -#[tauri::command] -fn get_device_info() -> Result { - let (card, select_resp) = connect_and_select()?; - - // 1. Parse Version & Serial from Select Response (see src/rescue.c) - // Response: [MCU, PROD, VER_MAJ, VER_MIN, SERIAL(8 bytes)..., 90, 00] - if select_resp.len() < 14 { - return Err(AppError::Device("Invalid response from device".into())); - } - - let version_major = select_resp[2]; - let version_minor = select_resp[3]; - let serial_bytes = &select_resp[4..12]; - let serial_str = hex::encode_upper(serial_bytes); - - // 2. Read Flash Info - // APDU: 80 1E 02 00 00 (Read Flash Info) - let apdu_read = [0x80, INS_READ, 0x02, 0x00, 0x00]; - let mut rx_buf = [0; 256]; - let rx = card.transmit(&apdu_read, &mut rx_buf)?; - - if !rx.ends_with(&[0x90, 0x00]) { - return Err(AppError::Device("Failed to read flash info".into())); - } - - // Response: [Free(4), Used(4), Total(4), Files(4), Size(4), 90, 00] - // We need 'Used' (index 4) and 'Total' (index 8) - // Data is Big Endian - let mut rdr = Cursor::new(&rx[..rx.len() - 2]); - let _free = rdr.read_u32::().unwrap_or(0); - let used = rdr.read_u32::().unwrap_or(0); - let total = rdr.read_u32::().unwrap_or(0); - - Ok(DeviceInfo { - serial: serial_str, - flash_used: used / 1024, // Convert to KB - flash_total: total / 1024, - firmware_version: format!("{}.{}", version_major, version_minor), - }) -} - -#[tauri::command] -fn write_config(config: AppConfigInput) -> Result { - // 1. Construct TLV Blob - let mut tlv = Vec::new(); - - // VID:PID (Tag 0x00) - if let (Some(vid_str), Some(pid_str)) = (&config.vid, &config.pid) { - let vid = - u16::from_str_radix(vid_str, 16).map_err(|_| AppError::Io("Invalid VID".into()))?; - let pid = - u16::from_str_radix(pid_str, 16).map_err(|_| AppError::Io("Invalid PID".into()))?; - - tlv.push(TAG_VIDPID); - tlv.push(0x04); - tlv.write_u16::(vid).unwrap(); - tlv.write_u16::(pid).unwrap(); - } - - // LED GPIO (Tag 0x04) - if let Some(val) = config.led_gpio { - tlv.push(TAG_LED_GPIO); - tlv.push(0x01); - tlv.push(val); - } - - // LED Brightness (Tag 0x05) - if let Some(val) = config.led_brightness { - tlv.push(TAG_LED_BRIGHTNESS); - tlv.push(0x01); - tlv.push(val); - } - - // Touch Timeout (Tag 0x08) - if let Some(val) = config.touch_timeout { - tlv.push(TAG_UP_BTN); - tlv.push(0x01); - tlv.push(val); - } - - // Options (Tag 0x06) - if let (Some(dim), Some(cycle), Some(steady)) = ( - config.led_dimmable, - config.power_cycle_on_reset, - config.led_steady, - ) { - let mut opts: u16 = 0; - - if dim { - opts |= OPT_LED_DIMMABLE; - } - - if !cycle { - opts |= OPT_DISABLE_POWER_RESET; - } - - if steady { - opts |= OPT_LED_STEADY; - } - - tlv.push(TAG_OPTS); - tlv.push(0x02); - tlv.write_u16::(opts).unwrap(); - } - - // Curves (Tag 0x0A) - if let Some(enabled) = config.enable_secp256k1 { - let mut curves: u32 = 0; - if enabled { - curves |= CURVE_SECP256K1; - } - - tlv.push(TAG_CURVES); - tlv.push(0x04); - tlv.write_u32::(curves).unwrap(); - } - - // LED Driver (Tag 0x0C) - if let Some(val) = config.led_driver { - tlv.push(TAG_LED_DRIVER); - tlv.push(0x01); - tlv.push(val); - } - - // Product Name (Tag 0x09) - if let Some(name) = config.product_name { - if !name.is_empty() { - let name_bytes = name.as_bytes(); - let len = name_bytes.len() + 1; - if len > 32 { - return Err(AppError::Io("Product name too long".into())); - } - - tlv.push(TAG_USB_PRODUCT); - tlv.push(len as u8); - tlv.extend_from_slice(name_bytes); - tlv.push(0x00); - } - } - - // 2. Connect and Send - if tlv.is_empty() { - return Ok("No changes to apply".into()); - } - - let (card, _) = connect_and_select()?; - - // APDU: 80 1C 01 00 [Lc] [Data] - let mut apdu = vec![0x80, INS_WRITE, 0x01, 0x00, tlv.len() as u8]; - apdu.extend_from_slice(&tlv); - - let mut rx_buf = [0; 256]; - let rx = card.transmit(&apdu, &mut rx_buf)?; - - if rx.ends_with(&[0x90, 0x00]) { - Ok("Configuration Applied Successfully".into()) - } else { - Err(AppError::Device(format!("Write failed: {:02X?}", rx))) - } -} - -#[tauri::command] -fn enable_secure_boot(lock: bool) -> Result { - let (card, _) = connect_and_select()?; - - // APDU: 80 1D [KeyIndex] [LockBool] 00 - // KeyIndex = 0 (Default), LockBool = 1 if true - let lock_byte = if lock { 0x01 } else { 0x00 }; - let apdu = [0x80, INS_SECURE, 0x00, lock_byte, 0x00]; - - let mut rx_buf = [0; 256]; - let rx = card.transmit(&apdu, &mut rx_buf)?; - - if rx.ends_with(&[0x90, 0x00]) { - Ok("Secure Boot Enabled".into()) - } else { - Err(AppError::Device(format!("Secure Boot failed: {:02X?}", rx))) - } -} +mod logging; +mod rescue; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { + logging::logger_init(); + log::info!("Initialisng PicoForge..."); + tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_opener::init()) .invoke_handler(tauri::generate_handler![ - read_device_details, - get_device_info, - write_config, + rescue::read_device_details, + rescue::get_device_info, + rescue::write_config, fido::change_fido_pin, fido::set_min_pin_length, - enable_secure_boot + rescue::enable_secure_boot ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/log.rs b/src-tauri/src/log.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src-tauri/src/logging.rs b/src-tauri/src/logging.rs new file mode 100644 index 0000000..9345fe9 --- /dev/null +++ b/src-tauri/src/logging.rs @@ -0,0 +1,79 @@ +use log::LevelFilter; +use log4rs::{ + append::{ + console::{ConsoleAppender, Target}, + rolling_file::{ + policy::compound::{ + roll::delete::DeleteRoller, trigger::size::SizeTrigger, CompoundPolicy, + }, + RollingFileAppender, + }, + }, + config::{Appender, Logger, Root}, + encode::pattern::PatternEncoder, +}; +use std::fs; +use directories::ProjectDirs; + +/// Initializes log4rs with custom configuration for stdout and file logging. +pub fn logger_init() { + let qual = "in"; + let org = "suyogtandel"; + let app = "picoforge"; + + // Determine the log file path using ProjectDirs for cross-platform compatibility + let log_file_path = { + let log_dir = if let Some(proj_dirs) = ProjectDirs::from(qual, org, app) { + proj_dirs.data_local_dir().join("logs") + } else { + eprintln!("Could not determine project directories. Falling back to local directory."); + std::path::PathBuf::from("logs") + }; + + if let Err(e) = fs::create_dir_all(&log_dir) { + eprintln!("Failed to create log directory at {:?}: {}", log_dir, e); + } + + log_dir.join("picoforge.log") + }; + + // TODO: Add session based log files or rolling log files with archiving of old files, to prevent a single log file from growing too large. + let size_trigger = SizeTrigger::new(10 * 1024 * 1024); // 10 MB limit + let roller = DeleteRoller::new(); + let policy = CompoundPolicy::new(Box::new(size_trigger), Box::new(roller)); + + // File Appender + let logfile = RollingFileAppender::builder() + .encoder(Box::new(PatternEncoder::new( + "[{d(%Y-%m-%d %H:%M:%S %Z)} {l} {t}] {m}{n}", + ))) + .build(log_file_path, Box::new(policy)) + .unwrap(); + + // Console Appender + let stdout = ConsoleAppender::builder() + .target(Target::Stdout) + .encoder(Box::new(PatternEncoder::new( + "[{d(%Y-%m-%d %H:%M:%S %Z)} {h({l})} {t}] {m}{n}", + ))) + .build(); + + let (app_level, root_level) = if cfg!(debug_assertions) { + (LevelFilter::Trace, LevelFilter::Debug) + } else { + (LevelFilter::Info, LevelFilter::Error) + }; + + let config = log4rs::Config::builder() + .appender(Appender::builder().build("stdout", Box::new(stdout))) + .appender(Appender::builder().build("logfile", Box::new(logfile))) + .logger(Logger::builder().build("picoforge", app_level)) + .build( + Root::builder() + .appenders(vec!["logfile", "stdout"]) + .build(root_level), + ) + .unwrap(); + + log4rs::init_config(config).unwrap(); +} \ No newline at end of file diff --git a/src-tauri/src/rescue.rs b/src-tauri/src/rescue.rs new file mode 100644 index 0000000..eb9ce72 --- /dev/null +++ b/src-tauri/src/rescue.rs @@ -0,0 +1,362 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use pcsc::{Context, Protocols, Scope, ShareMode}; +use std::io::Cursor; +use log; +use crate::types::*; + +// --- Helper Functions --- + +/// Connects to the first available reader and selects the Rescue Applet +fn connect_and_select() -> Result<(pcsc::Card, Vec), AppError> { + let ctx = Context::establish(Scope::User)?; + + let mut readers_buf = [0; 2048]; + let mut readers = ctx.list_readers(&mut readers_buf)?; + + // Use the first reader found + let reader = readers + .next() + .ok_or_else(|| { + log::error!("No Smart Card Reader found"); + AppError::Device("No Smart Card Reader found.".into()) + })?; + + let card = ctx.connect(reader, ShareMode::Shared, Protocols::ANY)?; + + // Select Applet APDU: 00 A4 04 04 [Len] [AID] + let mut apdu = vec![0x00, 0xA4, 0x04, 0x04, RESCUE_AID.len() as u8]; + apdu.extend_from_slice(RESCUE_AID); + + let mut rx_buf = [0; 256]; + let rx = card.transmit(&apdu, &mut rx_buf)?; + + // Check Success (0x90 0x00) + if !rx.ends_with(&[0x90, 0x00]) { + log::error!("Rescue Applet not found on the device!"); + return Err(AppError::Device( + // There is no such mode as fido, i tink the rescue applet stays active and at the same time fido mode works? + // Need to study this more. + "Rescue Applet not found on device. Is it in FIDO mode?".into(), + )); + } + + log::info!("Successfully connected to Rescue Applet"); + Ok((card, rx.to_vec())) +} + +// --- Tauri Commands --- + +#[tauri::command] +pub fn read_device_details() -> Result { + log::info!("Reading full device details"); + let (card, select_resp) = connect_and_select()?; + + // 1. Parse Basic Info (Same as your get_device_info) + if select_resp.len() < 14 { + return Err(AppError::Device("Invalid select response".into())); + } + let version_major = select_resp[2]; + let version_minor = select_resp[3]; + let serial_str = hex::encode_upper(&select_resp[4..12]); + + // 2. Read Flash Info (APDU: 80 1E 02 00 00) + let mut rx_buf = [0; 256]; + let rx_flash = card.transmit(&[0x80, INS_READ, 0x02, 0x00, 0x00], &mut rx_buf)?; + if !rx_flash.ends_with(&[0x90, 0x00]) { + return Err(AppError::Device("Failed to read flash".into())); + } + + let mut rdr = Cursor::new(&rx_flash[..rx_flash.len() - 2]); + let _free = rdr.read_u32::().unwrap_or(0); + let used = rdr.read_u32::().unwrap_or(0); + let total = rdr.read_u32::().unwrap_or(0); + + // 3. Read Secure Boot Status (APDU: 80 1E 03 00 00) -> [Enabled(1), Locked(1), Key(1)...] + let rx_secure = card.transmit(&[0x80, INS_READ, 0x03, 0x00, 0x00], &mut rx_buf)?; + let (sb_enabled, sb_locked) = if rx_secure.ends_with(&[0x90, 0x00]) && rx_secure.len() >= 4 { + (rx_secure[0] != 0, rx_secure[1] != 0) + } else { + (false, false) + }; + + // 4. Read PHY Config (APDU: 80 1E 01 01 00) -> TLV Data + let rx_phy = card.transmit(&[0x80, INS_READ, 0x01, 0x01, 0x00], &mut rx_buf)?; + if !rx_phy.ends_with(&[0x90, 0x00]) { + return Err(AppError::Device("Failed to read config".into())); + } + + // Parse TLV + let mut config = AppConfig::default(); + let data = &rx_phy[..rx_phy.len() - 2]; + let mut i = 0; + while i < data.len() { + if i + 2 > data.len() { + break; + } + let tag = data[i]; + let len = data[i + 1] as usize; + i += 2; + if i + len > data.len() { + break; + } + let val = &data[i..i + len]; + + match tag { + TAG_VIDPID => { + if val.len() == 4 { + let vid = u16::from_be_bytes([val[0], val[1]]); + let pid = u16::from_be_bytes([val[2], val[3]]); + config.vid = format!("{:04X}", vid); + config.pid = format!("{:04X}", pid); + } + } + TAG_LED_GPIO => { + if !val.is_empty() { + config.led_gpio = val[0]; + } + } + TAG_LED_BRIGHTNESS => { + if !val.is_empty() { + config.led_brightness = val[0]; + } + } + TAG_UP_BTN => { + if !val.is_empty() { + config.touch_timeout = val[0]; + } + } + TAG_USB_PRODUCT => { + // Remove null terminator if present + let s = std::str::from_utf8(val) + .unwrap_or("") + .trim_matches(char::from(0)); + config.product_name = s.to_string(); + } + TAG_OPTS => { + if val.len() >= 2 { + let opts = u16::from_be_bytes([val[0], val[1]]); + + config.led_dimmable = (opts & OPT_LED_DIMMABLE) != 0; + config.power_cycle_on_reset = (opts & OPT_DISABLE_POWER_RESET) == 0; + config.led_steady = (opts & OPT_LED_STEADY) != 0; + } + } + TAG_CURVES => { + if val.len() >= 4 { + let curves = u32::from_be_bytes([val[0], val[1], val[2], val[3]]); + config.enable_secp256k1 = (curves & CURVE_SECP256K1) != 0; + } + } + TAG_LED_DRIVER => { + if !val.is_empty() { + config.led_driver = Some(val[0]); + } + } + _ => {} + } + i += len; + } + + log::info!("Successfully read device details - Serial: {}, Firmware: {}.{}", serial_str, version_major, version_minor); + + Ok(FullDeviceStatus { + info: DeviceInfo { + serial: serial_str, + flash_used: used / 1024, + flash_total: total / 1024, + firmware_version: format!("{}.{}", version_major, version_minor), + }, + config, + secure_boot: sb_enabled, + secure_lock: sb_locked, + }) +} + +#[tauri::command] +pub fn get_device_info() -> Result { + let (card, select_resp) = connect_and_select()?; + + // 1. Parse Version & Serial from Select Response (see src/rescue.c) + // Response: [MCU, PROD, VER_MAJ, VER_MIN, SERIAL(8 bytes)..., 90, 00] + if select_resp.len() < 14 { + return Err(AppError::Device("Invalid response from device".into())); + } + + let version_major = select_resp[2]; + let version_minor = select_resp[3]; + let serial_bytes = &select_resp[4..12]; + let serial_str = hex::encode_upper(serial_bytes); + + // 2. Read Flash Info + // APDU: 80 1E 02 00 00 (Read Flash Info) + let apdu_read = [0x80, INS_READ, 0x02, 0x00, 0x00]; + let mut rx_buf = [0; 256]; + let rx = card.transmit(&apdu_read, &mut rx_buf)?; + + if !rx.ends_with(&[0x90, 0x00]) { + return Err(AppError::Device("Failed to read flash info".into())); + } + + // Response: [Free(4), Used(4), Total(4), Files(4), Size(4), 90, 00] + // We need 'Used' (index 4) and 'Total' (index 8) + // Data is Big Endian + let mut rdr = Cursor::new(&rx[..rx.len() - 2]); + let _free = rdr.read_u32::().unwrap_or(0); + let used = rdr.read_u32::().unwrap_or(0); + let total = rdr.read_u32::().unwrap_or(0); + + Ok(DeviceInfo { + serial: serial_str, + flash_used: used / 1024, // Convert to KB + flash_total: total / 1024, + firmware_version: format!("{}.{}", version_major, version_minor), + }) +} + +#[tauri::command] +pub fn write_config(config: AppConfigInput) -> Result { + log::info!("Writing configuration to device"); + log::debug!("Config input: {:?}", config); + + // 1. Construct TLV Blob + let mut tlv = Vec::new(); + + // VID:PID (Tag 0x00) + if let (Some(vid_str), Some(pid_str)) = (&config.vid, &config.pid) { + let vid = + u16::from_str_radix(vid_str, 16).map_err(|_| AppError::Io("Invalid VID".into()))?; + let pid = + u16::from_str_radix(pid_str, 16).map_err(|_| AppError::Io("Invalid PID".into()))?; + + tlv.push(TAG_VIDPID); + tlv.push(0x04); + tlv.write_u16::(vid).unwrap(); + tlv.write_u16::(pid).unwrap(); + } + + // LED GPIO (Tag 0x04) + if let Some(val) = config.led_gpio { + tlv.push(TAG_LED_GPIO); + tlv.push(0x01); + tlv.push(val); + } + + // LED Brightness (Tag 0x05) + if let Some(val) = config.led_brightness { + tlv.push(TAG_LED_BRIGHTNESS); + tlv.push(0x01); + tlv.push(val); + } + + // Touch Timeout (Tag 0x08) + if let Some(val) = config.touch_timeout { + tlv.push(TAG_UP_BTN); + tlv.push(0x01); + tlv.push(val); + } + + // Options (Tag 0x06) + if let (Some(dim), Some(cycle), Some(steady)) = ( + config.led_dimmable, + config.power_cycle_on_reset, + config.led_steady, + ) { + let mut opts: u16 = 0; + + if dim { + opts |= OPT_LED_DIMMABLE; + } + + if !cycle { + opts |= OPT_DISABLE_POWER_RESET; + } + + if steady { + opts |= OPT_LED_STEADY; + } + + tlv.push(TAG_OPTS); + tlv.push(0x02); + tlv.write_u16::(opts).unwrap(); + } + + // Curves (Tag 0x0A) + if let Some(enabled) = config.enable_secp256k1 { + let mut curves: u32 = 0; + if enabled { + curves |= CURVE_SECP256K1; + } + + tlv.push(TAG_CURVES); + tlv.push(0x04); + tlv.write_u32::(curves).unwrap(); + } + + // LED Driver (Tag 0x0C) + if let Some(val) = config.led_driver { + tlv.push(TAG_LED_DRIVER); + tlv.push(0x01); + tlv.push(val); + } + + // Product Name (Tag 0x09) + if let Some(name) = config.product_name { + if !name.is_empty() { + let name_bytes = name.as_bytes(); + let len = name_bytes.len() + 1; + if len > 32 { + return Err(AppError::Io("Product name too long".into())); + } + + tlv.push(TAG_USB_PRODUCT); + tlv.push(len as u8); + tlv.extend_from_slice(name_bytes); + tlv.push(0x00); + } + } + + // 2. Connect and Send + if tlv.is_empty() { + log::warn!("No configuration changes to apply"); + return Ok("No changes to apply".into()); + } + + log::debug!("TLV payload size: {} bytes", tlv.len()); + + let (card, _) = connect_and_select()?; + + // APDU: 80 1C 01 00 [Lc] [Data] + let mut apdu = vec![0x80, INS_WRITE, 0x01, 0x00, tlv.len() as u8]; + apdu.extend_from_slice(&tlv); + + let mut rx_buf = [0; 256]; + let rx = card.transmit(&apdu, &mut rx_buf)?; + + if rx.ends_with(&[0x90, 0x00]) { + log::info!("Configuration applied successfully"); + Ok("Configuration Applied Successfully".into()) + } else { + log::error!("Configuration write failed: {:02X?}", rx); + Err(AppError::Device(format!("Write failed: {:02X?}", rx))) + } +} + +/// UNSTABLE! (WIP) +#[tauri::command] +pub fn enable_secure_boot(lock: bool) -> Result { + let (card, _) = connect_and_select()?; + + // APDU: 80 1D [KeyIndex] [LockBool] 00 + // KeyIndex = 0 (Default), LockBool = 1 if true + let lock_byte = if lock { 0x01 } else { 0x00 }; + let apdu = [0x80, INS_SECURE, 0x00, lock_byte, 0x00]; + + let mut rx_buf = [0; 256]; + let rx = card.transmit(&apdu, &mut rx_buf)?; + + if rx.ends_with(&[0x90, 0x00]) { + Ok("Secure Boot Enabled".into()) + } else { + Err(AppError::Device(format!("Secure Boot failed: {:02X?}", rx))) + } +} \ No newline at end of file diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs new file mode 100644 index 0000000..2757352 --- /dev/null +++ b/src-tauri/src/types.rs @@ -0,0 +1,99 @@ +use serde::{Deserialize, Serialize}; + +// --- Constants --- + +// The Rescue Application ID (AID) from src/rescue.c +pub const RESCUE_AID: &[u8] = &[0xA0, 0x58, 0x3F, 0xC1, 0x9B, 0x7E, 0x4F, 0x21]; + +// APDU Instructions +pub const INS_WRITE: u8 = 0x1C; +pub const INS_SECURE: u8 = 0x1D; +pub const INS_READ: u8 = 0x1E; + +// PHY Tags from src/fs/phy.h +pub const TAG_VIDPID: u8 = 0x00; +pub const TAG_LED_GPIO: u8 = 0x04; +pub const TAG_LED_BRIGHTNESS: u8 = 0x05; +pub const TAG_OPTS: u8 = 0x06; +pub const TAG_UP_BTN: u8 = 0x08; // Presence Button Timeout +pub const TAG_USB_PRODUCT: u8 = 0x09; +pub const TAG_CURVES: u8 = 0x0A; +pub const TAG_LED_DRIVER: u8 = 0x0C; + +// Bitmasks for TAG_OPTS +pub const OPT_LED_DIMMABLE: u16 = 0x02; +pub const OPT_DISABLE_POWER_RESET: u16 = 0x04; +pub const OPT_LED_STEADY: u16 = 0x08; + +// Bitmasks for TAG_CURVES +pub const CURVE_SECP256K1: u32 = 0x08; + +// --- Data Structures --- + +#[derive(Serialize)] +pub struct DeviceInfo { + pub serial: String, + pub flash_used: u32, + pub flash_total: u32, + pub firmware_version: String, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct AppConfig { + pub vid: String, + pub pid: String, + pub product_name: String, + pub led_gpio: u8, + pub led_brightness: u8, + pub touch_timeout: u8, + #[serde(skip_serializing_if = "Option::is_none")] + pub led_driver: Option, + // New Options + pub led_dimmable: bool, + pub power_cycle_on_reset: bool, + pub led_steady: bool, + pub enable_secp256k1: bool, +} + +#[derive(Deserialize, Debug)] +pub struct AppConfigInput { + pub vid: Option, + pub pid: Option, + pub product_name: Option, + pub led_gpio: Option, + pub led_brightness: Option, + pub touch_timeout: Option, + pub led_driver: Option, + pub led_dimmable: Option, + pub power_cycle_on_reset: Option, + pub led_steady: Option, + pub enable_secp256k1: Option, +} + +#[derive(Serialize)] +pub struct FullDeviceStatus { + pub info: DeviceInfo, + pub config: AppConfig, + pub secure_boot: bool, + pub secure_lock: bool, +} + +#[derive(Debug, thiserror::Error)] +pub enum AppError { + #[error("PCSC Error: {0}")] + Pcsc(#[from] pcsc::Error), + #[error("IO/Hex Error: {0}")] + Io(String), + #[error("Device Error: {0}")] + Device(String), +} + +// Allow error to be serialized to string for Tauri +impl serde::Serialize for AppError { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} \ No newline at end of file