670 lines
22 KiB
JavaScript
670 lines
22 KiB
JavaScript
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);
|
||
}
|
||
}
|
||
} |