sal-modular/crypto_vault_extension/background.js

670 lines
22 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

let vault = null;
let isInitialized = false;
let currentSession = null;
let keepAliveInterval = null;
let sessionTimeoutDuration = 15; // Default 15 seconds
let sessionTimeoutId = null; // Background timer
let popupPort = null; // Track popup connection
// SigSocket service instance
let sigSocketService = null;
// Utility function to convert Uint8Array to hex
function toHex(uint8Array) {
return Array.from(uint8Array)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
// Background session timeout management
async function loadTimeoutSetting() {
const result = await chrome.storage.local.get(['sessionTimeout']);
sessionTimeoutDuration = result.sessionTimeout || 15;
}
function startSessionTimeout() {
clearSessionTimeout();
if (currentSession && sessionTimeoutDuration > 0) {
sessionTimeoutId = setTimeout(async () => {
if (vault && currentSession) {
// Lock the session
vault.lock_session();
// Keep the session info for SigSocket connection but mark it as timed out
const keyspace = currentSession.keyspace;
await sessionManager.clear();
// Maintain SigSocket connection for the locked keyspace to receive pending requests
if (sigSocketService && keyspace) {
try {
// Keep SigSocket connected to receive requests even when locked
console.log(`🔒 Session timed out but maintaining SigSocket connection for: ${keyspace}`);
} catch (error) {
console.warn('Failed to maintain SigSocket connection after timeout:', error);
}
}
// Notify popup if it's open
if (popupPort) {
popupPort.postMessage({
type: 'sessionTimeout',
message: 'Session timed out due to inactivity'
});
}
}
}, sessionTimeoutDuration * 1000);
}
}
function clearSessionTimeout() {
if (sessionTimeoutId) {
clearTimeout(sessionTimeoutId);
sessionTimeoutId = null;
}
}
function resetSessionTimeout() {
if (currentSession) {
startSessionTimeout();
}
}
// Session persistence functions
async function saveSession(keyspace) {
currentSession = { keyspace, timestamp: Date.now() };
// Save to both session and local storage for better persistence
await chrome.storage.session.set({ cryptoVaultSession: currentSession });
await chrome.storage.local.set({ cryptoVaultSessionBackup: currentSession });
}
async function loadSession() {
// Try session storage first
let result = await chrome.storage.session.get(['cryptoVaultSession']);
if (result.cryptoVaultSession) {
currentSession = result.cryptoVaultSession;
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;
}
return null;
}
async function clearSession() {
currentSession = null;
await chrome.storage.session.remove(['cryptoVaultSession']);
await chrome.storage.local.remove(['cryptoVaultSessionBackup']);
}
// Keep service worker alive
function startKeepAlive() {
if (keepAliveInterval) {
clearInterval(keepAliveInterval);
}
keepAliveInterval = setInterval(() => {
chrome.storage.session.get(['keepAlive']).catch(() => {});
}, 20000);
}
function stopKeepAlive() {
if (keepAliveInterval) {
clearInterval(keepAliveInterval);
keepAliveInterval = null;
}
}
// Consolidated session management
const sessionManager = {
async save(keyspace) {
await saveSession(keyspace);
startKeepAlive();
await loadTimeoutSetting();
startSessionTimeout();
},
async clear() {
await clearSession();
stopKeepAlive();
clearSessionTimeout();
}
};
async function restoreSession() {
const session = await loadSession();
if (session && vault) {
// Check if the session is still valid by testing if vault is unlocked
const isUnlocked = vault.is_unlocked();
if (isUnlocked) {
// Restart keep-alive for restored session
startKeepAlive();
// Connect to SigSocket for the restored session
if (sigSocketService) {
try {
const connected = await sigSocketService.connectToServer(session.keyspace);
if (connected) {
console.log(`🔗 SigSocket reconnected for restored workspace: ${session.keyspace}`);
}
} catch (error) {
// Don't show as warning if it's just "no workspace" - this is expected on fresh start
if (error.message && error.message.includes('Workspace not found')) {
console.log(` SigSocket connection skipped for restored session: No workspace available yet`);
} else {
console.warn('Failed to reconnect SigSocket for restored session:', error);
}
}
}
return session;
} else {
// Session exists but is locked - still try to connect SigSocket to receive pending requests
if (sigSocketService && session.keyspace) {
try {
const connected = await sigSocketService.connectToServer(session.keyspace);
if (connected) {
console.log(`🔗 SigSocket connected for locked workspace: ${session.keyspace} (will queue requests)`);
}
} catch (error) {
// Don't show as warning if it's just "no workspace" - this is expected on fresh start
if (error.message && error.message.includes('Workspace not found')) {
console.log(` SigSocket connection skipped for locked session: No workspace available yet`);
} else {
console.warn('Failed to connect SigSocket for locked session:', error);
}
}
}
// Don't clear the session - keep it for SigSocket connection
// await sessionManager.clear();
}
}
return session; // Return session even if locked, so we know which keyspace to use
}
// Import WASM module functions and SigSocket service
import init, * as wasmFunctions from './wasm/wasm_app.js';
import SigSocketService from './background/sigsocket.js';
// Initialize WASM module
async function initVault() {
try {
if (vault && isInitialized) return vault;
// Initialize with the WASM file
const wasmUrl = chrome.runtime.getURL('wasm/wasm_app_bg.wasm');
await init(wasmUrl);
// Use imported functions directly
vault = wasmFunctions;
isInitialized = true;
// Initialize SigSocket service
if (!sigSocketService) {
sigSocketService = new SigSocketService();
await sigSocketService.initialize(vault);
console.log('🔌 SigSocket service initialized');
}
// Try to restore previous session
await restoreSession();
return vault;
} catch (error) {
console.error('Failed to initialize CryptoVault:', error);
throw error;
}
}
// Consolidated message handlers
const messageHandlers = {
createKeyspace: async (request) => {
await vault.create_keyspace(request.keyspace, request.password);
return { success: true };
},
initSession: async (request) => {
await vault.init_session(request.keyspace, request.password);
await sessionManager.save(request.keyspace);
// Smart auto-connect to SigSocket when session is initialized
if (sigSocketService) {
try {
console.log(`🔗 Initializing SigSocket connection for workspace: ${request.keyspace}`);
// This will reuse existing connection if same workspace, or switch if different
const connected = await sigSocketService.connectToServer(request.keyspace);
if (connected) {
console.log(`✅ SigSocket ready for workspace: ${request.keyspace}`);
} else {
console.warn(`⚠️ SigSocket connection failed for workspace: ${request.keyspace}`);
}
} catch (error) {
console.warn('Failed to auto-connect to SigSocket:', error);
// If connection fails, try once more after a short delay
setTimeout(async () => {
try {
console.log(`🔄 Retrying SigSocket connection for workspace: ${request.keyspace}`);
await sigSocketService.connectToServer(request.keyspace);
} catch (retryError) {
console.warn('SigSocket retry connection also failed:', retryError);
}
}, 2000);
}
// Notify SigSocket service that keyspace is now unlocked
await sigSocketService.onKeypaceUnlocked();
}
return { success: true };
},
isUnlocked: () => ({ success: true, unlocked: vault.is_unlocked() }),
addKeypair: async (request) => {
const result = await vault.add_keypair(request.keyType, request.metadata);
return { success: true, result };
},
listKeypairs: async () => {
if (!vault.is_unlocked()) {
return { success: false, error: 'Session is not unlocked' };
}
const keypairsRaw = await vault.list_keypairs();
const keypairs = typeof keypairsRaw === 'string' ? JSON.parse(keypairsRaw) : keypairsRaw;
return { success: true, keypairs };
},
selectKeypair: (request) => {
vault.select_keypair(request.keyId);
return { success: true };
},
getCurrentKeypairMetadata: () => ({ success: true, metadata: vault.current_keypair_metadata() }),
getCurrentKeypairPublicKey: () => ({ success: true, publicKey: toHex(vault.current_keypair_public_key()) }),
sign: async (request) => {
const signature = await vault.sign(new Uint8Array(request.message));
return { success: true, signature };
},
encrypt: async (request) => {
if (!vault.is_unlocked()) {
return { success: false, error: 'Session is not unlocked' };
}
const messageBytes = new TextEncoder().encode(request.message);
const encryptedData = await vault.encrypt_data(messageBytes);
const encryptedMessage = btoa(String.fromCharCode(...new Uint8Array(encryptedData)));
return { success: true, encryptedMessage };
},
decrypt: async (request) => {
if (!vault.is_unlocked()) {
return { success: false, error: 'Session is not unlocked' };
}
const encryptedBytes = new Uint8Array(atob(request.encryptedMessage).split('').map(c => c.charCodeAt(0)));
const decryptedData = await vault.decrypt_data(encryptedBytes);
const decryptedMessage = new TextDecoder().decode(new Uint8Array(decryptedData));
return { success: true, decryptedMessage };
},
verify: async (request) => {
const metadata = vault.current_keypair_metadata();
if (!metadata) {
return { success: false, error: 'No keypair selected' };
}
const isValid = await vault.verify(new Uint8Array(request.message), request.signature);
return { success: true, isValid };
},
lockSession: async () => {
vault.lock_session();
await sessionManager.clear();
return { success: true };
},
getStatus: async () => {
const status = vault ? vault.is_unlocked() : false;
const session = await loadSession();
return {
success: true,
status,
session: session ? { keyspace: session.keyspace } : null
};
},
// Timeout management handlers
resetTimeout: async () => {
resetSessionTimeout();
return { success: true };
},
updateTimeout: async (request) => {
sessionTimeoutDuration = request.timeout;
await chrome.storage.local.set({ sessionTimeout: request.timeout });
resetSessionTimeout(); // Restart with new duration
return { success: true };
},
updateSigSocketUrl: async (request) => {
if (sigSocketService) {
// Update the server URL in the SigSocket service
sigSocketService.defaultServerUrl = request.serverUrl;
// Save to storage (already done in popup, but ensure consistency)
await chrome.storage.local.set({ sigSocketUrl: request.serverUrl });
console.log(`🔗 SigSocket server URL updated to: ${request.serverUrl}`);
}
return { success: true };
},
// SigSocket handlers
connectSigSocket: async (request) => {
if (!sigSocketService) {
return { success: false, error: 'SigSocket service not initialized' };
}
const connected = await sigSocketService.connectToServer(request.workspace);
return { success: connected };
},
disconnectSigSocket: async () => {
if (!sigSocketService) {
return { success: false, error: 'SigSocket service not initialized' };
}
await sigSocketService.disconnect();
return { success: true };
},
getSigSocketStatus: async () => {
if (!sigSocketService) {
return { success: false, error: 'SigSocket service not initialized' };
}
const status = await sigSocketService.getStatus();
return { success: true, status };
},
getSigSocketStatusWithTest: async () => {
if (!sigSocketService) {
return { success: false, error: 'SigSocket service not initialized' };
}
// Use the enhanced connection testing method
const status = await sigSocketService.getStatusWithConnectionTest();
return { success: true, status };
},
getPendingSignRequests: async () => {
if (!sigSocketService) {
return { success: false, error: 'SigSocket service not initialized' };
}
try {
// Use WASM filtered requests which handles workspace filtering
const requests = await sigSocketService.getFilteredRequests();
return { success: true, requests };
} catch (error) {
console.error('Failed to get pending requests:', error);
return { success: false, error: error.message };
}
},
approveSignRequest: async (request) => {
if (!sigSocketService) {
return { success: false, error: 'SigSocket service not initialized' };
}
const approved = await sigSocketService.approveSignRequest(request.requestId);
return { success: approved };
},
rejectSignRequest: async (request) => {
if (!sigSocketService) {
return { success: false, error: 'SigSocket service not initialized' };
}
const rejected = await sigSocketService.rejectSignRequest(request.requestId, request.reason);
return { success: rejected };
}
};
// Handle messages from popup and content scripts
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
const handleRequest = async () => {
try {
if (!vault) {
await initVault();
}
const handler = messageHandlers[request.action];
if (handler) {
return await handler(request);
} else {
throw new Error('Unknown action: ' + request.action);
}
} catch (error) {
return { success: false, error: error.message };
}
};
handleRequest().then(sendResponse);
return true; // Keep the message channel open for async response
});
// Initialize vault when extension starts
chrome.runtime.onStartup.addListener(() => {
initVault();
});
chrome.runtime.onInstalled.addListener(() => {
initVault();
});
// Handle popup connection for keep-alive and timeout notifications
chrome.runtime.onConnect.addListener((port) => {
if (port.name === 'popup') {
// Track popup connection
popupPort = port;
// Connect SigSocket service to popup
if (sigSocketService) {
sigSocketService.setPopupPort(port);
}
// If we have an active session, ensure keep-alive is running
if (currentSession) {
startKeepAlive();
}
// Handle messages from popup
port.onMessage.addListener(async (message) => {
if (message.type === 'REQUEST_IMMEDIATE_STATUS') {
// Immediately send current SigSocket status to popup
if (sigSocketService) {
try {
const status = await sigSocketService.getStatus();
port.postMessage({
type: 'CONNECTION_STATUS_CHANGED',
status: status
});
console.log('📡 Sent immediate status to popup:', status.isConnected ? 'Connected' : 'Disconnected');
} catch (error) {
console.warn('Failed to send immediate status:', error);
}
}
}
});
port.onDisconnect.addListener(() => {
// Popup closed, clear reference and stop keep-alive
popupPort = null;
stopKeepAlive();
// Disconnect SigSocket service from popup
if (sigSocketService) {
sigSocketService.setPopupPort(null);
}
});
}
});
// Handle notification clicks to open extension (notifications are now clickable without buttons)
chrome.notifications.onClicked.addListener(async (notificationId) => {
console.log(`🔔 Notification clicked: ${notificationId}`);
// Check if this is a SigSocket notification
if (notificationId.startsWith('sigsocket-request-')) {
console.log('🔔 SigSocket notification clicked, opening extension...');
try {
await openExtensionPopup();
// Clear the notification after successfully opening
chrome.notifications.clear(notificationId);
console.log('✅ Notification cleared after opening extension');
} catch (error) {
console.error('❌ Failed to handle notification click:', error);
}
} else {
console.log('🔔 Non-SigSocket notification clicked, ignoring');
}
});
// Note: Notification button handler removed - notifications are now clickable without buttons
// Function to open extension popup with best UX
async function openExtensionPopup() {
try {
console.log('🔔 Opening extension popup from notification...');
// First, check if there's already a popup window open
const windows = await chrome.windows.getAll({ populate: true });
const existingPopup = windows.find(window =>
window.type === 'popup' &&
window.tabs?.some(tab => tab.url?.includes('popup.html'))
);
if (existingPopup) {
// Focus existing popup and send focus message
await chrome.windows.update(existingPopup.id, { focused: true });
console.log('✅ Focused existing popup window');
// Send message to focus on SigSocket section
if (popupPort) {
popupPort.postMessage({
type: 'FOCUS_SIGSOCKET',
fromNotification: true
});
}
return;
}
// Best UX: Try to use the normal popup experience
// The action API gives the same popup as clicking the extension icon
try {
if (chrome.action && chrome.action.openPopup) {
await chrome.action.openPopup();
console.log('✅ Extension popup opened via action API (best UX - normal popup)');
// Send focus message after popup opens
setTimeout(() => {
if (popupPort) {
popupPort.postMessage({
type: 'FOCUS_SIGSOCKET',
fromNotification: true
});
}
}, 200);
return;
}
} catch (actionError) {
// The action API fails when there's no active browser window
// This is common when all browser windows are closed but extension is still running
console.log('⚠️ Action API failed (likely no active window):', actionError.message);
// Check if we have any normal browser windows
const allWindows = await chrome.windows.getAll();
const normalWindows = allWindows.filter(w => w.type === 'normal');
if (normalWindows.length > 0) {
// We have browser windows, try to focus one and retry action API
try {
const targetWindow = normalWindows.find(w => w.focused) || normalWindows[0];
await chrome.windows.update(targetWindow.id, { focused: true });
// Small delay and retry
await new Promise(resolve => setTimeout(resolve, 100));
await chrome.action.openPopup();
console.log('✅ Extension popup opened via action API after focusing window');
setTimeout(() => {
if (popupPort) {
popupPort.postMessage({
type: 'FOCUS_SIGSOCKET',
fromNotification: true
});
}
}, 200);
return;
} catch (retryError) {
console.log('⚠️ Action API retry also failed:', retryError.message);
}
}
}
// If action API fails completely, we need to create a window
// But let's make it as close to the normal popup experience as possible
console.log('⚠️ Creating popup window as fallback (action API unavailable)');
const popupUrl = chrome.runtime.getURL('popup.html?from=notification');
// Position the popup where the extension icon would normally show its popup
// Try to position it in the top-right area like a normal extension popup
let left = screen.width - 420; // 400px width + 20px margin
let top = 80; // Below browser toolbar area
try {
// If we have a browser window, position relative to it
const allWindows = await chrome.windows.getAll();
const normalWindows = allWindows.filter(w => w.type === 'normal');
if (normalWindows.length > 0) {
const referenceWindow = normalWindows[0];
left = (referenceWindow.left || 0) + (referenceWindow.width || 800) - 420;
top = (referenceWindow.top || 0) + 80;
}
} catch (positionError) {
console.log('⚠️ Could not get window position, using screen-based positioning');
}
const newWindow = await chrome.windows.create({
url: popupUrl,
type: 'popup',
width: 400,
height: 600,
left: Math.max(0, left),
top: Math.max(0, top),
focused: true
});
console.log(`✅ Extension popup window created: ${newWindow.id}`);
} catch (error) {
console.error('❌ Failed to open extension popup:', error);
// Final fallback: open in new tab (least ideal but still functional)
try {
const popupUrl = chrome.runtime.getURL('popup.html?from=notification');
await chrome.tabs.create({ url: popupUrl, active: true });
console.log('✅ Opened extension in new tab as final fallback');
} catch (tabError) {
console.error('❌ All popup opening methods failed:', tabError);
}
}
}