mirror of
https://github.com/keepassxreboot/keepassxc-browser.git
synced 2026-03-11 08:54:43 +00:00
First draft
This commit is contained in:
parent
d51e1fc6ad
commit
76b90dbe6d
5 changed files with 547 additions and 5 deletions
|
|
@ -65,6 +65,17 @@ const messageBuffer = {
|
|||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
matchAndRemoveV2(msg) {
|
||||
for (let i = 0; i < this.buffer.length; ++i) {
|
||||
if (msg?.requestID === this.buffer[i].requestID) {
|
||||
this.buffer.splice(i, 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
|
@ -86,7 +97,7 @@ keepassClient.sendNativeMessage = function(request, enableTimeout = false, timeo
|
|||
const isNotificationOrError = !msg.nonce && request.requestID === msg.requestID;
|
||||
|
||||
// Only resolve a matching response or a notification (without nonce)
|
||||
if (isNotificationOrError || messageBuffer.matchAndRemove(msg)) {
|
||||
if (isNotificationOrError || messageBuffer.matchAndRemoveV2(msg)) {
|
||||
port.removeListener(handler);
|
||||
if (enableTimeout) {
|
||||
clearTimeout(timeout);
|
||||
|
|
@ -175,6 +186,118 @@ keepassClient.sendMessage = async function(kpAction, tab, messageData, nonce, en
|
|||
return keepassClient.handleResponse(response, incrementedNonce, tab);
|
||||
};
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Protocol V2
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
keepassClient.sendNativeMessageV2 = function(requestAction, request, enableTimeout = false, timeoutValue) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let timeout;
|
||||
const ev = keepassClient.nativePort.onMessage;
|
||||
|
||||
const listener = ((port) => {
|
||||
const handler = (msg) => {
|
||||
if (msg && msg?.requestID === request.requestID) {
|
||||
// Only resolve a matching response
|
||||
if (messageBuffer.matchAndRemove(msg)) {
|
||||
port.removeListener(handler);
|
||||
if (enableTimeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
resolve(msg);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
return handler;
|
||||
})(ev);
|
||||
ev.addListener(listener);
|
||||
|
||||
const messageTimeout = timeoutValue || keepassClient.messageTimeout;
|
||||
|
||||
// Handle timeouts
|
||||
if (enableTimeout) {
|
||||
timeout = setTimeout(() => {
|
||||
const errorMessage = {
|
||||
action: requestAction,
|
||||
error: kpErrors.getError(kpErrors.TIMEOUT_OR_NOT_CONNECTED),
|
||||
errorCode: kpErrors.TIMEOUT_OR_NOT_CONNECTED
|
||||
};
|
||||
keepass.isKeePassXCAvailable = false;
|
||||
ev.removeListener(listener.handler);
|
||||
resolve(errorMessage);
|
||||
}, messageTimeout);
|
||||
}
|
||||
|
||||
// Store the request to the buffer
|
||||
messageBuffer.addMessage(request);
|
||||
|
||||
// Send the request
|
||||
if (keepassClient.nativePort) {
|
||||
keepassClient.nativePort.postMessage(request);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
keepassClient.sendMessageV2 = async function(kpAction, tab, messageData, nonce, enableTimeout = false, triggerUnlock = false) {
|
||||
const request = keepassClient.buildRequestV2(keepassClient.encrypt(messageData, nonce), nonce, keepass.clientID, messageData.requestID, triggerUnlock);
|
||||
const response = await keepassClient.sendNativeMessageV2(kpAction, request, enableTimeout);
|
||||
const incrementedNonce = keepassClient.incrementedNonce(nonce);
|
||||
|
||||
return keepassClient.handleResponseV2(response, incrementedNonce, tab);
|
||||
};
|
||||
|
||||
keepassClient.buildRequestV2 = function(encryptedMessage, nonce, clientID, requestID, triggerUnlock = false) {
|
||||
const request = {
|
||||
message: encryptedMessage,
|
||||
nonce: nonce,
|
||||
clientID: clientID,
|
||||
requestID: requestID
|
||||
};
|
||||
|
||||
if (triggerUnlock) {
|
||||
request.triggerUnlock = 'true';
|
||||
}
|
||||
|
||||
return request;
|
||||
};
|
||||
|
||||
keepassClient.handleResponseV2 = function(response, incrementedNonce, tab) {
|
||||
if (response.message && response.nonce) {
|
||||
const res = keepassClient.decrypt(response.message, response.nonce);
|
||||
if (!res) {
|
||||
keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const message = nacl.util.encodeUTF8(res);
|
||||
const parsed = JSON.parse(message);
|
||||
|
||||
if (keepassClient.verifyResponseV2(parsed, incrementedNonce)) {
|
||||
return parsed;
|
||||
}
|
||||
} else if (response.error && response.errorCode) {
|
||||
keepass.handleError(tab, response.errorCode, response.error);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
keepassClient.verifyResponseV2 = function(response, nonce) {
|
||||
if (!keepassClient.checkNonceLength(response.nonce)) {
|
||||
logError('Incorrect nonce length');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (response.nonce !== nonce) {
|
||||
logError('Nonce compare failed');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Utils
|
||||
//--------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ keepass.cacheTimeout = 30 * 1000; // Milliseconds
|
|||
keepass.databaseHash = '';
|
||||
keepass.previousDatabaseHash = '';
|
||||
keepass.reconnectLoop = null;
|
||||
keepass.protocolV2 = false;
|
||||
|
||||
const kpActions = {
|
||||
SET_LOGIN: 'set-login',
|
||||
|
|
@ -31,7 +32,10 @@ const kpActions = {
|
|||
GET_DATABASE_GROUPS: 'get-database-groups',
|
||||
CREATE_NEW_GROUP: 'create-new-group',
|
||||
GET_TOTP: 'get-totp',
|
||||
REQUEST_AUTOTYPE: 'request-autotype'
|
||||
REQUEST_AUTOTYPE: 'request-autotype',
|
||||
// V2
|
||||
CREATE_CREDENTIALS: 'create-credentials',
|
||||
GET_CREDENTIALS: 'get-credentials'
|
||||
};
|
||||
|
||||
browser.storage.local.get({ 'latestKeePassXC': { 'version': '', 'lastChecked': null }, 'keyRing': {} }).then((item) => {
|
||||
|
|
@ -146,7 +150,7 @@ keepass.retrieveCredentials = async function(tab, args = []) {
|
|||
|
||||
const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce);
|
||||
if (response) {
|
||||
entries = removeDuplicateEntries(response.entries);
|
||||
entries = keepass.removeDuplicateEntries(response.entries);
|
||||
keepass.updateLastUsed(keepass.databaseHash);
|
||||
|
||||
if (entries.length === 0) {
|
||||
|
|
@ -783,6 +787,7 @@ keepass.isAssociated = function() {
|
|||
keepass.setcurrentKeePassXCVersion = function(version) {
|
||||
if (version) {
|
||||
keepass.currentKeePassXC = version;
|
||||
keepass.protocolV2 = keepass.compareVersion('2.8.0', version);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -907,7 +912,7 @@ keepass.compareVersion = function(minimum, current, canBeEqual = true) {
|
|||
return (canBeEqual ? (min <= cur) : (min < cur));
|
||||
};
|
||||
|
||||
const removeDuplicateEntries = function(arr) {
|
||||
keepass.removeDuplicateEntries = function(arr) {
|
||||
const newArray = [];
|
||||
|
||||
for (const a of arr) {
|
||||
|
|
|
|||
412
keepassxc-browser/background/protocol.js
Normal file
412
keepassxc-browser/background/protocol.js
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
'use strict';
|
||||
|
||||
const protocol = {};
|
||||
|
||||
protocol.associate = async function(tab, args = []) {
|
||||
console.log('associate');
|
||||
|
||||
if (!keepass.isKeePassXCAvailable) {
|
||||
return AssociatedAction.NOT_ASSOCIATED;
|
||||
}
|
||||
|
||||
try {
|
||||
keepass.clearErrorMessage(tab);
|
||||
|
||||
const kpAction = kpActions.ASSOCIATE;
|
||||
const key = nacl.util.encodeBase64(keepass.keyPair.publicKey);
|
||||
const nonce = keepassClient.getNonce();
|
||||
const idKeyPair = nacl.box.keyPair();
|
||||
const idKey = nacl.util.encodeBase64(idKeyPair.publicKey);
|
||||
|
||||
const messageData = {
|
||||
action: kpAction,
|
||||
key: key,
|
||||
idKey: idKey
|
||||
};
|
||||
|
||||
const response = await keepassClient.sendMessageV2(kpAction, tab, messageData, nonce, false, true);
|
||||
if (response && response.id && response.hash) {
|
||||
keepass.setCryptoKey(response.id, key); // Save the new identification public key as id key for the database
|
||||
|
||||
browserAction.show(tab);
|
||||
return AssociatedAction.NEW_ASSOCIATION;
|
||||
}
|
||||
|
||||
keepass.handleError(tab, kpErrors.ASSOCIATION_FAILED);
|
||||
return AssociatedAction.NOT_ASSOCIATED;
|
||||
} catch (err) {
|
||||
logError(`associate failed: ${err}`);
|
||||
}
|
||||
|
||||
return AssociatedAction.NOT_ASSOCIATED;
|
||||
};
|
||||
|
||||
protocol.changePublicKeys = async function(tab, enableTimeout = false, connectionTimeout) {
|
||||
console.log('change-public-keys');
|
||||
|
||||
if (!keepass.isConnected) {
|
||||
keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED);
|
||||
return false;
|
||||
}
|
||||
|
||||
const kpAction = kpActions.CHANGE_PUBLIC_KEYS;
|
||||
const key = nacl.util.encodeBase64(keepass.keyPair.publicKey);
|
||||
const [ nonce, incrementedNonce ] = keepassClient.getNonces();
|
||||
keepass.clientID = nacl.util.encodeBase64(nacl.randomBytes(keepassClient.keySize));
|
||||
|
||||
const request = {
|
||||
action: kpAction,
|
||||
publicKey: key,
|
||||
nonce: nonce,
|
||||
clientID: keepass.clientID
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await keepassClient.sendNativeMessageV2(kpAction, request, enableTimeout, connectionTimeout);
|
||||
keepass.setcurrentKeePassXCVersion(response.version);
|
||||
|
||||
const verified = keepass.protocolV2
|
||||
? keepassClient.verifyResponseV2(response, incrementedNonce)
|
||||
: keepassClient.verifyKeyResponse(response, key, incrementedNonce);
|
||||
if (!response?.publicKey || !verified) {
|
||||
if (tab && page.tabs[tab.id]) {
|
||||
keepass.handleError(tab, kpErrors.KEY_CHANGE_FAILED);
|
||||
}
|
||||
|
||||
keepass.updateDatabaseHashToContent();
|
||||
return false;
|
||||
}
|
||||
|
||||
keepass.serverPublicKey = nacl.util.decodeBase64(response.publicKey);
|
||||
keepass.isKeePassXCAvailable = true;
|
||||
console.log(`${EXTENSION_NAME}: Server public key: ${nacl.util.encodeBase64(keepass.serverPublicKey)}`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
logError(`changePublicKeys failed: ${err}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
protocol.createCredentials = async function(tab, args = []) {
|
||||
console.log('create-credentials');
|
||||
|
||||
const [ username, password, url, group, groupUuid ] = args;
|
||||
return protocol.updateCredentials(tab, [ null, username, password, url, group, groupUuid ]);
|
||||
};
|
||||
|
||||
protocol.createNewGroup = async function(tab, args = []) {
|
||||
console.log('create-new-group');
|
||||
|
||||
if (!keepass.isConnected) {
|
||||
return [];
|
||||
}
|
||||
|
||||
keepass.clearErrorMessage(tab);
|
||||
|
||||
const [ groupName ] = args;
|
||||
const kpAction = kpActions.CREATE_NEW_GROUP;
|
||||
const nonce = keepassClient.getNonce();
|
||||
|
||||
const messageData = {
|
||||
action: kpAction,
|
||||
groupName: groupName
|
||||
};
|
||||
|
||||
try {
|
||||
// TODO: Handle errors
|
||||
const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce);
|
||||
if (response) {
|
||||
keepass.updateLastUsed(keepass.databaseHash); // ?
|
||||
return response;
|
||||
} else {
|
||||
logError('getDatabaseGroups rejected');
|
||||
}
|
||||
|
||||
browserAction.showDefault(tab);
|
||||
return [];
|
||||
} catch (err) {
|
||||
logError(`createNewGroup failed: ${err}`);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
protocol.generatePassword = async function(tab, args = []) {
|
||||
console.log('generate-password');
|
||||
|
||||
if (!keepass.isConnected) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!keepass.compareVersion(keepass.requiredKeePassXC, keepass.currentKeePassXC)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let password;
|
||||
const kpAction = kpActions.GENERATE_PASSWORD;
|
||||
const nonce = keepassClient.getNonce();
|
||||
|
||||
const messageData = {
|
||||
action: kpAction,
|
||||
nonce: nonce,
|
||||
clientID: keepass.clientID,
|
||||
requestID: keepassClient.getRequestId()
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce);
|
||||
if (response) {
|
||||
password = response.entries ?? response.password;
|
||||
keepass.updateLastUsed(keepass.databaseHash); // ?
|
||||
} else {
|
||||
logError('generatePassword rejected');
|
||||
}
|
||||
|
||||
return password;
|
||||
} catch (err) {
|
||||
logError(`generatePassword failed: ${err}`);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
protocol.getCredentials = async function(tab, args = []) {
|
||||
console.log('get-credentials');
|
||||
|
||||
if (!keepass.isConnected) {
|
||||
return [];
|
||||
}
|
||||
|
||||
keepass.clearErrorMessage(tab);
|
||||
|
||||
const [ url, submiturl, triggerUnlock = false, httpAuth = false ] = args;
|
||||
let entries = [];
|
||||
const kpAction = kpActions.GET_CREDENTIALS;
|
||||
const nonce = keepassClient.getNonce();
|
||||
const [ dbid ] = keepass.getCryptoKey();
|
||||
|
||||
const messageData = {
|
||||
action: kpAction,
|
||||
id: dbid,
|
||||
url: url,
|
||||
keys: keepass.getKeys()
|
||||
};
|
||||
|
||||
if (submiturl) {
|
||||
messageData.submitUrl = submiturl;
|
||||
}
|
||||
|
||||
if (httpAuth) {
|
||||
messageData.httpAuth = 'true';
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: Handle errors
|
||||
const response = await keepassClient.sendMessageV2(kpAction, tab, messageData, nonce, false, triggerUnlock);
|
||||
if (response) {
|
||||
entries = keepass.removeDuplicateEntries(response.entries);
|
||||
keepass.updateLastUsed(keepass.databaseHash); // What about this?
|
||||
|
||||
if (entries.length === 0) {
|
||||
// Questionmark-icon is not triggered, so we have to trigger for the normal symbol
|
||||
browserAction.showDefault(tab);
|
||||
}
|
||||
|
||||
logDebug(`Found ${entries.length} entries for url ${url}`);
|
||||
return entries;
|
||||
}
|
||||
|
||||
browserAction.showDefault(tab);
|
||||
return [];
|
||||
} catch (err) {
|
||||
logError(`retrieveCredentials failed: ${err}`);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
protocol.getDatabaseGroups = async function(tab, args = []) {
|
||||
console.log('get-database-groups');
|
||||
|
||||
if (!keepass.isConnected) {
|
||||
return [];
|
||||
}
|
||||
|
||||
keepass.clearErrorMessage(tab);
|
||||
|
||||
let groups = [];
|
||||
const kpAction = kpActions.GET_DATABASE_GROUPS;
|
||||
const nonce = keepassClient.getNonce();
|
||||
|
||||
const messageData = {
|
||||
action: kpAction
|
||||
};
|
||||
|
||||
try {
|
||||
// TODO: Handle errors
|
||||
const response = await keepassClient.sendMessageV2(kpAction, tab, messageData, nonce);
|
||||
if (response) {
|
||||
groups = response.groups;
|
||||
groups.defaultGroup = page.settings.defaultGroup;
|
||||
groups.defaultGroupAlwaysAsk = page.settings.defaultGroupAlwaysAsk;
|
||||
keepass.updateLastUsed(keepass.databaseHash); // ?
|
||||
return groups;
|
||||
}
|
||||
|
||||
browserAction.showDefault(tab);
|
||||
return [];
|
||||
} catch (err) {
|
||||
logError(`getDatabaseGroups failed: ${err}`);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// Obsolete? This should be checked inside KeePassXC
|
||||
protocol.getDatabaseStatuses = async function(tab, args = []) {
|
||||
console.log('get-database-statuses');
|
||||
};
|
||||
|
||||
protocol.getTotp = async function(tab, args = []) {
|
||||
console.log('get-totp');
|
||||
|
||||
if (!keepass.isConnected) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const [ uuid, oldTotp ] = args;
|
||||
if (!keepass.compareVersion('2.6.1', keepass.currentKeePassXC, true)) {
|
||||
return oldTotp;
|
||||
}
|
||||
|
||||
const kpAction = kpActions.GET_TOTP;
|
||||
const nonce = keepassClient.getNonce();
|
||||
|
||||
const messageData = {
|
||||
action: kpAction,
|
||||
uuid: uuid
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce);
|
||||
if (response) {
|
||||
keepass.updateLastUsed(keepass.databaseHash);
|
||||
return response.totp;
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (err) {
|
||||
logError(`getTotp failed: ${err}`);
|
||||
}
|
||||
};
|
||||
|
||||
protocol.lockDatabase = async function(tab, args = []) {
|
||||
console.log('lock-database');
|
||||
|
||||
if (!keepass.isConnected) {
|
||||
keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED);
|
||||
return false;
|
||||
}
|
||||
|
||||
const kpAction = kpActions.LOCK_DATABASE;
|
||||
const nonce = keepassClient.getNonce();
|
||||
|
||||
const messageData = {
|
||||
action: kpAction
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await keepassClient.sendMessageV2(kpAction, tab, messageData, nonce);
|
||||
if (response) {
|
||||
//keepass.isDatabaseClosed = true; // ?
|
||||
keepass.updateDatabase();
|
||||
|
||||
// Display error message in the popup
|
||||
keepass.handleError(tab, kpErrors.DATABASE_NOT_OPENED);
|
||||
return true;
|
||||
} else {
|
||||
//keepass.isDatabaseClosed = true; // ?
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (err) {
|
||||
logError(`lockDatabase failed: ${err}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
protocol.requestAutotype = async function(tab, args = []) {
|
||||
console.log('request-autotype');
|
||||
|
||||
if (!keepass.isConnected) {
|
||||
keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED);
|
||||
return false;
|
||||
}
|
||||
|
||||
const kpAction = kpActions.REQUEST_AUTOTYPE;
|
||||
const nonce = keepassClient.getNonce();
|
||||
const search = getTopLevelDomainFromUrl(args[0]);
|
||||
|
||||
const messageData = {
|
||||
action: kpAction,
|
||||
search: search
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await keepassClient.sendMessageV2(kpAction, tab, messageData, nonce);
|
||||
return response?.result;
|
||||
} catch (err) {
|
||||
logError(`requestAutotype failed: ${err}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
protocol.updateCredentials = async function(tab, args = []) {
|
||||
console.log('update-credentials');
|
||||
|
||||
if (!keepass.isConnected) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const [ entryId, username, password, url, group, groupUuid ] = args;
|
||||
const kpAction = kpActions.CREATE_CREDENTIALS;
|
||||
const [ dbid ] = keepass.getCryptoKey();
|
||||
const nonce = keepassClient.getNonce();
|
||||
|
||||
const messageData = {
|
||||
action: kpAction,
|
||||
id: dbid,
|
||||
login: username,
|
||||
password: password,
|
||||
url: url,
|
||||
submitUrl: url
|
||||
};
|
||||
|
||||
if (entryId) {
|
||||
messageData.uuid = entryId;
|
||||
}
|
||||
|
||||
if (!entryId && page.settings.downloadFaviconAfterSave) {
|
||||
messageData.downloadFavicon = true;
|
||||
}
|
||||
|
||||
if (group && groupUuid) {
|
||||
messageData.group = group;
|
||||
messageData.groupUuid = groupUuid;
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: Check response messages
|
||||
const response = await keepassClient.sendMessageV2(kpAction, tab, messageData, nonce);
|
||||
if (response) {
|
||||
// KeePassXC versions lower than 2.5.0 will have an empty parsed.error
|
||||
let successMessage = response.error;
|
||||
if (response?.result === true || response.error === '') {
|
||||
successMessage = entryId ? 'updated' : 'created';
|
||||
}
|
||||
|
||||
return successMessage;
|
||||
} else {
|
||||
return 'error';
|
||||
}
|
||||
} catch (err) {
|
||||
logError(`updateCredentials failed: ${err}`);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
|
@ -134,6 +134,7 @@ const iconClicked = async function(field, icon) {
|
|||
if (kpxc.databaseState !== DatabaseState.UNLOCKED) {
|
||||
// Triggers database unlock
|
||||
await sendMessage('page_set_manual_fill', ManualFill.BOTH);
|
||||
// TODO: Replace with open-database?
|
||||
await sendMessage('get_database_hash', [ false, true ]); // Set triggerUnlock to true
|
||||
field.focus();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,8 @@
|
|||
"background/browserAction.js",
|
||||
"background/page.js",
|
||||
"background/event.js",
|
||||
"background/init.js"
|
||||
"background/init.js",
|
||||
"background/protocol.js"
|
||||
]
|
||||
},
|
||||
"content_scripts": [
|
||||
|
|
|
|||
Loading…
Reference in a new issue