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:
+45
-5
@@ -8,7 +8,8 @@ import {
|
||||
pill, ring, secret, tile, type IconName,
|
||||
} from "./ui/kv";
|
||||
|
||||
interface AppState { has_account: boolean; locked: boolean; remembers_secret_key: boolean; ipc_error: string | null; idle_minutes: number }
|
||||
interface HelloStatus { available: boolean; enabled: boolean; offered: boolean }
|
||||
interface AppState { has_account: boolean; locked: boolean; remembers_secret_key: boolean; ipc_error: string | null; idle_minutes: number; hello: HelloStatus }
|
||||
interface ItemSummary { id: string; title: string; username: string | null; urls: string[]; favorite: boolean; has_totp: boolean; tags: string[] }
|
||||
interface SignInView { provider: string; provider_name: string; account: string | null }
|
||||
interface ItemDto { id: string; title: string; username: string | null; password: string | null; urls: string[]; totp: string | null; notes: string | null; tags: string[]; sign_in_with: SignInView | null }
|
||||
@@ -474,7 +475,22 @@ function renderUnlock(state: AppState) {
|
||||
const pw = input({ type: "password", autocomplete: "current-password" });
|
||||
const sk = input({ placeholder: "K1-XXXXXX-XXXXX-…", spellcheck: false }, true);
|
||||
const err = errBox();
|
||||
const btn = button("Unlock", "primary", undefined, "block");
|
||||
// Windows Hello, when offered, is the screen's one primary action; the password stays below it.
|
||||
const btn = button("Unlock", state.hello.offered ? "quiet" : "primary", undefined, "block");
|
||||
const hello = button("Unlock with Windows Hello", "primary", undefined, "block");
|
||||
hello.onclick = async () => {
|
||||
hello.disabled = true;
|
||||
err.replaceChildren();
|
||||
try {
|
||||
if (await invoke<boolean>("hello_unlock")) await render();
|
||||
else hello.disabled = false;
|
||||
} catch (e) {
|
||||
setErr(err, e);
|
||||
// A reset turned Windows Hello off: drop the button.
|
||||
if (!(await invoke<AppState>("state")).hello.offered) hello.remove();
|
||||
else hello.disabled = false;
|
||||
}
|
||||
};
|
||||
const submit = async () => {
|
||||
btn.disabled = true;
|
||||
err.replaceChildren();
|
||||
@@ -494,11 +510,15 @@ function renderUnlock(state: AppState) {
|
||||
pw.onkeydown = (e) => {
|
||||
if (e.key === "Enter") void submit();
|
||||
};
|
||||
const card = h("div", "kv-card auth-card", h("div", "unlock-mark", lockup(32)), h("h1", "kv-title-1", "Unlock your vault"), field("Master password", pw));
|
||||
const card = h("div", "kv-card auth-card", h("div", "unlock-mark", lockup(32)), h("h1", "kv-title-1", "Unlock your vault"));
|
||||
if (state.hello.offered) card.append(hello, p("kv-small kv-muted", "Or use your master password:"));
|
||||
else if (state.hello.enabled) card.append(p("kv-small kv-muted", "Kestrove asks for your master password every 14 days, even with Windows Hello on."));
|
||||
card.append(field("Master password", pw));
|
||||
if (!state.remembers_secret_key) card.append(field("Secret Key", sk, "It's on your Emergency Kit. Dashes are optional."));
|
||||
card.append(err, btn);
|
||||
centered(card);
|
||||
pw.focus();
|
||||
if (state.hello.offered) hello.focus();
|
||||
else pw.focus();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ vault
|
||||
@@ -854,6 +874,25 @@ async function renderSettings(target: HTMLElement, state: AppState) {
|
||||
else await publishTheme();
|
||||
await renderSettings(target, state);
|
||||
};
|
||||
const helloSwitch = input({ type: "checkbox", checked: state.hello.enabled });
|
||||
helloSwitch.className = "switch";
|
||||
helloSwitch.setAttribute("aria-label", "Unlock with Windows Hello");
|
||||
const helloErr = errBox();
|
||||
helloSwitch.onchange = async () => {
|
||||
helloSwitch.disabled = true;
|
||||
helloErr.replaceChildren();
|
||||
try {
|
||||
if (!(await invoke<boolean>("set_hello", { enabled: helloSwitch.checked }))) helloSwitch.checked = !helloSwitch.checked;
|
||||
} catch (e) {
|
||||
helloSwitch.checked = !helloSwitch.checked;
|
||||
setErr(helloErr, e);
|
||||
}
|
||||
helloSwitch.disabled = false;
|
||||
};
|
||||
const helloRow = state.hello.available || state.hello.enabled
|
||||
? h("div", "kv-frow", h("div", "val", h("span", "", "Unlock with Windows Hello"),
|
||||
h("span", "kv-hint", "Your face, fingerprint or PIN instead of your master password. Kestrove still asks for the password every 14 days."), helloErr), helloSwitch)
|
||||
: null;
|
||||
const list = h("div", "kv-card rows");
|
||||
for (const pr of pairings) {
|
||||
list.append(h("div", "kv-frow",
|
||||
@@ -875,7 +914,8 @@ async function renderSettings(target: HTMLElement, state: AppState) {
|
||||
h("div", "kv-frow", h("div", "val", h("span", "", "Light or dark")), theme),
|
||||
h("div", "kv-frow", h("div", "val", h("span", "", "Same theme on all my devices"),
|
||||
h("span", "kv-hint", "Saved in your encrypted vault, and used on every device where this is on.")), syncTheme),
|
||||
h("div", "kv-frow", h("div", "val", h("span", "", "Lock after idle for"), h("span", "kv-hint", "Kestrove also locks on screen lock and sleep.")), idle))),
|
||||
h("div", "kv-frow", h("div", "val", h("span", "", "Lock after idle for"), h("span", "kv-hint", "Kestrove also locks on screen lock and sleep.")), idle),
|
||||
helloRow)),
|
||||
section("Sync", syncSection),
|
||||
section("Devices", devicesSection),
|
||||
section("Emergency Kit", kitSection()),
|
||||
|
||||
Reference in New Issue
Block a user