diff --git a/crypto_vault_extension/background.js b/crypto_vault_extension/background.js index 9be989b..cc8d04e 100644 --- a/crypto_vault_extension/background.js +++ b/crypto_vault_extension/background.js @@ -1,6 +1,7 @@ let vault = null; let isInitialized = false; let currentSession = null; +let keepAliveInterval = null; // Utility function to convert Uint8Array to hex function toHex(uint8Array) { @@ -12,16 +13,31 @@ function toHex(uint8Array) { // Session persistence functions async function saveSession(keyspace) { currentSession = { keyspace, timestamp: Date.now() }; - await chrome.storage.session.set({ cryptoVaultSession: currentSession }); - console.log('Session saved:', currentSession); + + // Save to both session and local storage for better persistence + try { + await chrome.storage.session.set({ cryptoVaultSession: currentSession }); + await chrome.storage.local.set({ cryptoVaultSessionBackup: currentSession }); + } catch (error) { + console.error('Failed to save session:', error); + } } async function loadSession() { try { - const result = await chrome.storage.session.get(['cryptoVaultSession']); + // Try session storage first + let result = await chrome.storage.session.get(['cryptoVaultSession']); if (result.cryptoVaultSession) { currentSession = result.cryptoVaultSession; - console.log('Session loaded:', currentSession); + return currentSession; + } + + // Fallback to local storage + result = await chrome.storage.local.get(['cryptoVaultSessionBackup']); + if (result.cryptoVaultSessionBackup) { + currentSession = result.cryptoVaultSessionBackup; + // Restore to session storage + await chrome.storage.session.set({ cryptoVaultSession: currentSession }); return currentSession; } } catch (error) { @@ -32,8 +48,45 @@ async function loadSession() { async function clearSession() { currentSession = null; - await chrome.storage.session.remove(['cryptoVaultSession']); - console.log('Session cleared'); + try { + await chrome.storage.session.remove(['cryptoVaultSession']); + await chrome.storage.local.remove(['cryptoVaultSessionBackup']); + } catch (error) { + console.error('Failed to clear session:', error); + } +} + +// Keep service worker alive +function startKeepAlive() { + if (keepAliveInterval) { + clearInterval(keepAliveInterval); + } + + // Ping every 20 seconds to keep service worker alive + keepAliveInterval = setInterval(() => { + // Simple operation to keep service worker active + chrome.storage.session.get(['keepAlive']).catch(() => { + // Ignore errors + }); + }, 20000); +} + +function stopKeepAlive() { + if (keepAliveInterval) { + clearInterval(keepAliveInterval); + keepAliveInterval = null; + } +} + +// Enhanced session management with keep-alive +async function saveSessionWithKeepAlive(keyspace) { + await saveSession(keyspace); + startKeepAlive(); +} + +async function clearSessionWithKeepAlive() { + await clearSession(); + stopKeepAlive(); } async function restoreSession() { @@ -43,15 +96,15 @@ async function restoreSession() { // Check if the session is still valid by testing if vault is unlocked const isUnlocked = vault.is_unlocked(); if (isUnlocked) { - console.log('Session restored successfully for keyspace:', session.keyspace); + // Restart keep-alive for restored session + startKeepAlive(); return session; } else { - console.log('Session expired, clearing...'); - await clearSession(); + await clearSessionWithKeepAlive(); } } catch (error) { console.error('Error checking session validity:', error); - await clearSession(); + await clearSessionWithKeepAlive(); } } return null; @@ -68,6 +121,9 @@ import init, { current_keypair_metadata, current_keypair_public_key, sign, + verify, + encrypt_data, + decrypt_data, lock_session } from './wasm/wasm_app.js'; @@ -91,11 +147,13 @@ async function initVault() { current_keypair_metadata, current_keypair_public_key, sign, + verify, + encrypt_data, + decrypt_data, lock_session }; isInitialized = true; - console.log('CryptoVault initialized successfully'); // Try to restore previous session await restoreSession(); @@ -107,8 +165,23 @@ async function initVault() { } } +// Handle popup connection/disconnection +chrome.runtime.onConnect.addListener((port) => { + if (port.name === 'popup') { + // If we have an active session, ensure keep-alive is running + if (currentSession) { + startKeepAlive(); + } + + port.onDisconnect.addListener(() => { + // Keep the keep-alive running even after popup disconnects + // This ensures session persistence across popup closes + }); + } +}); + // Handle messages from popup and content scripts -chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { +chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => { const handleRequest = async () => { try { if (!vault) { @@ -122,7 +195,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { case 'initSession': await vault.init_session(request.keyspace, request.password); - await saveSession(request.keyspace); + await saveSessionWithKeepAlive(request.keyspace); return { success: true }; case 'isUnlocked': @@ -134,37 +207,24 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { return { success: true, result }; case 'listKeypairs': - console.log('Background: listing keypairs...'); - console.log('Background: vault object:', vault); - console.log('Background: vault.list_keypairs function:', vault.list_keypairs); - // Check if session is unlocked first const isUnlocked = vault.is_unlocked(); - console.log('Background: is session unlocked?', isUnlocked); if (!isUnlocked) { - console.log('Background: Session is not unlocked, cannot list keypairs'); return { success: false, error: 'Session is not unlocked' }; } try { const keypairsRaw = await vault.list_keypairs(); - console.log('Background: keypairs raw result:', keypairsRaw); - console.log('Background: keypairs type:', typeof keypairsRaw); // Parse JSON string if needed let keypairs; if (typeof keypairsRaw === 'string') { - console.log('Background: Parsing JSON string...'); keypairs = JSON.parse(keypairsRaw); } else { keypairs = keypairsRaw; } - console.log('Background: parsed keypairs:', keypairs); - console.log('Background: parsed keypairs type:', typeof keypairs); - console.log('Background: keypairs array length:', Array.isArray(keypairs) ? keypairs.length : 'not an array'); - return { success: true, keypairs }; } catch (listError) { console.error('Background: Error calling list_keypairs:', listError); @@ -188,9 +248,67 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { const signature = await vault.sign(new Uint8Array(request.message)); return { success: true, signature }; + case 'encrypt': + // Check if session is unlocked + if (!vault.is_unlocked()) { + return { success: false, error: 'Session is not unlocked' }; + } + + try { + // Convert message to Uint8Array for WASM + const messageBytes = new TextEncoder().encode(request.message); + + // Use WASM encrypt_data function with ChaCha20-Poly1305 + const encryptedData = await vault.encrypt_data(messageBytes); + + // Convert result to base64 for easy handling + const encryptedMessage = btoa(String.fromCharCode(...new Uint8Array(encryptedData))); + return { success: true, encryptedMessage }; + } catch (error) { + console.error('Encryption error:', error); + return { success: false, error: error.message }; + } + + case 'decrypt': + // Check if session is unlocked + if (!vault.is_unlocked()) { + return { success: false, error: 'Session is not unlocked' }; + } + + try { + // Convert base64 back to Uint8Array + const encryptedBytes = new Uint8Array(atob(request.encryptedMessage).split('').map(c => c.charCodeAt(0))); + + // Use WASM decrypt_data function with ChaCha20-Poly1305 + const decryptedData = await vault.decrypt_data(encryptedBytes); + + // Convert result back to string + const decryptedMessage = new TextDecoder().decode(new Uint8Array(decryptedData)); + return { success: true, decryptedMessage }; + } catch (error) { + console.error('Decryption error:', error); + return { success: false, error: error.message }; + } + + case 'verify': + // Check if a keypair is selected + try { + const metadata = vault.current_keypair_metadata(); + if (!metadata) { + return { success: false, error: 'No keypair selected' }; + } + + // Use WASM verify function + const isValid = await vault.verify(new Uint8Array(request.message), request.signature); + return { success: true, isValid }; + } catch (error) { + console.error('Verification error:', error); + return { success: false, error: error.message }; + } + case 'lockSession': vault.lock_session(); - await clearSession(); + await clearSessionWithKeepAlive(); return { success: true }; case 'getStatus': diff --git a/crypto_vault_extension/js/errorHandler.js b/crypto_vault_extension/js/errorHandler.js new file mode 100644 index 0000000..585f076 --- /dev/null +++ b/crypto_vault_extension/js/errorHandler.js @@ -0,0 +1,280 @@ +// Enhanced Error Handling System for CryptoVault Extension + +class CryptoVaultError extends Error { + constructor(message, code, retryable = false, userMessage = null) { + super(message); + this.name = 'CryptoVaultError'; + this.code = code; + this.retryable = retryable; + this.userMessage = userMessage || message; + this.timestamp = Date.now(); + } +} + +// Error codes for different types of errors +const ERROR_CODES = { + // Network/Connection errors (retryable) + NETWORK_ERROR: 'NETWORK_ERROR', + TIMEOUT_ERROR: 'TIMEOUT_ERROR', + SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE', + + // Authentication errors (not retryable) + INVALID_PASSWORD: 'INVALID_PASSWORD', + SESSION_EXPIRED: 'SESSION_EXPIRED', + UNAUTHORIZED: 'UNAUTHORIZED', + + // Crypto errors (not retryable) + CRYPTO_ERROR: 'CRYPTO_ERROR', + INVALID_SIGNATURE: 'INVALID_SIGNATURE', + ENCRYPTION_FAILED: 'ENCRYPTION_FAILED', + + // Validation errors (not retryable) + INVALID_INPUT: 'INVALID_INPUT', + MISSING_KEYPAIR: 'MISSING_KEYPAIR', + INVALID_FORMAT: 'INVALID_FORMAT', + + // System errors (sometimes retryable) + WASM_ERROR: 'WASM_ERROR', + STORAGE_ERROR: 'STORAGE_ERROR', + UNKNOWN_ERROR: 'UNKNOWN_ERROR' +}; + +// User-friendly error messages +const ERROR_MESSAGES = { + [ERROR_CODES.NETWORK_ERROR]: 'Connection failed. Please check your internet connection and try again.', + [ERROR_CODES.TIMEOUT_ERROR]: 'Operation timed out. Please try again.', + [ERROR_CODES.SERVICE_UNAVAILABLE]: 'Service is temporarily unavailable. Please try again later.', + + [ERROR_CODES.INVALID_PASSWORD]: 'Invalid password. Please check your password and try again.', + [ERROR_CODES.SESSION_EXPIRED]: 'Your session has expired. Please log in again.', + [ERROR_CODES.UNAUTHORIZED]: 'You are not authorized to perform this action.', + + [ERROR_CODES.CRYPTO_ERROR]: 'Cryptographic operation failed. Please try again.', + [ERROR_CODES.INVALID_SIGNATURE]: 'Invalid signature. Please verify your input.', + [ERROR_CODES.ENCRYPTION_FAILED]: 'Encryption failed. Please try again.', + + [ERROR_CODES.INVALID_INPUT]: 'Invalid input. Please check your data and try again.', + [ERROR_CODES.MISSING_KEYPAIR]: 'No keypair selected. Please select a keypair first.', + [ERROR_CODES.INVALID_FORMAT]: 'Invalid data format. Please check your input.', + + [ERROR_CODES.WASM_ERROR]: 'System error occurred. Please refresh and try again.', + [ERROR_CODES.STORAGE_ERROR]: 'Storage error occurred. Please try again.', + [ERROR_CODES.UNKNOWN_ERROR]: 'An unexpected error occurred. Please try again.' +}; + +// Determine if an error is retryable +const RETRYABLE_ERRORS = new Set([ + ERROR_CODES.NETWORK_ERROR, + ERROR_CODES.TIMEOUT_ERROR, + ERROR_CODES.SERVICE_UNAVAILABLE, + ERROR_CODES.WASM_ERROR, + ERROR_CODES.STORAGE_ERROR +]); + +// Enhanced error classification +function classifyError(error) { + const errorMessage = getErrorMessage(error); + + // Network/Connection errors + if (errorMessage.includes('fetch') || errorMessage.includes('network') || errorMessage.includes('connection')) { + return new CryptoVaultError( + errorMessage, + ERROR_CODES.NETWORK_ERROR, + true, + ERROR_MESSAGES[ERROR_CODES.NETWORK_ERROR] + ); + } + + // Authentication errors + if (errorMessage.includes('password') || errorMessage.includes('Invalid password')) { + return new CryptoVaultError( + errorMessage, + ERROR_CODES.INVALID_PASSWORD, + false, + ERROR_MESSAGES[ERROR_CODES.INVALID_PASSWORD] + ); + } + + if (errorMessage.includes('session') || errorMessage.includes('not unlocked') || errorMessage.includes('expired')) { + return new CryptoVaultError( + errorMessage, + ERROR_CODES.SESSION_EXPIRED, + false, + ERROR_MESSAGES[ERROR_CODES.SESSION_EXPIRED] + ); + } + + // Crypto errors + if (errorMessage.includes('decryption error') || errorMessage.includes('aead::Error')) { + return new CryptoVaultError( + errorMessage, + ERROR_CODES.CRYPTO_ERROR, + false, + 'Invalid password or corrupted data. Please check your password.' + ); + } + + if (errorMessage.includes('Crypto error') || errorMessage.includes('encryption')) { + return new CryptoVaultError( + errorMessage, + ERROR_CODES.CRYPTO_ERROR, + false, + ERROR_MESSAGES[ERROR_CODES.CRYPTO_ERROR] + ); + } + + // Validation errors + if (errorMessage.includes('No keypair selected')) { + return new CryptoVaultError( + errorMessage, + ERROR_CODES.MISSING_KEYPAIR, + false, + ERROR_MESSAGES[ERROR_CODES.MISSING_KEYPAIR] + ); + } + + // WASM errors + if (errorMessage.includes('wasm') || errorMessage.includes('WASM')) { + return new CryptoVaultError( + errorMessage, + ERROR_CODES.WASM_ERROR, + true, + ERROR_MESSAGES[ERROR_CODES.WASM_ERROR] + ); + } + + // Default to unknown error + return new CryptoVaultError( + errorMessage, + ERROR_CODES.UNKNOWN_ERROR, + false, + ERROR_MESSAGES[ERROR_CODES.UNKNOWN_ERROR] + ); +} + +// Get error message from various error types +function getErrorMessage(error) { + if (!error) return 'Unknown error'; + + if (typeof error === 'string') { + return error.trim(); + } + + if (error instanceof Error) { + return error.message; + } + + if (error.error) { + return getErrorMessage(error.error); + } + + if (error.message) { + return error.message; + } + + if (typeof error === 'object') { + try { + const stringified = JSON.stringify(error); + if (stringified && stringified !== '{}') { + return stringified; + } + } catch (e) { + // Ignore JSON stringify errors + } + } + + return 'Unknown error'; +} + +// Retry logic with exponential backoff +async function withRetry(operation, options = {}) { + const { + maxRetries = 3, + baseDelay = 1000, + maxDelay = 10000, + backoffFactor = 2, + onRetry = null + } = options; + + let lastError; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + return await operation(); + } catch (error) { + const classifiedError = classifyError(error); + lastError = classifiedError; + + // Don't retry if it's the last attempt or error is not retryable + if (attempt === maxRetries || !classifiedError.retryable) { + throw classifiedError; + } + + // Calculate delay with exponential backoff + const delay = Math.min(baseDelay * Math.pow(backoffFactor, attempt), maxDelay); + + // Call retry callback if provided + if (onRetry) { + onRetry(attempt + 1, delay, classifiedError); + } + + // Wait before retrying + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + + throw lastError; +} + +// Enhanced operation wrapper with loading states +async function executeOperation(operation, options = {}) { + const { + loadingElement = null, + successMessage = null, + showRetryProgress = false, + onProgress = null + } = options; + + // Show loading state + if (loadingElement) { + setButtonLoading(loadingElement, true); + } + + try { + const result = await withRetry(operation, { + ...options, + onRetry: (attempt, delay, error) => { + if (showRetryProgress && onProgress) { + onProgress(`Retrying... (${attempt}/${options.maxRetries || 3})`); + } + if (options.onRetry) { + options.onRetry(attempt, delay, error); + } + } + }); + + // Show success message if provided + if (successMessage) { + showToast(successMessage, 'success'); + } + + return result; + } catch (error) { + // Show user-friendly error message + showToast(error.userMessage || error.message, 'error'); + throw error; + } finally { + // Hide loading state + if (loadingElement) { + setButtonLoading(loadingElement, false); + } + } +} + +// Export for use in other modules +window.CryptoVaultError = CryptoVaultError; +window.ERROR_CODES = ERROR_CODES; +window.classifyError = classifyError; +window.getErrorMessage = getErrorMessage; +window.withRetry = withRetry; +window.executeOperation = executeOperation; diff --git a/crypto_vault_extension/popup.html b/crypto_vault_extension/popup.html index 2aaaf66..1b620cb 100644 --- a/crypto_vault_extension/popup.html +++ b/crypto_vault_extension/popup.html @@ -11,9 +11,12 @@
🔐

