04/21 Fix bugs

This commit is contained in:
2026-04-22 22:10:46 -04:00
parent 0aa2432942
commit f507dee19f
5 changed files with 1102 additions and 659 deletions
+76 -1
View File
@@ -170,7 +170,6 @@ ul {
/* Prevent iOS viewport auto-zoom on input focus (requires font-size >= 16px) */
@media (max-width: 768px) {
.form-group input,
.form-group select,
.form-group textarea {
@@ -1140,3 +1139,79 @@ body.is-mobile .form-group textarea {
font-size: 13px;
font-weight: 500;
}
/* ── CSP-safe replacements for former inline styles ─────────────────── */
/* TOTP timer: turns red when ≤5 s remain (toggled via JS class) */
.totp-timer--expiring {
color: #dc2626;
}
/* Generate-password button injected below password field */
.btn-generate {
margin-top: 6px;
}
/* Scroll-lock applied to <html> when mobile sidebar is open */
html.sidebar-open {
overflow: hidden;
}
/* Password Generator modal max-width */
.gen-modal-inner {
max-width: 420px;
}
/* Generator length slider */
#gen-length {
width: 100%;
margin-top: 4px;
}
/* Generator options row */
.gen-options {
display: flex;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 16px;
}
/* Generator form-group spacing */
.gen-form-group {
margin-bottom: 8px;
}
/* Generator input-with-toggle wrapper */
.gen-input-wrap {
margin-bottom: 4px;
}
/* Generator copy button */
#btn-gen-copy {
font-size: 14px;
}
/* Generator output field */
#gen-output {
padding-right: 40px;
}
/* Form field label — "optional" badge */
.field-label-optional {
font-size: 11px;
color: #9ca3af;
font-weight: 400;
}
/* Form field hint text below inputs */
.field-hint {
font-size: 11px;
color: #6b7280;
margin-top: 4px;
}
/* Monospace input — used for recovery codes and other token fields */
.input-monospace {
font-family: monospace;
letter-spacing: 0.05em;
}
+72 -50
View File
@@ -367,7 +367,9 @@ const Vault = (() => {
item.item_type,
);
const showLaunch = item.item_type === "password" && item.plain?.url;
const showTotp = item.item_type === "password" && !!extractTotpSecret(item.plain?.totp_uri);
const showTotp =
item.item_type === "password" &&
!!extractTotpSecret(item.plain?.totp_uri);
li.innerHTML = `
<div class="item-icon">${icon}</div>
@@ -400,7 +402,7 @@ const Vault = (() => {
const secs = totpSecondsLeft();
timerEl.textContent = " (" + secs + "s)";
// Colour the timer red when it's about to expire.
timerEl.style.color = secs <= 5 ? "#dc2626" : "#9ca3af";
timerEl.classList.toggle("totp-timer--expiring", secs <= 5);
}
refreshTotp();
// Refresh every second. Store interval id on the element for cleanup.
@@ -712,7 +714,8 @@ const Vault = (() => {
<span class="share-name">${escHtml(s.item_name)}</span>
<span class="share-meta">From ${escHtml(s.owner_email)}</span>
</div>
${!s.accepted
${
!s.accepted
? `<button class="btn-primary btn-sm" data-accept="${s.id}">Accept</button>`
: `<button class="btn-secondary btn-sm" data-view-share="${s.id}" data-owner-key="${escHtml(s.owner_public_key || "")}" data-enc="${escHtml(s.enc_data)}" data-iv="${escHtml(s.iv)}" data-name="${escHtml(s.item_name)}" data-type="${escHtml(s.item_type || "")}">View</button>`
}
@@ -1202,7 +1205,8 @@ const Vault = (() => {
<span class="em-vault-chevron">▸</span>
</button>
<div class="em-vault-item-body hidden" id="em-vault-body-${idx}">
${hasFields
${
hasFields
? '<div class="detail-body em-detail-body"></div>'
: '<p class="vault-empty">Could not decrypt this item.</p>'
}
@@ -1338,9 +1342,8 @@ const Vault = (() => {
const btn = document.createElement("button");
btn.type = "button";
btn.id = "btn-generate-pass";
btn.className = "btn-secondary btn-sm";
btn.className = "btn-secondary btn-sm btn-generate";
btn.textContent = "⚡ Generate";
btn.style.marginTop = "6px";
pwInput.closest(".form-group").appendChild(btn);
}
@@ -1362,16 +1365,16 @@ const Vault = (() => {
modal.setAttribute("role", "dialog");
modal.setAttribute("aria-modal", "true");
modal.innerHTML = `
<div class="modal" style="max-width:420px">
<div class="modal gen-modal-inner">
<div class="modal-header">
<h3>⚡ Password Generator</h3>
<button class="btn-icon btn-close" id="btn-close-gen-modal" aria-label="Close">✕</button>
</div>
<div class="form-group" style="margin-bottom:8px">
<div class="form-group gen-form-group">
<label>Length: <strong id="gen-length-display">20</strong></label>
<input type="range" id="gen-length" min="8" max="64" value="20" style="width:100%;margin-top:4px">
<input type="range" id="gen-length" min="8" max="64" value="20">
</div>
<div class="gen-options" style="display:flex;gap:16px;flex-wrap:wrap;margin-bottom:16px">
<div class="gen-options">
<label class="gen-opt"><input type="checkbox" id="gen-upper" checked> Uppercase</label>
<label class="gen-opt"><input type="checkbox" id="gen-lower" checked> Lowercase</label>
<label class="gen-opt"><input type="checkbox" id="gen-digits" checked> Numbers</label>
@@ -1379,9 +1382,9 @@ const Vault = (() => {
</div>
<div class="form-group">
<label>Generated Password</label>
<div class="input-with-toggle" style="margin-bottom:4px">
<input type="text" id="gen-output" readonly style="font-family:monospace;letter-spacing:.5px;padding-right:40px">
<button type="button" class="btn-show-pass" id="btn-gen-copy" title="Copy" style="font-size:14px">📋</button>
<div class="input-with-toggle gen-input-wrap">
<input type="text" id="gen-output" readonly>
<button type="button" class="btn-show-pass" id="btn-gen-copy" title="Copy">📋</button>
</div>
<div id="gen-strength" class="password-strength"></div>
</div>
@@ -2274,7 +2277,7 @@ const Vault = (() => {
"X-CSRFToken": csrfToken(),
},
body: JSON.stringify({ refresh_token: refreshToken }),
}).catch(() => { });
}).catch(() => {});
VaultSession.clear();
SharingSession.clear();
sessionStorage.removeItem("access_token");
@@ -2298,7 +2301,9 @@ const Vault = (() => {
try {
const secret = new URL(uri).searchParams.get("secret");
return secret ? secret.toUpperCase().replace(/\s+/g, "") : null;
} catch (e) { return null; }
} catch (e) {
return null;
}
}
// Plain base32 secret (spaces and hyphens stripped for convenience).
return uri.toUpperCase().replace(/[\s-]/g, "") || null;
@@ -2308,14 +2313,18 @@ const Vault = (() => {
function base32ToBytes(b32) {
const CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
b32 = b32.replace(/=+$/, "");
let bits = 0, val = 0;
let bits = 0,
val = 0;
const out = [];
for (const c of b32) {
const idx = CHARS.indexOf(c);
if (idx === -1) continue;
val = (val << 5) | idx;
bits += 5;
if (bits >= 8) { bits -= 8; out.push((val >> bits) & 0xff); }
if (bits >= 8) {
bits -= 8;
out.push((val >> bits) & 0xff);
}
}
return new Uint8Array(out);
}
@@ -2332,15 +2341,28 @@ const Vault = (() => {
const msg = new Uint8Array(8);
// Write counter as big-endian 64-bit integer.
let c = counter;
for (let i = 7; i >= 0; i--) { msg[i] = c & 0xff; c = Math.floor(c / 256); }
for (let i = 7; i >= 0; i--) {
msg[i] = c & 0xff;
c = Math.floor(c / 256);
}
const cryptoKey = await crypto.subtle.importKey(
"raw", keyBytes, { name: "HMAC", hash: "SHA-1" }, false, ["sign"]
"raw",
keyBytes,
{ name: "HMAC", hash: "SHA-1" },
false,
["sign"],
);
const sig = new Uint8Array(
await crypto.subtle.sign("HMAC", cryptoKey, msg),
);
const sig = new Uint8Array(await crypto.subtle.sign("HMAC", cryptoKey, msg));
const offset = sig[19] & 0x0f;
const code = ((sig[offset] & 0x7f) << 24 | sig[offset + 1] << 16 |
sig[offset + 2] << 8 | sig[offset + 3]) % 1_000_000;
const code =
(((sig[offset] & 0x7f) << 24) |
(sig[offset + 1] << 16) |
(sig[offset + 2] << 8) |
sig[offset + 3]) %
1_000_000;
return String(code).padStart(6, "0");
}
@@ -2349,12 +2371,12 @@ const Vault = (() => {
return 30 - (Math.floor(Date.now() / 1000) % 30);
}
function copyToClipboard(text, msg) {
navigator.clipboard
.writeText(text)
.then(() => showToast(msg))
.catch(() => { });
}
.catch(() => {});
}
function escHtml(str) {
return String(str)
@@ -2362,9 +2384,9 @@ const Vault = (() => {
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
}
function itemIcon(type) {
function itemIcon(type) {
return (
{
password: "🔑",
@@ -2375,31 +2397,31 @@ function itemIcon(type) {
ssn: "🪪",
}[type] || "🔑"
);
}
}
function showToast(message, type = "success") {
function showToast(message, type = "success") {
const toast = document.getElementById("toast");
if (!toast) return;
toast.textContent = message;
toast.className = `toast toast-${type} show`;
clearTimeout(toast._timer);
toast._timer = setTimeout(() => toast.classList.remove("show"), 2500);
}
}
function showLoadingState(loading) {
function showLoadingState(loading) {
document
.getElementById("vault-spinner")
?.classList.toggle("hidden", !loading);
}
}
// ── Unlock overlay ────────────────────────────────────────────────────────
// ── Unlock overlay ────────────────────────────────────────────────────────
function showUnlockOverlay() {
function showUnlockOverlay() {
document.getElementById("unlock-overlay").classList.add("open");
document.getElementById("unlock-password").focus();
}
}
async function handleUnlock(e) {
async function handleUnlock(e) {
e.preventDefault();
const password = document.getElementById("unlock-password").value;
const enc_key_salt = sessionStorage.getItem("enc_key_salt");
@@ -2426,11 +2448,11 @@ async function handleUnlock(e) {
btn.textContent = "Unlock";
document.getElementById("unlock-password").value = "";
}
}
}
// ── Tabs ──────────────────────────────────────────────────────────────────
// ── Tabs ──────────────────────────────────────────────────────────────────
function initTabs(container) {
function initTabs(container) {
const tabs = container.querySelectorAll(".tab-btn");
tabs.forEach((btn) => {
btn.addEventListener("click", () => {
@@ -2442,11 +2464,11 @@ function initTabs(container) {
});
});
});
}
}
// ── Init ──────────────────────────────────────────────────────────────────
// ── Init ──────────────────────────────────────────────────────────────────
async function init() {
async function init() {
if (!sessionStorage.getItem("access_token")) {
// Give the extension bridge up to 800 ms to inject a session before redirecting
await new Promise((resolve) => {
@@ -2756,12 +2778,12 @@ async function init() {
function openMobileSidebar() {
sidebar?.classList.add("mobile-open");
backdrop?.classList.add("open");
document.documentElement.style.overflow = "hidden";
document.documentElement.classList.add("sidebar-open");
}
function closeMobileSidebar() {
sidebar?.classList.remove("mobile-open");
backdrop?.classList.remove("open");
document.documentElement.style.overflow = "";
document.documentElement.classList.remove("sidebar-open");
}
mobileMenuBtn?.addEventListener("click", openMobileSidebar);
@@ -2795,9 +2817,9 @@ async function init() {
} else {
loadVault();
}
}
}
async function handleEmergencyFormSubmit(e) {
async function handleEmergencyFormSubmit(e) {
e.preventDefault();
const errEl = document.getElementById("emergency-error");
errEl.classList.add("hidden");
@@ -2827,10 +2849,10 @@ async function handleEmergencyFormSubmit(e) {
errEl.textContent = err.message;
errEl.classList.remove("hidden");
}
}
}
return { init };
}) ();
return { init };
})();
/**
* Mobile/desktop mode detection.
@@ -2863,7 +2885,7 @@ return { init };
if (!e.matches) {
document.getElementById("sidebar")?.classList.remove("mobile-open");
document.getElementById("sidebar-backdrop")?.classList.remove("open");
document.documentElement.style.overflow = "";
document.documentElement.classList.remove("sidebar-open");
}
});
})();
+62 -24
View File
@@ -1,8 +1,5 @@
{% extends "base.html" %}
{% block title %}Recover Account — PassKeeper{% endblock %}
{% block body_class %}auth-page{% endblock %}
{% block body %}
{% extends "base.html" %} {% block title %}Recover Account — PassKeeper{%
endblock %} {% block body_class %}auth-page{% endblock %} {% block body %}
<div class="auth-container">
<div class="auth-card">
<div class="auth-logo">
@@ -13,21 +10,40 @@
<!-- Step 1: Email + Recovery Code -->
<div id="recover-step-1">
<h1 class="auth-title">Recover your account</h1>
<p class="auth-step-hint">Enter your email and the recovery code you saved when you set up account recovery.</p>
<p class="auth-step-hint">
Enter your email and the recovery code you saved when you set up account
recovery.
</p>
<form id="recover-form-step1" novalidate>
<p id="recover-error-1" class="form-error hidden"></p>
<div class="form-group">
<label for="recover-email">Email address</label>
<input type="email" id="recover-email" name="email" required
autocomplete="email" placeholder="you@example.com">
<input
type="email"
id="recover-email"
name="email"
required
autocomplete="email"
placeholder="you@example.com"
/>
</div>
<div class="form-group">
<label for="recover-code">Recovery code</label>
<input type="text" id="recover-code" name="recovery_code" required
autocomplete="off" placeholder="xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx"
style="font-family: monospace; letter-spacing: 0.05em;">
<input
type="text"
id="recover-code"
name="recovery_code"
required
autocomplete="off"
placeholder="xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx"
class="input-monospace"
/>
</div>
<button type="submit" class="btn-primary btn-full" data-loading-text="Verifying…">
<button
type="submit"
class="btn-primary btn-full"
data-loading-text="Verifying…"
>
Verify recovery code
</button>
</form>
@@ -39,34 +55,56 @@
<!-- Step 2: Set new master password (hidden until step 1 verified) -->
<div id="recover-step-2" class="hidden">
<h1 class="auth-title">Set a new master password</h1>
<p class="auth-step-hint">Your recovery code is valid. Choose a new master password — your vault will be re-encrypted automatically.</p>
<p class="auth-step-hint">
Your recovery code is valid. Choose a new master password — your vault
will be re-encrypted automatically.
</p>
<form id="recover-form-step2" novalidate>
<p id="recover-error-2" class="form-error hidden"></p>
<div class="form-group">
<label for="recover-new-pass">New master password</label>
<div class="input-with-toggle">
<input type="password" id="recover-new-pass" name="new_password" required
autocomplete="new-password" placeholder="Minimum 12 characters">
<button type="button" class="btn-show-pass" id="toggle-recover-pass"
aria-label="Toggle password visibility">👁</button>
<input
type="password"
id="recover-new-pass"
name="new_password"
required
autocomplete="new-password"
placeholder="Minimum 12 characters"
/>
<button
type="button"
class="btn-show-pass"
id="toggle-recover-pass"
aria-label="Toggle password visibility"
>
👁
</button>
</div>
</div>
<div class="form-group">
<label for="recover-confirm-pass">Confirm new password</label>
<input type="password" id="recover-confirm-pass" name="confirm_password" required
autocomplete="new-password" placeholder="Repeat new password">
<input
type="password"
id="recover-confirm-pass"
name="confirm_password"
required
autocomplete="new-password"
placeholder="Repeat new password"
/>
</div>
<button type="submit" class="btn-primary btn-full" data-loading-text="Recovering account…">
<button
type="submit"
class="btn-primary btn-full"
data-loading-text="Recovering account…"
>
Recover account
</button>
</form>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{% endblock %} {% block scripts %}
<script src="{{ url_for('static', filename='js/crypto.js') }}"></script>
<script src="{{ url_for('static', filename='js/recover.js') }}"></script>
{% endblock %}
+358 -87
View File
@@ -7,7 +7,11 @@
</button>
<span class="mobile-logo">🔒 PassKeeper</span>
<button class="btn-fab btn-fab-mobile" id="btn-add-item-mobile" title="Add item">
<button
class="btn-fab btn-fab-mobile"
id="btn-add-item-mobile"
title="Add item"
>
+
</button>
</div>
@@ -20,56 +24,109 @@
<div class="sidebar-header">
<span class="logo-icon">🔒</span>
<span class="logo-text">PassKeeper</span>
<button class="btn-sidebar-toggle" id="btn-sidebar-toggle" aria-label="Collapse sidebar" title="Collapse sidebar">
<button
class="btn-sidebar-toggle"
id="btn-sidebar-toggle"
aria-label="Collapse sidebar"
title="Collapse sidebar"
>
&#x276E;
</button>
</div>
<nav class="sidebar-nav">
<ul>
<li class="sidebar-item active" id="sidebar-all" data-view="vault" data-tooltip="All Items">
<li
class="sidebar-item active"
id="sidebar-all"
data-view="vault"
data-tooltip="All Items"
>
<span class="sidebar-icon">🏠</span>
<span class="sidebar-label">All Items</span>
</li>
<li class="sidebar-section-header">Item Types</li>
<li class="sidebar-item" data-type-filter="password" data-tooltip="Passwords">
<li
class="sidebar-item"
data-type-filter="password"
data-tooltip="Passwords"
>
<span class="sidebar-icon">🔑</span>
<span class="sidebar-label">Passwords</span>
</li>
<li class="sidebar-item" data-type-filter="note" data-tooltip="Secure Notes">
<li
class="sidebar-item"
data-type-filter="note"
data-tooltip="Secure Notes"
>
<span class="sidebar-icon">📝</span>
<span class="sidebar-label">Secure Notes</span>
</li>
<li class="sidebar-item" data-type-filter="card" data-tooltip="Payment Cards">
<li
class="sidebar-item"
data-type-filter="card"
data-tooltip="Payment Cards"
>
<span class="sidebar-icon">💳</span>
<span class="sidebar-label">Payment Cards</span>
</li>
<li class="sidebar-item" data-type-filter="bank" data-tooltip="Bank Accounts">
<li
class="sidebar-item"
data-type-filter="bank"
data-tooltip="Bank Accounts"
>
<span class="sidebar-icon">🏦</span>
<span class="sidebar-label">Bank Accounts</span>
</li>
<li class="sidebar-item" data-type-filter="address" data-tooltip="Addresses">
<li
class="sidebar-item"
data-type-filter="address"
data-tooltip="Addresses"
>
<span class="sidebar-icon">🏠</span>
<span class="sidebar-label">Addresses</span>
</li>
<li class="sidebar-item" data-type-filter="ssn" data-tooltip="Identities">
<li
class="sidebar-item"
data-type-filter="ssn"
data-tooltip="Identities"
>
<span class="sidebar-icon">🪪</span>
<span class="sidebar-label">Identities</span>
</li>
<li class="sidebar-section-header">Tools</li>
<li class="sidebar-item" id="sidebar-security" data-view="security" data-tooltip="Security">
<li
class="sidebar-item"
id="sidebar-security"
data-view="security"
data-tooltip="Security"
>
<span class="sidebar-icon">🛡️</span>
<span class="sidebar-label">Security</span>
</li>
<li class="sidebar-item" id="sidebar-sharing" data-view="sharing" data-tooltip="Sharing">
<li
class="sidebar-item"
id="sidebar-sharing"
data-view="sharing"
data-tooltip="Sharing"
>
<span class="sidebar-icon">🔗</span>
<span class="sidebar-label">Sharing</span>
</li>
<li class="sidebar-item" id="sidebar-emergency" data-view="emergency" data-tooltip="Emergency Access">
<li
class="sidebar-item"
id="sidebar-emergency"
data-view="emergency"
data-tooltip="Emergency Access"
>
<span class="sidebar-icon">🚨</span>
<span class="sidebar-label">Emergency Access</span>
</li>
<li class="sidebar-item" id="sidebar-generator" data-tooltip="Password Generator">
<li
class="sidebar-item"
id="sidebar-generator"
data-tooltip="Password Generator"
>
<span class="sidebar-icon"></span>
<span class="sidebar-label">Password Generator</span>
</li>
@@ -81,9 +138,19 @@
</li>
<li id="new-folder-row" class="new-folder-row hidden">
<form id="new-folder-form">
<input type="text" id="new-folder-input" placeholder="Folder name" maxlength="128" />
<input
type="text"
id="new-folder-input"
placeholder="Folder name"
maxlength="128"
/>
<button type="submit" class="btn-icon" title="Save"></button>
<button type="button" class="btn-icon" id="btn-cancel-folder" title="Cancel">
<button
type="button"
class="btn-icon"
id="btn-cancel-folder"
title="Cancel"
>
</button>
</form>
@@ -93,11 +160,19 @@
</nav>
<div class="sidebar-footer">
<button class="sidebar-item btn-text" id="btn-settings" data-tooltip="Settings">
<button
class="sidebar-item btn-text"
id="btn-settings"
data-tooltip="Settings"
>
<span class="sidebar-icon">⚙️</span>
<span class="sidebar-label">Settings</span>
</button>
<button class="sidebar-item btn-text" id="btn-logout" data-tooltip="Sign out">
<button
class="sidebar-item btn-text"
id="btn-logout"
data-tooltip="Sign out"
>
<span class="sidebar-icon">🚪</span>
<span class="sidebar-label">Sign out</span>
</button>
@@ -114,7 +189,12 @@
<div class="vault-toolbar">
<div class="search-wrapper">
<span class="search-icon">🔍</span>
<input type="search" id="search-input" placeholder="Search vault…" autocomplete="off" />
<input
type="search"
id="search-input"
placeholder="Search vault…"
autocomplete="off"
/>
</div>
<div class="sort-wrapper">
<select id="sort-select" title="Sort by">
@@ -224,7 +304,13 @@
</div>
<!-- ── Unlock overlay ──────────────────────────────────────────────── -->
<div id="unlock-overlay" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="unlock-title">
<div
id="unlock-overlay"
class="modal-overlay"
role="dialog"
aria-modal="true"
aria-labelledby="unlock-title"
>
<div class="modal modal-unlock">
<div class="auth-logo unlock-logo">
<span class="logo-icon">🔒</span><span class="logo-text">PassKeeper</span>
@@ -237,8 +323,12 @@
<form id="unlock-form" novalidate>
<div class="form-group">
<label for="unlock-password">Master password</label>
<input type="password" id="unlock-password" autocomplete="current-password"
placeholder="Enter master password" />
<input
type="password"
id="unlock-password"
autocomplete="current-password"
placeholder="Enter master password"
/>
</div>
<div class="modal-actions">
<a href="/login" class="btn-secondary btn-link">Sign out</a>
@@ -249,11 +339,21 @@
</div>
<!-- ── Add / Edit Item Modal ─────────────────────────────────────────── -->
<div id="item-modal" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="modal-title">
<div
id="item-modal"
class="modal-overlay"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
>
<div class="modal modal-item">
<div class="modal-header">
<h3 id="modal-title">Add Item</h3>
<button class="btn-icon btn-close" id="btn-close-modal" aria-label="Close">
<button
class="btn-icon btn-close"
id="btn-close-modal"
aria-label="Close"
>
</button>
</div>
@@ -271,49 +371,92 @@
</div>
<div class="form-group">
<label for="field-name">Name <span class="required">*</span></label>
<input type="text" id="field-name" name="name" required placeholder="e.g. Gmail" />
<input
type="text"
id="field-name"
name="name"
required
placeholder="e.g. Gmail"
/>
</div>
<div class="type-fields" data-for-types="password">
<div class="form-group">
<label for="field-url">Website URL</label><input type="url" id="field-url"
placeholder="https://example.com" />
<label for="field-url">Website URL</label
><input type="url" id="field-url" placeholder="https://example.com" />
</div>
<div class="form-group">
<label for="field-username">Username / Email</label><input type="text" id="field-username" autocomplete="off"
placeholder="username or email" />
<label for="field-username">Username / Email</label
><input
type="text"
id="field-username"
autocomplete="off"
placeholder="username or email"
/>
</div>
<div class="form-group">
<label for="field-password">Password</label>
<div class="input-with-toggle">
<input type="password" id="field-password" autocomplete="new-password" placeholder="Enter password" />
<button type="button" class="btn-show-pass" id="btn-toggle-pass" aria-label="Toggle">
<input
type="password"
id="field-password"
autocomplete="new-password"
placeholder="Enter password"
/>
<button
type="button"
class="btn-show-pass"
id="btn-toggle-pass"
aria-label="Toggle"
>
👁
</button>
</div>
</div>
<div class="form-group">
<label for="field-totp-uri">Authenticator Key (TOTP) <span
style="font-size:11px;color:#9ca3af;font-weight:400;">optional</span></label>
<input type="text" id="field-totp-uri" autocomplete="off"
placeholder="otpauth://totp/… or plain base32 secret" />
<p style="font-size:11px;color:#6b7280;margin-top:4px;">Paste the otpauth URI from a QR code scanner, or the
plain base32 secret key. PassKeeper will generate your 2FA codes.</p>
<label for="field-totp-uri"
>Authenticator Key (TOTP)
<span class="field-label-optional">optional</span></label
>
<input
type="text"
id="field-totp-uri"
autocomplete="off"
placeholder="otpauth://totp/… or plain base32 secret"
/>
<p class="field-hint">
Paste the otpauth URI from a QR code scanner, or the plain base32
secret key. PassKeeper will generate your 2FA codes.
</p>
</div>
</div>
<div class="type-fields hidden" data-for-types="note">
<div class="form-group">
<label for="field-note-body">Note</label><textarea id="field-note-body" rows="6"
placeholder="Enter your secure note…"></textarea>
<label for="field-note-body">Note</label
><textarea
id="field-note-body"
rows="6"
placeholder="Enter your secure note…"
></textarea>
</div>
</div>
<div class="type-fields hidden" data-for-types="card">
<div class="form-group">
<label for="field-cardholder">Cardholder Name</label><input type="text" id="field-cardholder"
placeholder="Name on card" />
<label for="field-cardholder">Cardholder Name</label
><input
type="text"
id="field-cardholder"
placeholder="Name on card"
/>
</div>
<div class="form-group">
<label for="field-card-number">Card Number</label><input type="text" id="field-card-number" autocomplete="off"
placeholder="•••• •••• •••• ••••" maxlength="19" />
<label for="field-card-number">Card Number</label
><input
type="text"
id="field-card-number"
autocomplete="off"
placeholder="•••• •••• •••• ••••"
maxlength="19"
/>
</div>
<div class="form-row">
<div class="form-group">
@@ -335,19 +478,30 @@
</select>
</div>
<div class="form-group">
<label for="field-expiry-year">Year</label><input type="text" id="field-expiry-year" placeholder="YYYY"
maxlength="4" />
<label for="field-expiry-year">Year</label
><input
type="text"
id="field-expiry-year"
placeholder="YYYY"
maxlength="4"
/>
</div>
<div class="form-group">
<label for="field-cvv">CVV</label><input type="text" id="field-cvv" autocomplete="off" placeholder="•••"
maxlength="4" />
<label for="field-cvv">CVV</label
><input
type="text"
id="field-cvv"
autocomplete="off"
placeholder="•••"
maxlength="4"
/>
</div>
</div>
</div>
<div class="type-fields hidden" data-for-types="bank">
<div class="form-group">
<label for="field-bank-name">Bank Name</label><input type="text" id="field-bank-name"
placeholder="e.g. Chase" />
<label for="field-bank-name">Bank Name</label
><input type="text" id="field-bank-name" placeholder="e.g. Chase" />
</div>
<div class="form-group">
<label for="field-account-type">Account Type</label>
@@ -360,56 +514,88 @@
</select>
</div>
<div class="form-group">
<label for="field-routing">Routing Number</label><input type="text" id="field-routing" autocomplete="off"
placeholder="9-digit routing number" maxlength="9" />
<label for="field-routing">Routing Number</label
><input
type="text"
id="field-routing"
autocomplete="off"
placeholder="9-digit routing number"
maxlength="9"
/>
</div>
<div class="form-group">
<label for="field-account-number">Account Number</label><input type="text" id="field-account-number"
autocomplete="off" placeholder="Account number" />
<label for="field-account-number">Account Number</label
><input
type="text"
id="field-account-number"
autocomplete="off"
placeholder="Account number"
/>
</div>
</div>
<div class="type-fields hidden" data-for-types="address">
<div class="form-row">
<div class="form-group">
<label for="field-first-name">First Name</label><input type="text" id="field-first-name"
placeholder="First name" />
<label for="field-first-name">First Name</label
><input
type="text"
id="field-first-name"
placeholder="First name"
/>
</div>
<div class="form-group">
<label for="field-last-name">Last Name</label><input type="text" id="field-last-name"
placeholder="Last name" />
<label for="field-last-name">Last Name</label
><input type="text" id="field-last-name" placeholder="Last name" />
</div>
</div>
<div class="form-group">
<label for="field-company">Company</label><input type="text" id="field-company"
placeholder="Company (optional)" />
<label for="field-company">Company</label
><input
type="text"
id="field-company"
placeholder="Company (optional)"
/>
</div>
<div class="form-group">
<label for="field-address-line">Street Address</label><input type="text" id="field-address-line"
placeholder="123 Main St" />
<label for="field-address-line">Street Address</label
><input
type="text"
id="field-address-line"
placeholder="123 Main St"
/>
</div>
<div class="form-row">
<div class="form-group">
<label for="field-city">City</label><input type="text" id="field-city" placeholder="City" />
<label for="field-city">City</label
><input type="text" id="field-city" placeholder="City" />
</div>
<div class="form-group">
<label for="field-state">State</label><input type="text" id="field-state" placeholder="State" />
<label for="field-state">State</label
><input type="text" id="field-state" placeholder="State" />
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="field-zip">ZIP</label><input type="text" id="field-zip" placeholder="ZIP" />
<label for="field-zip">ZIP</label
><input type="text" id="field-zip" placeholder="ZIP" />
</div>
<div class="form-group">
<label for="field-country">Country</label><input type="text" id="field-country" placeholder="Country" />
<label for="field-country">Country</label
><input type="text" id="field-country" placeholder="Country" />
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="field-phone">Phone</label><input type="tel" id="field-phone" placeholder="+1 555-000-0000" />
<label for="field-phone">Phone</label
><input type="tel" id="field-phone" placeholder="+1 555-000-0000" />
</div>
<div class="form-group">
<label for="field-email">Email</label><input type="email" id="field-email"
placeholder="email@example.com" />
<label for="field-email">Email</label
><input
type="email"
id="field-email"
placeholder="email@example.com"
/>
</div>
</div>
</div>
@@ -417,8 +603,19 @@
<div class="form-group">
<label for="field-ssn-number">Social Security Number</label>
<div class="input-with-toggle">
<input type="password" id="field-ssn-number" autocomplete="off" placeholder="•••-••-••••" maxlength="11" />
<button type="button" class="btn-show-pass" id="btn-toggle-ssn" aria-label="Toggle">
<input
type="password"
id="field-ssn-number"
autocomplete="off"
placeholder="•••-••-••••"
maxlength="11"
/>
<button
type="button"
class="btn-show-pass"
id="btn-toggle-ssn"
aria-label="Toggle"
>
👁
</button>
</div>
@@ -432,8 +629,12 @@
</div>
<div class="type-fields" data-for-types="password card bank address ssn">
<div class="form-group">
<label for="field-notes">Notes</label><textarea id="field-notes" rows="3"
placeholder="Optional notes…"></textarea>
<label for="field-notes">Notes</label
><textarea
id="field-notes"
rows="3"
placeholder="Optional notes…"
></textarea>
</div>
</div>
<div class="modal-actions">
@@ -451,7 +652,11 @@
<div class="modal modal-settings">
<div class="modal-header">
<h3>Account Settings</h3>
<button class="btn-icon btn-close" id="btn-close-settings" aria-label="Close">
<button
class="btn-icon btn-close"
id="btn-close-settings"
aria-label="Close"
>
</button>
</div>
@@ -472,7 +677,13 @@
</p>
<div class="form-group">
<label for="mfa-verify-code">Verification code</label>
<input type="text" id="mfa-verify-code" inputmode="numeric" maxlength="6" placeholder="000000" />
<input
type="text"
id="mfa-verify-code"
inputmode="numeric"
maxlength="6"
placeholder="000000"
/>
</div>
<button class="btn-primary" id="btn-mfa-confirm">Enable MFA</button>
<button class="btn-secondary" id="btn-mfa-cancel-setup">
@@ -481,8 +692,16 @@
</div>
<div id="mfa-disable-area" class="hidden">
<div class="form-group">
<label for="mfa-disable-code">Enter your current code to disable MFA</label>
<input type="text" id="mfa-disable-code" inputmode="numeric" maxlength="6" placeholder="000000" />
<label for="mfa-disable-code"
>Enter your current code to disable MFA</label
>
<input
type="text"
id="mfa-disable-code"
inputmode="numeric"
maxlength="6"
placeholder="000000"
/>
</div>
<button class="btn-primary" id="btn-mfa-disable-confirm">
Disable MFA
@@ -513,15 +732,30 @@
<div id="change-password-area">
<div class="form-group">
<label for="cp-current">Current master password</label>
<input type="password" id="cp-current" autocomplete="current-password" placeholder="Current password" />
<input
type="password"
id="cp-current"
autocomplete="current-password"
placeholder="Current password"
/>
</div>
<div class="form-group">
<label for="cp-new">New master password</label>
<input type="password" id="cp-new" autocomplete="new-password" placeholder="Minimum 12 characters" />
<input
type="password"
id="cp-new"
autocomplete="new-password"
placeholder="Minimum 12 characters"
/>
</div>
<div class="form-group">
<label for="cp-confirm">Confirm new password</label>
<input type="password" id="cp-confirm" autocomplete="new-password" placeholder="Repeat new password" />
<input
type="password"
id="cp-confirm"
autocomplete="new-password"
placeholder="Repeat new password"
/>
</div>
<p id="cp-error" class="form-error hidden"></p>
<button class="btn-primary" id="btn-change-password">
@@ -542,7 +776,11 @@
<div id="recovery-code-display" class="hidden">
<div class="recovery-code-box">
<code id="recovery-code-value"></code>
<button class="btn-icon" id="btn-copy-recovery-code" title="Copy to clipboard">
<button
class="btn-icon"
id="btn-copy-recovery-code"
title="Copy to clipboard"
>
📋
</button>
</div>
@@ -566,8 +804,15 @@
<div id="delete-account-area">
<div id="delete-confirm-area" class="hidden">
<div class="form-group">
<label for="delete-password">Enter your master password to confirm</label>
<input type="password" id="delete-password" autocomplete="current-password" placeholder="Master password" />
<label for="delete-password"
>Enter your master password to confirm</label
>
<input
type="password"
id="delete-password"
autocomplete="current-password"
placeholder="Master password"
/>
</div>
<p id="delete-error" class="form-error hidden"></p>
<div class="modal-actions">
@@ -590,7 +835,11 @@
<div class="modal">
<div class="modal-header">
<h3>Share an Item</h3>
<button class="btn-icon btn-close" id="btn-close-share-modal" aria-label="Close">
<button
class="btn-icon btn-close"
id="btn-close-share-modal"
aria-label="Close"
>
</button>
</div>
@@ -602,7 +851,11 @@
</div>
<div class="form-group">
<label for="share-recipient-email">Recipient's email</label>
<input type="email" id="share-recipient-email" placeholder="recipient@example.com" />
<input
type="email"
id="share-recipient-email"
placeholder="recipient@example.com"
/>
</div>
<div class="modal-actions">
<button type="button" class="btn-secondary" id="btn-cancel-share">
@@ -619,7 +872,11 @@
<div class="modal">
<div class="modal-header">
<h3>Add Emergency Contact</h3>
<button class="btn-icon btn-close" id="btn-close-emergency-modal" aria-label="Close">
<button
class="btn-icon btn-close"
id="btn-close-emergency-modal"
aria-label="Close"
>
</button>
</div>
@@ -631,7 +888,11 @@
</p>
<div class="form-group">
<label for="emergency-email">Trusted contact's email</label>
<input type="email" id="emergency-email" placeholder="contact@example.com" />
<input
type="email"
id="emergency-email"
placeholder="contact@example.com"
/>
</div>
<div class="form-group">
<label for="emergency-wait-days">Wait period (days)</label>
@@ -654,11 +915,21 @@
</div>
<!-- ── Item Detail Modal (read-only: shared items) ────────────────────── -->
<div id="detail-modal" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="detail-modal-title">
<div
id="detail-modal"
class="modal-overlay"
role="dialog"
aria-modal="true"
aria-labelledby="detail-modal-title"
>
<div class="modal modal-detail">
<div class="modal-header">
<h3 id="detail-modal-title">Item Details</h3>
<button class="btn-icon btn-close" id="btn-close-detail-modal" aria-label="Close">
<button
class="btn-icon btn-close"
id="btn-close-detail-modal"
aria-label="Close"
>
</button>
</div>
+84 -47
View File
@@ -12,7 +12,7 @@
// Default: 10 minutes. User can change via the Account view in the popup.
const DEFAULT_IDLE_LOCK_SECONDS = 600;
const IDLE_TIMEOUT_KEY = 'idle_lock_seconds';
const IDLE_TIMEOUT_KEY = "idle_lock_seconds";
/** Apply the idle detection interval, reading the user's saved preference. */
async function applyIdleInterval() {
@@ -24,24 +24,27 @@ async function applyIdleInterval() {
} else {
chrome.idle.setDetectionInterval(Math.max(15, seconds));
}
console.log('[PassKeeper] Idle lock interval:', seconds === 0 ? 'never' : seconds + 's');
console.log(
"[PassKeeper] Idle lock interval:",
seconds === 0 ? "never" : seconds + "s",
);
}
// Apply on every SW startup (SW can be killed and restarted at any time).
applyIdleInterval();
chrome.idle.onStateChanged.addListener(async (newState) => {
if (newState === 'idle' || newState === 'locked') {
if (newState === "idle" || newState === "locked") {
// Check if the user set "Never" before locking.
const stored = await chrome.storage.local.get(IDLE_TIMEOUT_KEY);
if ((stored[IDLE_TIMEOUT_KEY] ?? DEFAULT_IDLE_LOCK_SECONDS) === 0) return;
console.log('[PassKeeper] System', newState, '— locking vault.');
console.log("[PassKeeper] System", newState, "— locking vault.");
await chrome.storage.session.clear();
await chrome.storage.local.remove('vault_items_cs');
await chrome.storage.local.remove("vault_items_cs");
const tabs = await chrome.tabs.query({});
tabs.forEach(tab => {
if (tab.id) chrome.action.setBadgeText({ text: '', tabId: tab.id });
tabs.forEach((tab) => {
if (tab.id) chrome.action.setBadgeText({ text: "", tabId: tab.id });
});
}
});
@@ -50,37 +53,49 @@ chrome.idle.onStateChanged.addListener(async (newState) => {
async function updateBadgeForTab(tabId, url) {
try {
const { vault_items } = await chrome.storage.session.get('vault_items');
const { vault_items } = await chrome.storage.session.get("vault_items");
if (!vault_items?.length) {
chrome.action.setBadgeText({ text: '', tabId });
chrome.action.setBadgeText({ text: "", tabId });
return;
}
let hostname;
try { hostname = new URL(url).hostname.replace(/^www\./, ''); }
catch { chrome.action.setBadgeText({ text: '', tabId }); return; }
const matches = vault_items.filter(item => {
if (item.item_type !== 'password' || !item.plain?.url) return false;
try {
const h = new URL(item.plain.url).hostname.replace(/^www\./, '');
return h === hostname || h.endsWith(`.${hostname}`) || hostname.endsWith(`.${h}`);
} catch { return false; }
hostname = new URL(url).hostname.replace(/^www\./, "");
} catch {
chrome.action.setBadgeText({ text: "", tabId });
return;
}
const matches = vault_items.filter((item) => {
if (item.item_type !== "password" || !item.plain?.url) return false;
try {
const h = new URL(item.plain.url).hostname.replace(/^www\./, "");
return (
h === hostname ||
h.endsWith(`.${hostname}`) ||
hostname.endsWith(`.${h}`)
);
} catch {
return false;
}
});
if (matches.length > 0) {
chrome.action.setBadgeText({ text: String(matches.length), tabId });
chrome.action.setBadgeBackgroundColor({ color: '#1a73e8', tabId });
chrome.action.setBadgeBackgroundColor({ color: "#1a73e8", tabId });
} else {
chrome.action.setBadgeText({ text: '', tabId });
chrome.action.setBadgeText({ text: "", tabId });
}
} catch {
/* tab may have closed */
}
} catch { /* tab may have closed */ }
}
async function refreshAllBadges() {
const tabs = await chrome.tabs.query({});
for (const tab of tabs) {
if (tab.url?.startsWith('http')) updateBadgeForTab(tab.id, tab.url);
if (tab.url?.startsWith("http")) updateBadgeForTab(tab.id, tab.url);
}
}
@@ -89,12 +104,12 @@ async function refreshAllBadges() {
chrome.tabs.onActivated.addListener(async ({ tabId }) => {
try {
const tab = await chrome.tabs.get(tabId);
if (tab.url?.startsWith('http')) updateBadgeForTab(tabId, tab.url);
} catch { }
if (tab.url?.startsWith("http")) updateBadgeForTab(tabId, tab.url);
} catch {}
});
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete' && tab.url?.startsWith('http')) {
if (changeInfo.status === "complete" && tab.url?.startsWith("http")) {
updateBadgeForTab(tabId, tab.url);
}
});
@@ -102,24 +117,30 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
// ── Message handler ──────────────────────────────────────────────────────────
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
// Content script detected a login form on the page — no action needed in the
// background, but we must respond to close the message port cleanly.
if (msg.type === "FORMS_DETECTED") {
sendResponse({ ok: true });
return false;
}
// Popup updated the idle lock timeout → re-apply immediately.
if (msg.type === 'SET_IDLE_TIMEOUT') {
if (msg.type === "SET_IDLE_TIMEOUT") {
applyIdleInterval();
sendResponse({ ok: true });
return false;
}
// Content script requests opening the vault tab (from suggestion dropdown).
if (msg.type === 'OPEN_VAULT') {
chrome.tabs.create({ url: 'https://pwkeeper.ngodanguyen.tech/vault' });
if (msg.type === "OPEN_VAULT") {
chrome.tabs.create({ url: "https://pwkeeper.ngodanguyen.tech/vault" });
sendResponse({ ok: true });
return false;
}
// Content script requests navigating the popup to the generator view.
if (msg.type === 'OPEN_GENERATOR') {
chrome.storage.session.set({ popup_nav: 'generator' });
if (msg.type === "OPEN_GENERATOR") {
chrome.storage.session.set({ popup_nav: "generator" });
chrome.action.openPopup().catch(() => {
// openPopup() requires user gesture in some Chrome versions — fallback is a no-op.
});
@@ -129,20 +150,26 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
// No-op ping from popup — keeps the service worker alive while the browser
// is open so chrome.storage.session is not wiped between popup openings.
if (msg.type === 'KEEPALIVE') {
if (msg.type === "KEEPALIVE") {
sendResponse({ ok: true });
return false;
}
// Popup signals vault cache was refreshed → re-check badges AND forward
// the decrypted items directly to all content scripts (avoids storage read).
if (msg.type === 'VAULT_UPDATED') {
if (msg.type === "VAULT_UPDATED") {
refreshAllBadges();
const payload = { type: 'VAULT_UPDATED', vault_items: msg.vault_items || [] };
const payload = {
type: "VAULT_UPDATED",
vault_items: msg.vault_items || [],
};
chrome.tabs.query({}, function (tabs) {
tabs.forEach(function (tab) {
if (tab.url && (tab.url.startsWith('http://') || tab.url.startsWith('https://'))) {
chrome.tabs.sendMessage(tab.id, payload).catch(function () { });
if (
tab.url &&
(tab.url.startsWith("http://") || tab.url.startsWith("https://"))
) {
chrome.tabs.sendMessage(tab.id, payload).catch(function () {});
}
});
});
@@ -151,7 +178,7 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
}
// Content script detected credentials on form submit → store for save-prompt.
if (msg.type === 'SAVE_CREDENTIALS') {
if (msg.type === "SAVE_CREDENTIALS") {
// Store in local storage so the prompt survives service worker restarts
// and is guaranteed to be present when the user next opens the popup.
chrome.storage.local.set({ pending_save: msg.data });
@@ -160,10 +187,10 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
}
// Bridge: web app logged in → store tokens in extension storage.
if (msg.type === 'WEB_SESSION_SYNC') {
if (msg.type === "WEB_SESSION_SYNC") {
chrome.storage.session.set({
access_token: msg.access_token,
enc_key_salt: msg.enc_key_salt || '',
enc_key_salt: msg.enc_key_salt || "",
});
// Also persist enc_key_salt locally so the unlock-only view survives browser restarts
chrome.storage.local.set({
@@ -175,25 +202,35 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
}
// Bridge: web app logged out → clear extension session.
if (msg.type === 'WEB_SESSION_CLEAR') {
chrome.storage.session.remove(['access_token', 'vault_key_jwk', 'vault_items', 'enc_key_salt']);
chrome.storage.local.remove('refresh_token');
if (msg.type === "WEB_SESSION_CLEAR") {
chrome.storage.session.remove([
"access_token",
"vault_key_jwk",
"vault_items",
"enc_key_salt",
]);
chrome.storage.local.remove("refresh_token");
sendResponse({ ok: true });
return false;
}
// Popup logged in via extension → inject session into open vault tabs.
if (msg.type === 'EXT_SESSION_SYNC') {
chrome.tabs.query({ url: 'https://pwkeeper.ngodanguyen.tech/*' }, tabs => {
tabs.forEach(tab => {
chrome.tabs.sendMessage(tab.id, {
type: 'INJECT_SESSION',
if (msg.type === "EXT_SESSION_SYNC") {
chrome.tabs.query(
{ url: "https://pwkeeper.ngodanguyen.tech/*" },
(tabs) => {
tabs.forEach((tab) => {
chrome.tabs
.sendMessage(tab.id, {
type: "INJECT_SESSION",
access_token: msg.access_token,
refresh_token: msg.refresh_token,
enc_key_salt: msg.enc_key_salt,
}).catch(() => { });
});
})
.catch(() => {});
});
},
);
sendResponse({ ok: true });
return false;
}