// Consolidated toast system function showToast(message, type = 'info') { document.querySelector('.toast-notification')?.remove(); const icons = { success: '', error: '', info: '' }; const toast = Object.assign(document.createElement('div'), { className: `toast-notification toast-${type}`, innerHTML: `
${icons[type] || icons.info}
${message}
` }); document.body.appendChild(toast); setTimeout(() => toast.classList.add('toast-show'), 10); setTimeout(() => { if (toast.parentElement) { toast.classList.add('toast-hide'); setTimeout(() => toast.remove(), 300); } }, 4000); } // Enhanced loading states for buttons function setButtonLoading(button, loading = true) { if (loading) { button.dataset.originalText = button.textContent; button.classList.add('loading'); button.disabled = true; } else { button.classList.remove('loading'); button.disabled = false; if (button.dataset.originalText) { button.textContent = button.dataset.originalText; } } } // Enhanced response error handling function getResponseError(response, operation = 'operation') { if (!response) { return `Failed to ${operation}: No response received`; } if (response.success === false || response.error) { const errorMsg = getErrorMessage(response.error, `${operation} failed`); // Handle specific error types if (errorMsg.includes('decryption error') || errorMsg.includes('aead::Error')) { return 'Invalid password or corrupted keyspace data'; } if (errorMsg.includes('Crypto error')) { return 'Keyspace not found or corrupted. Try creating a new one.'; } if (errorMsg.includes('not unlocked') || errorMsg.includes('session')) { return 'Session expired. Please login again.'; } return errorMsg; } return `Failed to ${operation}: Unknown error`; } function showSection(sectionId) { document.querySelectorAll('.section').forEach(s => s.classList.add('hidden')); document.getElementById(sectionId).classList.remove('hidden'); } function setStatus(text, isConnected = false) { const statusText = document.getElementById('statusText'); const statusSection = document.getElementById('vaultStatus'); const lockBtn = document.getElementById('lockBtn'); if (isConnected && text) { // Show keyspace name and status section statusText.textContent = text; statusSection.classList.remove('hidden'); if (lockBtn) { lockBtn.classList.remove('hidden'); } } } // Message handling async function sendMessage(action, data = {}) { return new Promise((resolve) => { chrome.runtime.sendMessage({ action, ...data }, resolve); }); } // Copy to clipboard async function copyToClipboard(text) { try { await navigator.clipboard.writeText(text); showToast('Copied to clipboard!', 'success'); } catch (err) { showToast('Failed to copy', 'error'); } } // Convert string to Uint8Array function stringToUint8Array(str) { return Array.from(new TextEncoder().encode(str)); } // DOM Elements const elements = { // Authentication elements keyspaceInput: document.getElementById('keyspaceInput'), passwordInput: document.getElementById('passwordInput'), createKeyspaceBtn: document.getElementById('createKeyspaceBtn'), loginBtn: document.getElementById('loginBtn'), // Header elements lockBtn: document.getElementById('lockBtn'), themeToggle: document.getElementById('themeToggle'), settingsToggle: document.getElementById('settingsToggle'), settingsDropdown: document.getElementById('settingsDropdown'), timeoutInput: document.getElementById('timeoutInput'), // Keypair management elements toggleAddKeypairBtn: document.getElementById('toggleAddKeypairBtn'), addKeypairCard: document.getElementById('addKeypairCard'), keyTypeSelect: document.getElementById('keyTypeSelect'), keyNameInput: document.getElementById('keyNameInput'), addKeypairBtn: document.getElementById('addKeypairBtn'), cancelAddKeypairBtn: document.getElementById('cancelAddKeypairBtn'), keypairsList: document.getElementById('keypairsList'), // Crypto operation elements - Sign tab messageInput: document.getElementById('messageInput'), signBtn: document.getElementById('signBtn'), signatureResult: document.getElementById('signatureResult'), copySignatureBtn: document.getElementById('copySignatureBtn'), // Crypto operation elements - Encrypt tab encryptMessageInput: document.getElementById('encryptMessageInput'), encryptBtn: document.getElementById('encryptBtn'), encryptResult: document.getElementById('encryptResult'), // Crypto operation elements - Decrypt tab encryptedMessageInput: document.getElementById('encryptedMessageInput'), decryptBtn: document.getElementById('decryptBtn'), decryptResult: document.getElementById('decryptResult'), // Crypto operation elements - Verify tab verifyMessageInput: document.getElementById('verifyMessageInput'), signatureToVerifyInput: document.getElementById('signatureToVerifyInput'), verifyBtn: document.getElementById('verifyBtn'), verifyResult: document.getElementById('verifyResult'), }; // Global state variables let currentKeyspace = null; let selectedKeypairId = null; let backgroundPort = null; let sessionTimeoutDuration = 15; // Default 15 seconds // Session timeout management function handleError(error, context, shouldShowToast = true) { const errorMessage = error?.message || 'An unexpected error occurred'; if (shouldShowToast) { showToast(`${context}: ${errorMessage}`, 'error'); } } function validateInput(value, fieldName, options = {}) { const { minLength = 1, maxLength = 1000, required = true } = options; if (required && (!value || !value.trim())) { showToast(`${fieldName} is required`, 'error'); return false; } if (value && value.length < minLength) { showToast(`${fieldName} must be at least ${minLength} characters`, 'error'); return false; } if (value && value.length > maxLength) { showToast(`${fieldName} must be less than ${maxLength} characters`, 'error'); return false; } return true; } async function loadTimeoutSetting() { const result = await chrome.storage.local.get(['sessionTimeout']); sessionTimeoutDuration = result.sessionTimeout || 15; if (elements.timeoutInput) { elements.timeoutInput.value = sessionTimeoutDuration; } } async function checkSessionTimeout() { const result = await chrome.storage.local.get(['sessionTimedOut']); if (result.sessionTimedOut) { // Clear the flag await chrome.storage.local.remove(['sessionTimedOut']); // Show timeout notification showToast('Session timed out due to inactivity', 'info'); } } async function saveTimeoutSetting(timeout) { sessionTimeoutDuration = timeout; await sendMessage('updateTimeout', { timeout }); } async function resetSessionTimeout() { if (currentKeyspace) { await sendMessage('resetTimeout'); } } // Theme management function initializeTheme() { const savedTheme = localStorage.getItem('cryptovault-theme') || 'light'; document.documentElement.setAttribute('data-theme', savedTheme); updateThemeIcon(savedTheme); } function toggleTheme() { const currentTheme = document.documentElement.getAttribute('data-theme'); const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', newTheme); localStorage.setItem('cryptovault-theme', newTheme); updateThemeIcon(newTheme); } // Settings dropdown management function toggleSettingsDropdown() { const dropdown = elements.settingsDropdown; if (dropdown) { dropdown.classList.toggle('hidden'); } } function closeSettingsDropdown() { const dropdown = elements.settingsDropdown; if (dropdown) { dropdown.classList.add('hidden'); } } function updateThemeIcon(theme) { const themeToggle = elements.themeToggle; if (!themeToggle) return; if (theme === 'dark') { // Bright sun SVG for dark theme themeToggle.innerHTML = ` `; themeToggle.title = 'Switch to light mode'; } else { // Dark crescent moon SVG for light theme themeToggle.innerHTML = ` `; themeToggle.title = 'Switch to dark mode'; } } // Establish connection to background script for keep-alive function connectToBackground() { backgroundPort = chrome.runtime.connect({ name: 'popup' }); // Listen for messages from background script backgroundPort.onMessage.addListener((message) => { if (message.type === 'sessionTimeout') { // Update UI state to reflect locked session currentKeyspace = null; selectedKeypairId = null; setStatus('', false); showSection('authSection'); clearVaultState(); // Clear form inputs if (elements.keyspaceInput) elements.keyspaceInput.value = ''; if (elements.passwordInput) elements.passwordInput.value = ''; // Show timeout notification showToast(message.message, 'info'); } }); backgroundPort.onDisconnect.addListener(() => { backgroundPort = null; }); } // Initialize document.addEventListener('DOMContentLoaded', async function() { // Initialize theme first initializeTheme(); // Load timeout setting await loadTimeoutSetting(); // Ensure lock button starts hidden const lockBtn = document.getElementById('lockBtn'); if (lockBtn) { lockBtn.classList.add('hidden'); } // Connect to background script for keep-alive connectToBackground(); // Consolidated event listeners const eventMap = { createKeyspaceBtn: createKeyspace, loginBtn: login, lockBtn: lockSession, themeToggle: toggleTheme, settingsToggle: toggleSettingsDropdown, toggleAddKeypairBtn: toggleAddKeypairForm, addKeypairBtn: addKeypair, cancelAddKeypairBtn: hideAddKeypairForm, signBtn: signMessage, encryptBtn: encryptMessage, decryptBtn: decryptMessage, verifyBtn: verifySignature }; Object.entries(eventMap).forEach(([elementKey, handler]) => { elements[elementKey]?.addEventListener('click', handler); }); // Tab functionality initializeTabs(); // Additional event listeners elements.copySignatureBtn?.addEventListener('click', () => { copyToClipboard(document.getElementById('signatureValue')?.textContent); }); elements.messageInput?.addEventListener('input', () => { if (elements.signBtn) { elements.signBtn.disabled = !elements.messageInput.value.trim() || !selectedKeypairId; } }); // Timeout setting event listener elements.timeoutInput?.addEventListener('change', async (e) => { const timeout = parseInt(e.target.value); if (timeout >= 3 && timeout <= 300) { await saveTimeoutSetting(timeout); } else { e.target.value = sessionTimeoutDuration; // Reset to current value if invalid } }); // Activity detection - reset timeout on any interaction document.addEventListener('click', (e) => { resetSessionTimeout(); // Close settings dropdown if clicking outside if (!elements.settingsToggle?.contains(e.target) && !elements.settingsDropdown?.contains(e.target)) { closeSettingsDropdown(); } }); document.addEventListener('keydown', resetSessionTimeout); document.addEventListener('input', resetSessionTimeout); // Keyboard shortcuts document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && !elements.addKeypairCard?.classList.contains('hidden')) { hideAddKeypairForm(); } if (e.key === 'Enter' && e.target === elements.keyNameInput && elements.keyNameInput.value.trim()) { e.preventDefault(); addKeypair(); } }); // Check for existing session await checkExistingSession(); }); async function checkExistingSession() { try { const response = await sendMessage('getStatus'); if (response && response.success && response.status && response.session) { // Session is active currentKeyspace = response.session.keyspace; elements.keyspaceInput.value = currentKeyspace; setStatus(currentKeyspace, true); showSection('vaultSection'); await loadKeypairs(); } else { // No active session setStatus('', false); showSection('authSection'); } } catch (error) { setStatus('', false); showSection('authSection'); } } // Toggle add keypair form function toggleAddKeypairForm() { const isHidden = elements.addKeypairCard.classList.contains('hidden'); if (isHidden) { showAddKeypairForm(); } else { hideAddKeypairForm(); } } function showAddKeypairForm() { elements.addKeypairCard.classList.remove('hidden'); elements.keyNameInput.focus(); } function hideAddKeypairForm() { elements.addKeypairCard.classList.add('hidden'); // Clear the form elements.keyNameInput.value = ''; elements.keyTypeSelect.selectedIndex = 0; } // Tab functionality function initializeTabs() { const tabContainer = document.querySelector('.operation-tabs'); if (tabContainer) { // Use event delegation for better performance tabContainer.addEventListener('click', (e) => { if (e.target.classList.contains('tab-btn')) { handleTabSwitch(e.target); } }); } // Initialize input validation initializeInputValidation(); } function handleTabSwitch(clickedTab) { const targetTab = clickedTab.getAttribute('data-tab'); const tabButtons = document.querySelectorAll('.tab-btn'); const tabContents = document.querySelectorAll('.tab-content'); // Remove active class from all tabs and contents tabButtons.forEach(btn => btn.classList.remove('active')); tabContents.forEach(content => content.classList.remove('active')); // Add active class to clicked tab and corresponding content clickedTab.classList.add('active'); const targetContent = document.getElementById(`${targetTab}-tab`); if (targetContent) { targetContent.classList.add('active'); } // Clear results when switching tabs clearTabResults(); // Update button states updateButtonStates(); } function clearTabResults() { // Hide all result sections (with null checks) if (elements.signatureResult) { elements.signatureResult.classList.add('hidden'); elements.signatureResult.innerHTML = ''; } if (elements.encryptResult) { elements.encryptResult.classList.add('hidden'); elements.encryptResult.innerHTML = ''; } if (elements.decryptResult) { elements.decryptResult.classList.add('hidden'); elements.decryptResult.innerHTML = ''; } if (elements.verifyResult) { elements.verifyResult.classList.add('hidden'); elements.verifyResult.innerHTML = ''; } } function initializeInputValidation() { // Sign tab validation (with null checks) if (elements.messageInput) { elements.messageInput.addEventListener('input', updateButtonStates); } // Encrypt tab validation (with null checks) if (elements.encryptMessageInput) { elements.encryptMessageInput.addEventListener('input', updateButtonStates); } // Decrypt tab validation (with null checks) if (elements.encryptedMessageInput) { elements.encryptedMessageInput.addEventListener('input', updateButtonStates); } // Verify tab validation (with null checks) if (elements.verifyMessageInput) { elements.verifyMessageInput.addEventListener('input', updateButtonStates); } if (elements.signatureToVerifyInput) { elements.signatureToVerifyInput.addEventListener('input', updateButtonStates); } } function updateButtonStates() { // Sign button (with null checks) if (elements.signBtn && elements.messageInput) { elements.signBtn.disabled = !elements.messageInput.value.trim() || !selectedKeypairId; } // Encrypt button (with null checks) - only needs message and keyspace session if (elements.encryptBtn && elements.encryptMessageInput) { elements.encryptBtn.disabled = !elements.encryptMessageInput.value.trim() || !currentKeyspace; } // Decrypt button (with null checks) - only needs encrypted message and keyspace session if (elements.decryptBtn && elements.encryptedMessageInput) { elements.decryptBtn.disabled = !elements.encryptedMessageInput.value.trim() || !currentKeyspace; } // Verify button (with null checks) - only needs message and signature if (elements.verifyBtn && elements.verifyMessageInput && elements.signatureToVerifyInput) { elements.verifyBtn.disabled = !elements.verifyMessageInput.value.trim() || !elements.signatureToVerifyInput.value.trim() || !selectedKeypairId; } } // Clear all vault-related state and UI function clearVaultState() { // Clear all crypto operation inputs (with null checks) if (elements.messageInput) elements.messageInput.value = ''; if (elements.encryptMessageInput) elements.encryptMessageInput.value = ''; if (elements.encryptedMessageInput) elements.encryptedMessageInput.value = ''; if (elements.verifyMessageInput) elements.verifyMessageInput.value = ''; if (elements.signatureToVerifyInput) elements.signatureToVerifyInput.value = ''; // Clear all result sections clearTabResults(); // Clear signature value with null check const signatureValue = document.getElementById('signatureValue'); if (signatureValue) signatureValue.textContent = ''; // Clear selected keypair state selectedKeypairId = null; updateButtonStates(); // Hide add keypair form if open hideAddKeypairForm(); // Clear keypairs list if (elements.keypairsList) { elements.keypairsList.innerHTML = '
Loading keypairs...
'; } } // Validation utilities const validateAuth = () => { const keyspace = elements.keyspaceInput.value.trim(); const password = elements.passwordInput.value.trim(); if (!validateInput(keyspace, 'Keyspace name', { minLength: 1, maxLength: 100 })) { return null; } if (!validateInput(password, 'Password', { minLength: 1, maxLength: 1000 })) { return null; } return { keyspace, password }; }; async function createKeyspace() { const auth = validateAuth(); if (!auth) return; try { await executeOperation( async () => { const response = await sendMessage('createKeyspace', auth); if (response?.success) { clearVaultState(); await login(); // Auto-login after creation return response; } else { throw new Error(getResponseError(response, 'create keyspace')); } }, { loadingElement: elements.createKeyspaceBtn, successMessage: 'Keyspace created successfully!', maxRetries: 1 } ); } catch (error) { handleError(error, 'Create keyspace'); } } async function login() { const auth = validateAuth(); if (!auth) return; try { await executeOperation( async () => { const response = await sendMessage('initSession', auth); if (response?.success) { currentKeyspace = auth.keyspace; setStatus(auth.keyspace, true); showSection('vaultSection'); clearVaultState(); await loadKeypairs(); return response; } else { throw new Error(getResponseError(response, 'login')); } }, { loadingElement: elements.loginBtn, successMessage: 'Logged in successfully!', maxRetries: 2 } ); } catch (error) { handleError(error, 'Create keyspace'); } } async function lockSession() { try { await sendMessage('lockSession'); currentKeyspace = null; selectedKeypairId = null; setStatus('', false); showSection('authSection'); // Clear all form inputs elements.keyspaceInput.value = ''; elements.passwordInput.value = ''; clearVaultState(); showToast('Session locked', 'info'); } catch (error) { showToast('Error: ' + error.message, 'error'); } } async function addKeypair() { const keyType = elements.keyTypeSelect.value; const keyName = elements.keyNameInput.value.trim(); if (!keyName) { showToast('Please enter a name for the keypair', 'error'); return; } await executeOperation( async () => { const metadata = JSON.stringify({ name: keyName }); const response = await sendMessage('addKeypair', { keyType, metadata }); if (response?.success) { hideAddKeypairForm(); await loadKeypairs(); return response; } else { throw new Error(getResponseError(response, 'add keypair')); } }, { loadingElement: elements.addKeypairBtn, successMessage: 'Keypair added successfully!' } ); } async function loadKeypairs() { const response = await sendMessage('listKeypairs'); if (response && response.success) { renderKeypairs(response.keypairs); } else { const errorMsg = getResponseError(response, 'load keypairs'); const container = elements.keypairsList; container.innerHTML = '
Failed to load keypairs. Try refreshing.
'; showToast(errorMsg, 'error'); } } function renderKeypairs(keypairs) { const container = elements.keypairsList; // Simple array handling const keypairArray = Array.isArray(keypairs) ? keypairs : []; if (keypairArray.length === 0) { container.innerHTML = '
No keypairs found. Add one above.
'; return; } container.innerHTML = keypairArray.map((keypair) => { const metadata = typeof keypair.metadata === 'string' ? JSON.parse(keypair.metadata) : keypair.metadata; return `
${metadata.name || 'Unnamed'}
${keypair.key_type}
`; }).join(''); // Add event listeners to all keypair cards container.querySelectorAll('.keypair-item').forEach(card => { card.addEventListener('click', (e) => { const keypairId = e.currentTarget.getAttribute('data-keypair-id'); selectKeypair(keypairId); }); // Add keyboard support card.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); const keypairId = e.currentTarget.getAttribute('data-keypair-id'); selectKeypair(keypairId); } }); }); // Restore selection state if there was a previously selected keypair if (selectedKeypairId) { updateKeypairSelection(selectedKeypairId); } } async function selectKeypair(keyId) { // Don't show loading overlay for selection - it's too disruptive try { // Update visual state immediately for better UX updateKeypairSelection(keyId); await sendMessage('selectKeypair', { keyId }); selectedKeypairId = keyId; // Get keypair details for internal use (but don't show the card) const metadataResponse = await sendMessage('getCurrentKeypairMetadata'); const publicKeyResponse = await sendMessage('getCurrentKeypairPublicKey'); if (metadataResponse && metadataResponse.success && publicKeyResponse && publicKeyResponse.success) { // Enable sign button if message is entered updateButtonStates(); } else { // Handle metadata or public key fetch failure const metadataError = getResponseError(metadataResponse, 'get keypair metadata'); const publicKeyError = getResponseError(publicKeyResponse, 'get public key'); const errorMsg = metadataResponse && !metadataResponse.success ? metadataError : publicKeyError; updateKeypairSelection(null); showToast(errorMsg, 'error'); } } catch (error) { const errorMsg = getErrorMessage(error, 'Failed to select keypair'); // Revert visual state if there was an error updateKeypairSelection(null); showToast(errorMsg, 'error'); } } function updateKeypairSelection(selectedId) { // Remove previous selection styling from all keypair items const allKeypairs = document.querySelectorAll('.keypair-item'); allKeypairs.forEach(item => { item.classList.remove('selected'); }); // Add selection styling to the selected keypair (if any) if (selectedId) { const selectedKeypair = document.querySelector(`[data-keypair-id="${selectedId}"]`); if (selectedKeypair) { selectedKeypair.classList.add('selected'); } } } // Shared templates const copyIcon = ` `; const createResultContainer = (label, value, btnId) => `
${value}
`; // Unified crypto operation handler async function performCryptoOperation(config) { const { input, validation, action, resultElement, button, successMsg, resultProcessor } = config; if (!validation()) { showToast(config.errorMsg, 'error'); return; } await executeOperation( async () => { const response = await sendMessage(action, input()); if (response?.success) { resultElement.classList.remove('hidden'); resultElement.innerHTML = resultProcessor(response); // Add copy button listener if result has copy button const copyBtn = resultElement.querySelector('.btn-copy'); if (copyBtn && config.copyValue) { copyBtn.addEventListener('click', () => copyToClipboard(config.copyValue(response))); } return response; } else { throw new Error(getResponseError(response, action)); } }, { loadingElement: button, successMessage: successMsg } ); } // Crypto operation functions using shared templates const signMessage = () => performCryptoOperation({ validation: () => elements.messageInput.value.trim() && selectedKeypairId, errorMsg: 'Please enter a message and select a keypair', action: 'sign', input: () => ({ message: stringToUint8Array(elements.messageInput.value.trim()) }), resultElement: elements.signatureResult, button: elements.signBtn, successMsg: 'Message signed successfully!', copyValue: (response) => response.signature, resultProcessor: (response) => createResultContainer('Signature', response.signature, 'copySignatureBtn') }); const encryptMessage = () => performCryptoOperation({ validation: () => elements.encryptMessageInput.value.trim() && currentKeyspace, errorMsg: 'Please enter a message and ensure you are connected to a keyspace', action: 'encrypt', input: () => ({ message: elements.encryptMessageInput.value.trim() }), resultElement: elements.encryptResult, button: elements.encryptBtn, successMsg: 'Message encrypted successfully!', copyValue: (response) => response.encryptedMessage, resultProcessor: (response) => createResultContainer('Encrypted Message', response.encryptedMessage, 'copyEncryptedBtn') }); const decryptMessage = () => performCryptoOperation({ validation: () => elements.encryptedMessageInput.value.trim() && currentKeyspace, errorMsg: 'Please enter encrypted message and ensure you are connected to a keyspace', action: 'decrypt', input: () => ({ encryptedMessage: elements.encryptedMessageInput.value.trim() }), resultElement: elements.decryptResult, button: elements.decryptBtn, successMsg: 'Message decrypted successfully!', copyValue: (response) => response.decryptedMessage, resultProcessor: (response) => createResultContainer('Decrypted Message', response.decryptedMessage, 'copyDecryptedBtn') }); const verifySignature = () => performCryptoOperation({ validation: () => elements.verifyMessageInput.value.trim() && elements.signatureToVerifyInput.value.trim() && selectedKeypairId, errorMsg: 'Please enter message, signature, and select a keypair', action: 'verify', input: () => ({ message: stringToUint8Array(elements.verifyMessageInput.value.trim()), signature: elements.signatureToVerifyInput.value.trim() }), resultElement: elements.verifyResult, button: elements.verifyBtn, successMsg: null, resultProcessor: (response) => { const isValid = response.isValid; const icon = isValid ? ` ` : ` `; const text = isValid ? 'Signature is valid' : 'Signature is invalid'; return `
${icon} ${text}
`; } });