Files
Password-Manager/index.html
T
Zaki 749dc87058 feat(unlock): Quick unlock via DPAPI (remember on this device)
User-controlled opt-in to skip the master-password prompt on subsequent
app starts. The vault state (raw AES key + salt + username + session
token) is bundled and handed to the Delphi side, which DPAPI-encrypts
it with CRYPTPROTECT_CURRENT_USER and stashes the blob at
%LOCALAPPDATA%\PMServer\quickunlock.bin.

Honest threat model
===================
This is NOT biometric authentication. The DPAPI scope is the Windows
USER ACCOUNT — any process running as the same user can decrypt the
blob via the same DPAPI call. The security perimeter is the Windows
account itself. The Settings UI label is "Quick unlock" with an
explainer:
  "convenient on a personal machine, not safe on a shared one"

If the user has Windows Hello / fingerprint / PIN configured at the
OS level, then Windows login is biometric-gated and that gating
transitively applies to DPAPI access — but the cryptographic strength
of the encryption isn't tied to the biometric, it's tied to the
Windows account secret. Honest framing matters here, so the feature
isn't sold as "biometric".

Backend
=======
New unit Source/PM.QuickUnlock.pas:
 - StoreQuickUnlock(bytes) → DPAPI-encrypt and persist to
   %LOCALAPPDATA%\PMServer\quickunlock.bin
 - LoadQuickUnlock(out bytes) → read file, DPAPI-decrypt
 - ClearQuickUnlock → forget-me
 - HasQuickUnlock → file existence probe
DPAPI declarations are local (CryptProtectData / CryptUnprotectData
from crypt32.dll) — Winapi.WinCrypt's signatures drift across Delphi
versions and we don't want to fight that.

Bridge commands (UMainForm.HandleBridgeCommand):
 cmd://quickunlock/store?data=<base64>   payload opaque to Delphi
 cmd://quickunlock/get                   → ExecuteJavaScript callback
                                           Bridge.onQuickUnlockResult(b64|null)
 cmd://quickunlock/clear                 forget-me
 cmd://quickunlock/status                → Bridge.onQuickUnlockStatus(bool)

The get / status results are returned via ExecuteJavaScript rather than
HTTP (the bridge is request-only) — JS resolves a Promise that the
caller awaited.

Client
======
state.quickUnlockEnabled mirrors localStorage flag, lazy-cleared if the
backing DPAPI blob has gone missing (e.g., user reset Windows profile).

enableQuickUnlock():
  1. askReauth + /reauth to verify it's actually the user.
  2. exportKey('raw', state.cryptoKey) — extractable already.
  3. JSON-bundle { v, username, salt, token, csrf, key } → base64.
  4. cmd://quickunlock/store sends the blob to Delphi.

tryQuickUnlock() (called from init):
  1. Probe localStorage flag.
  2. cmd://quickunlock/get, await Bridge.onQuickUnlockResult.
  3. Decode JSON, importKey, restore state.* + sessionStorage.
  4. Return true on success, false to fall through to master-pw login.

Two restore scenarios both covered:
 A. Same app session (sessionStorage still populated, only cryptoKey
    was wiped by lock). tryQuickUnlock just restores the key.
 B. Cold start (sessionStorage empty). tryQuickUnlock restores
    EVERYTHING from the DPAPI blob, including the session token.

UI
==
Settings panel → new "Quick unlock" section above Recovery key.
Single toggle button: "Enable on this device" / "Disable" with status
line above. Opens settings → bridgeQuickUnlockStatus() reconciles the
JS-side flag with the actual file (drift detection).

