// Enhanced toast system function showToast(message, type = 'info') { // Remove any existing toast const existingToast = document.querySelector('.toast-notification'); if (existingToast) { existingToast.remove(); } // Create new toast element const toast = document.createElement('div'); toast.className = `toast-notification toast-${type}`; // Add icon based on type const icon = getToastIcon(type); toast.innerHTML = `
${icon}
${message}
`; // Add to document document.body.appendChild(toast); // Trigger entrance animation setTimeout(() => toast.classList.add('toast-show'), 10); // Auto-remove after 4 seconds setTimeout(() => { if (toast.parentElement) { toast.classList.add('toast-hide'); setTimeout(() => toast.remove(), 300); } }, 4000); } function getToastIcon(type) { switch (type) { case 'success': return ` `; case 'error': return ` `; case 'info': default: return ` `; } } // 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) { document.getElementById('statusText').textContent = text; const indicator = document.getElementById('statusIndicator'); indicator.classList.toggle('connected', isConnected); // Show/hide lock button - only show when session is unlocked const lockBtn = document.getElementById('lockBtn'); if (lockBtn) { // Only show lock button when connected AND status indicates unlocked session const isUnlocked = isConnected && text.toLowerCase().startsWith('connected to'); lockBtn.classList.toggle('hidden', !isUnlocked); } } // 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 = { keyspaceInput: document.getElementById('keyspaceInput'), passwordInput: document.getElementById('passwordInput'), createKeyspaceBtn: document.getElementById('createKeyspaceBtn'), loginBtn: document.getElementById('loginBtn'), lockBtn: document.getElementById('lockBtn'), themeToggle: document.getElementById('themeToggle'), 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'), // Sign tab messageInput: document.getElementById('messageInput'), signBtn: document.getElementById('signBtn'), signatureResult: document.getElementById('signatureResult'), copySignatureBtn: document.getElementById('copySignatureBtn'), // Encrypt tab encryptMessageInput: document.getElementById('encryptMessageInput'), encryptBtn: document.getElementById('encryptBtn'), encryptResult: document.getElementById('encryptResult'), // Decrypt tab encryptedMessageInput: document.getElementById('encryptedMessageInput'), decryptBtn: document.getElementById('decryptBtn'), decryptResult: document.getElementById('decryptResult'), // Verify tab verifyMessageInput: document.getElementById('verifyMessageInput'), signatureToVerifyInput: document.getElementById('signatureToVerifyInput'), verifyBtn: document.getElementById('verifyBtn'), verifyResult: document.getElementById('verifyResult'), }; let currentKeyspace = null; let selectedKeypairId = null; let backgroundPort = null; // 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); } function updateThemeIcon(theme) { const themeToggle = elements.themeToggle; if (!themeToggle) return; if (theme === 'dark') { themeToggle.innerHTML = '☀️'; themeToggle.title = 'Switch to light mode'; } else { // Dark crescent moon SVG for better visibility themeToggle.innerHTML = ` `; themeToggle.title = 'Switch to dark mode'; } } // Establish connection to background script for keep-alive function connectToBackground() { try { backgroundPort = chrome.runtime.connect({ name: 'popup' }); backgroundPort.onDisconnect.addListener(() => { backgroundPort = null; }); } catch (error) { // Silently handle connection errors } } // Initialize document.addEventListener('DOMContentLoaded', async function() { // Initialize theme first initializeTheme(); // Ensure lock button starts hidden const lockBtn = document.getElementById('lockBtn'); if (lockBtn) { lockBtn.classList.add('hidden'); } setStatus('Initializing...', false); // Connect to background script for keep-alive connectToBackground(); // Event listeners (with null checks) if (elements.createKeyspaceBtn) { elements.createKeyspaceBtn.addEventListener('click', createKeyspace); } if (elements.loginBtn) { elements.loginBtn.addEventListener('click', login); } if (elements.lockBtn) { elements.lockBtn.addEventListener('click', lockSession); } if (elements.themeToggle) { elements.themeToggle.addEventListener('click', toggleTheme); } if (elements.toggleAddKeypairBtn) { elements.toggleAddKeypairBtn.addEventListener('click', toggleAddKeypairForm); } if (elements.addKeypairBtn) { elements.addKeypairBtn.addEventListener('click', addKeypair); } if (elements.cancelAddKeypairBtn) { elements.cancelAddKeypairBtn.addEventListener('click', hideAddKeypairForm); } // Crypto operation buttons (with null checks) if (elements.signBtn) { elements.signBtn.addEventListener('click', signMessage); } if (elements.encryptBtn) { elements.encryptBtn.addEventListener('click', encryptMessage); } if (elements.decryptBtn) { elements.decryptBtn.addEventListener('click', decryptMessage); } if (elements.verifyBtn) { elements.verifyBtn.addEventListener('click', verifySignature); } // Tab functionality initializeTabs(); // Copy button event listeners (with null checks) if (elements.copySignatureBtn) { elements.copySignatureBtn.addEventListener('click', () => { const signature = document.getElementById('signatureValue'); if (signature) { copyToClipboard(signature.textContent); } }); } // Enable sign button when message is entered (with null checks) if (elements.messageInput && elements.signBtn) { elements.messageInput.addEventListener('input', () => { elements.signBtn.disabled = !elements.messageInput.value.trim() || !selectedKeypairId; }); } // Basic keyboard shortcuts document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && elements.addKeypairCard && !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(`Connected to ${currentKeyspace}`, true); showSection('vaultSection'); await loadKeypairs(); } else { // No active session setStatus('Ready', false); showSection('authSection'); } } catch (error) { setStatus('Ready', 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 tabButtons = document.querySelectorAll('.tab-btn'); const tabContents = document.querySelectorAll('.tab-content'); tabButtons.forEach(button => { button.addEventListener('click', () => { const targetTab = button.getAttribute('data-tab'); // 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 button.classList.add('active'); document.getElementById(`${targetTab}-tab`).classList.add('active'); // Clear results when switching tabs clearTabResults(); // Update button states updateButtonStates(); }); }); // Initialize input validation initializeInputValidation(); } 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...
'; } } async function createKeyspace() { const keyspace = elements.keyspaceInput.value.trim(); const password = elements.passwordInput.value.trim(); if (!keyspace || !password) { showToast('Please enter keyspace name and password', 'error'); return; } try { await executeOperation( async () => { const response = await sendMessage('createKeyspace', { keyspace, password }); if (response && response.success) { // Clear any existing state before auto-login 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) { console.error('Create keyspace error:', error); } } async function login() { const keyspace = elements.keyspaceInput.value.trim(); const password = elements.passwordInput.value.trim(); if (!keyspace || !password) { showToast('Please enter keyspace name and password', 'error'); return; } try { await executeOperation( async () => { const response = await sendMessage('initSession', { keyspace, password }); if (response && response.success) { currentKeyspace = keyspace; setStatus(`Connected to ${keyspace}`, true); showSection('vaultSection'); // Clear any previous vault state before loading new keyspace clearVaultState(); await loadKeypairs(); return response; } else { throw new Error(getResponseError(response, 'login')); } }, { loadingElement: elements.loginBtn, successMessage: 'Logged in successfully!', maxRetries: 2 } ); } catch (error) { console.error('Login error:', error); } } async function lockSession() { try { await sendMessage('lockSession'); currentKeyspace = null; selectedKeypairId = null; setStatus('Locked', 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; } try { 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!' } ); } catch (error) { // Error already handled by executeOperation } } async function loadKeypairs() { try { 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'); } } catch (error) { const errorMsg = getErrorMessage(error, 'Failed to load keypairs'); console.error('Error loading keypairs:', error); const container = elements.keypairsList; container.innerHTML = '
Error loading 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'); console.error('Error selecting keypair:', error); // 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'); } } } async function signMessage() { const messageText = elements.messageInput.value.trim(); if (!messageText || !selectedKeypairId) { showToast('Please enter a message and select a keypair', 'error'); return; } try { await executeOperation( async () => { const messageBytes = stringToUint8Array(messageText); const response = await sendMessage('sign', { message: messageBytes }); if (response?.success) { elements.signatureResult.classList.remove('hidden'); elements.signatureResult.innerHTML = `
${response.signature}
`; document.getElementById('copySignatureBtn').addEventListener('click', () => { copyToClipboard(response.signature); }); return response; } else { throw new Error(getResponseError(response, 'sign message')); } }, { loadingElement: elements.signBtn, successMessage: 'Message signed successfully!' } ); } catch (error) { elements.signatureResult.classList.add('hidden'); } } async function encryptMessage() { const messageText = elements.encryptMessageInput.value.trim(); if (!messageText || !currentKeyspace) { showToast('Please enter a message and ensure you are connected to a keyspace', 'error'); return; } try { await executeOperation( async () => { const response = await sendMessage('encrypt', { message: messageText }); if (response?.success) { elements.encryptResult.classList.remove('hidden'); elements.encryptResult.innerHTML = `
${response.encryptedMessage}
`; document.getElementById('copyEncryptedBtn').addEventListener('click', () => { copyToClipboard(response.encryptedMessage); }); return response; } else { throw new Error(getResponseError(response, 'encrypt message')); } }, { loadingElement: elements.encryptBtn, successMessage: 'Message encrypted successfully!' } ); } catch (error) { elements.encryptResult.classList.add('hidden'); } } async function decryptMessage() { const encryptedText = elements.encryptedMessageInput.value.trim(); if (!encryptedText || !currentKeyspace) { showToast('Please enter encrypted message and ensure you are connected to a keyspace', 'error'); return; } try { await executeOperation( async () => { const response = await sendMessage('decrypt', { encryptedMessage: encryptedText }); if (response?.success) { elements.decryptResult.classList.remove('hidden'); elements.decryptResult.innerHTML = `
${response.decryptedMessage}
`; document.getElementById('copyDecryptedBtn').addEventListener('click', () => { copyToClipboard(response.decryptedMessage); }); return response; } else { throw new Error(getResponseError(response, 'decrypt message')); } }, { loadingElement: elements.decryptBtn, successMessage: 'Message decrypted successfully!' } ); } catch (error) { elements.decryptResult.classList.add('hidden'); } } async function verifySignature() { const messageText = elements.verifyMessageInput.value.trim(); const signature = elements.signatureToVerifyInput.value.trim(); if (!messageText || !signature || !selectedKeypairId) { showToast('Please enter message, signature, and select a keypair', 'error'); return; } try { await executeOperation( async () => { const messageBytes = stringToUint8Array(messageText); const response = await sendMessage('verify', { message: messageBytes, signature }); if (response?.success) { const isValid = response.isValid; const icon = isValid ? '✅' : '❌'; const text = isValid ? 'Signature is valid' : 'Signature is invalid'; elements.verifyResult.classList.remove('hidden'); elements.verifyResult.innerHTML = `
${icon} ${text}
`; return response; } else { throw new Error(getResponseError(response, 'verify signature')); } }, { loadingElement: elements.verifyBtn, successMessage: null // No success message for verification } ); } catch (error) { elements.verifyResult.classList.add('hidden'); } }