- 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.
477 lines
18 KiB
JavaScript
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;
|