Stale-blob protection
=====================
The stored blob holds the AES key BYTES, which would become useless
if the vault were re-encrypted under a different key. Three paths
that re-encrypt the vault now also wipe the DPAPI blob:
 - Explicit doLogout (user said "I'm done")
 - Master password change (new key, old blob can't decrypt anything)
 - (Recovery redeem already forces master pw change → covered.)

The blob persists across the passive lockVault() flow on purpose —
that's the whole point: lock without losing convenience.

Init wiring
===========
On app start, the existing "restore session from sessionStorage" path
now falls through to tryQuickUnlock if either sessionStorage is empty
OR the cryptoKey is gone. Auth screen shows up only after both
attempts fail.
2026-05-23 11:25:39 +01:00

612 lines
35 KiB
HTML

<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vault</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<!-- ============================================================ -->
<!-- ICON SPRITE (Lucide subset) -->
<!-- ============================================================ -->
<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></symbol>
<symbol id="i-unlock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 9.9-1"/></symbol>
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></symbol>
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5v14"/></symbol>
<symbol id="i-folder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></symbol>
<symbol id="i-star" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></symbol>
<symbol id="i-trash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></symbol>
<symbol id="i-eye" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></symbol>
<symbol id="i-eye-off" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.88 9.88a3 3 0 1 0 4.24 4.24M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61M2 2l20 20"/></symbol>
<symbol id="i-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></symbol>
<symbol id="i-edit" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1Z"/></symbol>
<symbol id="i-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></symbol>
<symbol id="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79Z"/></symbol>
<symbol id="i-globe" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10Z"/></symbol>
<symbol id="i-dice" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8" cy="8" r="1.2" fill="currentColor"/><circle cx="16" cy="8" r="1.2" fill="currentColor"/><circle cx="12" cy="12" r="1.2" fill="currentColor"/><circle cx="8" cy="16" r="1.2" fill="currentColor"/><circle cx="16" cy="16" r="1.2" fill="currentColor"/></symbol>
<symbol id="i-tag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.59 13.41 13.42 20.58a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82Z"/><line x1="7" y1="7" x2="7.01" y2="7"/></symbol>
<symbol id="i-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></symbol>
<symbol id="i-log-in" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" y1="12" x2="3" y2="12"/></symbol>
<symbol id="i-rotate-ccw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5"/></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></symbol>
<symbol id="i-command" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 3a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3H6a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3V6a3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3h12a3 3 0 0 0 3-3 3 3 0 0 0-3-3Z"/></symbol>
<symbol id="i-log-out" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9"/></symbol>
<symbol id="i-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></symbol>
<symbol id="i-list" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></symbol>
<symbol id="i-more" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1.4" fill="currentColor"/><circle cx="12" cy="12" r="1.4" fill="currentColor"/><circle cx="12" cy="19" r="1.4" fill="currentColor"/></symbol>
<symbol id="i-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></symbol>
<symbol id="i-empty-vault" viewBox="0 0 120 120" fill="none">
<circle cx="60" cy="60" r="50" fill="var(--accent-soft)"/>
<rect x="40" y="50" width="40" height="35" rx="4" fill="var(--bg-elev)" stroke="var(--accent)" stroke-width="2"/>
<path d="M48 50V40a12 12 0 0 1 24 0v10" fill="none" stroke="var(--accent)" stroke-width="2" stroke-linecap="round"/>
<circle cx="60" cy="65" r="3" fill="var(--accent)"/>
<line x1="60" y1="68" x2="60" y2="74" stroke="var(--accent)" stroke-width="2" stroke-linecap="round"/>
</symbol>
<symbol id="i-empty-trash" viewBox="0 0 120 120" fill="none">
<circle cx="60" cy="60" r="50" fill="var(--accent-soft)"/>
<path d="M40 45h40M48 45v-5a3 3 0 0 1 3-3h18a3 3 0 0 1 3 3v5M44 45v35a4 4 0 0 0 4 4h24a4 4 0 0 0 4-4V45" stroke="var(--accent)" stroke-width="2" stroke-linecap="round" fill="none"/>
<path d="M54 55v22M66 55v22" stroke="var(--accent)" stroke-width="2" stroke-linecap="round"/>
</symbol>
<symbol id="i-empty-search" viewBox="0 0 120 120" fill="none">
<circle cx="60" cy="60" r="50" fill="var(--accent-soft)"/>
<circle cx="55" cy="55" r="14" stroke="var(--accent)" stroke-width="2.5" fill="none"/>
<path d="m66 66 11 11" stroke="var(--accent)" stroke-width="2.5" stroke-linecap="round"/>
</symbol>
</svg>
<!-- ============================================================ -->
<!-- TOAST CONTAINER -->
<!-- ============================================================ -->
<div id="toastContainer" class="toast-container"></div>
<!-- ============================================================ -->
<!-- IDLE WARNING (auto-lock countdown) -->
<!-- ============================================================ -->
<div id="idleWarning" class="idle-warning is-hidden">
<svg class="idle-icon"><use href="#i-lock"/></svg>
<h3>Auto-lock</h3>
<p>Vault locks in <b id="idleCountdown">30</b>s due to inactivity</p>
<button class="btn btn-primary btn-block" id="idleStayBtn">Stay unlocked</button>
</div>
<!-- ============================================================ -->
<!-- AUTH SCREEN (login + register) -->
<!-- ============================================================ -->
<section id="authScreen" class="auth-screen">
<div class="auth-card">
<div class="auth-brand">
<svg class="auth-brand-icon"><use href="#i-lock"/></svg>
<h1>Vault</h1>
<p>Your passwords, encrypted locally.</p>
</div>
<div class="auth-tabs">
<button class="auth-tab is-active" data-tab="login">Login</button>
<button class="auth-tab" data-tab="register">Register</button>
</div>
<div id="authHint" class="auth-hint"></div>
<form id="loginForm" class="auth-form" autocomplete="off">
<label class="field">
<span>Username</span>
<input id="loginUsername" type="text" required>
</label>
<label class="field">
<span>Master password</span>
<input id="loginPassword" type="password" required>
</label>
<button id="loginBtn" type="submit" class="btn btn-primary btn-block">
<svg><use href="#i-unlock"/></svg>
Unlock
</button>
<button id="passkeyBtn" type="button" class="btn btn-ghost btn-block">
<svg><use href="#i-lock"/></svg>
Use a passkey
</button>
<button id="recoveryBtn" type="button" class="btn btn-link btn-block" style="font-size:12px;margin-top:4px">
Forgot master password? Use a recovery code
</button>
</form>
<form id="registerForm" class="auth-form is-hidden" autocomplete="off">
<label class="field">
<span>Username <span class="hint">min 3 chars</span></span>
<input id="regUsername" type="text" required minlength="3">
</label>
<label class="field">
<span>Master password <span class="hint">min 8 chars</span></span>
<input id="regPassword" type="password" required minlength="8">
<div class="strength-bar" id="regStrengthBar"></div>
</label>
<button id="registerBtn" type="submit" class="btn btn-primary btn-block">
<svg><use href="#i-plus"/></svg>
Create vault
</button>
</form>
</div>
</section>
<!-- ============================================================ -->
<!-- APP SHELL (sidebar + main + slide-over) -->
<!-- ============================================================ -->
<div id="appShell" class="app-shell is-hidden">
<!-- ---- SIDEBAR ----------------------------------------- -->
<aside class="sidebar">
<div class="sidebar-brand">
<svg><use href="#i-lock"/></svg>
<span>Vault</span>
</div>
<nav class="sidebar-nav">
<button class="nav-item is-active" data-view="all">
<svg><use href="#i-globe"/></svg>
<span>All items</span>
<span class="nav-count" id="countAll">0</span>
</button>
<button class="nav-item" data-view="favorites">
<svg><use href="#i-star"/></svg>
<span>Favorites</span>
<span class="nav-count" id="countFav">0</span>
</button>
</nav>
<div class="sidebar-section">
<div class="sidebar-section-header">
<span>Folders</span>
<button class="icon-btn icon-btn-sm" id="addFolderBtn" title="New folder">
<svg><use href="#i-plus"/></svg>
</button>
</div>
<nav class="sidebar-nav" id="foldersList"></nav>
</div>
<div class="sidebar-section">
<div class="sidebar-section-header"><span>Tags</span></div>
<nav class="sidebar-nav" id="tagsList"></nav>
</div>
<div class="sidebar-section">
<div class="sidebar-section-header"><span>Tools</span></div>
<nav class="sidebar-nav">
<button class="nav-item" id="sidebarGenBtn">
<svg><use href="#i-dice"/></svg>
<span>Generator</span>
</button>
<button class="nav-item" id="sidebarImportBtn">
<svg><use href="#i-log-in"/></svg>
<span>Import vault</span>
</button>
<button class="nav-item" id="sidebarExportBtn">
<svg><use href="#i-log-out"/></svg>
<span>Export vault</span>
</button>
</nav>
</div>
<div class="sidebar-bottom">
<button class="nav-item" data-view="trash">
<svg><use href="#i-trash"/></svg>
<span>Trash</span>
<span class="nav-count" id="countTrash">0</span>
</button>
<button class="nav-item" id="settingsBtn">
<svg><use href="#i-settings"/></svg>
<span>Settings</span>
</button>
</div>
</aside>
<!-- ---- MAIN -------------------------------------------- -->
<main class="main">
<header class="topbar">
<div class="search">
<svg><use href="#i-search"/></svg>
<input id="searchInput" type="search" placeholder="Search…" autocomplete="off">
<kbd>Ctrl+K</kbd>
</div>
<div class="topbar-actions">
<div class="view-toggle">
<button class="view-btn is-active" id="viewCardsBtn" data-view="cards" title="Cards view">
<svg><use href="#i-grid"/></svg>
</button>
<button class="view-btn" id="viewListBtn" data-view="list" title="List view">
<svg><use href="#i-list"/></svg>
</button>
</div>
<button class="icon-btn" id="themeBtn" title="Toggle theme">
<svg class="theme-icon theme-icon-dark"><use href="#i-sun"/></svg>
<svg class="theme-icon theme-icon-light"><use href="#i-moon"/></svg>
</button>
<button class="btn btn-primary btn-sm" id="newEntryBtn">
<svg><use href="#i-plus"/></svg>
New
</button>
<div class="user-menu">
<button class="user-chip" id="userChip">
<span id="userName">user</span>
</button>
<div class="user-dropdown is-hidden" id="userDropdown">
<button class="dropdown-item" id="lockBtn">
<svg><use href="#i-lock"/></svg> Lock vault
</button>
<button class="dropdown-item" id="logoutBtn">
<svg><use href="#i-log-out"/></svg> Sign out
</button>
</div>
</div>
</div>
</header>
<div class="content">
<div class="content-header">
<h2 id="contentTitle">All items</h2>
<span class="content-meta" id="contentMeta">0 items</span>
</div>
<!-- Entry grid -->
<div id="entryGrid" class="entry-grid"></div>
<!-- Empty state — illustration + message are filled dynamically -->
<div id="emptyState" class="empty-state is-hidden">
<svg class="empty-illustration" id="emptyIllustration"><use href="#i-empty-vault"/></svg>
<h3 id="emptyTitle">Nothing here yet</h3>
<p id="emptyMessage">Click <b>+ New</b> to add your first password.</p>
</div>
</div>
</main>
<!-- ---- SLIDE-OVER DETAIL ------------------------------- -->
<aside class="slideover" id="slideover">
<header class="slideover-header">
<h3 id="slideoverTitle">Entry</h3>
<button class="icon-btn" id="slideoverClose"><svg><use href="#i-x"/></svg></button>
</header>
<div class="slideover-body" id="slideoverBody"></div>
</aside>
<!-- ---- SETTINGS SLIDE-OVER ----------------------------- -->
<aside class="slideover" id="settingsPanel">
<header class="slideover-header">
<h3>Settings</h3>
<button class="icon-btn" id="settingsClose"><svg><use href="#i-x"/></svg></button>
</header>
<div class="slideover-body">
<div class="slideover-field">
<div class="slideover-field-label">Appearance</div>
<div class="setting-row">
<span>Theme</span>
<select id="settingTheme">
<option value="dark">Dark</option>
<option value="light">Light</option>
</select>
</div>
<div class="setting-row">
<span>Compact action menu (⋯)</span>
<label class="toggle">
<input type="checkbox" id="settingCompact">
<span class="toggle-slider"></span>
</label>
</div>
<div class="setting-row">
<span>Mask usernames (ab***)</span>
<label class="toggle">
<input type="checkbox" id="settingMaskUser">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="slideover-field">
<div class="slideover-field-label">Security</div>
<div class="setting-row">
<span>Auto-lock after (minutes)</span>
<select id="settingAutoLock">
<option value="0">Never</option>
<option value="1">1</option>
<option value="5">5</option>
<option value="15">15</option>
<option value="30">30</option>
<option value="60">60</option>
</select>
</div>
<div class="setting-row">
<span>Ask before moving to trash</span>
<label class="toggle">
<input type="checkbox" id="settingAskDelete">
<span class="toggle-slider"></span>
</label>
</div>
<div class="setting-row">
<span>
Check passwords against breach database (HIBP)
<small class="setting-hint">Only the first 5 chars of the SHA-1 are sent — your passwords never leave the machine.</small>
</span>
<label class="toggle">
<input type="checkbox" id="settingHIBP">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="slideover-field">
<div class="slideover-field-label">Clipboard privacy</div>
<p style="font-size:12px;color:var(--text-dim);margin:0 0 8px;line-height:1.5">
Windows keeps copied passwords in <b>clipboard history (Win+V)</b>.
To prevent that, disable history in <i>Settings → System → Clipboard</i>.
</p>
<button class="btn btn-ghost btn-sm" id="openClipboardSettings">Why?</button>
</div>
<div class="slideover-field">
<div class="slideover-field-label">Export</div>
<p style="font-size:12px;color:var(--text-dim);margin:0 0 8px;line-height:1.5">
Download an <b>encrypted</b> backup of your vault. You'll
choose a password independent of your master password —
save it carefully, you need it to restore.
</p>
<button class="btn btn-ghost btn-sm" id="exportBtn">
<svg><use href="#i-log-out"/></svg> Export vault
</button>
</div>
<div class="slideover-field">
<div class="slideover-field-label">Import</div>
<p style="font-size:12px;color:var(--text-dim);margin:0 0 8px;line-height:1.5">
Import a vault from a JSON export (this app) or a CSV file
(Bitwarden, KeePass, Chrome, 1Password…). Entries are added
to your current vault — duplicates are NOT removed.
</p>
<button class="btn btn-ghost btn-sm" id="importBtn">
<svg><use href="#i-log-in"/></svg> Import vault
</button>
</div>
<div class="slideover-field">
<div class="slideover-field-label">Account</div>
<p style="font-size:12px;color:var(--text-dim);margin:0 0 8px">
Signed in as <b id="settingUser"></b>
</p>
<button class="btn btn-ghost btn-sm" id="changeMasterBtn">
<svg><use href="#i-lock"/></svg> Change master password
</button>
</div>
<div class="slideover-field">
<div class="slideover-field-label">Quick unlock</div>
<p id="quickUnlockStatus" style="font-size:12px;color:var(--text-dim);margin:0 0 8px;line-height:1.5">
Disabled.
</p>
<p style="font-size:11px;color:var(--text-faint);margin:0 0 8px;line-height:1.4">
Unlock the vault on this device without retyping your
master password. The key is stored encrypted with
Windows DPAPI (tied to your Windows account) — convenient
on a personal machine, not safe on a shared one.
</p>
<button class="btn btn-ghost btn-sm" id="quickUnlockToggleBtn">
Enable on this device
</button>
</div>
<div class="slideover-field">
<div class="slideover-field-label">Recovery key</div>
<p id="recoveryStatus" style="font-size:12px;color:var(--text-dim);margin:0 0 8px;line-height:1.5">
No recovery key set.
</p>
<p style="font-size:11px;color:var(--text-faint);margin:0 0 8px;line-height:1.4">
A single-use code that lets you recover access if you
forget your master password. Generate it once, save the
code somewhere offline — the server never sees it again.
</p>
<div style="display:flex;gap:6px;flex-wrap:wrap">
<button class="btn btn-ghost btn-sm" id="recoverySetupBtn">
Generate recovery code
</button>
<button class="btn btn-ghost btn-sm is-danger" id="recoveryRemoveBtn" style="display:none">
Remove
</button>
</div>
</div>
</div>
</aside>
<!-- ============================================================ -->
<!-- MODAL: Re-auth (for export and other sensitive ops) -->
<!-- ============================================================ -->
<div id="reauthModal" class="modal is-hidden" role="dialog" aria-modal="true">
<div class="modal-backdrop" data-close></div>
<div class="modal-panel modal-panel-sm">
<header class="modal-header">
<h3 id="reauthTitle">Confirm master password</h3>
<button class="icon-btn" data-close><svg><use href="#i-x"/></svg></button>
</header>
<form id="reauthForm" class="modal-body" autocomplete="off">
<p style="margin:0 0 12px;color:var(--text-dim);font-size:13px" id="reauthMessage">
This action requires your master password.
</p>
<label class="field">
<span>Master password</span>
<input id="reauthPassword" type="password" required autofocus>
</label>
</form>
<footer class="modal-footer">
<button type="button" class="btn btn-ghost" data-close>Cancel</button>
<button type="submit" form="reauthForm" class="btn btn-primary" id="reauthConfirm">
<svg><use href="#i-check"/></svg> Confirm
</button>
</footer>
</div>
</div>
<!-- ============================================================ -->
<!-- MODAL: Change Master Password -->
<!-- ============================================================ -->
<div id="changeMasterModal" class="modal is-hidden" role="dialog" aria-modal="true">
<div class="modal-backdrop" data-close></div>
<div class="modal-panel modal-panel-sm">
<header class="modal-header">
<h3>Change master password</h3>
<button class="icon-btn" data-close><svg><use href="#i-x"/></svg></button>
</header>
<form id="changeMasterForm" class="modal-body" autocomplete="off">
<p style="margin:0 0 12px;color:var(--text-dim);font-size:13px;line-height:1.5">
Your vault will be re-encrypted with the new key.
All other sessions will be signed out.
</p>
<label class="field">
<span>Current master password</span>
<input id="cmCurrentPwd" type="password" required>
</label>
<label class="field">
<span>New master password (min 8 chars)</span>
<input id="cmNewPwd" type="password" required minlength="8">
</label>
<label class="field">
<span>Confirm new master password</span>
<input id="cmConfirmPwd" type="password" required>
</label>
<p id="cmError" style="margin:8px 0 0;color:#dc2626;font-size:12px;display:none"></p>
</form>
<footer class="modal-footer">
<button type="button" class="btn btn-ghost" data-close>Cancel</button>
<button type="submit" form="changeMasterForm" class="btn btn-primary" id="cmConfirmBtn">
<svg><use href="#i-check"/></svg> Change password
</button>
</footer>
</div>
</div>
</div>
<!-- ============================================================ -->
<!-- MODAL: New / Edit Entry -->
<!-- ============================================================ -->
<div id="entryModal" class="modal is-hidden" role="dialog" aria-modal="true">
<div class="modal-backdrop" data-close></div>
<div class="modal-panel">
<header class="modal-header">
<h3 id="entryModalTitle">New entry</h3>
<button class="icon-btn" data-close><svg><use href="#i-x"/></svg></button>
</header>
<form id="entryForm" class="modal-body" autocomplete="off">
<input type="hidden" id="entryId">
<label class="field">
<span>Website / App</span>
<input id="entrySite" type="text" required placeholder="example.com">
</label>
<label class="field">
<span>Username / Email</span>
<input id="entryUsername" type="text" placeholder="me@example.com">
</label>
<label class="field">
<span>Password</span>
<div class="input-with-action">
<input id="entryPassword" type="password" required placeholder="••••••••">
<button type="button" class="icon-btn" id="entryPwToggle"><svg><use href="#i-eye"/></svg></button>
<button type="button" class="icon-btn" id="entryPwGen" title="Generate"><svg><use href="#i-dice"/></svg></button>
</div>
<div class="strength-bar" id="entryStrengthBar"></div>
</label>
<label class="field">
<span>Folder</span>
<select id="entryFolder"></select>
</label>
<label class="field">
<span>Tags <span class="hint">enter to add</span></span>
<div class="chip-input" id="entryTagsInput">
<input id="entryTagsField" type="text" placeholder="add a tag…" autocomplete="off">
</div>
<input type="hidden" id="entryTags">
</label>
</form>
<footer class="modal-footer">
<button class="btn btn-ghost" data-close>Cancel</button>
<button class="btn btn-primary" id="entrySaveBtn">
<svg><use href="#i-check"/></svg> Save
</button>
</footer>
</div>
</div>
<!-- ============================================================ -->
<!-- MODAL: Password Generator -->
<!-- ============================================================ -->
<div id="genModal" class="modal is-hidden" role="dialog" aria-modal="true">
<div class="modal-backdrop" data-close></div>
<div class="modal-panel modal-panel-sm">
<header class="modal-header">
<h3>Password generator</h3>
<button class="icon-btn" data-close><svg><use href="#i-x"/></svg></button>
</header>
<div class="modal-body">
<div class="gen-preview" id="genPreview">click regenerate</div>
<label class="field">
<span>Length <b id="genLenLabel">16</b></span>
<input id="genLen" type="range" min="6" max="128" value="16">
</label>
<div class="gen-options">
<label class="check"><input type="checkbox" id="genUpper" checked><span>A-Z</span></label>
<label class="check"><input type="checkbox" id="genLower" checked><span>a-z</span></label>
<label class="check"><input type="checkbox" id="genNum" checked><span>0-9</span></label>
<label class="check"><input type="checkbox" id="genSym" checked><span>!@#</span></label>
</div>
</div>
<footer class="modal-footer">
<button class="btn btn-ghost" id="genRegen"><svg><use href="#i-rotate-ccw"/></svg> Regenerate</button>
<button class="btn btn-ghost" id="genCopy"><svg><use href="#i-copy"/></svg> Copy</button>
<button class="btn btn-primary" id="genUse"><svg><use href="#i-check"/></svg> Use</button>
</footer>
</div>
</div>
<!-- ============================================================ -->
<!-- CONFIRM / PROMPT MODAL (replaces native confirm/prompt) -->
<!-- ============================================================ -->
<div id="confirmModal" class="modal is-hidden" role="dialog" aria-modal="true">
<div class="modal-backdrop" data-confirm-cancel></div>
<div class="modal-panel modal-panel-sm">
<header class="modal-header">
<h3 id="confirmTitle">Confirm</h3>
<button class="icon-btn" data-confirm-cancel><svg><use href="#i-x"/></svg></button>
</header>
<form id="confirmForm" class="modal-body">
<p id="confirmMessage" style="margin:0 0 12px;color:var(--text-dim);font-size:13px;line-height:1.5">
Are you sure?
</p>
<label class="field is-hidden" id="confirmInputField">
<input id="confirmInput" type="text" autocomplete="off">
</label>
</form>
<footer class="modal-footer">
<button type="button" class="btn btn-ghost" data-confirm-cancel id="confirmCancelBtn">Cancel</button>
<button type="submit" form="confirmForm" class="btn btn-primary" id="confirmOkBtn">
<svg><use href="#i-check"/></svg> Confirm
</button>
</footer>
</div>
</div>
<!-- ============================================================ -->
<!-- COMMAND PALETTE (Cmd+K) — phase 3, shell only for now -->
<!-- ============================================================ -->
<div id="cmdPalette" class="cmd-palette is-hidden" role="dialog" aria-modal="true">
<div class="modal-backdrop" data-close></div>
<div class="cmd-panel">
<div class="cmd-input">
<svg><use href="#i-command"/></svg>
<input id="cmdInput" type="text" placeholder="Type a command or search…">
</div>
<div class="cmd-results" id="cmdResults"></div>
</div>
</div>
<script src="js/app.js"></script>
</body>
</html>