Official app icon and Windows Hello unlock (beta)
ci / build (ubuntu-22.04) (push) Canceled after 0s
ci / build (windows-2025) (push) Canceled after 0s
ci / deny (push) Canceled after 0s

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:
2026-10-30 00:14:32 +04:00
co-authored by Claude Opus 5.5
parent 10a9507aa2
commit 27f13b4743
23 changed files with 552 additions and 7 deletions
+45 -5
View File
@@ -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()),