Official app icon and Windows Hello unlock (beta)
Icons: the taskbar, title bar, tray and installer icons are now the Kestrove mark (kestrel wing and keyhole on the Spruce tile) instead of the placeholder keyhole. Generated with `tauri icon` from icons/kestrove-mark.svg, the brand mark with the eye in the `accent` token (#b4481f), as the brand book asks. Windows Hello (docs/02 "Local unlock"): Settings > General > "Unlock with Windows Hello". Turning it on creates a TPM-backed KeyCredentialManager key for this vault, which signs a fixed label plus a random salt; HKDF of the deterministic RSA signature seals the core's Local Unlock Key with XChaCha20-Poly1305 in windows-hello.json. The unlock screen then offers "Unlock with Windows Hello" above the master password. - The master password is still required every 14 days; Hello is not offered while it is due, and the LUK is kept. - A cancelled prompt falls back to the password. The LUK is deleted only when Hello's key is gone or doesn't open it. - Hello calls block, so they run on a blocking thread; the prompt is brought in front of the window. - Not offered on Linux or where Windows Hello isn't set up. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
+54
-2
@@ -3,15 +3,16 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use std::{io, thread};
|
||||
|
||||
use kestrove_desktop::autolock::{self, Activity, Reason};
|
||||
use kestrove_desktop::broker::{Broker, PairingStore, PairingView, SaveView, Ui};
|
||||
use kestrove_desktop::passkey::PasskeyView;
|
||||
use kestrove_desktop::device_key::DeviceKey;
|
||||
use kestrove_desktop::hello::{self, HelloError};
|
||||
use kestrove_desktop::passkey::PasskeyView;
|
||||
use kestrove_desktop::sync::{
|
||||
self, AddDeviceView, DeviceView, JoinConfirmView, JoinPollView, JoinView, SetupView, SignIn, SyncService,
|
||||
SyncStatus,
|
||||
@@ -34,6 +35,8 @@ struct App {
|
||||
data_dir: PathBuf,
|
||||
ipc_error: Mutex<Option<String>>,
|
||||
idle_minutes: Arc<AtomicU64>,
|
||||
/// Windows Hello is set up on this computer (checked off the main thread; see `refresh_hello`).
|
||||
hello_available: Arc<AtomicBool>,
|
||||
// Kept alive: on X11 the clipboard owner must outlive the copy.
|
||||
clipboard: Mutex<Option<arboard::Clipboard>>,
|
||||
}
|
||||
@@ -42,6 +45,14 @@ impl App {
|
||||
fn settings_path(&self) -> PathBuf {
|
||||
self.data_dir.join("settings.json")
|
||||
}
|
||||
|
||||
/// Re-checks Windows Hello in the background (it can be set up or removed while Kestrove runs).
|
||||
fn refresh_hello(&self) {
|
||||
let available = self.hello_available.clone();
|
||||
let _ = thread::Builder::new().name("hello-check".into()).spawn(move || {
|
||||
available.store(hello::is_supported(), Ordering::Relaxed);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -58,6 +69,8 @@ struct AppState {
|
||||
/// Browser integration could not start (e.g. another instance is running).
|
||||
ipc_error: Option<String>,
|
||||
idle_minutes: u64,
|
||||
/// Unlock with Windows Hello: can be turned on, is on, is offered on the unlock screen now.
|
||||
hello: hello::Status,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -179,6 +192,7 @@ fn state(app: State<'_, App>) -> AppState {
|
||||
remembers_secret_key: app.sync.stored_secret_key().is_some(),
|
||||
ipc_error: app.ipc_error.lock().unwrap_or_else(|e| e.into_inner()).clone(),
|
||||
idle_minutes: app.idle_minutes.load(Ordering::Relaxed),
|
||||
hello: hello::status(&app.core, &app.data_dir, app.hello_available.load(Ordering::Relaxed)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,11 +224,45 @@ async fn unlock(app: State<'_, App>, master_password: String, secret_key: Option
|
||||
if let Some(sk) = typed {
|
||||
app.sync.remember_secret_key(&sk);
|
||||
}
|
||||
hello::password_unlocked(&app.data_dir);
|
||||
app.activity.touch();
|
||||
app.sync.request_sync();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Windows Hello calls block until the prompt closes, so they run off the main thread and the async runtime.
|
||||
async fn off_thread<T: Send + 'static>(f: impl FnOnce() -> T + Send + 'static) -> CmdResult<T> {
|
||||
tauri::async_runtime::spawn_blocking(f).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Unlock with Windows Hello. `false` when the prompt was cancelled (the password field stays).
|
||||
#[tauri::command]
|
||||
async fn hello_unlock(app: State<'_, App>) -> CmdResult<bool> {
|
||||
let (core, dir) = (app.core.clone(), app.data_dir.clone());
|
||||
match off_thread(move || hello::unlock(&core, &dir)).await? {
|
||||
Ok(()) => {
|
||||
app.activity.touch();
|
||||
app.sync.request_sync();
|
||||
Ok(true)
|
||||
}
|
||||
Err(HelloError::Cancelled) => Ok(false),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Settings → "Unlock with Windows Hello". `false` when turning it on was cancelled.
|
||||
#[tauri::command]
|
||||
async fn set_hello(app: State<'_, App>, enabled: bool) -> CmdResult<bool> {
|
||||
let (core, dir) = (app.core.clone(), app.data_dir.clone());
|
||||
let r = off_thread(move || if enabled { hello::enable(&core, &dir) } else { hello::disable(&core, &dir) }).await?;
|
||||
app.refresh_hello();
|
||||
match r {
|
||||
Ok(()) => Ok(true),
|
||||
Err(HelloError::Cancelled) => Ok(false),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn lock(app: State<'_, App>) -> CmdResult<()> {
|
||||
app.core.lock();
|
||||
@@ -713,8 +761,10 @@ fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
data_dir,
|
||||
ipc_error: Mutex::new(ipc_error),
|
||||
idle_minutes,
|
||||
hello_available: Arc::new(AtomicBool::new(false)),
|
||||
clipboard: Mutex::new(None),
|
||||
});
|
||||
app.state::<App>().refresh_hello();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -733,6 +783,8 @@ fn main() {
|
||||
set_title_bar_colors,
|
||||
create_account,
|
||||
unlock,
|
||||
hello_unlock,
|
||||
set_hello,
|
||||
lock,
|
||||
touch,
|
||||
set_idle_minutes,
|
||||
|
||||
Reference in New Issue
Block a user