public/background.js (704 lines of code) (raw):
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
let faviconUrls = {};
// Helper functions
function arrayBufferToBase64(buffer) {
let binary = '';
const bytes = new Uint8Array(buffer);
bytes.forEach(b => (binary += String.fromCharCode(b)));
return btoa(binary);
}
function base64ToArrayBuffer(base64) {
const binary = atob(base64);
const len = binary.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
function uint8ArrayToBase64(uint8Array) {
return arrayBufferToBase64(uint8Array.buffer);
}
function base64ToUint8Array(base64) {
const arrayBuffer = base64ToArrayBuffer(base64);
return new Uint8Array(arrayBuffer);
}
async function encryptData(data, key) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const encoded = new TextEncoder().encode(data);
const encrypted = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv,
},
key,
encoded
);
// Convert encrypted data and iv to Base64 strings
const ciphertextBase64 = arrayBufferToBase64(encrypted);
const ivBase64 = uint8ArrayToBase64(iv);
return { ciphertext: ciphertextBase64, iv: ivBase64 };
}
async function decryptData(ciphertextBase64, ivBase64, key) {
const ciphertext = base64ToArrayBuffer(ciphertextBase64);
const iv = base64ToUint8Array(ivBase64);
const decrypted = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: iv,
},
key,
ciphertext
);
const dec = new TextDecoder();
return dec.decode(decrypted);
}
// Function to get full hostname from URL
function getBaseDomain(url) {
try {
const urlObj = new URL(url);
return urlObj.hostname;
} catch (error) {
console.error('Invalid URL:', url);
return '';
}
}
// Function to escape special characters in GraphQL strings (useful for text fields)
function escapeGraphQLString(str) {
if (typeof str !== 'string') {
return '';
}
return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
}
// Function to get the favicon URL when a tab is updated
function updateFaviconUrl(tabId, changeInfo, tab) {
if (changeInfo.status === 'complete' && tab.url) {
const domain = getBaseDomain(tab.url);
if (domain && tab.favIconUrl) {
faviconUrls[domain] = tab.favIconUrl;
// Also store in chrome.storage.local
chrome.storage.local.get(['faviconUrls'], (result) => {
let storedFavicons = result.faviconUrls || {};
storedFavicons[domain] = tab.favIconUrl;
chrome.storage.local.set({ faviconUrls: storedFavicons });
});
}
}
}
// Add the listener for tab updates
chrome.tabs.onUpdated.addListener(updateFaviconUrl);
// Function to generate UUID v4
function generateUUID() {
// Public Domain/MIT
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
(c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)
);
}
chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
if (request.action === 'storeKeys') {
(async function () {
try {
// Generate new key material and encrypt the new keys
const keyMaterial = await crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 },
true,
['encrypt', 'decrypt']
);
const encryptedPublicKey = await encryptData(request.publicKey, keyMaterial);
const encryptedPrivateKey = await encryptData(request.privateKey, keyMaterial);
const encryptedUrl = await encryptData(request.url, keyMaterial);
// Export the key material to JWK format
const exportedKey = await crypto.subtle.exportKey('jwk', keyMaterial);
// Store the encrypted keys per domain and net
chrome.storage.local.get(['keys', 'connectedNets'], (result) => {
let keys = result.keys || {};
let connectedNets = result.connectedNets || {};
if (!keys[request.domain]) {
keys[request.domain] = {};
}
keys[request.domain][request.net] = {
publicKey: encryptedPublicKey,
privateKey: encryptedPrivateKey,
url: encryptedUrl,
exportedKey: exportedKey, // Store the exported key
};
connectedNets[request.domain] = request.net;
console.log('Storing keys for domain:', request.domain);
console.log('Storing net:', request.net);
console.log('ConnectedNets after storing:', connectedNets);
chrome.storage.local.set({ keys, connectedNets }, () => {
console.log('Keys stored for domain:', request.domain, 'and net:', request.net);
sendResponse({ success: true });
});
});
} catch (error) {
console.error('Error in storeKeys:', error);
sendResponse({ success: false, error: error.message });
}
})();
return true; // Keep the message channel open for async sendResponse
} else if (request.action === 'disconnectKeys') {
(async function () {
// Remove the keys for the domain and net
chrome.storage.local.get(['keys', 'connectedNets'], (result) => {
let keys = result.keys || {};
let connectedNets = result.connectedNets || {};
if (keys[request.domain] && keys[request.domain][request.net]) {
delete keys[request.domain][request.net];
if (Object.keys(keys[request.domain]).length === 0) {
delete keys[request.domain];
}
if (connectedNets[request.domain] === request.net) {
delete connectedNets[request.domain];
}
chrome.storage.local.set({ keys, connectedNets }, () => {
console.log('Keys disconnected for domain:', request.domain, 'and net:', request.net);
console.log('ConnectedNets after disconnecting:', connectedNets);
sendResponse({ success: true });
});
} else {
sendResponse({ success: false, error: 'No keys found to disconnect' });
}
});
})();
return true; // Keep the message channel open for async sendResponse
} else if (request.action === 'requestData') {
(async function () {
chrome.storage.local.get(['keys', 'faviconUrls'], async function (result) {
const keys = result.keys || {};
const storedFavicons = result.faviconUrls || {};
const faviconUrlForDomain = storedFavicons[request.domain] || '';
if (keys[request.domain] && keys[request.domain][request.net]) {
const { publicKey, privateKey, url, exportedKey } = keys[request.domain][request.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);
const decryptedPrivateKey = await decryptData(privateKey.ciphertext, privateKey.iv, keyMaterial);
const decryptedUrl = await decryptData(url.ciphertext, url.iv, keyMaterial);
sendResponse({
publicKey: decryptedPublicKey,
privateKey: decryptedPrivateKey,
url: decryptedUrl,
faviconUrl: faviconUrlForDomain,
});
} catch (error) {
console.error('Error decrypting data:', error);
sendResponse({ error: 'Failed to decrypt data', faviconUrl: faviconUrlForDomain });
}
} else {
sendResponse({ error: 'No keys found for domain and net', faviconUrl: faviconUrlForDomain });
}
});
})();
return true; // Keep the message channel open for async sendResponse
} else if (request.action === 'getConnectedNet') {
(async function () {
chrome.storage.local.get(['connectedNets'], function (result) {
const connectedNets = result.connectedNets || {};
const net = connectedNets[request.domain];
if (net) {
sendResponse({ net: net });
} else {
sendResponse({ error: 'No connected net for domain' });
}
});
})();
return true; // Keep the message channel open for async sendResponse
}
// ------------------------------------------------
// Below: Example: Using GraphQL variables for postTransaction
// ------------------------------------------------
else if (request.action === 'submitTransactionFromDashboard') {
(async function () {
// Retrieve the necessary data
const transactionData = request.transactionData;
const domain = request.domain;
const net = request.net;
// Validate transactionData
if (!transactionData || !transactionData.asset || !transactionData.recipientAddress || !transactionData.amount) {
sendResponse({ success: false, error: 'Invalid transaction data.' });
return;
}
// Retrieve the signer's keys and URL from storage
chrome.storage.local.get(['keys', 'connectedNets'], async function (result) {
const keys = result.keys || {};
const connectedNets = result.connectedNets || {};
if (!connectedNets[domain] || connectedNets[domain] !== net) {
sendResponse({ success: false, error: 'Not connected to the specified net for this domain.' });
return;
}
if (!keys[domain] || !keys[domain][net]) {
sendResponse({ success: false, error: 'Keys not found for the specified domain and net.' });
return;
}
const { publicKey, privateKey, url, 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);
const decryptedPrivateKey = await decryptData(privateKey.ciphertext, privateKey.iv, keyMaterial);
const decryptedUrl = await decryptData(url.ciphertext, url.iv, keyMaterial);
// Build the GraphQL mutation with variables
const mutation = `
mutation postTransaction(
$operation: String!,
$amount: Int!,
$signerPublicKey: String!,
$signerPrivateKey: String!,
$recipientPublicKey: String!,
$asset: JSONScalar!
) {
postTransaction(
data: {
operation: $operation,
amount: $amount,
signerPublicKey: $signerPublicKey,
signerPrivateKey: $signerPrivateKey,
recipientPublicKey: $recipientPublicKey,
asset: $asset
}
) {
id
}
}
`;
const variables = {
operation: 'CREATE',
amount: parseInt(transactionData.amount),
signerPublicKey: decryptedPublicKey,
signerPrivateKey: decryptedPrivateKey,
recipientPublicKey: transactionData.recipientAddress,
asset: transactionData.asset, // pass JS object
};
// Send the mutation with variables
const response = await fetch(decryptedUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query: mutation, variables }),
});
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.statusText}`);
}
const resultData = await response.json();
if (resultData.errors) {
console.error('GraphQL errors:', resultData.errors);
sendResponse({
success: false,
error: 'GraphQL errors occurred.',
errors: resultData.errors,
});
} else {
console.log('Transaction submitted successfully:', resultData.data);
sendResponse({ success: true, data: resultData.data });
}
} catch (error) {
console.error('Error submitting transaction:', error);
sendResponse({ success: false, error: error.message });
}
});
})();
return true; // Keep the message channel open for async sendResponse
}
else if (request.action === 'submitTransaction') {
(async function () {
console.log('Handling submitTransaction action');
console.log('Sender:', sender);
let senderUrl = null;
if (sender.tab && sender.tab.url) {
senderUrl = sender.tab.url;
} else if (sender.url) {
senderUrl = sender.url;
} else if (sender.origin) {
senderUrl = sender.origin;
} else {
console.error('Sender URL is undefined');
sendResponse({ success: false, error: 'Cannot determine sender URL' });
return;
}
console.log('Sender URL:', senderUrl);
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);
if (keys[domain] && keys[domain][net]) {
const { publicKey, privateKey, url, 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);
const decryptedPrivateKey = await decryptData(privateKey.ciphertext, privateKey.iv, keyMaterial);
const decryptedUrl = await decryptData(url.ciphertext, url.iv, keyMaterial);
// Check if required fields are defined
if (!decryptedPublicKey || !decryptedPrivateKey || !request.recipient) {
console.error('Missing required fields for transaction submission');
sendResponse({
success: false,
error: 'Missing required fields for transaction',
});
return;
}
// Prepare data for GraphQL mutation
const mutation = `
mutation postTransaction(
$operation: String!,
$amount: Int!,
$signerPublicKey: String!,
$signerPrivateKey: String!,
$recipientPublicKey: String!,
$asset: JSONScalar!
) {
postTransaction(
data: {
operation: $operation,
amount: $amount,
signerPublicKey: $signerPublicKey,
signerPrivateKey: $signerPrivateKey,
recipientPublicKey: $recipientPublicKey,
asset: $asset
}
) {
id
}
}
`;
const variables = {
operation: 'CREATE',
amount: parseInt(request.amount),
signerPublicKey: decryptedPublicKey,
signerPrivateKey: decryptedPrivateKey,
recipientPublicKey: request.recipient,
asset: {
data: request.data || {},
},
};
const response = await fetch(decryptedUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: mutation,
variables,
}),
});
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.statusText}`);
}
const resultData = await response.json();
if (resultData.errors) {
console.error('GraphQL errors:', resultData.errors);
sendResponse({ success: false, errors: resultData.errors });
} else {
console.log('Transaction submitted successfully:', resultData.data);
sendResponse({ success: true, data: resultData.data });
}
} catch (error) {
console.error('Error submitting 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' });
}
});
})();
return true; // Keep the message channel open for async sendResponse
}
else if (request.action === 'submitLoginTransaction') {
(async function () {
console.log('Handling submitLoginTransaction action');
console.log('Sender:', sender);
let senderUrl = null;
if (sender.tab && sender.tab.url) {
senderUrl = sender.tab.url;
} else if (sender.url) {
senderUrl = sender.url;
} else if (sender.origin) {
senderUrl = sender.origin;
} else {
console.error('Sender URL is undefined');
sendResponse({ success: false, error: 'Cannot determine sender URL' });
return;
}
console.log('Sender URL:', senderUrl);
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);
if (keys[domain] && keys[domain][net]) {
const { publicKey, privateKey, url, 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);
const decryptedPrivateKey = await decryptData(privateKey.ciphertext, privateKey.iv, keyMaterial);
const decryptedUrl = await decryptData(url.ciphertext, url.iv, keyMaterial);
// Prepare asset data with current timestamp and unique login_transaction_id
const currentTimestamp = Math.floor(Date.now() / 1000);
let loginTransactionId = '';
if (crypto.randomUUID) {
loginTransactionId = crypto.randomUUID().replace(/[^a-zA-Z0-9]/g, '');
} else {
loginTransactionId = generateUUID().replace(/[^a-zA-Z0-9]/g, '');
}
// Build variables
const mutation = `
mutation postTransaction(
$operation: String!,
$amount: Int!,
$signerPublicKey: String!,
$signerPrivateKey: String!,
$recipientPublicKey: String!,
$asset: JSONScalar!
) {
postTransaction(
data: {
operation: $operation,
amount: $amount,
signerPublicKey: $signerPublicKey,
signerPrivateKey: $signerPrivateKey,
recipientPublicKey: $recipientPublicKey,
asset: $asset
}
) {
id
}
}
`;
const variables = {
operation: 'CREATE',
amount: 1,
signerPublicKey: decryptedPublicKey,
signerPrivateKey: decryptedPrivateKey,
recipientPublicKey: decryptedPublicKey, // self
asset: {
data: {
login_timestamp: currentTimestamp,
login_transaction_id: loginTransactionId,
},
},
};
const response = await fetch(decryptedUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query: mutation, variables }),
});
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.statusText}`);
}
const resultData = await response.json();
if (resultData.errors) {
console.error('GraphQL errors:', resultData.errors);
sendResponse({ success: false, errors: resultData.errors });
} else {
console.log('Login transaction submitted successfully:', resultData.data);
sendResponse({ success: true, data: resultData.data });
}
} catch (error) {
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' });
}
});
})();
return true; // Keep the message channel open for async sendResponse
}
// The rest (deployContractChain, etc.) can remain the same or be adapted similarly to use variables.
else if (request.action === 'deployContractChain') {
// Handler for deploying contract chain
(async function () {
const domain = request.domain;
const net = request.net;
const ownerAddress = request.ownerAddress;
const soliditySource = request.soliditySource;
const deployConfig = request.deployConfig; // Contains arguments and contract_name
// Retrieve the signer's keys and URL from storage
chrome.storage.local.get(['keys', 'connectedNets'], async function (result) {
const keys = result.keys || {};
const connectedNets = result.connectedNets || {};
if (!connectedNets[domain] || connectedNets[domain] !== net) {
sendResponse({ success: false, error: 'Not connected to the specified net for this domain.' });
return;
}
if (!keys[domain] || !keys[domain][net]) {
sendResponse({ success: false, error: 'Keys not found for the specified domain and net.' });
return;
}
const { url, 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 decryptedUrl = await decryptData(url.ciphertext, url.iv, keyMaterial);
// 1. Perform addAddress mutation
const addAddressMutation = `
mutation {
addAddress(
config: "5 127.0.0.1 10005",
address: "${escapeGraphQLString(ownerAddress)}",
type: "data"
)
}
`;
const addAddressResponse = await fetch(decryptedUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query: addAddressMutation }),
});
if (!addAddressResponse.ok) {
throw new Error(`Network response was not ok: ${addAddressResponse.statusText}`);
}
const addAddressResult = await addAddressResponse.json();
if (addAddressResult.errors) {
console.error('GraphQL errors in addAddress:', addAddressResult.errors);
sendResponse({
success: false,
error: 'Error in addAddress mutation.',
errors: addAddressResult.errors,
});
return;
}
// Check if addAddress was successful
if (
addAddressResult.data &&
addAddressResult.data.addAddress === 'Address added successfully'
) {
// 2. Perform compileContract mutation
const escapedSoliditySource = escapeGraphQLString(soliditySource);
const compileContractMutation = `
mutation {
compileContract(
source: """${escapedSoliditySource}""",
type: "data"
)
}
`;
const compileContractResponse = await fetch(decryptedUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query: compileContractMutation }),
});
if (!compileContractResponse.ok) {
throw new Error(`Network response was not ok: ${compileContractResponse.statusText}`);
}
const compileContractResult = await compileContractResponse.json();
if (compileContractResult.errors) {
console.error('GraphQL errors in compileContract:', compileContractResult.errors);
sendResponse({
success: false,
error: 'Error in compileContract mutation.',
errors: compileContractResult.errors,
});
return;
}
// Extract the contract filename
const contractFilename = compileContractResult.data.compileContract;
if (!contractFilename) {
sendResponse({
success: false,
error: 'Failed to compile contract.',
});
return;
}
// 3. Perform deployContract mutation
const { arguments: args, contract_name } = deployConfig;
const deployContractMutation = `
mutation {
deployContract(
config: "5 127.0.0.1 10005",
contract: "${escapeGraphQLString(contractFilename)}",
name: "/tmp/${escapeGraphQLString(
contractFilename.replace('.json', '.sol')
)}:${escapeGraphQLString(contract_name)}",
arguments: "${escapeGraphQLString(args)}",
owner: "${escapeGraphQLString(ownerAddress)}",
type: "data"
){
ownerAddress
contractAddress
contractName
}
}
`;
const deployContractResponse = await fetch(decryptedUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query: deployContractMutation }),
});
if (!deployContractResponse.ok) {
throw new Error(
`Network response was not ok: ${deployContractResponse.statusText}`
);
}
const deployContractResult = await deployContractResponse.json();
if (deployContractResult.errors) {
console.error('GraphQL errors in deployContract:', deployContractResult.errors);
sendResponse({
success: false,
error: 'Error in deployContract mutation.',
errors: deployContractResult.errors,
});
return;
}
// Extract the contract address and return success
if (
deployContractResult.data &&
deployContractResult.data.deployContract &&
deployContractResult.data.deployContract.contractAddress
) {
const contractAddress =
deployContractResult.data.deployContract.contractAddress;
sendResponse({ success: true, contractAddress: contractAddress });
return;
} else {
sendResponse({
success: false,
error: 'Failed to deploy contract.',
});
return;
}
} else {
sendResponse({ success: false, error: 'Failed to add address.' });
return;
}
} catch (error) {
console.error('Error deploying contract chain:', error);
sendResponse({ success: false, error: error.message });
}
});
})();
return true; // Keep the message channel open for async sendResponse
}
});