First draft

This commit is contained in:
varjolintu 2023-04-20 18:16:57 +03:00
parent d51e1fc6ad
commit 76b90dbe6d
5 changed files with 547 additions and 5 deletions

View file

@ -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
//--------------------------------------------------------------------------

View file

@ -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) {

View 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 [];
}
};

View file

@ -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();
}

View file

@ -43,7 +43,8 @@
"background/browserAction.js",
"background/page.js",
"background/event.js",
"background/init.js"
"background/init.js",
"background/protocol.js"
]
},
"content_scripts": [