feat: add logging config for backend

- Added logging config using log and log4rs crates in the backend
- Organised backend code into separate files
- Logging statemetns were added with error handling in the backend
This commit is contained in:
Suyog Tandel
2026-01-09 14:42:21 +05:30
parent cb5954034a
commit 36d3007369
7 changed files with 566 additions and 452 deletions
+10
View File
@@ -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",
+6 -4
View File
@@ -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.
+10 -448
View File
@@ -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<u8>,
// 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<String>,
pid: Option<String>,
product_name: Option<String>,
led_gpio: Option<u8>,
led_brightness: Option<u8>,
touch_timeout: Option<u8>,
led_driver: Option<u8>,
led_dimmable: Option<bool>,
power_cycle_on_reset: Option<bool>,
led_steady: Option<bool>,
enable_secp256k1: Option<bool>,
}
#[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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<u8>), 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<FullDeviceStatus, AppError> {
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::<BigEndian>().unwrap_or(0);
let used = rdr.read_u32::<BigEndian>().unwrap_or(0);
let total = rdr.read_u32::<BigEndian>().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<DeviceInfo, AppError> {
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::<BigEndian>().unwrap_or(0);
let used = rdr.read_u32::<BigEndian>().unwrap_or(0);
let total = rdr.read_u32::<BigEndian>().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<String, AppError> {
// 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::<BigEndian>(vid).unwrap();
tlv.write_u16::<BigEndian>(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::<BigEndian>(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::<BigEndian>(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<String, AppError> {
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");
View File
+79
View File
@@ -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();
}
+362
View File
@@ -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<u8>), 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<FullDeviceStatus, AppError> {
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::<BigEndian>().unwrap_or(0);
let used = rdr.read_u32::<BigEndian>().unwrap_or(0);
let total = rdr.read_u32::<BigEndian>().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<DeviceInfo, AppError> {
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::<BigEndian>().unwrap_or(0);
let used = rdr.read_u32::<BigEndian>().unwrap_or(0);
let total = rdr.read_u32::<BigEndian>().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<String, AppError> {
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::<BigEndian>(vid).unwrap();
tlv.write_u16::<BigEndian>(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::<BigEndian>(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::<BigEndian>(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<String, AppError> {
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)))
}
}
+99
View File
@@ -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<u8>,
// 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<String>,
pub pid: Option<String>,
pub product_name: Option<String>,
pub led_gpio: Option<u8>,
pub led_brightness: Option<u8>,
pub touch_timeout: Option<u8>,
pub led_driver: Option<u8>,
pub led_dimmable: Option<bool>,
pub power_cycle_on_reset: Option<bool>,
pub led_steady: Option<bool>,
pub enable_secp256k1: Option<bool>,
}
#[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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}