sal-modular/crypto_vault_extension/background/sigsocket.js
Sameh Abouel-saad 580fd72dce feat: Implement Sign Request Manager component for handling sign requests in the popup (WIP)
- Added SignRequestManager.js to manage sign requests, including UI states for keyspace lock, mismatch, and approval.
- Implemented methods for loading state, rendering UI, and handling user interactions (approve/reject requests).
- Integrated background message listeners for keyspace unlock events and request updates.
2025-06-04 16:55:15 +03:00

477 lines
18 KiB
JavaScript

/**
* SigSocket Service for Browser Extension
*
* Handles SigSocket client functionality including:
* - Auto-connecting to SigSocket server when workspace is created
* - Managing pending sign requests
* - Handling user approval/rejection flow
* - Validating keyspace matches before showing approval UI
*/
class SigSocketService {
constructor() {
this.connection = null;
this.pendingRequests = new Map(); // requestId -> SignRequestData
this.connectedPublicKey = null;
this.isConnected = false;
this.defaultServerUrl = "ws://localhost:8080/ws";
// Initialize WASM module reference
this.wasmModule = null;
// Reference to popup port for communication
this.popupPort = null;
}
/**
* Initialize the service with WASM module
* @param {Object} wasmModule - The loaded WASM module
*/
async initialize(wasmModule) {
this.wasmModule = wasmModule;
// Load server URL from storage
try {
const result = await chrome.storage.local.get(['sigSocketUrl']);
if (result.sigSocketUrl) {
this.defaultServerUrl = result.sigSocketUrl;
}
} catch (error) {
console.warn('Failed to load SigSocket URL from storage:', error);
}
// Set up global callbacks for WASM
globalThis.onSignRequestReceived = this.handleIncomingRequest.bind(this);
globalThis.onConnectionStateChanged = this.handleConnectionStateChange.bind(this);
}
/**
* Connect to SigSocket server for a workspace
* @param {string} workspaceId - The workspace/keyspace identifier
* @returns {Promise<boolean>} - True if connected successfully
*/
async connectToServer(workspaceId) {
try {
if (!this.wasmModule) {
throw new Error('WASM module not initialized');
}
// Check if already connected to this workspace
if (this.isConnected && this.connection) {
console.log(`Already connected to SigSocket server for workspace: ${workspaceId}`);
return true;
}
// Disconnect any existing connection first
if (this.connection) {
this.disconnect();
}
// Get the workspace default public key
const publicKeyHex = await this.wasmModule.get_workspace_default_public_key(workspaceId);
if (!publicKeyHex) {
throw new Error('No public key found for workspace');
}
console.log(`Connecting to SigSocket server for workspace: ${workspaceId} with key: ${publicKeyHex.substring(0, 16)}...`);
// Create new SigSocket connection
console.log('Creating new SigSocketConnection instance');
this.connection = new this.wasmModule.SigSocketConnection();
console.log('SigSocketConnection instance created');
// Connect to server
await this.connection.connect(this.defaultServerUrl, publicKeyHex);
this.connectedPublicKey = publicKeyHex;
// Clear pending requests if switching to a different workspace
if (this.currentWorkspace && this.currentWorkspace !== workspaceId) {
console.log(`Switching workspace from ${this.currentWorkspace} to ${workspaceId}, clearing pending requests`);
this.pendingRequests.clear();
this.updateBadge();
}
this.currentWorkspace = workspaceId;
this.isConnected = true;
console.log(`Successfully connected to SigSocket server for workspace: ${workspaceId}`);
return true;
} catch (error) {
console.error('Failed to connect to SigSocket server:', error);
this.isConnected = false;
this.connectedPublicKey = null;
this.currentWorkspace = null;
if (this.connection) {
this.connection.disconnect();
this.connection = null;
}
return false;
}
}
/**
* Handle incoming sign request from server
* @param {string} requestId - Unique request identifier
* @param {string} messageBase64 - Message to be signed (base64-encoded)
*/
handleIncomingRequest(requestId, messageBase64) {
console.log(`Received sign request: ${requestId}`);
// Security check: Only accept requests if we have an active connection
if (!this.isConnected || !this.connectedPublicKey || !this.currentWorkspace) {
console.warn(`Rejecting sign request ${requestId}: No active workspace connection`);
return;
}
// Store the request with workspace info
const requestData = {
id: requestId,
message: messageBase64,
timestamp: Date.now(),
status: 'pending',
workspace: this.currentWorkspace,
connectedPublicKey: this.connectedPublicKey
};
this.pendingRequests.set(requestId, requestData);
console.log(`Stored sign request for workspace: ${this.currentWorkspace}`);
// Show notification to user
this.showSignRequestNotification();
// Update extension badge
this.updateBadge();
// Notify popup about new request if it's open and keyspace is unlocked
this.notifyPopupOfNewRequest();
}
/**
* Handle connection state changes
* @param {boolean} connected - True if connected, false if disconnected
*/
handleConnectionStateChange(connected) {
this.isConnected = connected;
console.log(`SigSocket connection state changed: ${connected ? 'connected' : 'disconnected'}`);
if (!connected) {
this.connectedPublicKey = null;
this.currentWorkspace = null;
// Optionally attempt reconnection here
}
}
/**
* Called when keyspace is unlocked - validate and show/hide approval UI
*/
async onKeypaceUnlocked() {
try {
if (!this.wasmModule) {
return;
}
// Only check keyspace match if we have a connection
if (!this.isConnected || !this.connectedPublicKey) {
console.log('No SigSocket connection to validate against');
return;
}
// Get the currently unlocked workspace name
const unlockedWorkspaceName = this.wasmModule.get_current_keyspace_name();
// Get workspace default public key for the UNLOCKED workspace (not connected workspace)
const unlockedWorkspacePublicKey = await this.wasmModule.get_workspace_default_public_key(unlockedWorkspaceName);
// Check if the unlocked workspace matches the connected workspace
const workspaceMatches = unlockedWorkspaceName === this.currentWorkspace;
const publicKeyMatches = unlockedWorkspacePublicKey === this.connectedPublicKey;
const keypaceMatches = workspaceMatches && publicKeyMatches;
console.log(`Keyspace unlock validation:`);
console.log(` Connected workspace: ${this.currentWorkspace}`);
console.log(` Unlocked workspace: ${unlockedWorkspaceName}`);
console.log(` Connected public key: ${this.connectedPublicKey}`);
console.log(` Unlocked public key: ${unlockedWorkspacePublicKey}`);
console.log(` Workspace matches: ${workspaceMatches}`);
console.log(` Public key matches: ${publicKeyMatches}`);
console.log(` Overall match: ${keypaceMatches}`);
// Always get current pending requests (filtered by connected workspace)
const currentPendingRequests = this.getPendingRequests();
// Notify popup about keyspace state
console.log(`Sending KEYSPACE_UNLOCKED message to popup: keypaceMatches=${keypaceMatches}, pendingRequests=${currentPendingRequests.length}`);
if (this.popupPort) {
this.popupPort.postMessage({
type: 'KEYSPACE_UNLOCKED',
keypaceMatches,
pendingRequests: currentPendingRequests
});
console.log('KEYSPACE_UNLOCKED message sent to popup');
} else {
console.log('No popup port available to send KEYSPACE_UNLOCKED message');
}
} catch (error) {
if (error.message && error.message.includes('Workspace not found')) {
console.log(`Keyspace unlock: Different workspace unlocked (connected to: ${this.currentWorkspace})`);
// Send message with no match and empty requests
if (this.popupPort) {
this.popupPort.postMessage({
type: 'KEYSPACE_UNLOCKED',
keypaceMatches: false,
pendingRequests: []
});
}
} else {
console.error('Error handling keyspace unlock:', error);
}
}
}
/**
* Approve a sign request
* @param {string} requestId - Request to approve
* @returns {Promise<boolean>} - True if approved successfully
*/
async approveSignRequest(requestId) {
try {
const request = this.pendingRequests.get(requestId);
if (!request) {
throw new Error('Request not found');
}
// Validate request is for current workspace
if (request.workspace !== this.currentWorkspace) {
throw new Error(`Request is for workspace '${request.workspace}', but current workspace is '${this.currentWorkspace}'`);
}
if (request.connectedPublicKey !== this.connectedPublicKey) {
throw new Error('Request public key does not match current connection');
}
// Validate keyspace is still unlocked and matches
if (!this.wasmModule.is_unlocked()) {
throw new Error('Keyspace is locked');
}
const currentPublicKey = await this.wasmModule.get_workspace_default_public_key(this.currentWorkspace);
if (currentPublicKey !== this.connectedPublicKey) {
throw new Error('Keyspace mismatch');
}
// Decode message from base64
const messageBytes = atob(request.message).split('').map(c => c.charCodeAt(0));
// Sign the message with default keypair (doesn't require selected keypair)
const signatureHex = await this.wasmModule.sign_with_default_keypair(new Uint8Array(messageBytes));
// Send response to server
await this.connection.send_response(requestId, request.message, signatureHex);
// Update request status
request.status = 'approved';
request.signature = signatureHex;
// Remove from pending requests
this.pendingRequests.delete(requestId);
// Update badge
this.updateBadge();
console.log(`Approved sign request: ${requestId}`);
return true;
} catch (error) {
console.error('Failed to approve sign request:', error);
return false;
}
}
/**
* Reject a sign request
* @param {string} requestId - Request to reject
* @param {string} reason - Reason for rejection (optional)
* @returns {Promise<boolean>} - True if rejected successfully
*/
async rejectSignRequest(requestId, reason = 'User rejected') {
try {
const request = this.pendingRequests.get(requestId);
if (!request) {
throw new Error('Request not found');
}
// Send rejection to server
await this.connection.send_rejection(requestId, reason);
// Update request status
request.status = 'rejected';
request.reason = reason;
// Remove from pending requests
this.pendingRequests.delete(requestId);
// Update badge
this.updateBadge();
console.log(`Rejected sign request: ${requestId}, reason: ${reason}`);
return true;
} catch (error) {
console.error('Failed to reject sign request:', error);
return false;
}
}
/**
* Get all pending requests for the current workspace
* @returns {Array} - Array of pending request data for current workspace
*/
getPendingRequests() {
const allRequests = Array.from(this.pendingRequests.values());
// Filter requests to only include those for the current workspace
const filteredRequests = allRequests.filter(request => {
const isCurrentWorkspace = request.workspace === this.currentWorkspace;
const isCurrentPublicKey = request.connectedPublicKey === this.connectedPublicKey;
if (!isCurrentWorkspace || !isCurrentPublicKey) {
console.log(`Filtering out request ${request.id}: workspace=${request.workspace} (current=${this.currentWorkspace}), publicKey match=${isCurrentPublicKey}`);
}
return isCurrentWorkspace && isCurrentPublicKey;
});
console.log(`getPendingRequests: ${allRequests.length} total, ${filteredRequests.length} for current workspace`);
return filteredRequests;
}
/**
* Show notification for new sign request
*/
showSignRequestNotification() {
// Create notification
chrome.notifications.create({
type: 'basic',
iconUrl: 'icons/icon48.png',
title: 'Sign Request',
message: 'New signature request received. Click to review.'
});
}
/**
* Notify popup about new request if popup is open and keyspace is unlocked
*/
async notifyPopupOfNewRequest() {
// Only notify if popup is connected
if (!this.popupPort) {
console.log('No popup port available, skipping new request notification');
return;
}
// Check if we have WASM module and can validate keyspace
if (!this.wasmModule) {
console.log('WASM module not available, skipping new request notification');
return;
}
try {
// Check if keyspace is unlocked
if (!this.wasmModule.is_unlocked()) {
console.log('Keyspace is locked, skipping new request notification');
return;
}
// Get the currently unlocked workspace name
const unlockedWorkspaceName = this.wasmModule.get_current_keyspace_name();
// Get workspace default public key for the UNLOCKED workspace
const unlockedWorkspacePublicKey = await this.wasmModule.get_workspace_default_public_key(unlockedWorkspaceName);
// Check if the unlocked workspace matches the connected workspace
const workspaceMatches = unlockedWorkspaceName === this.currentWorkspace;
const publicKeyMatches = unlockedWorkspacePublicKey === this.connectedPublicKey;
const keypaceMatches = workspaceMatches && publicKeyMatches;
console.log(`New request notification check: keypaceMatches=${keypaceMatches}, workspace=${unlockedWorkspaceName}, connected=${this.currentWorkspace}`);
// Get current pending requests (filtered by connected workspace)
const currentPendingRequests = this.getPendingRequests();
// SECURITY: Only send requests if workspace matches, otherwise send empty array
const requestsToSend = keypaceMatches ? currentPendingRequests : [];
// Send update to popup
this.popupPort.postMessage({
type: 'NEW_SIGN_REQUEST',
keypaceMatches,
pendingRequests: requestsToSend
});
console.log(`Sent NEW_SIGN_REQUEST message to popup: keypaceMatches=${keypaceMatches}, ${requestsToSend.length} requests (${currentPendingRequests.length} total for connected workspace)`);
} catch (error) {
console.log('Error in notifyPopupOfNewRequest:', error);
}
}
/**
* Update extension badge with pending request count for current workspace
*/
updateBadge() {
// Only count requests for the current workspace
const currentWorkspaceRequests = this.getPendingRequests();
const count = currentWorkspaceRequests.length;
const badgeText = count > 0 ? count.toString() : '';
console.log(`Updating badge: ${this.pendingRequests.size} total requests, ${count} for current workspace, badge text: "${badgeText}"`);
chrome.action.setBadgeText({ text: badgeText });
chrome.action.setBadgeBackgroundColor({ color: '#ff6b6b' });
}
/**
* Disconnect from SigSocket server
*/
disconnect() {
if (this.connection) {
this.connection.disconnect();
this.connection = null;
}
this.isConnected = false;
this.connectedPublicKey = null;
this.currentWorkspace = null;
this.pendingRequests.clear();
this.updateBadge();
}
/**
* Get connection status
* @returns {Object} - Connection status information
*/
getStatus() {
return {
isConnected: this.isConnected,
connectedPublicKey: this.connectedPublicKey,
pendingRequestCount: this.getPendingRequests().length,
serverUrl: this.defaultServerUrl
};
}
/**
* Set the popup port for communication
* @param {chrome.runtime.Port} port - The popup port
*/
setPopupPort(port) {
this.popupPort = port;
}
}
// Export for use in background script
export default SigSocketService;