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'
+ }
+ }, '*');
+ }
+ }
+ );
+}