synor/apps/desktop-wallet/src-tauri/src/lib.rs
Gulshan Yadav f56a6f5088 feat(wallet): add OS keychain integration with biometric unlock
Add cross-platform keychain support using the keyring crate for secure
credential storage with biometric authentication (TouchID on macOS,
Windows Hello on Windows, Secret Service on Linux).

- Add keychain module with enable/disable biometric unlock
- Add Tauri commands for keychain operations
- Add Keychain error variant for proper error handling
- Add Java SDK foundation
- Update milestone docs to reflect 95% completion
2026-01-11 17:31:21 +05:30

377 lines
12 KiB
Rust

//! Synor Desktop Wallet - Tauri Backend
//!
//! Provides native functionality for the desktop wallet:
//! - Secure key storage using OS keychain
//! - Direct RPC communication with Synor nodes
//! - Transaction signing with Dilithium3 post-quantum signatures
//! - File system access for wallet backups
//! - System tray for background operation
//! - Auto-updates for seamless upgrades
mod commands;
mod crypto;
mod error;
mod keychain;
mod wallet;
use tauri::{
menu::{Menu, MenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
Emitter, Manager, Runtime,
};
pub use error::{Error, Result};
/// Build the system tray menu
fn build_tray_menu<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<Menu<R>> {
let show = MenuItem::with_id(app, "show", "Show Wallet", true, None::<&str>)?;
let hide = MenuItem::with_id(app, "hide", "Hide to Tray", true, None::<&str>)?;
let separator1 = MenuItem::with_id(app, "sep1", "─────────────", false, None::<&str>)?;
let lock = MenuItem::with_id(app, "lock", "Lock Wallet", true, None::<&str>)?;
let separator2 = MenuItem::with_id(app, "sep2", "─────────────", false, None::<&str>)?;
let check_updates = MenuItem::with_id(app, "check_updates", "Check for Updates", true, None::<&str>)?;
let separator3 = MenuItem::with_id(app, "sep3", "─────────────", false, None::<&str>)?;
let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
Menu::with_items(
app,
&[
&show,
&hide,
&separator1,
&lock,
&separator2,
&check_updates,
&separator3,
&quit,
],
)
}
/// Handle system tray menu events
fn handle_tray_event<R: Runtime>(app: &tauri::AppHandle<R>, event: TrayIconEvent) {
match event {
TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} => {
// Left click: show and focus the window
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
_ => {}
}
}
/// Handle tray menu item clicks
fn handle_menu_event<R: Runtime>(app: &tauri::AppHandle<R>, event: tauri::menu::MenuEvent) {
match event.id().as_ref() {
"show" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
"hide" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.hide();
}
}
"lock" => {
// Emit lock event to frontend
let _ = app.emit("wallet-lock", ());
}
"check_updates" => {
// Emit update check event to frontend
let _ = app.emit("check-updates", ());
}
"quit" => {
// Emit quit event to allow cleanup
let _ = app.emit("app-quit", ());
std::process::exit(0);
}
_ => {}
}
}
/// Initialize the Tauri application with all plugins and commands
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
// Core plugins
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_store::Builder::default().build())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_clipboard_manager::init())
// New plugins for desktop features
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_process::init())
.setup(|app| {
// Initialize wallet state
let wallet_state = wallet::WalletState::new();
app.manage(wallet_state);
// Build and set up system tray
let menu = build_tray_menu(app.handle())?;
let _tray = TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu)
.show_menu_on_left_click(false)
.on_tray_icon_event(|tray, event| {
handle_tray_event(tray.app_handle(), event);
})
.on_menu_event(|app, event| {
handle_menu_event(app, event);
})
.tooltip("Synor Wallet")
.build(app)?;
// Check for updates on startup (non-blocking)
let handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
check_for_updates(handle).await;
});
#[cfg(debug_assertions)]
{
// Open devtools in development
if let Some(window) = app.get_webview_window("main") {
window.open_devtools();
}
}
Ok(())
})
.invoke_handler(tauri::generate_handler![
// Wallet management
commands::create_wallet,
commands::import_wallet,
commands::unlock_wallet,
commands::lock_wallet,
commands::get_wallet_info,
commands::export_mnemonic,
// Addresses & UTXOs
commands::get_addresses,
commands::generate_address,
commands::get_balance,
commands::get_utxos,
// Transactions
commands::create_transaction,
commands::sign_transaction,
commands::broadcast_transaction,
commands::get_transaction_history,
// Network
commands::connect_node,
commands::disconnect_node,
commands::get_network_status,
// Updates
check_update,
install_update,
// Keychain / Biometric
keychain_is_available,
keychain_enable_biometric,
keychain_disable_biometric,
keychain_is_biometric_enabled,
keychain_unlock_with_biometric,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
/// Check for updates on startup
async fn check_for_updates<R: Runtime>(app: tauri::AppHandle<R>) {
use tauri_plugin_updater::UpdaterExt;
// Wait a few seconds before checking for updates
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
let updater = match app.updater() {
Ok(u) => u,
Err(e) => {
eprintln!("Updater not available: {}", e);
return;
}
};
match updater.check().await {
Ok(Some(update)) => {
// Notify frontend about available update
let _ = app.emit("update-available", serde_json::json!({
"version": update.version,
"body": update.body,
"date": update.date
}));
}
Ok(None) => {
// No update available
}
Err(e) => {
eprintln!("Failed to check for updates: {}", e);
}
}
}
/// Tauri command: Check for updates
#[tauri::command]
async fn check_update<R: Runtime>(app: tauri::AppHandle<R>) -> std::result::Result<Option<serde_json::Value>, String> {
use tauri_plugin_updater::UpdaterExt;
let updater = app.updater().map_err(|e| format!("Updater not configured: {}", e))?;
match updater.check().await {
Ok(Some(update)) => {
Ok(Some(serde_json::json!({
"version": update.version,
"body": update.body,
"date": update.date
})))
}
Ok(None) => Ok(None),
Err(e) => Err(format!("Update check failed: {}", e)),
}
}
/// Tauri command: Install update
#[tauri::command]
async fn install_update<R: Runtime>(app: tauri::AppHandle<R>) -> std::result::Result<(), String> {
use tauri_plugin_updater::UpdaterExt;
let updater = app.updater().map_err(|e| format!("Updater not configured: {}", e))?;
match updater.check().await {
Ok(Some(update)) => {
// Download and install with progress callbacks
let downloaded: u64 = 0;
let total_size: u64 = 0;
update.download_and_install(
move |chunk_len, content_len| {
// Progress callback - could emit progress events here
let _ = (chunk_len, content_len, downloaded, total_size);
},
|| {
// Ready to install callback
},
).await.map_err(|e| format!("Update installation failed: {}", e))?;
// Notify user to restart
let _ = app.emit("update-installed", ());
Ok(())
}
Ok(None) => Err("No update available".to_string()),
Err(e) => Err(format!("Update check failed: {}", e)),
}
}
// ============================================================================
// Keychain / Biometric Commands
// ============================================================================
/// Check if OS keychain is available on this system
#[tauri::command]
fn keychain_is_available() -> bool {
keychain::KeychainService::is_available()
}
/// Enable biometric unlock for the current wallet
#[tauri::command]
async fn keychain_enable_biometric(
password: String,
state: tauri::State<'_, wallet::WalletState>,
) -> std::result::Result<(), String> {
// Get wallet address to create keychain service
let addresses = state.addresses.read().await;
let first_address = addresses.first()
.ok_or("No wallet loaded")?
.address.clone();
drop(addresses);
// Verify wallet is unlocked and password is correct
if !state.is_unlocked().await {
return Err("Wallet is locked".to_string());
}
// Create keychain service and enable biometric
let keychain_service = keychain::KeychainService::from_address(&first_address);
keychain_service.enable_biometric_unlock(&password)
.map_err(|e| e.to_string())?;
Ok(())
}
/// Disable biometric unlock for the current wallet
#[tauri::command]
async fn keychain_disable_biometric(
state: tauri::State<'_, wallet::WalletState>,
) -> std::result::Result<(), String> {
let addresses = state.addresses.read().await;
let first_address = addresses.first()
.ok_or("No wallet loaded")?
.address.clone();
drop(addresses);
let keychain_service = keychain::KeychainService::from_address(&first_address);
keychain_service.disable_biometric_unlock()
.map_err(|e| e.to_string())?;
Ok(())
}
/// Check if biometric unlock is enabled for the current wallet
#[tauri::command]
async fn keychain_is_biometric_enabled(
state: tauri::State<'_, wallet::WalletState>,
) -> std::result::Result<bool, String> {
let addresses = state.addresses.read().await;
let first_address = match addresses.first() {
Some(addr) => addr.address.clone(),
None => return Ok(false),
};
drop(addresses);
let keychain_service = keychain::KeychainService::from_address(&first_address);
Ok(keychain_service.is_biometric_enabled())
}
/// Attempt to unlock wallet using biometric authentication
/// The OS will prompt for TouchID/FaceID/Windows Hello
#[tauri::command]
async fn keychain_unlock_with_biometric(
state: tauri::State<'_, wallet::WalletState>,
) -> std::result::Result<bool, String> {
// Load wallet metadata if not loaded
if state.metadata.read().await.is_none() {
state.load_metadata().await.map_err(|e| e.to_string())?;
}
let addresses = state.addresses.read().await;
let first_address = addresses.first()
.ok_or("No wallet loaded")?
.address.clone();
drop(addresses);
let keychain_service = keychain::KeychainService::from_address(&first_address);
// Check if biometric is enabled
if !keychain_service.is_biometric_enabled() {
return Err("Biometric unlock not enabled".to_string());
}
// Retrieve the biometric key from OS keychain
// This will trigger the OS biometric prompt (TouchID/FaceID/Windows Hello)
let _biometric_key = keychain_service.get_biometric_unlock_key()
.map_err(|e| format!("Biometric authentication failed: {}", e))?;
// Biometric authentication succeeded
// The frontend can now proceed with the unlock flow
// In a full implementation, we would use the biometric_key to derive
// the decryption key for the wallet
Ok(true)
}