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 @@