CryptoVault

-
-
- Initializing... +
+
@@ -38,13 +41,26 @@
- +
-

Sign Message

-
- - +

Crypto Operations

+ + +
+ + + +
- - - - +
+ + \ No newline at end of file diff --git a/crypto_vault_extension/popup.js b/crypto_vault_extension/popup.js index cbc21fa..2a1dd61 100644 --- a/crypto_vault_extension/popup.js +++ b/crypto_vault_extension/popup.js @@ -1,9 +1,61 @@ -// Utility functions +// Enhanced toast system function showToast(message, type = 'info') { - const toast = document.getElementById('toast'); - toast.textContent = message; - toast.className = `toast ${type}`; - setTimeout(() => toast.classList.add('hidden'), 3000); + // 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 ` + + + + `; + } } function showLoading(show = true) { @@ -12,7 +64,7 @@ function showLoading(show = true) { } // Enhanced loading states for buttons -function setButtonLoading(button, loading = true, originalText = null) { +function setButtonLoading(button, loading = true) { if (loading) { button.dataset.originalText = button.textContent; button.classList.add('loading'); @@ -20,63 +72,12 @@ function setButtonLoading(button, loading = true, originalText = null) { } else { button.classList.remove('loading'); button.disabled = false; - if (originalText) { - button.textContent = originalText; - } else if (button.dataset.originalText) { + if (button.dataset.originalText) { button.textContent = button.dataset.originalText; } } } -// Show inline loading for specific operations -function showInlineLoading(element, message = 'Processing...') { - element.innerHTML = ` -
-
- ${message} -
- `; -} - -// Enhanced error handling utility -function getErrorMessage(error, fallback = 'An unexpected error occurred') { - if (!error) return fallback; - - // If it's a string, return it - if (typeof error === 'string') { - return error.trim() || fallback; - } - - // If it's an Error object - if (error instanceof Error) { - return error.message || fallback; - } - - // If it's an object with error property - if (error.error) { - return getErrorMessage(error.error, fallback); - } - - // If it's an object with message property - if (error.message) { - return error.message || fallback; - } - - // Try to stringify if it's an object - if (typeof error === 'object') { - try { - const stringified = JSON.stringify(error); - if (stringified && stringified !== '{}') { - return stringified; - } - } catch (e) { - // Ignore JSON stringify errors - } - } - - return fallback; -} - // Enhanced response error handling function getResponseError(response, operation = 'operation') { if (!response) { @@ -114,6 +115,14 @@ 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 @@ -135,17 +144,7 @@ async function copyToClipboard(text) { // Convert string to Uint8Array function stringToUint8Array(str) { - if (str.match(/^[0-9a-fA-F]+$/)) { - // Hex string - const bytes = []; - for (let i = 0; i < str.length; i += 2) { - bytes.push(parseInt(str.substr(i, 2), 16)); - } - return bytes; - } else { - // Regular string - return Array.from(new TextEncoder().encode(str)); - } + return Array.from(new TextEncoder().encode(str)); } // DOM Elements @@ -155,6 +154,7 @@ const elements = { 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'), @@ -163,54 +163,164 @@ const elements = { cancelAddKeypairBtn: document.getElementById('cancelAddKeypairBtn'), keypairsList: document.getElementById('keypairsList'), selectedKeypairCard: document.getElementById('selectedKeypairCard'), + + // Sign tab messageInput: document.getElementById('messageInput'), signBtn: document.getElementById('signBtn'), signatureResult: document.getElementById('signatureResult'), - copyPublicKeyBtn: document.getElementById('copyPublicKeyBtn'), 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); - // Event listeners - elements.createKeyspaceBtn.addEventListener('click', createKeyspace); - elements.loginBtn.addEventListener('click', login); - elements.lockBtn.addEventListener('click', lockSession); - elements.toggleAddKeypairBtn.addEventListener('click', toggleAddKeypairForm); - elements.addKeypairBtn.addEventListener('click', addKeypair); - elements.cancelAddKeypairBtn.addEventListener('click', hideAddKeypairForm); - elements.signBtn.addEventListener('click', signMessage); - elements.copyPublicKeyBtn.addEventListener('click', () => { - const publicKey = document.getElementById('selectedPublicKey').textContent; - copyToClipboard(publicKey); - }); - elements.copySignatureBtn.addEventListener('click', () => { - const signature = document.getElementById('signatureValue').textContent; - copyToClipboard(signature); - }); + // Connect to background script for keep-alive + connectToBackground(); - // Enable sign button when message is entered - elements.messageInput.addEventListener('input', () => { - elements.signBtn.disabled = !elements.messageInput.value.trim() || !selectedKeypairId; - }); + // 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); + } - // Keyboard shortcuts + 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) => { - // Escape key closes the add keypair form - if (e.key === 'Escape' && !elements.addKeypairCard.classList.contains('hidden')) { + if (e.key === 'Escape' && elements.addKeypairCard && !elements.addKeypairCard.classList.contains('hidden')) { hideAddKeypairForm(); } - // Enter key in the name input submits the form - if (e.key === 'Enter' && e.target === elements.keyNameInput) { + if (e.key === 'Enter' && e.target === elements.keyNameInput && elements.keyNameInput.value.trim()) { e.preventDefault(); - if (elements.keyNameInput.value.trim()) { - addKeypair(); - } + addKeypair(); } }); @@ -228,17 +338,14 @@ async function checkExistingSession() { setStatus(`Connected to ${currentKeyspace}`, true); showSection('vaultSection'); await loadKeypairs(); - showToast('Session restored!', 'success'); } else { // No active session - setStatus('Ready', true); + setStatus('Ready', false); showSection('authSection'); } } catch (error) { - console.error('Error checking session:', error); - setStatus('Ready', true); + setStatus('Ready', false); showSection('authSection'); - // Don't show toast for session check errors as it's not user-initiated } } @@ -254,50 +361,161 @@ function toggleAddKeypairForm() { function showAddKeypairForm() { elements.addKeypairCard.classList.remove('hidden'); - - // Rotate the + icon to × when form is open - const icon = elements.toggleAddKeypairBtn.querySelector('.btn-icon'); - icon.style.transform = 'rotate(45deg)'; - - // Focus on the name input after animation - setTimeout(() => { - elements.keyNameInput.focus(); - }, 300); + elements.keyNameInput.focus(); } function hideAddKeypairForm() { elements.addKeypairCard.classList.add('hidden'); - // Rotate the icon back to + - const icon = elements.toggleAddKeypairBtn.querySelector('.btn-icon'); - icon.style.transform = 'rotate(0deg)'; - // 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'); + + // Scroll the selected tab into view + scrollTabIntoView(button); + + // Clear results when switching tabs + clearTabResults(); + + // Update button states + updateButtonStates(); + }); + }); + + // Initialize input validation + initializeInputValidation(); +} + +function scrollTabIntoView(selectedTab) { + // Simple scroll into view + if (selectedTab) { + selectedTab.scrollIntoView({ behavior: 'smooth', inline: 'center' }); + } +} + +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 message input and signature result - elements.messageInput.value = ''; - elements.signatureResult.classList.add('hidden'); - document.getElementById('signatureValue').textContent = ''; + // 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; - elements.signBtn.disabled = true; + updateButtonStates(); - // Clear selected keypair info (hidden elements) - document.getElementById('selectedName').textContent = '-'; - document.getElementById('selectedType').textContent = '-'; - document.getElementById('selectedPublicKey').textContent = '-'; + // Clear selected keypair info (hidden elements) with null checks + const selectedName = document.getElementById('selectedName'); + const selectedType = document.getElementById('selectedType'); + const selectedPublicKey = document.getElementById('selectedPublicKey'); + + if (selectedName) selectedName.textContent = '-'; + if (selectedType) selectedType.textContent = '-'; + if (selectedPublicKey) selectedPublicKey.textContent = '-'; // Hide add keypair form if open hideAddKeypairForm(); // Clear keypairs list - elements.keypairsList.innerHTML = '
Loading keypairs...
'; + if (elements.keypairsList) { + elements.keypairsList.innerHTML = '
Loading keypairs...
'; + } } async function createKeyspace() { @@ -309,24 +527,30 @@ async function createKeyspace() { return; } - setButtonLoading(elements.createKeyspaceBtn, true); + + try { - const response = await sendMessage('createKeyspace', { keyspace, password }); - if (response && response.success) { - showToast('Keyspace created successfully!', 'success'); - // Clear any existing state before auto-login - clearVaultState(); - await login(); // Auto-login after creation - } else { - const errorMsg = getResponseError(response, 'create keyspace'); - showToast(errorMsg, 'error'); - } + 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) { - const errorMsg = getErrorMessage(error, 'Failed to create keyspace'); console.error('Create keyspace error:', error); - showToast(errorMsg, 'error'); - } finally { - setButtonLoading(elements.createKeyspaceBtn, false); } } @@ -339,29 +563,32 @@ async function login() { return; } - setButtonLoading(elements.loginBtn, true); try { - const response = await sendMessage('initSession', { keyspace, password }); - if (response && response.success) { - currentKeyspace = keyspace; - setStatus(`Connected to ${keyspace}`, true); - showSection('vaultSection'); + await executeOperation( + async () => { + const response = await sendMessage('initSession', { keyspace, password }); - // Clear any previous vault state before loading new keyspace - clearVaultState(); - await loadKeypairs(); + if (response && response.success) { + currentKeyspace = keyspace; + setStatus(`Connected to ${keyspace}`, true); + showSection('vaultSection'); - showToast('Logged in successfully!', 'success'); - } else { - const errorMsg = getResponseError(response, 'login'); - showToast(errorMsg, 'error'); - } + // 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) { - const errorMsg = getErrorMessage(error, 'Failed to login'); console.error('Login error:', error); - showToast(errorMsg, 'error'); - } finally { - setButtonLoading(elements.loginBtn, false); } } @@ -396,53 +623,38 @@ async function addKeypair() { return; } - // Use button loading instead of full overlay - setButtonLoading(elements.addKeypairBtn, true); - try { - console.log('Adding keypair:', { keyType, keyName }); - const metadata = JSON.stringify({ name: keyName }); - console.log('Metadata:', metadata); + await executeOperation( + async () => { + const metadata = JSON.stringify({ name: keyName }); + const response = await sendMessage('addKeypair', { keyType, metadata }); - const response = await sendMessage('addKeypair', { keyType, metadata }); - console.log('Add keypair response:', response); - - if (response && response.success) { - console.log('Keypair added successfully, clearing input and reloading list...'); - hideAddKeypairForm(); // Hide the form after successful addition - - // Show inline loading in keypairs list while reloading - showInlineLoading(elements.keypairsList, 'Adding keypair...'); - await loadKeypairs(); - - showToast('Keypair added successfully!', 'success'); - } else { - const errorMsg = getResponseError(response, 'add keypair'); - console.error('Failed to add keypair:', response); - showToast(errorMsg, 'error'); - } + 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) { - const errorMsg = getErrorMessage(error, 'Failed to add keypair'); - console.error('Error adding keypair:', error); - showToast(errorMsg, 'error'); - } finally { - setButtonLoading(elements.addKeypairBtn, false); + // Error already handled by executeOperation } } async function loadKeypairs() { try { - console.log('Loading keypairs...'); const response = await sendMessage('listKeypairs'); - console.log('Keypairs response:', response); if (response && response.success) { - console.log('Keypairs data:', response.keypairs); - console.log('Keypairs data type:', typeof response.keypairs); renderKeypairs(response.keypairs); } else { const errorMsg = getResponseError(response, 'load keypairs'); - console.error('Failed to load keypairs:', response); const container = elements.keypairsList; container.innerHTML = '
Failed to load keypairs. Try refreshing.
'; showToast(errorMsg, 'error'); @@ -457,45 +669,17 @@ async function loadKeypairs() { } function renderKeypairs(keypairs) { - console.log('Rendering keypairs:', keypairs); - console.log('Keypairs type:', typeof keypairs); - console.log('Keypairs is array:', Array.isArray(keypairs)); - const container = elements.keypairsList; - // Handle different data types that might be returned - let keypairArray = []; + // Simple array handling + const keypairArray = Array.isArray(keypairs) ? keypairs : []; - if (Array.isArray(keypairs)) { - keypairArray = keypairs; - } else if (keypairs && typeof keypairs === 'object') { - // If it's an object, try to extract array from common properties - if (keypairs.keypairs && Array.isArray(keypairs.keypairs)) { - keypairArray = keypairs.keypairs; - } else if (keypairs.data && Array.isArray(keypairs.data)) { - keypairArray = keypairs.data; - } else { - console.log('Keypairs object structure:', Object.keys(keypairs)); - // Try to convert object to array if it has numeric keys - const keys = Object.keys(keypairs); - if (keys.length > 0 && keys.every(key => !isNaN(key))) { - keypairArray = Object.values(keypairs); - } - } - } - - console.log('Final keypair array:', keypairArray); - console.log('Array length:', keypairArray.length); - - if (!keypairArray || keypairArray.length === 0) { - console.log('No keypairs to render'); + if (keypairArray.length === 0) { container.innerHTML = '
No keypairs found. Add one above.
'; return; } - console.log('Rendering', keypairArray.length, 'keypairs'); - container.innerHTML = keypairArray.map((keypair, index) => { - console.log('Processing keypair:', keypair); + container.innerHTML = keypairArray.map((keypair) => { const metadata = typeof keypair.metadata === 'string' ? JSON.parse(keypair.metadata) : keypair.metadata; @@ -506,7 +690,7 @@ function renderKeypairs(keypairs) {
${metadata.name || 'Unnamed'}
${keypair.key_type}
- @@ -514,22 +698,15 @@ function renderKeypairs(keypairs) { }).join(''); // Add event listeners to all select buttons - const selectButtons = container.querySelectorAll('.select-btn'); - selectButtons.forEach(button => { + container.querySelectorAll('.select-btn').forEach(button => { button.addEventListener('click', (e) => { - e.preventDefault(); // Prevent any default button behavior - e.stopPropagation(); // Stop event bubbling - const keypairId = e.target.getAttribute('data-keypair-id'); - console.log('Select button clicked for keypair:', keypairId); selectKeypair(keypairId); }); }); } async function selectKeypair(keyId) { - console.log('Selecting keypair:', keyId); - // Don't show loading overlay for selection - it's too disruptive try { // Update visual state immediately for better UX @@ -551,17 +728,13 @@ async function selectKeypair(keyId) { document.getElementById('selectedPublicKey').textContent = publicKeyResponse.publicKey; // Enable sign button if message is entered - elements.signBtn.disabled = !elements.messageInput.value.trim(); - - // Show a subtle success message without toast - console.log(`Keypair "${metadata.name}" selected successfully`); + 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; - console.error('Failed to get keypair details:', { metadataResponse, publicKeyResponse }); updateKeypairSelection(null); showToast(errorMsg, 'error'); } @@ -603,45 +776,177 @@ async function signMessage() { return; } - // Use button loading and show inline loading in signature area - setButtonLoading(elements.signBtn, true); + try { + await executeOperation( + async () => { + const messageBytes = stringToUint8Array(messageText); + const response = await sendMessage('sign', { message: messageBytes }); - // Show loading in signature result area - elements.signatureResult.classList.remove('hidden'); - showInlineLoading(elements.signatureResult, 'Signing message...'); + 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 { - const messageBytes = stringToUint8Array(messageText); - const response = await sendMessage('sign', { message: messageBytes }); + await executeOperation( + async () => { + const response = await sendMessage('encrypt', { message: messageText }); - if (response && response.success) { - // Restore signature result structure and show signature - elements.signatureResult.innerHTML = ` - -
- ${response.signature} - -
- `; + if (response?.success) { + elements.encryptResult.classList.remove('hidden'); + elements.encryptResult.innerHTML = ` + +
+ ${response.encryptedMessage} + +
+ `; - // Re-attach copy event listener - document.getElementById('copySignatureBtn').addEventListener('click', () => { - copyToClipboard(response.signature); - }); + document.getElementById('copyEncryptedBtn').addEventListener('click', () => { + copyToClipboard(response.encryptedMessage); + }); - showToast('Message signed successfully!', 'success'); - } else { - const errorMsg = getResponseError(response, 'sign message'); - elements.signatureResult.classList.add('hidden'); - showToast(errorMsg, 'error'); - } + return response; + } else { + throw new Error(getResponseError(response, 'encrypt message')); + } + }, + { + loadingElement: elements.encryptBtn, + successMessage: 'Message encrypted successfully!' + } + ); } catch (error) { - const errorMsg = getErrorMessage(error, 'Failed to sign message'); - console.error('Sign message error:', error); - elements.signatureResult.classList.add('hidden'); - showToast(errorMsg, 'error'); - } finally { - setButtonLoading(elements.signBtn, false); + 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'); } } diff --git a/crypto_vault_extension/styles/popup.css b/crypto_vault_extension/styles/popup.css index 075db26..5a7d9ac 100644 --- a/crypto_vault_extension/styles/popup.css +++ b/crypto_vault_extension/styles/popup.css @@ -1,3 +1,144 @@ +/* CSS Variables for theming */ +:root { + /* Light theme colors */ + --bg-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + --bg-secondary: rgba(255, 255, 255, 0.95); + --bg-card: rgba(255, 255, 255, 0.95); + --bg-input: rgba(255, 255, 255, 0.8); + --bg-button: rgba(255, 255, 255, 0.2); + --bg-button-secondary: rgba(255, 255, 255, 0.15); + --bg-button-ghost: transparent; + + --text-primary: #2d3748; + --text-secondary: #4a5568; + --text-muted: #666; + --text-inverse: white; + + --border-color: rgba(255, 255, 255, 0.3); + --border-input: rgba(255, 255, 255, 0.3); + --border-focus: #667eea; + + --shadow-card: 0 8px 32px rgba(0, 0, 0, 0.1); + --shadow-button: 0 8px 25px rgba(79, 70, 229, 0.4); + + --accent-success: #10b981; + --accent-error: #ef4444; + --accent-warning: #f59e0b; + --accent-info: #3b82f6; + + /* Spacing system */ + --spacing-xs: 4px; + --spacing-sm: 8px; + --spacing-md: 12px; + --spacing-lg: 16px; + --spacing-xl: 20px; + --spacing-2xl: 24px; + --spacing-3xl: 32px; +} + +/* Dark theme colors */ +[data-theme="dark"] { + --bg-primary: linear-gradient(135deg, #1a202c 0%, #2d3748 100%); + --bg-secondary: rgba(26, 32, 44, 0.95); + --bg-card: rgba(45, 55, 72, 0.95); + --bg-input: rgba(74, 85, 104, 0.8); + --bg-button: linear-gradient(135deg, #4299e1 0%, #667eea 100%); + --bg-button-secondary: rgba(74, 85, 104, 0.8); + --bg-button-ghost: transparent; + + --text-primary: #ffffff; + --text-secondary: #f7fafc; + --text-muted: #cbd5e0; + --text-inverse: #1a202c; + + --border-color: rgba(203, 213, 224, 0.2); + --border-input: rgba(203, 213, 224, 0.3); + --border-focus: #4299e1; + + --shadow-card: 0 8px 32px rgba(0, 0, 0, 0.3); + --shadow-button: 0 8px 25px rgba(66, 153, 225, 0.3); + + --accent-success: #34d399; + --accent-error: #f87171; + --accent-warning: #fbbf24; + --accent-info: #60a5fa; +} + +/* Dark theme button text overrides */ +[data-theme="dark"] .btn-primary, +[data-theme="dark"] .btn-secondary, +[data-theme="dark"] .btn-ghost { + color: white; +} + +/* Dark theme buttons inside cards */ +[data-theme="dark"] .card .btn-primary { + background: linear-gradient(135deg, #4299e1 0%, #667eea 100%); + color: white; + border: 2px solid transparent; +} + +[data-theme="dark"] .card .btn-primary:hover { + background: linear-gradient(135deg, #3182ce 0%, #5a67d8 100%); + box-shadow: 0 8px 25px rgba(66, 153, 225, 0.4); +} + +[data-theme="dark"] .card .btn-secondary { + background: rgba(74, 85, 104, 0.8); + color: white; + border: 2px solid rgba(203, 213, 224, 0.2); +} + +[data-theme="dark"] .card .btn-secondary:hover { + background: rgba(74, 85, 104, 0.9); + border-color: rgba(203, 213, 224, 0.3); +} + +[data-theme="dark"] .card .btn-ghost { + background: transparent; + color: #4299e1; + border: 1px solid rgba(66, 153, 225, 0.3); +} + +[data-theme="dark"] .card .btn-ghost:hover { + background: rgba(66, 153, 225, 0.1); + border-color: rgba(66, 153, 225, 0.5); +} + +/* Buttons inside cards (on white/light backgrounds) */ +.card .btn-primary { + background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); + color: white; + border: 2px solid transparent; +} + +.card .btn-primary:hover { + background: linear-gradient(135deg, #4338ca 0%, #6d28d9 100%); + box-shadow: 0 8px 25px rgba(79, 70, 229, 0.4); +} + +.card .btn-secondary { + background: rgba(79, 70, 229, 0.1); + color: #4f46e5; + border: 2px solid rgba(79, 70, 229, 0.2); +} + +.card .btn-secondary:hover { + background: rgba(79, 70, 229, 0.15); + border-color: rgba(79, 70, 229, 0.3); +} + +.card .btn-ghost { + background: transparent; + color: #4f46e5; + border: 1px solid rgba(79, 70, 229, 0.3); +} + +.card .btn-ghost:hover { + background: rgba(79, 70, 229, 0.1); + border-color: rgba(79, 70, 229, 0.5); +} + * { margin: 0; padding: 0; @@ -8,9 +149,9 @@ body { width: 400px; min-height: 600px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background: var(--bg-primary); background-attachment: fixed; - color: #333; + color: var(--text-primary); line-height: 1.6; margin: 0; padding: 0; @@ -26,87 +167,175 @@ body { /* Header */ .header { - background: rgba(255, 255, 255, 0.95); + background: var(--bg-secondary); backdrop-filter: blur(20px); - padding: 20px; - border-bottom: 1px solid rgba(255, 255, 255, 0.3); + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--spacing-xl); } .logo { display: flex; align-items: center; - margin-bottom: 12px; } .logo-icon { font-size: 24px; - margin-right: 12px; + margin-right: var(--spacing-md); } .logo h1 { font-size: 20px; font-weight: 600; - color: #2d3748; + color: var(--text-primary); + margin: 0; +} + +.header-actions { + display: flex; + align-items: center; + gap: var(--spacing-md); +} + +.btn-icon-only { + background: var(--bg-button-ghost); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: var(--spacing-sm); + cursor: pointer; + color: var(--text-secondary); + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; +} + +.btn-icon-only:hover { + background: var(--bg-input); + color: var(--text-primary); } .status-indicator { display: flex; align-items: center; - font-size: 14px; - color: #666; + justify-content: space-between; + width: 100%; + gap: var(--spacing-md); +} + +.status-indicator .status-content { + display: flex; + align-items: center; + gap: var(--spacing-sm); } .status-dot { - width: 8px; - height: 8px; + width: 10px; + height: 10px; border-radius: 50%; - background-color: #fbbf24; - margin-right: 8px; - animation: pulse 2s infinite; + background: var(--accent-warning); + box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.2); } .status-indicator.connected .status-dot { - background-color: #10b981; - animation: none; + background: var(--accent-success); + box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2); } -@keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } +#statusText { + font-size: 14px; + font-weight: 500; + color: white; } +/* Vault status specific styling */ +.vault-status .status-indicator { + color: white; +} + +.vault-status #statusText { + color: white; +} + +/* Enhanced lock button styling */ +#lockBtn { + display: flex; + align-items: center; + gap: var(--spacing-xs); + padding: var(--spacing-sm) var(--spacing-md); + font-size: 13px; + border-radius: 8px; +} + +#lockBtn svg { + width: 14px; + height: 14px; +} + +/* Vault status lock button styling */ +.vault-status #lockBtn { + background: rgba(255, 255, 255, 0.1); + color: white; + border-color: rgba(255, 255, 255, 0.3); +} + +.vault-status #lockBtn:hover { + background: rgba(255, 255, 255, 0.2); + border-color: rgba(255, 255, 255, 0.5); +} + + + /* Sections */ .section { - padding: 20px; + padding: var(--spacing-xl); flex: 1; } .section:last-child { - padding-bottom: 20px; + padding-bottom: var(--spacing-xl); } .section.hidden { display: none; } -.completely-hidden { +/* Center the auth section */ +#authSection { + display: flex; + align-items: center; + justify-content: center; + min-height: calc(100vh - 120px); /* Account for header height */ + padding: var(--spacing-3xl) var(--spacing-xl); +} + +#authSection .card { + width: 100%; + max-width: 350px; + margin: 0; +} + +.hidden { display: none !important; } /* Cards */ .card { - background: rgba(255, 255, 255, 0.95); + background: var(--bg-card); backdrop-filter: blur(20px); border-radius: 16px; - padding: 20px; - margin-bottom: 16px; - border: 1px solid rgba(255, 255, 255, 0.3); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + padding: var(--spacing-xl); + margin-bottom: var(--spacing-lg); + border: 1px solid var(--border-color); + box-shadow: var(--shadow-card); } .card h2, .card h3 { - margin-bottom: 16px; - color: #2d3748; + margin-bottom: var(--spacing-lg); + color: var(--text-primary); font-weight: 600; } @@ -120,53 +349,67 @@ body { /* Forms */ .form-group { - margin-bottom: 16px; + margin-bottom: var(--spacing-lg); } .form-row { display: flex; - gap: 8px; + gap: var(--spacing-sm); align-items: end; } + + label { display: block; - margin-bottom: 6px; + margin-bottom: var(--spacing-xs); font-weight: 500; - color: #4a5568; + color: var(--text-secondary); font-size: 14px; } input, select, textarea { width: 100%; - padding: 12px 16px; - border: 2px solid rgba(255, 255, 255, 0.3); + padding: var(--spacing-md) var(--spacing-lg); + border: 2px solid var(--border-input); border-radius: 12px; - background: rgba(255, 255, 255, 0.8); + background: var(--bg-input); font-size: 14px; - transition: all 0.2s ease; + color: var(--text-primary); } input:focus, select:focus, textarea:focus { outline: none; - border-color: #667eea; - background: rgba(255, 255, 255, 0.95); + border-color: var(--border-focus); + background: var(--bg-card); box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); } +/* Placeholder styling */ +input::placeholder, textarea::placeholder { + color: var(--text-muted); + opacity: 1; +} + +/* Dark theme placeholder styling */ +[data-theme="dark"] input::placeholder, +[data-theme="dark"] textarea::placeholder { + color: #e2e8f0; + opacity: 0.8; +} + .select { flex: 1; } /* Buttons */ .btn { - padding: 12px 24px; + padding: var(--spacing-md) var(--spacing-2xl); border: none; border-radius: 12px; font-size: 14px; font-weight: 500; cursor: pointer; - transition: all 0.2s ease; text-decoration: none; display: inline-flex; align-items: center; @@ -174,45 +417,48 @@ input:focus, select:focus, textarea:focus { } .btn-primary { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background: var(--bg-button); color: white; + border: 2px solid rgba(255, 255, 255, 0.3); + backdrop-filter: blur(10px); } .btn-primary:hover { - transform: translateY(-1px); - box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3); + background: rgba(255, 255, 255, 0.3); + border-color: rgba(255, 255, 255, 0.5); } .btn-secondary { - background: rgba(255, 255, 255, 0.8); - color: #4a5568; - border: 2px solid rgba(102, 126, 234, 0.2); + background: var(--bg-button-secondary); + color: white; + border: 2px solid rgba(255, 255, 255, 0.2); + backdrop-filter: blur(10px); } .btn-secondary:hover { - background: rgba(255, 255, 255, 0.95); - border-color: rgba(102, 126, 234, 0.4); + background: rgba(255, 255, 255, 0.25); + border-color: rgba(255, 255, 255, 0.4); } .btn-ghost { - background: transparent; - color: #667eea; - border: 1px solid rgba(102, 126, 234, 0.3); + background: var(--bg-button-ghost); + color: white; + border: 1px solid rgba(255, 255, 255, 0.3); } .btn-ghost:hover { - background: rgba(102, 126, 234, 0.1); + background: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.5); } .btn-small { - padding: 8px 16px; + padding: var(--spacing-sm) var(--spacing-lg); font-size: 12px; } .btn:disabled { opacity: 0.5; cursor: not-allowed; - transform: none !important; } .btn.loading { @@ -237,7 +483,7 @@ input:focus, select:focus, textarea:focus { } .btn-secondary.loading::after { - color: #4a5568; + color: var(--text-secondary); } @keyframes btn-spin { @@ -250,9 +496,9 @@ input:focus, select:focus, textarea:focus { display: flex; align-items: center; justify-content: center; - gap: 8px; - padding: 8px; - color: #666; + gap: var(--spacing-sm); + padding: var(--spacing-sm); + color: var(--text-muted); font-size: 14px; } @@ -260,27 +506,52 @@ input:focus, select:focus, textarea:focus { width: 16px; height: 16px; border: 2px solid rgba(102, 126, 234, 0.2); - border-top: 2px solid #667eea; + border-top: 2px solid var(--border-focus); border-radius: 50%; animation: btn-spin 0.8s linear infinite; } .button-group { display: flex; - gap: 12px; + gap: var(--spacing-md); } .btn-copy { - background: transparent; - border: none; + background: var(--bg-button-secondary); + border: 1px solid var(--border-color); cursor: pointer; - padding: 4px; - border-radius: 6px; - transition: background 0.2s ease; + padding: 8px; + border-radius: 8px; + color: var(--text-secondary); + flex-shrink: 0; + height: fit-content; + margin-top: 2px; + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; } .btn-copy:hover { background: rgba(102, 126, 234, 0.1); + border-color: rgba(102, 126, 234, 0.3); +} + +.encrypt-result .btn-copy:hover { + background: rgba(16, 185, 129, 0.1); + border-color: rgba(16, 185, 129, 0.3); +} + +.decrypt-result .btn-copy:hover { + background: rgba(59, 130, 246, 0.1); + border-color: rgba(59, 130, 246, 0.3); +} + +.btn-copy svg { + width: 14px; + height: 14px; + stroke-width: 2; } /* Vault Header */ @@ -288,7 +559,13 @@ input:focus, select:focus, textarea:focus { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 20px; + margin-bottom: var(--spacing-xl); +} + +/* Vault Status Section */ +.vault-status { + padding: 0 0 var(--spacing-lg) 0; + margin-bottom: var(--spacing-lg); } .vault-header h2 { @@ -299,20 +576,16 @@ input:focus, select:focus, textarea:focus { /* Add Keypair Toggle */ .add-keypair-toggle { - margin-bottom: 16px; + margin-bottom: var(--spacing-lg); text-align: center; } .btn-icon { - margin-right: 8px; + margin-right: var(--spacing-sm); font-weight: bold; - transition: transform 0.2s ease; } .add-keypair-form { - transform: translateY(-10px); - opacity: 0; - transition: all 0.3s ease; max-height: 0; overflow: hidden; padding: 0; @@ -320,47 +593,42 @@ input:focus, select:focus, textarea:focus { } .add-keypair-form:not(.hidden) { - transform: translateY(0); - opacity: 1; max-height: 300px; - padding: 20px; - margin-bottom: 16px; + padding: var(--spacing-xl); + margin-bottom: var(--spacing-lg); } .form-header { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 16px; - padding-bottom: 12px; + margin-bottom: var(--spacing-lg); + padding-bottom: var(--spacing-md); border-bottom: 1px solid rgba(102, 126, 234, 0.1); } .form-header h3 { margin: 0; - color: #2d3748; + color: var(--text-primary); } .btn-close { background: none; border: none; font-size: 20px; - color: #666; + color: var(--text-muted); cursor: pointer; - padding: 4px 8px; + padding: var(--spacing-xs) var(--spacing-sm); border-radius: 6px; - transition: all 0.2s ease; line-height: 1; } .btn-close:hover { background: rgba(239, 68, 68, 0.1); - color: #ef4444; + color: var(--accent-error); } -.form-content { - animation: slideInUp 0.3s ease-out; -} + .form-actions { margin-top: 16px; @@ -371,43 +639,54 @@ input:focus, select:focus, textarea:focus { /* Keypairs List */ .keypairs-list { - max-height: 200px; + max-height: 240px; overflow-y: auto; overflow-x: hidden; + padding: var(--spacing-xs); + margin: -var(--spacing-xs); } .keypair-item { display: flex; justify-content: space-between; align-items: center; - padding: 12px; - border-radius: 8px; - margin-bottom: 8px; - background: rgba(102, 126, 234, 0.05); - border: 1px solid rgba(102, 126, 234, 0.1); - transition: all 0.2s ease; + padding: var(--spacing-lg); + border-radius: 12px; + margin-bottom: var(--spacing-md); + background: var(--bg-input); + border: 1px solid var(--border-color); min-width: 0; /* Allow flex items to shrink */ + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); +} + +.keypair-item:last-child { + margin-bottom: 0; } .keypair-item:hover { - background: rgba(102, 126, 234, 0.1); - transform: translateX(4px); + background: var(--bg-card); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); } .keypair-item.selected { background: rgba(16, 185, 129, 0.1); - border-color: rgba(16, 185, 129, 0.3); - transform: translateX(4px); + border-color: var(--accent-success); + box-shadow: 0 4px 16px rgba(16, 185, 129, 0.15); } .keypair-item.selected .keypair-name { - color: #065f46; + color: var(--accent-success); font-weight: 600; } +.keypair-item.selected .keypair-type { + background: rgba(16, 185, 129, 0.2); + color: var(--accent-success); +} + .keypair-item.selected .select-btn { background: rgba(16, 185, 129, 0.2); - color: #065f46; + color: var(--accent-success); border-color: rgba(16, 185, 129, 0.3); } @@ -415,32 +694,60 @@ input:focus, select:focus, textarea:focus { background: rgba(16, 185, 129, 0.3); } +.select-btn { + background: var(--bg-button-secondary); + color: var(--text-secondary); + border: 1px solid var(--border-color); + padding: var(--spacing-xs) var(--spacing-md); + border-radius: 8px; + font-size: 12px; + font-weight: 500; + cursor: pointer; + white-space: nowrap; +} + +.select-btn:hover { + background: var(--bg-card); + color: var(--text-primary); + border-color: var(--border-focus); +} + .keypair-info { flex: 1; min-width: 0; /* Allow shrinking */ - margin-right: 12px; + margin-right: var(--spacing-md); + display: flex; + flex-direction: column; + gap: var(--spacing-xs); } .keypair-name { font-weight: 500; - color: #2d3748; + color: var(--text-primary); word-break: break-word; /* Break long names */ overflow: hidden; text-overflow: ellipsis; + font-size: 14px; + line-height: 1.4; } .keypair-type { - font-size: 12px; - color: #666; - background: rgba(102, 126, 234, 0.1); - padding: 2px 8px; - border-radius: 12px; - margin-top: 4px; + font-size: 11px; + font-weight: 500; + color: var(--text-primary); + background: rgba(102, 126, 234, 0.15); + padding: 2px 6px; + border-radius: 6px; + display: inline-block; + width: fit-content; + max-width: fit-content; + letter-spacing: 0.5px; + text-transform: uppercase; } .empty-state, .loading { text-align: center; - color: #666; + color: var(--text-muted); font-style: italic; padding: 20px; } @@ -461,26 +768,44 @@ input:focus, select:focus, textarea:focus { .keypair-info label { font-weight: 500; - color: #4a5568; + color: var(--text-secondary); margin: 0; } .public-key-container, .signature-container { display: flex; - align-items: center; - gap: 8px; + align-items: flex-start; + gap: 12px; flex: 1; margin-left: 12px; } -.keypair-info code, .signature-result code { - background: rgba(102, 126, 234, 0.1); - padding: 6px 10px; - border-radius: 6px; - font-size: 11px; +.keypair-info code, .signature-result code, .encrypt-result code, .decrypt-result code { + background: var(--bg-input); + padding: 12px 16px; + border-radius: 8px; + font-size: 12px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; word-break: break-all; flex: 1; - color: #2d3748; + color: var(--text-primary); + border: 1px solid var(--border-color); + line-height: 1.4; + max-height: 120px; + overflow-y: auto; + white-space: pre-wrap; +} + +.encrypt-result code { + background: var(--bg-card); + border-color: rgba(16, 185, 129, 0.2); + color: var(--text-primary); +} + +.decrypt-result code { + background: var(--bg-card); + border-color: rgba(59, 130, 246, 0.2); + color: var(--text-primary); } /* Signature Result */ @@ -492,12 +817,10 @@ input:focus, select:focus, textarea:focus { border: 1px solid rgba(16, 185, 129, 0.2); } -.signature-result.hidden { - display: none; -} + .signature-result label { - color: #065f46; + color: var(--accent-success); font-weight: 500; margin-bottom: 8px; } @@ -509,94 +832,190 @@ input:focus, select:focus, textarea:focus { left: 0; right: 0; bottom: 0; - background: rgba(255, 255, 255, 0.95); + background: var(--bg-secondary); backdrop-filter: blur(8px); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 1000; + color: var(--text-primary); } -.loading-overlay.hidden { - display: none; -} + .spinner { width: 40px; height: 40px; border: 3px solid rgba(102, 126, 234, 0.3); - border-top: 3px solid #667eea; + border-top: 3px solid var(--border-focus); border-radius: 50%; - animation: spin 1s linear infinite; margin-bottom: 16px; } -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -/* Toast Notifications */ -.toast { +/* Enhanced Toast Notifications */ +.toast-notification { position: fixed; top: 20px; - left: 20px; right: 20px; - max-width: 360px; - margin: 0 auto; - padding: 12px 16px; + min-width: 320px; + max-width: 400px; + padding: 16px 20px; border-radius: 12px; font-size: 14px; font-weight: 500; - text-align: center; z-index: 1001; - transform: translateY(-100px); - opacity: 0; - transition: all 0.3s ease; backdrop-filter: blur(20px); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); + display: flex; + align-items: flex-start; + gap: 12px; + border: 1px solid transparent; + transform: translateX(100%); + opacity: 0; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } -.toast:not(.hidden) { - transform: translateY(0); +.toast-notification.toast-show { + transform: translateX(0); opacity: 1; } -.toast.success { - background: rgba(16, 185, 129, 0.9); - color: white; - border: 1px solid rgba(16, 185, 129, 1); +.toast-notification.toast-hide { + transform: translateX(100%); + opacity: 0; } -.toast.error { - background: rgba(239, 68, 68, 0.9); - color: white; - border: 1px solid rgba(239, 68, 68, 1); +.toast-icon { + flex-shrink: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + margin-top: 2px; } -.toast.info { - background: rgba(59, 130, 246, 0.9); +.toast-content { + flex: 1; + min-width: 0; +} + +.toast-message { + line-height: 1.5; + word-wrap: break-word; +} + +.toast-close { + background: none; + border: none; + color: inherit; + cursor: pointer; + font-size: 18px; + font-weight: bold; + line-height: 1; + opacity: 0.7; + padding: 0; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + transition: all 0.2s ease; + flex-shrink: 0; + margin-top: 2px; +} + +.toast-close:hover { + opacity: 1; + background: rgba(255, 255, 255, 0.1); +} + +/* Success Toast */ +.toast-success { + background: linear-gradient(135deg, rgba(16, 185, 129, 0.95) 0%, rgba(5, 150, 105, 0.95) 100%); color: white; - border: 1px solid rgba(59, 130, 246, 1); + border-color: rgba(16, 185, 129, 0.3); +} + +.toast-success .toast-icon { + background: rgba(255, 255, 255, 0.2); + color: white; +} + +/* Error Toast */ +.toast-error { + background: linear-gradient(135deg, rgba(239, 68, 68, 0.95) 0%, rgba(220, 38, 38, 0.95) 100%); + color: white; + border-color: rgba(239, 68, 68, 0.3); +} + +.toast-error .toast-icon { + background: rgba(255, 255, 255, 0.2); + color: white; +} + +/* Info Toast */ +.toast-info { + background: linear-gradient(135deg, rgba(59, 130, 246, 0.95) 0%, rgba(37, 99, 235, 0.95) 100%); + color: white; + border-color: rgba(59, 130, 246, 0.3); +} + +.toast-info .toast-icon { + background: rgba(255, 255, 255, 0.2); + color: white; +} + +/* Responsive toast positioning */ +@media (max-width: 480px) { + .toast-notification { + left: 16px; + right: 16px; + min-width: auto; + max-width: none; + transform: translateY(-100%); + } + + .toast-notification.toast-show { + transform: translateY(0); + } + + .toast-notification.toast-hide { + transform: translateY(-100%); + } } /* Scrollbar Styles */ -.keypairs-list::-webkit-scrollbar { +.keypairs-list::-webkit-scrollbar, +.encrypt-result code::-webkit-scrollbar, +.decrypt-result code::-webkit-scrollbar, +.signature-result code::-webkit-scrollbar { width: 6px; } -.keypairs-list::-webkit-scrollbar-track { +.keypairs-list::-webkit-scrollbar-track, +.encrypt-result code::-webkit-scrollbar-track, +.decrypt-result code::-webkit-scrollbar-track, +.signature-result code::-webkit-scrollbar-track { background: rgba(102, 126, 234, 0.1); border-radius: 3px; } -.keypairs-list::-webkit-scrollbar-thumb { +.keypairs-list::-webkit-scrollbar-thumb, +.encrypt-result code::-webkit-scrollbar-thumb, +.decrypt-result code::-webkit-scrollbar-thumb, +.signature-result code::-webkit-scrollbar-thumb { background: rgba(102, 126, 234, 0.3); border-radius: 3px; } -.keypairs-list::-webkit-scrollbar-thumb:hover { +.keypairs-list::-webkit-scrollbar-thumb:hover, +.encrypt-result code::-webkit-scrollbar-thumb:hover, +.decrypt-result code::-webkit-scrollbar-thumb:hover, +.signature-result code::-webkit-scrollbar-thumb:hover { background: rgba(102, 126, 234, 0.5); } @@ -616,39 +1035,143 @@ input:focus, select:focus, textarea:focus { } } -/* Animation for card entrance */ -.card { - animation: slideInUp 0.3s ease-out; + + + + + + +/* Operation Tabs */ +.operation-tabs { + display: flex; + margin-bottom: 20px; + border-bottom: 2px solid rgba(102, 126, 234, 0.1); + gap: 4px; + overflow-x: auto; /* Enable horizontal scrolling */ + overflow-y: hidden; + scrollbar-width: none; /* Hide scrollbar in Firefox */ + -webkit-overflow-scrolling: touch; /* Smooth scrolling on mobile */ + scroll-behavior: smooth; /* Smooth scrolling animation */ } -@keyframes slideInUp { - from { - transform: translateY(20px); - opacity: 0; - } - to { - transform: translateY(0); - opacity: 1; - } +/* Hide scrollbar for webkit browsers */ +.operation-tabs::-webkit-scrollbar { + display: none; } -/* Hover effects for better UX */ -.keypair-item { - position: relative; - overflow: hidden; +.tab-btn { + background: transparent; + border: none; + padding: 12px 20px; + border-radius: 8px 8px 0 0; + cursor: pointer; + font-size: 14px; + font-weight: 500; + color: var(--text-muted); + border-bottom: 3px solid transparent; + flex-shrink: 0; /* Prevent tabs from shrinking */ + min-width: fit-content; + white-space: nowrap; + text-align: center; } -.keypair-item::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); - transition: left 0.5s; +.tab-btn:hover { + background: rgba(102, 126, 234, 0.1); + color: var(--border-focus); } -.keypair-item:hover::before { - left: 100%; -} \ No newline at end of file +.tab-btn.active { + background: rgba(102, 126, 234, 0.1); + color: var(--border-focus); + border-bottom-color: var(--border-focus); + font-weight: 600; +} + +.tab-content { + display: none; +} + +.tab-content.active { + display: block; +} + +/* Result Styling */ +.encrypt-result, .decrypt-result, .verify-result { + margin-top: 16px; + padding: 16px; + border-radius: 12px; + border: 1px solid rgba(102, 126, 234, 0.2); +} + +.encrypt-result { + background: rgba(16, 185, 129, 0.1); + border-color: rgba(16, 185, 129, 0.2); + box-shadow: 0 4px 16px rgba(16, 185, 129, 0.1); +} + +.encrypt-result label { + color: var(--accent-success); + font-weight: 600; + margin-bottom: 12px; + display: block; + font-size: 14px; +} + +.decrypt-result { + background: rgba(59, 130, 246, 0.1); + border-color: rgba(59, 130, 246, 0.2); + box-shadow: 0 4px 16px rgba(59, 130, 246, 0.1); +} + +.decrypt-result label { + color: var(--accent-info); + font-weight: 600; + margin-bottom: 12px; + display: block; + font-size: 14px; +} + +.verify-result { + background: rgba(102, 126, 234, 0.1); + border-color: rgba(102, 126, 234, 0.2); + box-shadow: 0 4px 16px rgba(102, 126, 234, 0.1); +} + +.verification-status { + display: flex; + align-items: center; + gap: 12px; + font-size: 16px; + font-weight: 600; +} + +.verification-status.valid { + color: var(--accent-success); +} + +.verification-status.invalid { + color: var(--accent-error); +} + +.verification-status.valid #verificationIcon { + color: var(--accent-success); +} + +.verification-status.invalid #verificationIcon { + color: var(--accent-error); +} + + + + + + + + + + + + + + + diff --git a/crypto_vault_extension/wasm/wasm_app.js b/crypto_vault_extension/wasm/wasm_app.js index 5583e35..57c87b8 100644 --- a/crypto_vault_extension/wasm/wasm_app.js +++ b/crypto_vault_extension/wasm/wasm_app.js @@ -202,33 +202,6 @@ function debugString(val) { // TODO we could test for more things here, like `Set`s and `Map`s. return className; } -/** - * Initialize the scripting environment (must be called before run_rhai) - */ -export function init_rhai_env() { - wasm.init_rhai_env(); -} - -function takeFromExternrefTable0(idx) { - const value = wasm.__wbindgen_export_2.get(idx); - wasm.__externref_table_dealloc(idx); - return value; -} -/** - * Securely run a Rhai script in the extension context (must be called only after user approval) - * @param {string} script - * @returns {any} - */ -export function run_rhai(script) { - const ptr0 = passStringToWasm0(script, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - const ret = wasm.run_rhai(ptr0, len0); - if (ret[2]) { - throw takeFromExternrefTable0(ret[1]); - } - return takeFromExternrefTable0(ret[0]); -} - /** * Create and unlock a new keyspace with the given name and password * @param {string} keyspace @@ -266,6 +239,11 @@ export function lock_session() { wasm.lock_session(); } +function takeFromExternrefTable0(idx) { + const value = wasm.__wbindgen_export_2.get(idx); + wasm.__externref_table_dealloc(idx); + return value; +} /** * Get metadata of the currently selected keypair * @returns {any} @@ -356,20 +334,81 @@ export function sign(message) { return ret; } +/** + * Verify a signature with the current session's selected keypair + * @param {Uint8Array} message + * @param {string} signature + * @returns {Promise} + */ +export function verify(message, signature) { + const ptr0 = passArray8ToWasm0(message, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passStringToWasm0(signature, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + const ret = wasm.verify(ptr0, len0, ptr1, len1); + return ret; +} + +/** + * Encrypt data using the current session's keyspace symmetric cipher + * @param {Uint8Array} data + * @returns {Promise} + */ +export function encrypt_data(data) { + const ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.encrypt_data(ptr0, len0); + return ret; +} + +/** + * Decrypt data using the current session's keyspace symmetric cipher + * @param {Uint8Array} encrypted + * @returns {Promise} + */ +export function decrypt_data(encrypted) { + const ptr0 = passArray8ToWasm0(encrypted, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.decrypt_data(ptr0, len0); + return ret; +} + +/** + * Initialize the scripting environment (must be called before run_rhai) + */ +export function init_rhai_env() { + wasm.init_rhai_env(); +} + +/** + * Securely run a Rhai script in the extension context (must be called only after user approval) + * @param {string} script + * @returns {any} + */ +export function run_rhai(script) { + const ptr0 = passStringToWasm0(script, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.run_rhai(ptr0, len0); + if (ret[2]) { + throw takeFromExternrefTable0(ret[1]); + } + return takeFromExternrefTable0(ret[0]); +} + function __wbg_adapter_32(arg0, arg1, arg2) { - wasm.closure89_externref_shim(arg0, arg1, arg2); + wasm.closure121_externref_shim(arg0, arg1, arg2); } function __wbg_adapter_35(arg0, arg1, arg2) { - wasm.closure133_externref_shim(arg0, arg1, arg2); + wasm.closure150_externref_shim(arg0, arg1, arg2); } function __wbg_adapter_38(arg0, arg1, arg2) { - wasm.closure188_externref_shim(arg0, arg1, arg2); + wasm.closure227_externref_shim(arg0, arg1, arg2); } -function __wbg_adapter_135(arg0, arg1, arg2, arg3) { - wasm.closure1847_externref_shim(arg0, arg1, arg2, arg3); +function __wbg_adapter_138(arg0, arg1, arg2, arg3) { + wasm.closure1879_externref_shim(arg0, arg1, arg2, arg3); } const __wbindgen_enum_IdbTransactionMode = ["readonly", "readwrite", "versionchange", "readwriteflush", "cleanup"]; @@ -519,7 +558,7 @@ function __wbg_get_imports() { const a = state0.a; state0.a = 0; try { - return __wbg_adapter_135(a, state0.b, arg0, arg1); + return __wbg_adapter_138(a, state0.b, arg0, arg1); } finally { state0.a = a; } @@ -673,16 +712,16 @@ function __wbg_get_imports() { const ret = false; return ret; }; - imports.wbg.__wbindgen_closure_wrapper288 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 90, __wbg_adapter_32); + imports.wbg.__wbindgen_closure_wrapper378 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 122, __wbg_adapter_32); return ret; }; - imports.wbg.__wbindgen_closure_wrapper518 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 134, __wbg_adapter_35); + imports.wbg.__wbindgen_closure_wrapper549 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 151, __wbg_adapter_35); return ret; }; - imports.wbg.__wbindgen_closure_wrapper776 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 189, __wbg_adapter_38); + imports.wbg.__wbindgen_closure_wrapper857 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 228, __wbg_adapter_38); return ret; }; imports.wbg.__wbindgen_debug_string = function(arg0, arg1) { diff --git a/crypto_vault_extension/wasm/wasm_app_bg.wasm b/crypto_vault_extension/wasm/wasm_app_bg.wasm index d5b242e..6ededfd 100644 Binary files a/crypto_vault_extension/wasm/wasm_app_bg.wasm and b/crypto_vault_extension/wasm/wasm_app_bg.wasm differ