This commit is contained in:
Sameh Abouel-saad
2025-06-06 05:31:03 +03:00
parent 203cde1cba
commit 6f42e5ab8d
10 changed files with 1582 additions and 400 deletions

View File

@@ -1,31 +1,35 @@
/**
* 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
* SigSocket Service - Clean Implementation with New WASM APIs
*
* This service provides a clean interface for SigSocket functionality using
* the new WASM-based APIs that handle all WebSocket management, request storage,
* and security validation internally.
*
* Architecture:
* - WASM handles: WebSocket connection, message parsing, request storage, security
* - Extension handles: UI notifications, badge updates, user interactions
*/
class SigSocketService {
constructor() {
this.connection = null;
this.pendingRequests = new Map(); // requestId -> SignRequestData
this.connectedPublicKey = null;
// Connection state
this.isConnected = false;
this.currentWorkspace = null;
this.connectedPublicKey = null;
// Configuration
this.defaultServerUrl = "ws://localhost:8080/ws";
// Initialize WASM module reference
// WASM module reference
this.wasmModule = null;
// Reference to popup port for communication
// UI communication
this.popupPort = null;
}
/**
* Initialize the service with WASM module
* @param {Object} wasmModule - The loaded WASM module
* @param {Object} wasmModule - The loaded WASM module with SigSocketManager
*/
async initialize(wasmModule) {
this.wasmModule = wasmModule;
@@ -40,427 +44,333 @@ class SigSocketService {
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);
console.log('🔌 SigSocket service initialized with WASM APIs');
}
/**
* Connect to SigSocket server for a workspace
* Connect to SigSocket server using WASM APIs
* WASM handles all connection logic (reuse, switching, etc.)
* @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');
if (!this.wasmModule?.SigSocketManager) {
throw new Error('WASM SigSocketManager not available');
}
// Check if already connected to this workspace
if (this.isConnected && this.connection) {
console.log(`Already connected to SigSocket server for workspace: ${workspaceId}`);
return true;
}
console.log(`🔗 Requesting SigSocket connection for workspace: ${workspaceId}`);
// Disconnect any existing connection first
if (this.connection) {
this.disconnect();
}
// Let WASM handle all connection logic (reuse, switching, etc.)
const connectionInfo = await this.wasmModule.SigSocketManager.connect_workspace_with_events(
workspaceId,
this.defaultServerUrl,
(event) => this.handleSigSocketEvent(event)
);
// 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');
}
// Parse connection info
const info = JSON.parse(connectionInfo);
this.currentWorkspace = info.workspace;
this.connectedPublicKey = info.public_key;
this.isConnected = info.is_connected;
console.log(`Connecting to SigSocket server for workspace: ${workspaceId} with key: ${publicKeyHex.substring(0, 16)}...`);
console.log(`✅ SigSocket connection result:`, {
workspace: this.currentWorkspace,
publicKey: this.connectedPublicKey?.substring(0, 16) + '...',
connected: this.isConnected
});
// Create new SigSocket connection
console.log('Creating new SigSocketConnection instance');
this.connection = new this.wasmModule.SigSocketConnection();
console.log('SigSocketConnection instance created');
// Update badge to show current state
this.updateBadge();
// 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;
return this.isConnected;
} catch (error) {
console.error('Failed to connect to SigSocket server:', error);
console.error('❌ SigSocket connection failed:', error);
this.isConnected = false;
this.connectedPublicKey = null;
this.currentWorkspace = null;
if (this.connection) {
this.connection.disconnect();
this.connection = null;
}
this.connectedPublicKey = null;
return false;
}
}
/**
* Handle incoming sign request from server
* @param {string} requestId - Unique request identifier
* @param {string} messageBase64 - Message to be signed (base64-encoded)
* Handle events from the WASM SigSocket client
* This is called automatically when requests arrive
* @param {Object} event - Event from WASM layer
*/
handleIncomingRequest(requestId, messageBase64) {
console.log(`Received sign request: ${requestId}`);
handleSigSocketEvent(event) {
console.log('📨 Received SigSocket event:', event);
// 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;
}
if (event.type === 'sign_request') {
console.log(`🔐 New sign request: ${event.request_id}`);
// 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
// The request is automatically stored by WASM
// We just handle UI updates
this.showSignRequestNotification();
this.updateBadge();
this.notifyPopupOfNewRequest();
}
}
/**
* 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
* Approve a sign request using WASM APIs
* @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');
if (!this.wasmModule?.SigSocketManager) {
throw new Error('WASM SigSocketManager not available');
}
// 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}'`);
}
console.log(`✅ Approving request: ${requestId}`);
if (request.connectedPublicKey !== this.connectedPublicKey) {
throw new Error('Request public key does not match current connection');
}
// WASM handles all validation, signing, and server communication
await this.wasmModule.SigSocketManager.approve_request(requestId);
// Validate keyspace is still unlocked and matches
if (!this.wasmModule.is_unlocked()) {
throw new Error('Keyspace is locked');
}
console.log(`🎉 Request approved successfully: ${requestId}`);
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
// Update UI
this.updateBadge();
console.log(`Approved sign request: ${requestId}`);
this.notifyPopupOfRequestUpdate();
return true;
} catch (error) {
console.error('Failed to approve sign request:', error);
console.error(`Failed to approve request ${requestId}:`, error);
return false;
}
}
/**
* Reject a sign request
* Reject a sign request using WASM APIs
* @param {string} requestId - Request to reject
* @param {string} reason - Reason for rejection (optional)
* @param {string} reason - Reason for rejection
* @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');
if (!this.wasmModule?.SigSocketManager) {
throw new Error('WASM SigSocketManager not available');
}
// 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
console.log(`❌ Rejecting request: ${requestId}, reason: ${reason}`);
// WASM handles rejection and server communication
await this.wasmModule.SigSocketManager.reject_request(requestId, reason);
console.log(`✅ Request rejected successfully: ${requestId}`);
// Update UI
this.updateBadge();
console.log(`Rejected sign request: ${requestId}, reason: ${reason}`);
this.notifyPopupOfRequestUpdate();
return true;
} catch (error) {
console.error('Failed to reject sign request:', error);
console.error(`Failed to reject request ${requestId}:`, error);
return false;
}
}
/**
* Get all pending requests for the current workspace
* @returns {Array} - Array of pending request data for current workspace
* Get pending requests from WASM (filtered by current workspace)
* @returns {Promise<Array>} - Array of pending requests for current workspace
*/
getPendingRequests() {
const allRequests = Array.from(this.pendingRequests.values());
async getPendingRequests() {
return this.getFilteredRequests();
}
// 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}`);
/**
* Get filtered requests from WASM (workspace-aware)
* @returns {Promise<Array>} - Array of filtered requests
*/
async getFilteredRequests() {
try {
if (!this.wasmModule?.SigSocketManager) {
return [];
}
return isCurrentWorkspace && isCurrentPublicKey;
});
const requestsJson = await this.wasmModule.SigSocketManager.get_filtered_requests();
const requests = JSON.parse(requestsJson);
console.log(`getPendingRequests: ${allRequests.length} total, ${filteredRequests.length} for current workspace`);
return filteredRequests;
console.log(`📋 Retrieved ${requests.length} filtered requests for current workspace`);
return requests;
} catch (error) {
console.error('Failed to get filtered requests:', error);
return [];
}
}
/**
* Check if a request can be approved (keyspace validation)
* @param {string} requestId - Request ID to check
* @returns {Promise<boolean>} - True if can be approved
*/
async canApproveRequest(requestId) {
try {
if (!this.wasmModule?.SigSocketManager) {
return false;
}
return await this.wasmModule.SigSocketManager.can_approve_request(requestId);
} catch (error) {
console.error('Failed to check request approval status:', error);
return false;
}
}
/**
* 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.'
});
try {
if (chrome.notifications && chrome.notifications.create) {
chrome.notifications.create({
type: 'basic',
iconUrl: 'icons/icon48.png',
title: 'SigSocket Sign Request',
message: 'New signature request received. Click to review.'
});
console.log('📢 Notification shown for sign request');
} else {
console.log('📢 Notifications not available, skipping notification');
}
} catch (error) {
console.warn('Failed to show notification:', error);
}
}
/**
* Notify popup about new request if popup is open and keyspace is unlocked
* Update extension badge with pending request count
*/
async updateBadge() {
try {
const requests = await this.getPendingRequests();
const count = requests.length;
const badgeText = count > 0 ? count.toString() : '';
console.log(`🔢 Updating badge: ${count} pending requests`);
chrome.action.setBadgeText({ text: badgeText });
chrome.action.setBadgeBackgroundColor({ color: '#ff6b6b' });
} catch (error) {
console.error('Failed to update badge:', error);
}
}
/**
* Notify popup about new request
*/
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');
console.log('No popup connected, skipping notification');
return;
}
try {
// Check if keyspace is unlocked
if (!this.wasmModule.is_unlocked()) {
console.log('Keyspace is locked, skipping new request notification');
return;
}
const requests = await this.getPendingRequests();
const canApprove = requests.length > 0 ? await this.canApproveRequest(requests[0].id) : false;
// 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
canApprove,
pendingRequests: requests
});
console.log(`Sent NEW_SIGN_REQUEST message to popup: keypaceMatches=${keypaceMatches}, ${requestsToSend.length} requests (${currentPendingRequests.length} total for connected workspace)`);
console.log(`📤 Notified popup: ${requests.length} requests, canApprove: ${canApprove}`);
} catch (error) {
console.log('Error in notifyPopupOfNewRequest:', error);
console.error('Failed to notify popup:', error);
}
}
/**
* Update extension badge with pending request count for current workspace
* Notify popup about request updates
*/
updateBadge() {
// Only count requests for the current workspace
const currentWorkspaceRequests = this.getPendingRequests();
const count = currentWorkspaceRequests.length;
const badgeText = count > 0 ? count.toString() : '';
async notifyPopupOfRequestUpdate() {
if (!this.popupPort) return;
console.log(`Updating badge: ${this.pendingRequests.size} total requests, ${count} for current workspace, badge text: "${badgeText}"`);
try {
const requests = await this.getPendingRequests();
chrome.action.setBadgeText({ text: badgeText });
chrome.action.setBadgeBackgroundColor({ color: '#ff6b6b' });
this.popupPort.postMessage({
type: 'REQUESTS_UPDATED',
pendingRequests: requests
});
} catch (error) {
console.error('Failed to notify popup of update:', error);
}
}
/**
* Disconnect from SigSocket server
* WASM handles all disconnection logic
*/
disconnect() {
if (this.connection) {
this.connection.disconnect();
this.connection = null;
async disconnect() {
try {
if (this.wasmModule?.SigSocketManager) {
await this.wasmModule.SigSocketManager.disconnect();
}
// Clear local state
this.isConnected = false;
this.currentWorkspace = null;
this.connectedPublicKey = null;
this.updateBadge();
console.log('🔌 SigSocket disconnection requested');
} catch (error) {
console.error('Failed to disconnect:', error);
}
this.isConnected = false;
this.connectedPublicKey = null;
this.currentWorkspace = null;
this.pendingRequests.clear();
this.updateBadge();
}
/**
* Get connection status
* @returns {Object} - Connection status information
* Get connection status from WASM
* @returns {Promise<Object>} - Connection status information
*/
getStatus() {
return {
isConnected: this.isConnected,
connectedPublicKey: this.connectedPublicKey,
pendingRequestCount: this.getPendingRequests().length,
serverUrl: this.defaultServerUrl
};
async getStatus() {
try {
if (!this.wasmModule?.SigSocketManager) {
return {
isConnected: false,
workspace: null,
publicKey: null,
pendingRequestCount: 0,
serverUrl: this.defaultServerUrl
};
}
// Let WASM provide the authoritative status
const statusJson = await this.wasmModule.SigSocketManager.get_connection_status();
const status = JSON.parse(statusJson);
const requests = await this.getPendingRequests();
return {
isConnected: status.is_connected,
workspace: status.workspace,
publicKey: status.public_key,
pendingRequestCount: requests.length,
serverUrl: this.defaultServerUrl
};
} catch (error) {
console.error('Failed to get status:', error);
return {
isConnected: false,
workspace: null,
publicKey: null,
pendingRequestCount: 0,
serverUrl: this.defaultServerUrl
};
}
}
/**
@@ -469,8 +379,32 @@ class SigSocketService {
*/
setPopupPort(port) {
this.popupPort = port;
console.log('📱 Popup connected to SigSocket service');
}
/**
* Called when keyspace is unlocked - notify popup of current state
*/
async onKeypaceUnlocked() {
if (!this.popupPort) return;
try {
const requests = await this.getPendingRequests();
const canApprove = requests.length > 0 ? await this.canApproveRequest(requests[0].id) : false;
this.popupPort.postMessage({
type: 'KEYSPACE_UNLOCKED',
canApprove,
pendingRequests: requests
});
console.log(`🔓 Keyspace unlocked notification sent: ${requests.length} requests, canApprove: ${canApprove}`);
} catch (error) {
console.error('Failed to handle keyspace unlock:', error);
}
}
}
// Export for use in background script
export default SigSocketService;
export default SigSocketService;