🌐 ' + highlightText(e.site, searchQuery) + '';
+ if (showMail) html += '
👤 ' + highlightText(e.username, searchQuery) + '';
if (!showTrash) {
html += '
📁 ' + esc(e.folder || 'All') + '';
html += '
••••••••' +
@@ -610,8 +630,8 @@ function compC(e) {
html += '';
}
html += '
';
- html += '
🌐 ' + esc(e.site) + '';
- if (showMail) html += '
👤 ' + esc(e.username) + '';
+ html += '
🌐 ' + highlightText(e.site, searchQuery) + '';
+ if (showMail) html += '
👤 ' + highlightText(e.username, searchQuery) + '';
if (!showTrash) {
html += '
📁 ' + esc(e.folder || 'All') + '';
html += '
••••••••';
@@ -878,9 +898,10 @@ async function delEntry(id) {
// ==================== UTILS ====================
function searchEntries() {
const input = document.getElementById('searchInput');
+ searchQuery = input.value.trim();
const btn = document.getElementById('clearSearchBtn');
- if (btn) btn.style.display = input.value.trim() ? 'block' : 'none';
- loadEntries(input.value);
+ if (btn) btn.style.display = searchQuery ? 'block' : 'none';
+ loadEntries(searchQuery);
}
function showExportModal() {
const overlay = document.createElement('div');
@@ -916,9 +937,23 @@ function showExportModal() {
playSound('open');
}
function esc(t) { const d = document.createElement('div'); d.textContent = t; return d.innerHTML; }
+function escRegex(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
+function highlightText(text, query) {
+ if (!query || !query.trim()) return esc(text);
+ const re = new RegExp('(' + escRegex(query.trim()) + ')', 'gi');
+ return esc(text).replace(re, '
$1');
+}
+function showShortcutsHelp() {
+ const overlay = document.createElement('div');
+ overlay.className = 'custom-modal-overlay show';
+ overlay.innerHTML = `
⌨️ Keyboard Shortcuts
Ctrl+NNew entryCtrl+FSearchCtrl+TToggle trashCtrl+LLock vaultCtrl+SSave entryEscClose modal / settings?Show this help
`;
+ document.body.appendChild(overlay);
+ overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
+}
function clearSearch() {
const input = document.getElementById('searchInput');
input.value = '';
+ searchQuery = '';
const btn = document.getElementById('clearSearchBtn');
if (btn) btn.style.display = 'none';
loadEntries();
@@ -947,9 +982,8 @@ if (token && curUser) {
document.addEventListener('keypress', e => { if (e.key === 'Enter') { if (document.getElementById('passwordInput') === document.activeElement) addEntry(); else if (document.getElementById('loginPassword') === document.activeElement) login(); else if (document.getElementById('regPassword') === document.activeElement) register(); } });
document.getElementById('genModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGen(); });
document.getElementById('editModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeEdit(); });
-// ==================== KEYBOARD SHORTCUTS (override browser) ====================
+// ==================== KEYBOARD SHORTCUTS ====================
document.addEventListener('keydown', function(e) {
- // Don't fire when typing in inputs / textareas / selects
const tag = document.activeElement?.tagName;
const isInput = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
@@ -967,34 +1001,34 @@ document.addEventListener('keydown', function(e) {
return;
}
- // Only handle Ctrl+key without Shift, Alt, etc.
+ // ? or / to show shortcuts help (only in vault)
+ if ((e.key === '?' || e.key === '/') && !isInput) {
+ if (!document.getElementById('authSection').classList.contains('hidden')) return;
+ e.preventDefault();
+ showShortcutsHelp();
+ return;
+ }
+
+ // Only handle Ctrl+[key], no Shift/Alt/Meta
if (!e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) return;
- if (e.key === 'n' || e.key === 'N') {
- e.preventDefault();
- if (!isInput && !document.getElementById('addModal').classList.contains('show')) {
- openAdd();
- }
- } else if (e.key === 'f' || e.key === 'F') {
- e.preventDefault();
- const searchInput = document.getElementById('searchInput');
- if (searchInput) {
- searchInput.focus();
- searchInput.select();
- }
- } else if (e.key === 't' || e.key === 'T') {
+ // Prevent browser defaults for ALL our shortcuts BEFORE dispatching
+ const code = e.code;
+ if (code === 'KeyN' || code === 'KeyF' || code === 'KeyT' || code === 'KeyL' || code === 'KeyS') {
e.preventDefault();
+ }
+
+ if (code === 'KeyN') {
+ if (!isInput && !document.getElementById('addModal').classList.contains('show')) openAdd();
+ } else if (code === 'KeyF') {
+ const el = document.getElementById('searchInput');
+ if (el) { el.focus(); el.select(); }
+ } else if (code === 'KeyT') {
if (!isInput) toggleTrash();
- } else if (e.key === 'l' || e.key === 'L') {
- e.preventDefault();
+ } else if (code === 'KeyL') {
if (!isInput) doLogout();
- } else if (e.key === 's' || e.key === 'S') {
- e.preventDefault();
- // Save if add/edit modal is open
- if (document.getElementById('addModal').classList.contains('show')) {
- addEntry();
- } else if (document.getElementById('editModal').classList.contains('show')) {
- saveEdit();
- }
+ } else if (code === 'KeyS') {
+ if (document.getElementById('addModal').classList.contains('show')) addEntry();
+ else if (document.getElementById('editModal').classList.contains('show')) saveEdit();
}
});
\ No newline at end of file
diff --git a/security-issues.md b/security-issues.md
new file mode 100644
index 0000000..d6bc218
--- /dev/null
+++ b/security-issues.md
@@ -0,0 +1,8 @@
+# Remaining Security Issues
+
+1. **No rate limiting on `/reauth`** — brute-force possible via export dialog
+2. **No Content Security Policy (CSP)** header — XSS could leak crypto key from sessionStorage
+3. **Crypto key in sessionStorage (extractable)** — necessary for refresh persistence, but XSS can steal it. HttpOnly cookie + service worker is more secure but complex
+4. **No session rotation** — same token until logout; if leaked, valid for 24h
+5. **No 2FA** — opted out of TOTP implementation
+6. **Password generator modulo bias** — `c.charAt(arr[i] % c.length)` has slight bias when c.length does not divide 2^32; not practically exploitable