cca8184b81
Adds the canonical PM feature: let the user pick a new master password
and have every entry transparently re-encrypted under the new key,
without ever exposing plaintext to the server.
Backend endpoint: POST /change-master-password
==============================================
Body:
{
currentMasterPassword, verified against current stored hash
newMasterPassword, basis for the new hash + new client key
newSalt, 64-char hex, client-generated
entries: [{ id, encrypted_password, iv,
totp_secret?, totp_iv? }, ...]
}
Flow:
1. Authenticate + RequireCSRF (caller already logged in).
2. RejectIfAccountLocked — pw change is brute-forceable through a
hijacked session, so it respects the same per-account lockout as
/login.
3. Verify currentMasterPassword against the stored hash. Branches on
hash_algo to handle both legacy 'pbkdf2' and current 'pbkdf2-sha256'.
Wrong pw → RecordFailedAccountAttempt + audit + 401.
4. Compute new auth hash = SHA256(PBKDF2(new_pw, new_salt, 600k)),
always using the current scheme (migration baked in).
5. ATOMIC transaction:
UPDATE users SET password_hash, salt, kdf_iterations, hash_algo
UPDATE vault_entries SET encrypted_password, iv, totp_secret, totp_iv
(per entry)
Any failure → rollback, user stays on the old config.
6. DeleteAllUserSessions — every OTHER session is invalidated so a
leaked old token can't keep working past the rotation. The current
caller's session stays valid.
7. ClearAccountLockout + audit_log entry.
8. Returns { message, salt, kdfIterations }.
Client
======
New modal in index.html (#changeMasterModal) with three password
fields (current / new / confirm) + inline error display. Added a
"Change master password" button in the Settings panel → Account
section. Escape-key handler routes through it like the other modals.
doChangeMasterPassword():
1. Local validation: all fields filled, new ≥ 8 chars, new == confirm,
new ≠ current. Fast failure beats a round trip.
2. randomHexSalt() → 32 secure random bytes, hex-encoded.
3. Derive newKey = PBKDF2(new_pw, new_salt, 600k).
4. Walk state.entries: decrypt password + (optional) TOTP under the
current key, re-encrypt under newKey with fresh random IVs.
One decrypt failure aborts the whole change — better than partial
commit.
5. POST to /change-master-password.
6. On success: swap state.salt + state.cryptoKey, persistCryptoKey,
update sessionStorage, refresh cached ciphertexts in state.entries,
close modal, toast.
7. On 401 / 429 / generic error: show inline error in the modal so
the user can fix and retry without re-typing everything.
Threat model notes
==================
- The current session token stays valid because the new server hash
only invalidates OTHER sessions. Self-logout would be needlessly
disruptive (user already proved knowledge of both pws).
- Server still sees the old + new master pws transiently in /change-
master-password. Same trade-off as /login — eliminating it requires
redesigning to send pre-computed verifiers (SRP-style), tracked
separately.
- The salt rotates with the password — best-practice against any
precomputed dictionary attack tied to the previous salt.
573 lines
33 KiB
HTML
573 lines
33 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>
|
|
</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>
|
|
</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>
|