This is an automated email from the ASF dual-hosted git repository.

bisman pushed a commit to branch main
in repository 
https://gitbox.apache.org/repos/asf/incubator-resilientdb-resvault.git


The following commit(s) were added to refs/heads/main by this push:
     new d74d9ed  Add wallet signing and public key retrieval (#8)
d74d9ed is described below

commit d74d9ed8cfdcfd710974739ef4e578a2689888ef
Author: Henry Chou <[email protected]>
AuthorDate: Wed Oct 29 15:03:32 2025 -0700

    Add wallet signing and public key retrieval (#8)
    
    Provides getPublicKey and getSigningKeys handlers to enable web 
applications to request cryptographic signatures and retrieve wallet public 
keys:
    
    - Add getPublicKey action to retrieve user's public key from encrypted 
storage
    - Add getSigningKeys action to enable client-side message signing
    - Extend content.js to handle getPublicKey and sign message types
    - Add hexToBytes/bytesToHex conversion utilities
    
    Enables ResCanvas secure rooms to cryptographically sign strokes while
    maintaining key security in extension storage. Also provides general-purpose
    wallet integration for any dApp (authentication, message signing, identity).
    
    Keys remain encrypted in extension while signing is delegated to page 
context using
    web app's nacl library for security isolation.
---
 public/background.js | 230 ++++++++++++++++++++++++++++++++++++++-------------
 public/content.js    |  81 +++++++++++++++++-
 2 files changed, 251 insertions(+), 60 deletions(-)

diff --git a/public/background.js b/public/background.js
index e577140..cd229d3 100644
--- a/public/background.js
+++ b/public/background.js
@@ -48,6 +48,20 @@ function base64ToUint8Array(base64) {
     return new Uint8Array(arrayBuffer);
 }
 
+function hexToBytes(hex) {
+    const bytes = new Uint8Array(hex.length / 2);
+    for (let i = 0; i < hex.length; i += 2) {
+        bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
+    }
+    return bytes;
+}
+
+function bytesToHex(bytes) {
+    return Array.from(bytes)
+        .map(b => b.toString(16).padStart(2, '0'))
+        .join('');
+}
+
 async function encryptData(data, key) {
     const iv = crypto.getRandomValues(new Uint8Array(12));
     const encoded = new TextEncoder().encode(data);
@@ -131,10 +145,10 @@ function generateUUID() {
 }
 
 // KV service helper function
-async function callKVService(url, configData) {
+async function callKVService(url, configData, signerPublicKey) {
     try {
         let graphqlQuery = '';
-        
+
         switch (configData.command) {
             case 'set_balance':
                 // Use the correct GraphQL schema with PrepareAsset data format
@@ -146,7 +160,8 @@ async function callKVService(url, configData) {
                             amount: "${configData.balance || '0'}",
                             operation: "TRANSFER",
                             recipient: "${configData.address || 
'0x0000000000000000000000000000000000000000'}",
-                            type: "CREATE"
+                            type: "CREATE",
+                            signerPublicKey: "${signerPublicKey || ''}"
                         }
                     ) { 
                         id 
@@ -170,15 +185,15 @@ async function callKVService(url, configData) {
         }
 
         const result = await response.json();
-        
+
         if (result.data && result.data.postTransaction) {
             return { success: true, transactionId: 
result.data.postTransaction.id };
         }
-        
+
         if (result.errors) {
             return { success: false, error: result.errors[0].message };
         }
-        
+
         return { success: true, data: result };
     } catch (error) {
         console.error('Error calling KV service:', error);
@@ -191,7 +206,7 @@ async function callContractService(url, configData) {
     try {
         // Convert JSON commands to GraphQL mutations
         let graphqlQuery = '';
-        
+
         switch (configData.command) {
             case 'create_account':
                 graphqlQuery = `mutation { createAccount(config: 
"/opt/resilientdb/service/tools/config/interface/service.config") }`;
@@ -251,7 +266,7 @@ async function callContractService(url, configData) {
         }
 
         const result = await response.json();
-        
+
         // Convert GraphQL response to expected format
         if (result.data) {
             const data = result.data;
@@ -260,8 +275,8 @@ async function callContractService(url, configData) {
             } else if (data.setBalance) {
                 return { success: true, result: data.setBalance };
             } else if (data.deployContract) {
-                return { 
-                    success: true, 
+                return {
+                    success: true,
                     contractAddress: data.deployContract.contractAddress,
                     ownerAddress: data.deployContract.ownerAddress,
                     contractName: data.deployContract.contractName
@@ -270,11 +285,11 @@ async function callContractService(url, configData) {
                 return { success: true, result: data.executeContract };
             }
         }
-        
+
         if (result.errors) {
             return { success: false, error: result.errors[0].message };
         }
-        
+
         return { success: true, data: result };
     } catch (error) {
         console.error('Error calling contract service:', error);
@@ -417,6 +432,104 @@ chrome.runtime.onMessage.addListener(function (request, 
sender, sendResponse) {
         return true; // Keep the message channel open for async sendResponse
     }
 
+    // Handle getPublicKey request
+    else if (request.action === 'getPublicKey') {
+        (async function () {
+            const domain = request.domain;
+
+            chrome.storage.local.get(['keys', 'connectedNets'], async function 
(result) {
+                const keys = result.keys || {};
+                const connectedNets = result.connectedNets || {};
+                const net = connectedNets[domain];
+
+                if (keys[domain] && keys[domain][net]) {
+                    const { publicKey, exportedKey } = keys[domain][net];
+
+                    try {
+                        // Import the key material from JWK format
+                        const keyMaterial = await crypto.subtle.importKey(
+                            'jwk',
+                            exportedKey,
+                            { name: 'AES-GCM' },
+                            true,
+                            ['encrypt', 'decrypt']
+                        );
+
+                        const decryptedPublicKey = await 
decryptData(publicKey.ciphertext, publicKey.iv, keyMaterial);
+
+                        sendResponse({
+                            success: true,
+                            publicKey: decryptedPublicKey
+                        });
+                    } catch (error) {
+                        console.error('Error retrieving public key:', error);
+                        sendResponse({
+                            success: false,
+                            error: 'Failed to retrieve public key'
+                        });
+                    }
+                } else {
+                    sendResponse({
+                        success: false,
+                        error: 'No keys found. Please connect your wallet 
first.'
+                    });
+                }
+            });
+        })();
+
+        return true; // Keep the message channel open for async sendResponse
+    }
+
+    // Handle signMessage request (returns keys for client-side signing)
+    else if (request.action === 'getSigningKeys') {
+        (async function () {
+            const domain = request.domain;
+
+            chrome.storage.local.get(['keys', 'connectedNets'], async function 
(result) {
+                const keys = result.keys || {};
+                const connectedNets = result.connectedNets || {};
+                const net = connectedNets[domain];
+
+                if (keys[domain] && keys[domain][net]) {
+                    const { privateKey, publicKey, exportedKey } = 
keys[domain][net];
+
+                    try {
+                        // Import the key material from JWK format
+                        const keyMaterial = await crypto.subtle.importKey(
+                            'jwk',
+                            exportedKey,
+                            { name: 'AES-GCM' },
+                            true,
+                            ['encrypt', 'decrypt']
+                        );
+
+                        const decryptedPrivateKey = await 
decryptData(privateKey.ciphertext, privateKey.iv, keyMaterial);
+                        const decryptedPublicKey = await 
decryptData(publicKey.ciphertext, publicKey.iv, keyMaterial);
+
+                        sendResponse({
+                            success: true,
+                            privateKey: decryptedPrivateKey,
+                            publicKey: decryptedPublicKey
+                        });
+                    } catch (error) {
+                        console.error('Error retrieving signing keys:', error);
+                        sendResponse({
+                            success: false,
+                            error: 'Failed to retrieve signing keys: ' + 
error.message
+                        });
+                    }
+                } else {
+                    sendResponse({
+                        success: false,
+                        error: 'No keys found. Please connect your wallet 
first.'
+                    });
+                }
+            });
+        })();
+
+        return true; // Keep the message channel open for async sendResponse
+    }
+
     // ------------------------------------------------
     // Updated: Using unified contract service for KV operations
     // ------------------------------------------------
@@ -449,7 +562,7 @@ chrome.runtime.onMessage.addListener(function (request, 
sender, sendResponse) {
                     return;
                 }
 
-                const { url, exportedKey } = keys[domain][net];
+                const { publicKey, privateKey, url, exportedKey } = 
keys[domain][net];
 
                 try {
                     // Import the key material from JWK format
@@ -461,6 +574,7 @@ chrome.runtime.onMessage.addListener(function (request, 
sender, sendResponse) {
                         ['encrypt', 'decrypt']
                     );
 
+                    const decryptedPublicKey = await 
decryptData(publicKey.ciphertext, publicKey.iv, keyMaterial);
                     const decryptedUrl = await decryptData(url.ciphertext, 
url.iv, keyMaterial);
 
                     // Use KV service for balance operations
@@ -472,8 +586,8 @@ chrome.runtime.onMessage.addListener(function (request, 
sender, sendResponse) {
 
                     // Use KV endpoint for balance operations
                     const kvUrl = decryptedUrl.replace('8400', '8000'); // 
Contract endpoint to KV endpoint
-                    const result = await callKVService(kvUrl, configData);
-                    
+                    const result = await callKVService(kvUrl, configData, 
decryptedPublicKey);
+
                     if (result.success) {
                         sendResponse({ success: true, data: { postTransaction: 
{ id: result.transactionId || generateUUID() } } });
                     } else {
@@ -518,7 +632,7 @@ chrome.runtime.onMessage.addListener(function (request, 
sender, sendResponse) {
                 console.log('Net for domain:', domain, 'is', net);
 
                 if (keys[domain] && keys[domain][net]) {
-                    const { url, exportedKey } = keys[domain][net];
+                    const { publicKey, privateKey, url, exportedKey } = 
keys[domain][net];
 
                     try {
                         // Import the key material from JWK format
@@ -530,6 +644,7 @@ chrome.runtime.onMessage.addListener(function (request, 
sender, sendResponse) {
                             ['encrypt', 'decrypt']
                         );
 
+                        const decryptedPublicKey = await 
decryptData(publicKey.ciphertext, publicKey.iv, keyMaterial);
                         const decryptedUrl = await decryptData(url.ciphertext, 
url.iv, keyMaterial);
 
                         // Check if required fields are defined
@@ -551,8 +666,8 @@ chrome.runtime.onMessage.addListener(function (request, 
sender, sendResponse) {
 
                         // Use KV endpoint for balance operations
                         const kvUrl = decryptedUrl.replace('8400', '8000'); // 
Contract endpoint to KV endpoint
-                        const result = await callKVService(kvUrl, configData);
-                        
+                        const result = await callKVService(kvUrl, configData, 
decryptedPublicKey);
+
                         if (result.success) {
                             sendResponse({ success: true, data: { 
postTransaction: { id: result.transactionId || generateUUID() } } });
                         } else {
@@ -594,28 +709,30 @@ chrome.runtime.onMessage.addListener(function (request, 
sender, sendResponse) {
             const domain = getBaseDomain(senderUrl);
             console.log('Domain:', domain);
 
-            chrome.storage.local.get(['keys', 'connectedNets'], async function 
(result) {
-                const keys = result.keys || {};
-                const connectedNets = result.connectedNets || {};
-                console.log('ConnectedNets:', connectedNets);
-                const net = connectedNets[domain];
-                console.log('Net for domain:', domain, 'is', net);
+            // For login transactions, get the public key from sync storage 
and URL from local storage
+            chrome.storage.sync.get(['store'], async function (syncResult) {
+                if (!syncResult.store || !syncResult.store.publicKey) {
+                    console.error('No authenticated user found in sync 
storage');
+                    sendResponse({ success: false, error: 'User not 
authenticated. Please log in to ResVault first.' });
+                    return;
+                }
 
-                if (keys[domain] && keys[domain][net]) {
-                    const { url, exportedKey } = keys[domain][net];
+                const publicKey = syncResult.store.publicKey;
+                console.log('Using public key from sync storage:', publicKey);
 
-                    try {
-                        // Import the key material from JWK format
-                        const keyMaterial = await crypto.subtle.importKey(
-                            'jwk',
-                            exportedKey,
-                            { name: 'AES-GCM' },
-                            true,
-                            ['encrypt', 'decrypt']
-                        );
+                // Get the user's active network URL from local storage
+                chrome.storage.local.get(['activeNetUrl'], async function 
(localResult) {
+                    let resilientDBUrl = localResult.activeNetUrl;
 
-                        const decryptedUrl = await decryptData(url.ciphertext, 
url.iv, keyMaterial);
+                    // If no active URL is set, use the default mainnet URL
+                    if (!resilientDBUrl) {
+                        resilientDBUrl = 
'https://cloud.resilientdb.com/graphql';
+                        console.log('No active network URL found, using 
default mainnet:', resilientDBUrl);
+                    } else {
+                        console.log('Using active network URL:', 
resilientDBUrl);
+                    }
 
+                    try {
                         // Use KV service for login transaction
                         const configData = {
                             command: "set_balance",
@@ -623,10 +740,9 @@ chrome.runtime.onMessage.addListener(function (request, 
sender, sendResponse) {
                             balance: "1"
                         };
 
-                        // Use KV endpoint for balance operations
-                        const kvUrl = decryptedUrl.replace('8400', '8000'); // 
Contract endpoint to KV endpoint
-                        const result = await callKVService(kvUrl, configData);
-                        
+                        // Use the user's active network URL for login 
operations
+                        const result = await callKVService(resilientDBUrl, 
configData, publicKey);
+
                         if (result.success) {
                             sendResponse({ success: true, data: { 
postTransaction: { id: result.transactionId || generateUUID() } } });
                         } else {
@@ -636,11 +752,7 @@ chrome.runtime.onMessage.addListener(function (request, 
sender, sendResponse) {
                         console.error('Error submitting login transaction:', 
error);
                         sendResponse({ success: false, error: error.message });
                     }
-                } else {
-                    console.error('No keys found for domain:', domain, 'and 
net:', net);
-                    console.log('Available keys:', keys);
-                    sendResponse({ error: 'No keys found for domain and net' 
});
-                }
+                });
             });
         })();
 
@@ -713,7 +825,7 @@ chrome.runtime.onMessage.addListener(function (request, 
sender, sendResponse) {
 
                     // Step 2: Deploy the contract using the new unified 
service
                     const { arguments: args, contract_name } = 
request.deployConfig;
-                    
+
                     // Step 2: Compile the Solidity contract on the server
                     const escapedSoliditySource = 
soliditySource.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, 
'\\n');
 
@@ -763,7 +875,7 @@ chrome.runtime.onMessage.addListener(function (request, 
sender, sendResponse) {
                     const escapedArgs = args.replace(/\\/g, 
'\\\\').replace(/"/g, '\\"');
                     const escapedOwnerAddress = 
createdAccountAddress.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
                     const escapedContractFilename = 
contractFilename.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
-                    
+
                     const deployContractMutation = `
                     mutation {
                       deployContract(
@@ -794,10 +906,10 @@ chrome.runtime.onMessage.addListener(function (request, 
sender, sendResponse) {
                     }
 
                     const deployContractResult = await 
deployContractResponse.json();
-                    
+
                     // Debug logging to see what we actually receive
                     console.log('Deploy contract response:', 
deployContractResult);
-                    
+
                     if (deployContractResult.errors) {
                         console.error('GraphQL errors in deployContract:', 
deployContractResult.errors);
                         sendResponse({
@@ -810,28 +922,28 @@ chrome.runtime.onMessage.addListener(function (request, 
sender, sendResponse) {
                     // Check for different possible response formats
                     if (deployContractResult.data && 
deployContractResult.data.deployContract) {
                         const deployData = 
deployContractResult.data.deployContract;
-                        
+
                         // Handle both object and string responses
                         if (typeof deployData === 'string') {
                             // Parse string response that might contain 
deployment info
-                            sendResponse({ 
-                                success: true, 
+                            sendResponse({
+                                success: true,
                                 contractAddress: "deployment_successful",
                                 ownerAddress: ownerAddress,
                                 contractName: contract_name,
                                 rawResponse: deployData
                             });
                         } else if (deployData.contractAddress) {
-                            sendResponse({ 
-                                success: true, 
+                            sendResponse({
+                                success: true,
                                 contractAddress: deployData.contractAddress,
                                 ownerAddress: deployData.ownerAddress,
                                 contractName: deployData.contractName
                             });
                         } else {
                             // Deployment succeeded but no specific contract 
address format
-                            sendResponse({ 
-                                success: true, 
+                            sendResponse({
+                                success: true,
                                 contractAddress: "deployed_successfully",
                                 ownerAddress: ownerAddress,
                                 contractName: contract_name,
@@ -905,10 +1017,10 @@ chrome.runtime.onMessage.addListener(function (request, 
sender, sendResponse) {
                     };
 
                     const result = await callContractService(decryptedUrl, 
configData);
-                    
+
                     if (result.success) {
-                        sendResponse({ 
-                            success: true, 
+                        sendResponse({
+                            success: true,
                             transactionId: result.transactionId || 
generateUUID(),
                             result: result.result,
                             message: 'Contract function executed successfully.'
diff --git a/public/content.js b/public/content.js
index a7503ee..383305c 100644
--- a/public/content.js
+++ b/public/content.js
@@ -45,13 +45,20 @@ function sendMessageToPage(request) {
 // Add event listener to listen for messages from the web page
 window.addEventListener('message', (event) => {
   if (event.source === window) {
-    const { direction } = event.data;
+    const { direction, type } = event.data;
     if (direction === 'commit') {
       handleCommitOperation(event);
     } else if (direction === 'login') {
       handleLoginOperation(event);
     } else if (direction === 'custom') {
       handleCustomOperation(event);
+    } else if (direction === 'request' && type === 'getPublicKey') {
+      handleGetPublicKeyOperation(event);
+    } else if (direction === 'request' && type === 'sign') {
+      handleSignOperation(event);
+    } else if (direction === 'info' && type === 'siteSignerInfo') {
+      // Handle signer info updates from the web app (optional, can be ignored)
+      console.log('[ResVault] Received siteSignerInfo:', event.data);
     }
   }
 });
@@ -473,3 +480,75 @@ function handleTransactionSubmit({ amount, data, recipient 
}) {
     }
   );
 }
+
+// Handle getPublicKey request
+function handleGetPublicKeyOperation(event) {
+  const domain = window.location.hostname;
+
+  chrome.runtime.sendMessage(
+    {
+      action: 'getPublicKey',
+      domain: domain
+    },
+    (response) => {
+      if (response) {
+        // Send the response back to the page
+        window.postMessage({
+          type: 'FROM_CONTENT_SCRIPT',
+          data: {
+            type: 'getPublicKey',
+            direction: 'response',
+            publicKey: response.publicKey || null,
+            pubkey: response.publicKey || null,
+            success: response.success || false,
+            error: response.error || null
+          }
+        }, '*');
+      }
+    }
+  );
+}
+
+// Handle sign request
+function handleSignOperation(event) {
+  const domain = window.location.hostname;
+  const { payload } = event.data;
+
+  // Send message to background to get the private key for signing
+  chrome.runtime.sendMessage(
+    {
+      action: 'getSigningKeys',
+      domain: domain,
+      message: payload
+    },
+    (response) => {
+      if (response && response.success) {
+        // We have the keys, now sign in the page context using the web app's 
nacl library
+        // We'll send the keys back to the page and let it sign
+        window.postMessage({
+          type: 'FROM_CONTENT_SCRIPT',
+          data: {
+            type: 'signWithKeys',
+            direction: 'request',
+            privateKey: response.privateKey,
+            publicKey: response.publicKey,
+            message: payload
+          }
+        }, '*');
+      } else {
+        // Send error response
+        window.postMessage({
+          type: 'FROM_CONTENT_SCRIPT',
+          data: {
+            type: 'sign',
+            direction: 'response',
+            signature: null,
+            sig: null,
+            success: false,
+            error: response.error || 'Failed to retrieve keys for signing'
+          }
+        }, '*');
+      }
+    }
+  );
+}

Reply via email to