New draft

This commit is contained in:
varjolintu 2023-04-24 14:19:42 +03:00
parent 76b90dbe6d
commit 8639ab2288
14 changed files with 1392 additions and 1153 deletions

View file

@ -115,6 +115,7 @@
"jQuery": true,
"keepass": true,
"keepassClient": true,
"keepassProtocol": true,
"kpActions": true,
"kpErrors": true,
"kpxc": true,
@ -149,6 +150,8 @@
"MIN_TOTP_INPUT_LENGTH": true,
"nacl": true,
"page": true,
"protocol": true,
"protocolClient": true,
"Pixels": true,
"PREDEFINED_SITELIST": true,
"resizePopup": true,

View file

@ -1,468 +0,0 @@
'use strict';
const keepassClient = {};
keepassClient.keySize = 24;
keepassClient.messageTimeout = 500; // Milliseconds
keepassClient.nativeHostName = 'org.keepassxc.keepassxc_browser';
keepassClient.nativePort = null;
const kpErrors = {
UNKNOWN_ERROR: 0,
DATABASE_NOT_OPENED: 1,
DATABASE_HASH_NOT_RECEIVED: 2,
CLIENT_PUBLIC_KEY_NOT_RECEIVED: 3,
CANNOT_DECRYPT_MESSAGE: 4,
TIMEOUT_OR_NOT_CONNECTED: 5,
ACTION_CANCELLED_OR_DENIED: 6,
PUBLIC_KEY_NOT_FOUND: 7,
ASSOCIATION_FAILED: 8,
KEY_CHANGE_FAILED: 9,
ENCRYPTION_KEY_UNRECOGNIZED: 10,
NO_SAVED_DATABASES_FOUND: 11,
INCORRECT_ACTION: 12,
EMPTY_MESSAGE_RECEIVED: 13,
NO_URL_PROVIDED: 14,
NO_LOGINS_FOUND: 15,
errorMessages: {
0: { msg: tr('errorMessageUnknown') },
1: { msg: tr('errorMessageDatabaseNotOpened') },
2: { msg: tr('errorMessageDatabaseHash') },
3: { msg: tr('errorMessageClientPublicKey') },
4: { msg: tr('errorMessageDecrypt') },
5: { msg: tr('errorMessageTimeout') },
6: { msg: tr('errorMessageCanceled') },
7: { msg: tr('errorMessageEncrypt') },
8: { msg: tr('errorMessageAssociate') },
9: { msg: tr('errorMessageKeyExchange') },
10: { msg: tr('errorMessageEncryptionKey') },
11: { msg: tr('errorMessageSavedDatabases') },
12: { msg: tr('errorMessageIncorrectAction') },
13: { msg: tr('errorMessageEmptyMessage') },
14: { msg: tr('errorMessageNoURL') },
15: { msg: tr('errorMessageNoLogins') }
},
getError(errorCode) {
return this.errorMessages[errorCode].msg;
}
};
const messageBuffer = {
buffer: [],
addMessage(msg) {
if (!this.buffer.includes(msg)) {
this.buffer.push(msg);
}
},
matchAndRemove(msg) {
for (let i = 0; i < this.buffer.length; ++i) {
if (msg.nonce && msg.nonce === keepassClient.incrementedNonce(this.buffer[i].nonce)) {
this.buffer.splice(i, 1);
return true;
}
}
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;
}
};
//--------------------------------------------------------------------------
// Messaging
//--------------------------------------------------------------------------
keepassClient.sendNativeMessage = function(request, enableTimeout = false, timeoutValue) {
return new Promise((resolve, reject) => {
let timeout;
const requestAction = request.action;
const ev = keepassClient.nativePort.onMessage;
const listener = ((port, action) => {
const handler = (msg) => {
if (msg && msg?.action === action) {
// If the request has a separate requestID, check if it matches when there's no nonce (an error message)
const isNotificationOrError = !msg.nonce && request.requestID === msg.requestID;
// Only resolve a matching response or a notification (without nonce)
if (isNotificationOrError || messageBuffer.matchAndRemoveV2(msg)) {
port.removeListener(handler);
if (enableTimeout) {
clearTimeout(timeout);
}
resolve(msg);
return;
}
}
};
return handler;
})(ev, requestAction);
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.handleResponse = 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.verifyResponse(parsed, incrementedNonce)) {
return parsed;
}
} else if (response.error && response.errorCode) {
keepass.handleError(tab, response.errorCode, response.error);
}
return undefined;
};
keepassClient.buildRequest = function(action, encrypted, nonce, clientID, triggerUnlock = false) {
const request = {
action: action,
message: encrypted,
nonce: nonce,
clientID: clientID
};
if (triggerUnlock) {
request.triggerUnlock = 'true';
}
return request;
};
keepassClient.sendMessage = async function(kpAction, tab, messageData, nonce, enableTimeout = false, triggerUnlock = false) {
const request = keepassClient.buildRequest(kpAction, keepassClient.encrypt(messageData, nonce), nonce, keepass.clientID, triggerUnlock);
if (messageData.requestID) {
request['requestID'] = messageData.requestID;
}
const response = await keepassClient.sendNativeMessage(request, enableTimeout);
const incrementedNonce = keepassClient.incrementedNonce(nonce);
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
//--------------------------------------------------------------------------
keepassClient.getNonce = function() {
return nacl.util.encodeBase64(nacl.randomBytes(keepassClient.keySize));
};
// Creates a random 8 character string for Request ID
keepassClient.getRequestId = function() {
return Math.random().toString(16).substring(2, 10);
};
keepassClient.incrementedNonce = function(nonce) {
const oldNonce = nacl.util.decodeBase64(nonce);
const newNonce = oldNonce.slice(0);
// from libsodium/utils.c
let i = 0;
let c = 1;
for (; i < newNonce.length; ++i) {
c += newNonce[i];
newNonce[i] = c;
c >>= 8;
}
return nacl.util.encodeBase64(newNonce);
};
keepassClient.getNonces = function() {
const nonce = keepassClient.getNonce();
const incrementedNonce = keepassClient.incrementedNonce(nonce);
return [ nonce, incrementedNonce ];
};
keepassClient.verifyKeyResponse = function(response, key, nonce) {
if (!response.success || !response.publicKey) {
keepass.associated.hash = null;
return false;
}
if (!keepassClient.checkNonceLength(response.nonce)) {
logError('Invalid nonce length.');
return false;
}
const reply = (response.nonce === nonce);
if (response.publicKey && reply) {
keepass.serverPublicKey = nacl.util.decodeBase64(response.publicKey);
return true;
}
return reply;
};
keepassClient.verifyResponse = function(response, nonce, id) {
keepass.associated.value = response.success;
if (response.success !== 'true') {
keepass.associated.hash = null;
return false;
}
keepass.associated.hash = keepass.databaseHash;
if (!keepassClient.checkNonceLength(response.nonce)) {
return false;
}
keepass.associated.value = (response.nonce === nonce);
if (keepass.associated.value === false) {
logError('Nonce compare failed');
return false;
}
if (id) {
keepass.associated.value = (keepass.associated.value && id === response.id);
}
keepass.associated.hash = (keepass.associated.value) ? keepass.databaseHash : null;
return keepass.isAssociated();
};
keepassClient.verifyDatabaseResponse = function(response, nonce) {
if (response.success !== 'true') {
keepass.associated.hash = null;
return false;
}
if (!keepassClient.checkNonceLength(response.nonce)) {
logError('Invalid nonce length.');
return false;
}
if (response.nonce !== nonce) {
logError('Nonce compare failed.');
return false;
}
keepass.associated.hash = response.hash;
return response.hash !== '' && response.success === 'true';
};
keepassClient.checkNonceLength = function(nonce) {
return nacl.util.decodeBase64(nonce).length === nacl.secretbox.nonceLength;
};
keepassClient.encrypt = function(input, nonce) {
const messageData = nacl.util.decodeUTF8(JSON.stringify(input));
const messageNonce = nacl.util.decodeBase64(nonce);
if (keepass.serverPublicKey) {
const message = nacl.box(messageData, messageNonce, keepass.serverPublicKey, keepass.keyPair.secretKey);
if (message) {
return nacl.util.encodeBase64(message);
}
}
return '';
};
keepassClient.decrypt = function(input, nonce) {
const m = nacl.util.decodeBase64(input);
const n = nacl.util.decodeBase64(nonce);
const res = nacl.box.open(m, n, keepass.serverPublicKey, keepass.keyPair.secretKey);
return res;
};
//--------------------------------------------------------------------------
// Native Messaging related
//--------------------------------------------------------------------------
keepassClient.connectToNative = function() {
if (keepassClient.nativePort) {
keepassClient.nativePort.disconnect();
}
keepassClient.nativeConnect();
};
keepassClient.nativeConnect = function() {
console.log(`${EXTENSION_NAME}: Connecting to native messaging host ${keepassClient.nativeHostName}`);
keepassClient.nativePort = browser.runtime.connectNative(keepassClient.nativeHostName);
keepassClient.nativePort.onMessage.addListener(keepassClient.onNativeMessage);
keepassClient.nativePort.onDisconnect.addListener(onDisconnected);
keepass.isConnected = true;
return keepassClient.nativePort;
};
function onDisconnected() {
keepassClient.nativePort = null;
keepass.isConnected = false;
keepass.isDatabaseClosed = true;
keepass.isKeePassXCAvailable = false;
keepass.associated.value = false;
keepass.associated.hash = null;
keepass.databaseHash = '';
page.clearAllLogins();
keepass.updatePopup('cross');
keepass.updateDatabaseHashToContent();
logError(`Failed to connect: ${(browser.runtime.lastError === null ? 'Unknown error' : browser.runtime.lastError.message)}`);
}
keepassClient.onNativeMessage = function(response) {
// Handle database lock/unlock status
if (response.action === kpActions.DATABASE_LOCKED || response.action === kpActions.DATABASE_UNLOCKED) {
keepass.updateDatabase();
}
};

View file

@ -15,7 +15,7 @@ kpxcEvent.onMessage = async function(request, sender) {
kpxcEvent.showStatus = async function(tab, configured, internalPoll) {
let keyId = null;
if (configured && keepass.databaseHash !== '') {
if (configured && keepass.databaseHash !== '' && keepass.keyRing[keepass.databaseHash]) {
keyId = keepass.keyRing[keepass.databaseHash].id;
}
@ -27,16 +27,17 @@ kpxcEvent.showStatus = async function(tab, configured, internalPoll) {
const usernameFieldDetected = page.tabs[tab.id]?.usernameFieldDetected ?? false;
return {
identifier: keyId,
associated: keepass.isAssociated(),
configured: configured,
databaseClosed: keepass.isDatabaseClosed,
keePassXCAvailable: keepass.isKeePassXCAvailable,
databaseAssociationStatuses: keepass.databaseAssosiationStatuses,
encryptionKeyUnrecognized: keepass.isEncryptionKeyUnrecognized,
associated: keepass.isAssociated(),
error: errorMessage,
usernameFieldDetected: usernameFieldDetected,
error: errorMessage || null,
identifier: keyId,
keePassXCAvailable: keepass.isKeePassXCAvailable,
showGettingStartedGuideAlert: page.settings.showGettingStartedGuideAlert,
showTroubleshootingGuideAlert: page.settings.showTroubleshootingGuideAlert
showTroubleshootingGuideAlert: page.settings.showTroubleshootingGuideAlert,
usernameFieldDetected: usernameFieldDetected
};
};
@ -54,6 +55,7 @@ kpxcEvent.onLoadKeyRing = async function() {
});
keepass.keyRing = item.keyRing;
// TODO: What to do here?
if (keepass.isAssociated() && !keepass.keyRing[keepass.associated.hash]) {
keepass.associated = {
value: false,
@ -73,14 +75,22 @@ kpxcEvent.onGetStatus = async function(tab, args = []) {
// When internalPoll is true the event is triggered from content script in intervals -> don't poll KeePassXC
try {
const [ internalPoll = false, triggerUnlock = false ] = args;
if (!internalPoll) {
const response = await keepass.testAssociation(tab, [ true, triggerUnlock ]);
if (!response) {
return kpxcEvent.showStatus(tab, false);
let configured = false;
if (keepass.protocolV2) {
const response = await protocol.testAssociationFromDatabaseStatuses(tab, [ true, triggerUnlock ]);
configured = response.isAnyAssociated;
} else {
if (!internalPoll) {
const response = await keepassProtocol.testAssociation(tab, [ true, triggerUnlock ]);
if (!response) {
return kpxcEvent.showStatus(tab, false);
}
}
configured = await keepass.isConfigured();
}
const configured = await keepass.isConfigured();
return kpxcEvent.showStatus(tab, configured, internalPoll);
} catch (err) {
logError('No status shown: ' + err);
@ -125,10 +135,11 @@ kpxcEvent.onGetConnectedDatabase = async function() {
};
kpxcEvent.onGetKeePassXCVersions = async function(tab) {
if (keepass.currentKeePassXC === '') {
await keepass.getDatabaseHash(tab);
// TODO: Maybe this is not needed?
/*if (keepass.currentKeePassXC === '') {
await keepass.getDatabaseHash(tab); // TODO: How to get just the version? A separate API call?
return { 'current': keepass.currentKeePassXC, 'latest': keepass.latestKeePassXC.version };
}
}*/
return { 'current': keepass.currentKeePassXC, 'latest': keepass.latestKeePassXC.version };
};
@ -227,11 +238,11 @@ kpxcEvent.sendBackToTabs = async function(tab, args = []) {
// All methods named in this object have to be declared BEFORE this!
kpxcEvent.messageHandlers = {
'add_credentials': keepass.addCredentials,
'associate': keepass.associate,
'check_database_hash': keepass.checkDatabaseHash,
'check_update_keepassxc': kpxcEvent.onCheckUpdateKeePassXC,
'compare_version': kpxcEvent.compareVersion,
'create_credentials': keepass.createCredentials,
'create_new_group': keepass.createNewGroup,
'enable_automatic_reconnect': keepass.enableAutomaticReconnect,
'disable_automatic_reconnect': keepass.disableAutomaticReconnect,
@ -240,7 +251,7 @@ kpxcEvent.messageHandlers = {
'generate_password': keepass.generatePassword,
'get_color_theme': kpxcEvent.getColorTheme,
'get_connected_database': kpxcEvent.onGetConnectedDatabase,
'get_database_hash': keepass.getDatabaseHash,
'get_database_hash': keepass.getDatabaseHash, // TODO ?
'get_database_groups': keepass.getDatabaseGroups,
'get_keepassxc_versions': kpxcEvent.onGetKeePassXCVersions,
'get_login_list': page.getLoginList,

View file

@ -33,583 +33,98 @@ const kpActions = {
CREATE_NEW_GROUP: 'create-new-group',
GET_TOTP: 'get-totp',
REQUEST_AUTOTYPE: 'request-autotype',
// V2
// Protocol V2
CREATE_CREDENTIALS: 'create-credentials',
GET_CREDENTIALS: 'get-credentials'
GET_CREDENTIALS: 'get-credentials',
GET_DATABASE_STATUSES: 'get-database-statuses'
};
const kpErrors = {
UNKNOWN_ERROR: 0,
DATABASE_NOT_OPENED: 1,
DATABASE_HASH_NOT_RECEIVED: 2,
CLIENT_PUBLIC_KEY_NOT_RECEIVED: 3,
CANNOT_DECRYPT_MESSAGE: 4,
TIMEOUT_OR_NOT_CONNECTED: 5,
ACTION_CANCELLED_OR_DENIED: 6,
PUBLIC_KEY_NOT_FOUND: 7,
ASSOCIATION_FAILED: 8,
KEY_CHANGE_FAILED: 9,
ENCRYPTION_KEY_UNRECOGNIZED: 10,
NO_SAVED_DATABASES_FOUND: 11,
INCORRECT_ACTION: 12,
EMPTY_MESSAGE_RECEIVED: 13,
NO_URL_PROVIDED: 14,
NO_LOGINS_FOUND: 15,
errorMessages: {
0: { msg: tr('errorMessageUnknown') },
1: { msg: tr('errorMessageDatabaseNotOpened') },
2: { msg: tr('errorMessageDatabaseHash') },
3: { msg: tr('errorMessageClientPublicKey') },
4: { msg: tr('errorMessageDecrypt') },
5: { msg: tr('errorMessageTimeout') },
6: { msg: tr('errorMessageCanceled') },
7: { msg: tr('errorMessageEncrypt') },
8: { msg: tr('errorMessageAssociate') },
9: { msg: tr('errorMessageKeyExchange') },
10: { msg: tr('errorMessageEncryptionKey') },
11: { msg: tr('errorMessageSavedDatabases') },
12: { msg: tr('errorMessageIncorrectAction') },
13: { msg: tr('errorMessageEmptyMessage') },
14: { msg: tr('errorMessageNoURL') },
15: { msg: tr('errorMessageNoLogins') }
},
getError(errorCode) {
return this.errorMessages[errorCode].msg;
}
};
browser.storage.local.get({ 'latestKeePassXC': { 'version': '', 'lastChecked': null }, 'keyRing': {} }).then((item) => {
keepass.latestKeePassXC = item.latestKeePassXC;
keepass.keyRing = item.keyRing;
});
//--------------------------------------------------------------------------
// Commands
// Command wrappers
//--------------------------------------------------------------------------
keepass.addCredentials = async function(tab, args = []) {
const [ username, password, url, group, groupUuid ] = args;
return keepass.updateCredentials(tab, [ null, username, password, url, group, groupUuid ]);
keepass.associate = async function(tab, args = []) {
return keepass.protocolV2 ? await protocol.associate(tab, args) : await keepassProtocol.associate(tab, args);
};
keepass.updateCredentials = async function(tab, args = []) {
try {
const [ entryId, username, password, url, group, groupUuid ] = args;
const taResponse = await keepass.testAssociation(tab);
if (!taResponse) {
browserAction.showDefault(tab);
return [];
}
const kpAction = kpActions.SET_LOGIN;
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;
}
const response = await keepassClient.sendMessage(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.error === 'success' || response.error === '') {
successMessage = entryId ? 'updated' : 'created';
}
return successMessage;
} else {
return 'error';
}
} catch (err) {
logError(`updateCredentials failed: ${err}`);
return [];
}
};
keepass.retrieveCredentials = async function(tab, args = []) {
try {
const [ url, submiturl, triggerUnlock = false, httpAuth = false ] = args;
const taResponse = await keepass.testAssociation(tab, [ false, triggerUnlock ]);
if (!taResponse) {
browserAction.showDefault(tab);
return [];
}
keepass.clearErrorMessage(tab);
if (!keepass.isConnected) {
return [];
}
let entries = [];
const keys = [];
const kpAction = kpActions.GET_LOGINS;
const nonce = keepassClient.getNonce();
const [ dbid ] = keepass.getCryptoKey();
for (const keyHash in keepass.keyRing) {
keys.push({
id: keepass.keyRing[keyHash].id,
key: keepass.keyRing[keyHash].key
});
}
const messageData = {
action: kpAction,
id: dbid,
url: url,
keys: keys
};
if (submiturl) {
messageData.submitUrl = submiturl;
}
if (httpAuth) {
messageData.httpAuth = 'true';
}
const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce);
if (response) {
entries = keepass.removeDuplicateEntries(response.entries);
keepass.updateLastUsed(keepass.databaseHash);
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 [];
}
};
keepass.generatePassword = async function(tab) {
if (!keepass.isConnected) {
return [];
}
try {
const taResponse = await keepass.testAssociation(tab);
if (!taResponse) {
browserAction.showDefault(tab);
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()
};
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 [];
}
};
keepass.associate = async function(tab) {
if (keepass.isAssociated()) {
return AssociatedAction.ASSOCIATED;
}
try {
await keepass.getDatabaseHash(tab);
if (keepass.isDatabaseClosed || !keepass.isKeePassXCAvailable) {
return AssociatedAction.NOT_ASSOCIATED;
}
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.sendMessage(kpAction, tab, messageData, nonce, false, true);
if (response) {
// Use public key as identification key with older KeePassXC releases
const savedKey = keepass.compareVersion('2.3.4', keepass.currentKeePassXC) ? idKey : key;
keepass.setCryptoKey(response.id, savedKey); // Save the new identification public key as id key for the database
keepass.associated.value = true;
keepass.associated.hash = response.hash || 0;
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;
};
keepass.testAssociation = async function(tab, args = []) {
keepass.clearErrorMessage(tab);
try {
const [ enableTimeout = false, triggerUnlock = false ] = args;
const dbHash = await keepass.getDatabaseHash(tab, [ enableTimeout, triggerUnlock ]);
if (!dbHash) {
return false;
}
if (keepass.isDatabaseClosed || !keepass.isKeePassXCAvailable) {
return false;
}
if (keepass.isAssociated()) {
return true;
}
if (!keepass.serverPublicKey) {
if (tab && page.tabs[tab.id]) {
keepass.handleError(tab, kpErrors.PUBLIC_KEY_NOT_FOUND);
}
return false;
}
const kpAction = kpActions.TEST_ASSOCIATE;
const nonce = keepassClient.getNonce();
const [ dbid, dbkey ] = keepass.getCryptoKey();
if (dbkey === null || dbid === null) {
if (tab && page.tabs[tab.id]) {
keepass.handleError(tab, kpErrors.NO_SAVED_DATABASES_FOUND);
}
return false;
}
const messageData = {
action: kpAction,
id: dbid,
key: dbkey
};
const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce, enableTimeout);
if (!response) {
const hash = response.hash || 0;
keepass.deleteKey(hash);
keepass.isEncryptionKeyUnrecognized = true;
keepass.handleError(tab, kpErrors.ENCRYPTION_KEY_UNRECOGNIZED);
keepass.associated.value = false;
keepass.associated.hash = null;
} else if (!keepass.isAssociated()) {
keepass.handleError(tab, kpErrors.ASSOCIATION_FAILED);
} else {
keepass.isEncryptionKeyUnrecognized = false;
keepass.clearErrorMessage(tab);
}
return keepass.isAssociated();
} catch (err) {
logError(`testAssociation failed: ${err}`);
return false;
}
};
keepass.getDatabaseHash = async function(tab, args = []) {
if (!keepass.isConnected) {
keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED);
return '';
}
if (!keepass.serverPublicKey) {
keepass.changePublicKeys(tab);
}
const [ enableTimeout = false, triggerUnlock = false ] = args;
const kpAction = kpActions.GET_DATABASE_HASH;
const [ nonce, incrementedNonce ] = keepassClient.getNonces();
const messageData = {
action: kpAction,
connectedKeys: Object.keys(keepass.keyRing) // This will be removed in the future
};
const encrypted = keepassClient.encrypt(messageData, nonce);
if (encrypted.length <= 0) {
keepass.handleError(tab, kpErrors.PUBLIC_KEY_NOT_FOUND);
keepass.updateDatabaseHashToContent();
return keepass.databaseHash;
}
try {
const request = keepassClient.buildRequest(kpAction, keepassClient.encrypt(messageData, nonce), nonce, keepass.clientID, triggerUnlock);
const response = await keepassClient.sendNativeMessage(request, enableTimeout);
if (response.message && response.nonce) {
const res = keepassClient.decrypt(response.message, response.nonce);
if (!res) {
keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE);
return '';
}
const message = nacl.util.encodeUTF8(res);
const parsed = JSON.parse(message);
if (keepassClient.verifyDatabaseResponse(parsed, incrementedNonce) && parsed.hash) {
const oldDatabaseHash = keepass.databaseHash;
keepass.setcurrentKeePassXCVersion(parsed.version);
keepass.databaseHash = parsed.hash || '';
if (oldDatabaseHash && oldDatabaseHash !== keepass.databaseHash) {
keepass.associated.value = false;
keepass.associated.hash = null;
}
keepass.isDatabaseClosed = false;
keepass.isKeePassXCAvailable = true;
// Update the databaseHash from legacy hash
if (parsed.oldHash) {
keepass.updateDatabaseHash(parsed.oldHash, parsed.hash);
}
return parsed.hash;
} else if (parsed.errorCode) {
keepass.databaseHash = '';
keepass.isDatabaseClosed = true;
keepass.handleError(tab, kpErrors.DATABASE_NOT_OPENED);
return keepass.databaseHash;
}
return keepass.databaseHash;
}
keepass.databaseHash = '';
keepass.isDatabaseClosed = true;
if (response.message && response.message === '') {
keepass.isKeePassXCAvailable = false;
keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED);
} else {
keepass.handleError(tab, response.errorCode, response.error);
}
return keepass.databaseHash;
} catch (err) {
logError(`getDatabaseHash failed: ${err}`);
return keepass.databaseHash;
}
};
keepass.changePublicKeys = async function(tab, enableTimeout = false, connectionTimeout) {
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.sendNativeMessage(request, enableTimeout, connectionTimeout);
keepass.setcurrentKeePassXCVersion(response.version);
if (!keepassClient.verifyKeyResponse(response, key, incrementedNonce)) {
if (tab && page.tabs[tab.id]) {
keepass.handleError(tab, kpErrors.KEY_CHANGE_FAILED);
}
keepass.updateDatabaseHashToContent();
return false;
}
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;
}
};
keepass.lockDatabase = async function(tab) {
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.sendMessage(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(`ockDatabase failed: ${err}`);
return false;
}
};
keepass.getDatabaseGroups = async function(tab) {
try {
const taResponse = await keepass.testAssociation(tab, [ false ]);
if (!taResponse) {
browserAction.showDefault(tab);
return [];
}
keepass.clearErrorMessage(tab);
if (!keepass.isConnected) {
return [];
}
let groups = [];
const kpAction = kpActions.GET_DATABASE_GROUPS;
const nonce = keepassClient.getNonce();
const messageData = {
action: kpAction
};
const response = await keepassClient.sendMessage(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 [];
}
keepass.createCredentials = async function(tab, args = []) {
return keepass.protocolV2 ? await protocol.createCredentials(tab, args) : await keepassProtocol.addCredentials(tab, args);
};
keepass.createNewGroup = async function(tab, args = []) {
try {
const [ groupName ] = args;
const taResponse = await keepass.testAssociation(tab, [ false ]);
if (!taResponse) {
browserAction.showDefault(tab);
return [];
}
return keepass.protocolV2 ? await protocol.createNewGroup(tab, args) : await keepassProtocol.createNewGroup(tab, args);
};
keepass.clearErrorMessage(tab);
keepass.getCredentials = async function(tab, args = []) {
return keepass.protocolV2 ? await protocol.getCredentials(tab, args) : await keepassProtocol.retrieveCredentials(tab, args);
};
if (!keepass.isConnected) {
return [];
}
keepass.getDatabaseGroups = async function(tab, args = []) {
return keepass.protocolV2 ? await protocol.getDatabaseGroups(tab, args) : await keepassProtocol.getDatabaseGroups(tab, args);
};
const kpAction = kpActions.CREATE_NEW_GROUP;
const nonce = keepassClient.getNonce();
const messageData = {
action: kpAction,
groupName: groupName
};
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 [];
}
keepass.getDatabaseHash = async function(tab, args = []) {
return keepass.protocolV2 ? await protocol.getDatabaseStatuses(tab, args) : await keepassProtocol.getDatabaseHash(tab, args);
};
keepass.getTotp = async function(tab, args = []) {
const [ uuid, oldTotp ] = args;
if (!keepass.compareVersion('2.6.1', keepass.currentKeePassXC, true)) {
return oldTotp;
}
const taResponse = await keepass.testAssociation(tab, [ false ]);
if (!taResponse || !keepass.isConnected) {
return;
}
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}`);
}
return keepass.protocolV2 ? await protocol.getTotp(tab, args) : await keepassProtocol.getTotp(tab, args);
};
keepass.requestAutotype = async function(tab, args = []) {
if (!keepass.isConnected) {
keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED);
return false;
}
keepass.lockDatabase = async function(tab, args = []) {
return keepass.protocolV2 ? await protocol.lockDatabase(tab, args) : await keepassProtocol.lockDatabase(tab, args);
};
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.sendMessage(kpAction, tab, messageData, nonce);
return response;
} catch (err) {
logError(`requestAutotype failed: ${err}`);
return false;
}
keepass.updateCredentials = async function(tab, args = []) {
return keepass.protocolV2 ? await protocol.updateCredentials(tab, args) : await keepassProtocol.updateCredentials(tab, args);
};
//--------------------------------------------------------------------------
@ -739,34 +254,55 @@ keepass.disableAutomaticReconnect = function() {
};
keepass.reconnect = async function(tab, connectionTimeout) {
keepassClient.connectToNative();
keepass.generateNewKeyPair();
const keyChangeResult = await keepass.changePublicKeys(tab, true, connectionTimeout).catch(() => false);
protocolClient.connectToNative();
protocolClient.generateNewKeyPair();
const keyChangeResult = await protocol.changePublicKeys(tab, true, connectionTimeout).catch(() => false);
// Change public keys timeout
if (!keyChangeResult) {
return false;
}
const hash = await keepass.getDatabaseHash(tab);
if (hash !== '') {
keepass.clearErrorMessage(tab);
}
// What to do here?
if (!keepass.protocolV2) {
// Needed?
const hash = await keepass.getDatabaseHash(tab);
if (hash !== '') {
keepass.clearErrorMessage(tab);
}
await keepass.testAssociation();
await keepass.isConfigured();
await keepass.testAssociation();
await keepass.isConfigured();
}
keepass.updateDatabaseHashToContent();
return true;
};
//--------------------------------------------------------------------------
// Utils
// Error handling
//--------------------------------------------------------------------------
keepass.generateNewKeyPair = function() {
keepass.keyPair = nacl.box.keyPair();
keepass.clearErrorMessage = function(tab) {
if (tab && page.tabs[tab.id]) {
page.tabs[tab.id].errorMessage = undefined;
}
};
keepass.handleError = function(tab, errorCode, errorMessage = '') {
if (errorMessage.length === 0) {
errorMessage = kpErrors.getError(errorCode);
}
logError(`${errorCode}: ${errorMessage}`);
if (tab && page.tabs[tab.id]) {
page.tabs[tab.id].errorMessage = errorMessage;
}
};
//--------------------------------------------------------------------------
// Utils
//--------------------------------------------------------------------------
keepass.isConfigured = async function() {
if (typeof(keepass.databaseHash) === 'undefined') {
const hash = keepass.getDatabaseHash();
@ -787,7 +323,6 @@ keepass.isAssociated = function() {
keepass.setcurrentKeePassXCVersion = function(version) {
if (version) {
keepass.currentKeePassXC = version;
keepass.protocolV2 = keepass.compareVersion('2.8.0', version);
}
};
@ -837,23 +372,6 @@ keepass.checkForNewKeePassXCVersion = function() {
keepass.latestKeePassXC.lastChecked = new Date().valueOf();
};
keepass.clearErrorMessage = function(tab) {
if (tab && page.tabs[tab.id]) {
page.tabs[tab.id].errorMessage = undefined;
}
};
keepass.handleError = function(tab, errorCode, errorMessage = '') {
if (errorMessage.length === 0) {
errorMessage = kpErrors.getError(errorCode);
}
logError(`${errorCode}: ${errorMessage}`);
if (tab && page.tabs[tab.id]) {
page.tabs[tab.id].errorMessage = errorMessage;
}
};
keepass.updatePopup = function() {
if (page && page.tabs.length > 0) {
browserAction.showDefault();
@ -866,19 +384,29 @@ keepass.updateDatabase = async function() {
keepass.associated.hash = null;
page.clearAllLogins();
await keepass.testAssociation(null, [ true ]);
if (keepass.protocolV2) {
// TODO: Only show "Connect" if the active database is not connected?
// TODO: What if there are credentials from another database but the selected one is not connected?
const result = await protocol.testAssociationFromDatabaseStatuses();
keepass.updatePopup();
keepass.updateDatabaseHashToContent(result);
return;
}
// Legacy protocol
await keepassProtocol.testAssociation(null, [ true ]);
keepass.updatePopup();
keepass.updateDatabaseHashToContent();
};
keepass.updateDatabaseHashToContent = async function() {
keepass.updateDatabaseHashToContent = async function(associateResult = {}) {
try {
const tab = await getCurrentTab();
if (tab) {
// Send message to content script
browser.tabs.sendMessage(tab.id, {
action: 'check_database_hash',
associateResult: associateResult,
hash: { old: keepass.previousDatabaseHash, new: keepass.databaseHash },
connected: keepass.isKeePassXCAvailable
}).catch((err) => {

View file

@ -0,0 +1,576 @@
'use strict';
// Legacy protocol, "old" client for KeePassXC 2.7.5 and older.
const keepassProtocol = {};
//--------------------------------------------------------------------------
// Commands
//--------------------------------------------------------------------------
keepassProtocol.addCredentials = async function(tab, args = []) {
const [ username, password, url, group, groupUuid ] = args;
return keepass.updateCredentials(tab, [ null, username, password, url, group, groupUuid ]);
};
keepassProtocol.associate = async function(tab) {
if (keepass.isAssociated()) {
return AssociatedAction.ASSOCIATED;
}
try {
await keepassProtocol.getDatabaseHash(tab);
if (keepass.isDatabaseClosed || !keepass.isKeePassXCAvailable) {
return AssociatedAction.NOT_ASSOCIATED;
}
keepass.clearErrorMessage(tab);
const kpAction = kpActions.ASSOCIATE;
const key = nacl.util.encodeBase64(keepass.keyPair.publicKey);
const nonce = protocolClient.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.sendMessage(kpAction, tab, messageData, nonce, false, true);
if (response) {
// Use public key as identification key with older KeePassXC releases
const savedKey = keepass.compareVersion('2.3.4', keepass.currentKeePassXC) ? idKey : key;
keepass.setCryptoKey(response.id, savedKey); // Save the new identification public key as id key for the database
keepass.associated.value = true;
keepass.associated.hash = response.hash || 0;
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;
};
keepassProtocol.createNewGroup = async function(tab, args = []) {
try {
const [ groupName ] = args;
const taResponse = await keepassProtocol.testAssociation(tab, [ false ]);
if (!taResponse) {
browserAction.showDefault(tab);
return [];
}
keepass.clearErrorMessage(tab);
if (!keepass.isConnected) {
return [];
}
const kpAction = kpActions.CREATE_NEW_GROUP;
const nonce = protocolClient.getNonce();
const messageData = {
action: kpAction,
groupName: groupName
};
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 [];
}
};
keepassProtocol.changePublicKeys = async function(tab, enableTimeout = false, connectionTimeout) {
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 ] = protocolClient.getNonces();
keepass.clientID = nacl.util.encodeBase64(nacl.randomBytes(protocolClient.keySize));
const request = {
action: kpAction,
publicKey: key,
nonce: nonce,
clientID: keepass.clientID
};
try {
const response = await keepassClient.sendNativeMessage(request, enableTimeout, connectionTimeout);
keepass.setcurrentKeePassXCVersion(response.version);
if (!keepassClient.verifyKeyResponse(response, key, incrementedNonce)) {
if (tab && page.tabs[tab.id]) {
keepass.handleError(tab, kpErrors.KEY_CHANGE_FAILED);
}
keepass.updateDatabaseHashToContent();
return false;
}
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;
}
};
keepassProtocol.generatePassword = async function(tab) {
if (!keepass.isConnected) {
return [];
}
try {
const taResponse = await keepassProtocol.testAssociation(tab);
if (!taResponse) {
browserAction.showDefault(tab);
return [];
}
if (!keepass.compareVersion(keepass.requiredKeePassXC, keepass.currentKeePassXC)) {
return [];
}
let password;
const kpAction = kpActions.GENERATE_PASSWORD;
const nonce = protocolClient.getNonce();
const messageData = {
action: kpAction,
nonce: nonce,
clientID: keepass.clientID,
requestID: protocolClient.getRequestId()
};
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 [];
}
};
keepassProtocol.getDatabaseGroups = async function(tab) {
try {
const taResponse = await keepassProtocol.testAssociation(tab, [ false ]);
if (!taResponse) {
browserAction.showDefault(tab);
return [];
}
keepass.clearErrorMessage(tab);
if (!keepass.isConnected) {
return [];
}
let groups = [];
const kpAction = kpActions.GET_DATABASE_GROUPS;
const nonce = protocolClient.getNonce();
const messageData = {
action: kpAction
};
const response = await keepassClient.sendMessage(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 [];
}
};
keepassProtocol.getDatabaseHash = async function(tab, args = []) {
if (!keepass.isConnected) {
keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED);
return '';
}
if (!keepass.serverPublicKey) {
keepassProtocol.changePublicKeys(tab);
}
const [ enableTimeout = false, triggerUnlock = false ] = args;
const kpAction = kpActions.GET_DATABASE_HASH;
const [ nonce, incrementedNonce ] = protocolClient.getNonces();
const messageData = {
action: kpAction,
connectedKeys: Object.keys(keepass.keyRing) // This will be removed in the future
};
// Why is this here?
/*const encrypted = protocolClient.encrypt(messageData, nonce);
if (encrypted.length <= 0) {
keepass.handleError(tab, kpErrors.PUBLIC_KEY_NOT_FOUND);
keepass.updateDatabaseHashToContent();
return keepass.databaseHash;
}*/
try {
const request = keepassClient.buildRequest(kpAction, protocolClient.encrypt(messageData, nonce), nonce, keepass.clientID, triggerUnlock);
const response = await keepassClient.sendNativeMessage(request, enableTimeout);
if (response.message && response.nonce) {
const res = protocolClient.decrypt(response.message, response.nonce);
if (!res) {
keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE);
return '';
}
const message = nacl.util.encodeUTF8(res);
const parsed = JSON.parse(message);
if (keepassClient.verifyDatabaseResponse(parsed, incrementedNonce) && parsed.hash) {
const oldDatabaseHash = keepass.databaseHash;
keepass.setcurrentKeePassXCVersion(parsed.version);
keepass.databaseHash = parsed.hash || '';
if (oldDatabaseHash && oldDatabaseHash !== keepass.databaseHash) {
keepass.associated.value = false;
keepass.associated.hash = null;
}
keepass.isDatabaseClosed = false;
keepass.isKeePassXCAvailable = true;
// Update the databaseHash from legacy hash
if (parsed.oldHash) {
keepass.updateDatabaseHash(parsed.oldHash, parsed.hash);
}
return parsed.hash;
} else if (parsed.errorCode) {
keepass.databaseHash = '';
keepass.isDatabaseClosed = true;
keepass.handleError(tab, kpErrors.DATABASE_NOT_OPENED);
return keepass.databaseHash;
}
return keepass.databaseHash;
}
keepass.databaseHash = '';
keepass.isDatabaseClosed = true;
if (response.message && response.message === '') {
keepass.isKeePassXCAvailable = false;
keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED);
} else {
keepass.handleError(tab, response.errorCode, response.error);
}
return keepass.databaseHash;
} catch (err) {
logError(`getDatabaseHash failed: ${err}`);
return keepass.databaseHash;
}
};
keepassProtocol.getTotp = async function(tab, args = []) {
const [ uuid, oldTotp ] = args;
if (!keepass.compareVersion('2.6.1', keepass.currentKeePassXC, true)) {
return oldTotp;
}
const taResponse = await keepassProtocol.testAssociation(tab, [ false ]);
if (!taResponse || !keepass.isConnected) {
return;
}
const kpAction = kpActions.GET_TOTP;
const nonce = protocolClient.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}`);
}
};
keepassProtocol.lockDatabase = async function(tab) {
if (!keepass.isConnected) {
keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED);
return false;
}
const kpAction = kpActions.LOCK_DATABASE;
const nonce = protocolClient.getNonce();
const messageData = {
action: kpAction
};
try {
const response = await keepassClient.sendMessage(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(`ockDatabase failed: ${err}`);
return false;
}
};
keepassProtocol.requestAutotype = async function(tab, args = []) {
if (!keepass.isConnected) {
keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED);
return false;
}
const kpAction = kpActions.REQUEST_AUTOTYPE;
const nonce = protocolClient.getNonce();
const search = getTopLevelDomainFromUrl(args[0]);
const messageData = {
action: kpAction,
search: search
};
try {
const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce);
return response;
} catch (err) {
logError(`requestAutotype failed: ${err}`);
return false;
}
};
keepassProtocol.retrieveCredentials = async function(tab, args = []) {
try {
const [ url, submiturl, triggerUnlock = false, httpAuth = false ] = args;
const taResponse = await keepassProtocol.testAssociation(tab, [ false, triggerUnlock ]);
if (!taResponse) {
browserAction.showDefault(tab);
return [];
}
keepass.clearErrorMessage(tab);
if (!keepass.isConnected) {
return [];
}
let entries = [];
const keys = [];
const kpAction = kpActions.GET_LOGINS;
const nonce = protocolClient.getNonce();
const [ dbid ] = keepass.getCryptoKey();
for (const keyHash in keepass.keyRing) {
keys.push({
id: keepass.keyRing[keyHash].id,
key: keepass.keyRing[keyHash].key
});
}
const messageData = {
action: kpAction,
id: dbid,
url: url,
keys: keys
};
if (submiturl) {
messageData.submitUrl = submiturl;
}
if (httpAuth) {
messageData.httpAuth = 'true';
}
const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce);
if (response) {
entries = keepass.removeDuplicateEntries(response.entries);
keepass.updateLastUsed(keepass.databaseHash);
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 [];
}
};
keepassProtocol.testAssociation = async function(tab, args = []) {
keepass.clearErrorMessage(tab);
try {
const [ enableTimeout = false, triggerUnlock = false ] = args;
const dbHash = await keepassProtocol.getDatabaseHash(tab, [ enableTimeout, triggerUnlock ]);
if (!dbHash) {
return false;
}
if (keepass.isDatabaseClosed || !keepass.isKeePassXCAvailable) {
return false;
}
if (keepass.isAssociated()) {
return true;
}
if (!keepass.serverPublicKey) {
if (tab && page.tabs[tab.id]) {
keepass.handleError(tab, kpErrors.PUBLIC_KEY_NOT_FOUND);
}
return false;
}
const kpAction = kpActions.TEST_ASSOCIATE;
const nonce = protocolClient.getNonce();
const [ dbid, dbkey ] = keepass.getCryptoKey();
if (dbkey === null || dbid === null) {
if (tab && page.tabs[tab.id]) {
keepass.handleError(tab, kpErrors.NO_SAVED_DATABASES_FOUND);
}
return false;
}
const messageData = {
action: kpAction,
id: dbid,
key: dbkey
};
const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce, enableTimeout);
if (!response) {
const hash = response.hash || 0;
keepass.deleteKey(hash);
keepass.isEncryptionKeyUnrecognized = true;
keepass.handleError(tab, kpErrors.ENCRYPTION_KEY_UNRECOGNIZED);
keepass.associated.value = false;
keepass.associated.hash = null;
} else if (!keepass.isAssociated()) {
keepass.handleError(tab, kpErrors.ASSOCIATION_FAILED);
} else {
keepass.isEncryptionKeyUnrecognized = false;
keepass.clearErrorMessage(tab);
}
return keepass.isAssociated();
} catch (err) {
logError(`testAssociation failed: ${err}`);
return false;
}
};
keepassProtocol.updateCredentials = async function(tab, args = []) {
try {
const [ entryId, username, password, url, group, groupUuid ] = args;
const taResponse = await keepassProtocol.testAssociation(tab);
if (!taResponse) {
browserAction.showDefault(tab);
return [];
}
const kpAction = kpActions.SET_LOGIN;
const [ dbid ] = keepass.getCryptoKey();
const nonce = protocolClient.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;
}
const response = await keepassClient.sendMessage(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.error === 'success' || response.error === '') {
successMessage = entryId ? 'updated' : 'created';
}
return successMessage;
} else {
return 'error';
}
} catch (err) {
logError(`updateCredentials failed: ${err}`);
return [];
}
};

View file

@ -0,0 +1,201 @@
'use strict';
const messageBuffer = {
buffer: [],
addMessage(msg) {
if (!this.buffer.includes(msg)) {
this.buffer.push(msg);
}
},
matchAndRemove(msg) {
for (let i = 0; i < this.buffer.length; ++i) {
if (msg.nonce && msg.nonce === protocolClient.incrementedNonce(this.buffer[i].nonce)) {
this.buffer.splice(i, 1);
return true;
}
}
return false;
}
};
const keepassClient = {};
//--------------------------------------------------------------------------
// Messaging
//--------------------------------------------------------------------------
keepassClient.sendNativeMessage = function(request, enableTimeout = false, timeoutValue) {
return new Promise((resolve, reject) => {
let timeout;
const requestAction = request.action;
const ev = protocolClient.nativePort.onMessage;
const listener = ((port, action) => {
const handler = (msg) => {
if (msg && msg?.action === action) {
// If the request has a separate requestID, check if it matches when there's no nonce (an error message)
const isNotificationOrError = !msg.nonce && request.requestID === msg.requestID;
// Only resolve a matching response or a notification (without nonce)
if (isNotificationOrError || messageBuffer.matchAndRemove(msg)) {
port.removeListener(handler);
if (enableTimeout) {
clearTimeout(timeout);
}
resolve(msg);
return;
}
}
};
return handler;
})(ev, requestAction);
ev.addListener(listener);
const messageTimeout = timeoutValue || protocolClient.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 (protocolClient.nativePort) {
protocolClient.nativePort.postMessage(request);
}
});
};
keepassClient.handleResponse = function(response, incrementedNonce, tab) {
if (response.message && response.nonce) {
const res = protocolClient.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.verifyResponse(parsed, incrementedNonce)) {
return parsed;
}
} else if (response.error && response.errorCode) {
keepass.handleError(tab, response.errorCode, response.error);
}
return undefined;
};
keepassClient.buildRequest = function(action, encrypted, nonce, clientID, triggerUnlock = false) {
const request = {
action: action,
message: encrypted,
nonce: nonce,
clientID: clientID
};
if (triggerUnlock) {
request.triggerUnlock = 'true';
}
return request;
};
keepassClient.sendMessage = async function(kpAction, tab, messageData, nonce, enableTimeout = false, triggerUnlock = false) {
const request = keepassClient.buildRequest(kpAction, protocolClient.encrypt(messageData, nonce), nonce, keepass.clientID, triggerUnlock);
if (messageData.requestID) {
request['requestID'] = messageData.requestID;
}
const response = await keepassClient.sendNativeMessage(request, enableTimeout);
const incrementedNonce = protocolClient.incrementedNonce(nonce);
return keepassClient.handleResponse(response, incrementedNonce, tab);
};
//--------------------------------------------------------------------------
// Utils
//--------------------------------------------------------------------------
keepassClient.verifyKeyResponse = function(response, key, nonce) {
if (!response.success || !response.publicKey) {
keepass.associated.hash = null;
return false;
}
if (!protocolClient.checkNonceLength(response.nonce)) {
logError('Invalid nonce length.');
return false;
}
const reply = (response.nonce === nonce);
if (response.publicKey && reply) {
keepass.serverPublicKey = nacl.util.decodeBase64(response.publicKey);
return true;
}
return reply;
};
keepassClient.verifyResponse = function(response, nonce, id) {
keepass.associated.value = response.success;
if (response.success !== 'true') {
keepass.associated.hash = null;
return false;
}
keepass.associated.hash = keepass.databaseHash;
if (!protocolClient.checkNonceLength(response.nonce)) {
return false;
}
keepass.associated.value = (response.nonce === nonce);
if (keepass.associated.value === false) {
logError('Nonce compare failed');
return false;
}
if (id) {
keepass.associated.value = (keepass.associated.value && id === response.id);
}
keepass.associated.hash = (keepass.associated.value) ? keepass.databaseHash : null;
return keepass.isAssociated();
};
keepassClient.verifyDatabaseResponse = function(response, nonce) {
if (response.success !== 'true') {
keepass.associated.hash = null;
return false;
}
if (!protocolClient.checkNonceLength(response.nonce)) {
logError('Invalid nonce length.');
return false;
}
if (response.nonce !== nonce) {
logError('Nonce compare failed.');
return false;
}
keepass.associated.hash = response.hash;
return response.hash !== '' && response.success === 'true';
};

View file

@ -209,7 +209,8 @@ page.retrieveCredentials = async function(tab, args = []) {
page.currentRequest.submitUrl = submitUrl;
}
const credentials = await keepass.retrieveCredentials(tab, args);
// TODO: Make keepass.js to handle protocol/protocolClient and legacyProtocol/legacyProcotolClient
const credentials = await keepass.getCredentials(tab, args);
page.tabs[tab.id].credentials = credentials;
return credentials;
};

View file

@ -1,10 +1,12 @@
'use strict';
//--------------------------------------------------------------------------
// Protocol V2
//--------------------------------------------------------------------------
const protocol = {};
protocol.associate = async function(tab, args = []) {
console.log('associate');
if (!keepass.isKeePassXCAvailable) {
return AssociatedAction.NOT_ASSOCIATED;
}
@ -14,7 +16,6 @@ protocol.associate = async function(tab, args = []) {
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);
@ -24,7 +25,7 @@ protocol.associate = async function(tab, args = []) {
idKey: idKey
};
const response = await keepassClient.sendMessageV2(kpAction, tab, messageData, nonce, false, true);
const response = await protocolClient.sendMessage(kpAction, tab, messageData, 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
@ -42,8 +43,6 @@ protocol.associate = async function(tab, args = []) {
};
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;
@ -58,15 +57,17 @@ protocol.changePublicKeys = async function(tab, enableTimeout = false, connectio
action: kpAction,
publicKey: key,
nonce: nonce,
clientID: keepass.clientID
clientID: keepass.clientID,
requestID: keepassClient.getRequestId()
};
try {
const response = await keepassClient.sendNativeMessageV2(kpAction, request, enableTimeout, connectionTimeout);
const response = await protocolClient.sendNativeMessage(kpAction, request, enableTimeout, connectionTimeout);
keepass.setcurrentKeePassXCVersion(response.version);
keepass.protocolV2 = response?.protocolVersion === 2;
const verified = keepass.protocolV2
? keepassClient.verifyResponseV2(response, incrementedNonce)
? protocolClient.verifyNonce(response, incrementedNonce)
: keepassClient.verifyKeyResponse(response, key, incrementedNonce);
if (!response?.publicKey || !verified) {
if (tab && page.tabs[tab.id]) {
@ -88,33 +89,30 @@ protocol.changePublicKeys = async function(tab, enableTimeout = false, connectio
};
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 [ groupName ] = args;
const [ dbid ] = keepass.getCryptoKey();
const messageData = {
action: kpAction,
groupName: groupName
id: dbid,
groupName: groupName,
};
try {
// TODO: Handle errors
const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce);
const response = await protocolClient.sendMessage(kpAction, tab, messageData);
if (response) {
keepass.updateLastUsed(keepass.databaseHash); // ?
return response;
@ -131,8 +129,6 @@ protocol.createNewGroup = async function(tab, args = []) {
};
protocol.generatePassword = async function(tab, args = []) {
console.log('generate-password');
if (!keepass.isConnected) {
return [];
}
@ -141,19 +137,17 @@ protocol.generatePassword = async function(tab, args = []) {
return [];
}
let password;
const kpAction = kpActions.GENERATE_PASSWORD;
const nonce = keepassClient.getNonce();
let password;
const messageData = {
action: kpAction,
nonce: nonce,
clientID: keepass.clientID,
requestID: keepassClient.getRequestId()
requestID: keepassClient.getRequestId() // Needed?
};
try {
const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce);
const response = await protocolClient.sendMessage(kpAction, tab, messageData);
if (response) {
password = response.entries ?? response.password;
keepass.updateLastUsed(keepass.databaseHash); // ?
@ -169,25 +163,20 @@ protocol.generatePassword = async function(tab, args = []) {
};
protocol.getCredentials = async function(tab, args = []) {
console.log('get-credentials');
if (!keepass.isConnected) {
return [];
}
keepass.clearErrorMessage(tab);
const kpAction = kpActions.GET_CREDENTIALS;
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()
keys: protocol.getKeys()
};
if (submiturl) {
@ -200,7 +189,7 @@ protocol.getCredentials = async function(tab, args = []) {
try {
// TODO: Handle errors
const response = await keepassClient.sendMessageV2(kpAction, tab, messageData, nonce, false, triggerUnlock);
const response = await protocolClient.sendMessage(kpAction, tab, messageData, false, triggerUnlock);
if (response) {
entries = keepass.removeDuplicateEntries(response.entries);
keepass.updateLastUsed(keepass.databaseHash); // What about this?
@ -223,25 +212,24 @@ protocol.getCredentials = async function(tab, args = []) {
};
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 [ dbid ] = keepass.getCryptoKey();
let groups = [];
const messageData = {
action: kpAction
action: kpAction,
id: dbid
};
try {
// TODO: Handle errors
const response = await keepassClient.sendMessageV2(kpAction, tab, messageData, nonce);
const response = await protocolClient.sendMessage(kpAction, tab, messageData);
if (response) {
groups = response.groups;
groups.defaultGroup = page.settings.defaultGroup;
@ -258,33 +246,72 @@ protocol.getDatabaseGroups = async function(tab, args = []) {
}
};
// Obsolete? This should be checked inside KeePassXC
protocol.getDatabaseStatuses = async function(tab, args = []) {
console.log('get-database-statuses');
if (!keepass.isConnected) {
keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED);
return;
}
if (!keepass.serverPublicKey) {
await protocol.changePublicKeys(tab);
}
const kpAction = kpActions.GET_DATABASE_STATUSES;
const [ enableTimeout = false, triggerUnlock = false ] = args;
const messageData = {
action: kpAction,
keys: protocol.getKeys()
};
try {
const response = await protocolClient.sendMessage(kpAction, tab, messageData, enableTimeout, triggerUnlock);
if (response) {
keepass.databaseHash = response.hash;
// Return this error only if all databases are closed
if (response?.statuses.every(s => s.locked)) {
keepass.databaseHash = '';
keepass.isDatabaseClosed = true;
keepass.handleError(tab, kpErrors.DATABASE_NOT_OPENED);
}
return response;
}
// TODO: Check if these are even possible ..?
keepass.databaseHash = '';
keepass.isDatabaseClosed = true;
if (response.message && response.message === '') {
// ..?
keepass.isKeePassXCAvailable = false;
keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED);
} else {
keepass.handleError(tab, response.errorCode, response.error);
}
} catch (err) {
logError(`getDatabaseStatuses failed: ${err}`);
}
};
protocol.getTotp = async function(tab, args = []) {
console.log('get-totp');
if (!keepass.isConnected) {
return [];
}
const kpAction = kpActions.GET_TOTP;
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);
const response = await protocolClient.sendMessage(kpAction, tab, messageData);
if (response) {
keepass.updateLastUsed(keepass.databaseHash);
return response.totp;
@ -297,22 +324,18 @@ protocol.getTotp = async function(tab, args = []) {
};
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);
const response = await protocolClient.sendMessage(kpAction, tab, messageData);
if (response) {
//keepass.isDatabaseClosed = true; // ?
keepass.updateDatabase();
@ -332,15 +355,12 @@ protocol.lockDatabase = async function(tab, args = []) {
};
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 = {
@ -349,7 +369,7 @@ protocol.requestAutotype = async function(tab, args = []) {
};
try {
const response = await keepassClient.sendMessageV2(kpAction, tab, messageData, nonce);
const response = await protocolClient.sendMessage(kpAction, tab, messageData);
return response?.result;
} catch (err) {
logError(`requestAutotype failed: ${err}`);
@ -357,6 +377,55 @@ protocol.requestAutotype = async function(tab, args = []) {
}
};
protocol.testAssociationFromDatabaseStatuses = async function(tab, args = []) {
const databaseStatuses = await protocol.getDatabaseStatuses(tab, args);
console.log(databaseStatuses);
const result = {
areAllLocked: true,
associationNeeded: false,
databaseHash: undefined,
isAnyAssociated: false
};
// TODO: Handle this already in getDatabaseStatuses?
if (!databaseStatuses || databaseStatuses.statuses.length === 0) {
keepass.handleError(tab, kpErrors.DATABASE_NOT_OPENED);
return result;
}
const currentDatabaseStatus = databaseStatuses.statuses.filter(s => s.hash === databaseStatuses.hash);
const isCurrentAssociated = currentDatabaseStatus[0]?.associated;
const isCurrentLocked = currentDatabaseStatus[0]?.locked;
const isAnyAssociated = databaseStatuses.statuses.some(s => s.associated);
const areAllLocked = databaseStatuses.statuses.every(s => s.locked);
// TODO: Add a warning notification if two databases with identical hashes are regognized.
// To where? DOM? KeePassXC? Popup? Maybe this feature should be in KeePassXC instead when making the request and not here.
if (currentDatabaseStatus.length > 1) {
console.log('Identical databases found.');
}
// TODO: If the current one is not associated, activate the Connect button in the popup?
// But only if the current database is not locked..
if (!isCurrentAssociated && !isCurrentLocked) {
console.log('Current one is not associated');
}
// This should be true only if all databases are locked
keepass.isDatabaseClosed = areAllLocked; // ?
result.areAllLocked = areAllLocked;
result.associationNeeded = !isCurrentAssociated && !isCurrentLocked;
result.databaseHash = databaseStatuses.hash;
result.isAnyAssociated = isAnyAssociated;
keepass.databaseAssosiationStatuses = result;
return result;
};
protocol.updateCredentials = async function(tab, args = []) {
console.log('update-credentials');
@ -364,10 +433,9 @@ protocol.updateCredentials = async function(tab, args = []) {
return [];
}
const [ entryId, username, password, url, group, groupUuid ] = args;
const kpAction = kpActions.CREATE_CREDENTIALS;
const [ entryId, username, password, url, group, groupUuid ] = args;
const [ dbid ] = keepass.getCryptoKey();
const nonce = keepassClient.getNonce();
const messageData = {
action: kpAction,
@ -393,7 +461,7 @@ protocol.updateCredentials = async function(tab, args = []) {
try {
// TODO: Check response messages
const response = await keepassClient.sendMessageV2(kpAction, tab, messageData, nonce);
const response = await protocolClient.sendMessage(kpAction, tab, messageData);
if (response) {
// KeePassXC versions lower than 2.5.0 will have an empty parsed.error
let successMessage = response.error;
@ -410,3 +478,20 @@ protocol.updateCredentials = async function(tab, args = []) {
return [];
}
};
//--------------------------------------------------------------------------
// Utils
//--------------------------------------------------------------------------
protocol.getKeys = function() {
const keys = [];
for (const keyHash in keepass.keyRing) {
keys.push({
id: keepass.keyRing[keyHash].id,
key: keepass.keyRing[keyHash].key
});
}
return keys;
};

View file

@ -0,0 +1,258 @@
'use strict';
// Stores 'requestID' and 'action' to internal buffer
const protocolBuffer = {
buffer: [],
addMessage(msg) {
if (!this.buffer.includes(msg)) {
this.buffer.push({
action: msg?.action,
requestID: msg?.requestID
});
}
},
matchAndRemove(msg) {
for (let i = 0; i < this.buffer.length; ++i) {
if (msg?.requestID === this.buffer[i].requestID
|| (msg.action === 'change-public-keys' && msg?.action === this.buffer[i].action)) {
this.buffer.splice(i, 1);
return true;
}
}
return false;
}
};
//--------------------------------------------------------------------------
// Protocol V2
//--------------------------------------------------------------------------
const protocolClient = {};
protocolClient.keySize = 24;
protocolClient.messageTimeout = 500; // Milliseconds
protocolClient.nativeHostName = 'org.keepassxc.keepassxc_browser';
protocolClient.nativePort = null;
protocolClient.sendNativeMessage = function(requestAction, request, enableTimeout = false, timeoutValue) {
return new Promise((resolve, reject) => {
let timeout;
const ev = protocolClient.nativePort.onMessage;
const listener = ((port) => {
const handler = (msg) => {
if (msg && (msg?.requestID === request.requestID || msg?.action === 'change-public-keys')) {
// Only resolve a matching response
if (protocolBuffer.matchAndRemove(msg)) {
port.removeListener(handler);
if (enableTimeout) {
clearTimeout(timeout);
}
resolve(msg);
return;
}
}
};
return handler;
})(ev);
ev.addListener(listener);
const messageTimeout = timeoutValue || protocolClient.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
protocolBuffer.addMessage(request);
// Send the request
if (protocolClient.nativePort) {
protocolClient.nativePort.postMessage(request);
}
});
};
protocolClient.sendMessage = async function(kpAction, tab, messageData, enableTimeout = false, triggerUnlock = false) {
const nonce = protocolClient.getNonce();
const request = protocolClient.buildRequest(protocolClient.encrypt(messageData, nonce), nonce, keepass.clientID, triggerUnlock);
const response = await protocolClient.sendNativeMessage(kpAction, request, enableTimeout);
const incrementedNonce = keepassClient.incrementedNonce(nonce);
return protocolClient.handleResponse(response, incrementedNonce, tab);
};
protocolClient.buildRequest = function(encryptedMessage, nonce, clientID, triggerUnlock = false) {
const request = {
message: encryptedMessage,
nonce: nonce,
clientID: clientID,
requestID: keepassClient.getRequestId()
};
if (triggerUnlock) {
request.triggerUnlock = true;
}
return request;
};
protocolClient.handleResponse = function(response, incrementedNonce, tab) {
if (response.message && protocolClient.verifyNonce(response, incrementedNonce)) {
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);
return parsed;
} else if (response.error && response.errorCode) {
keepass.handleError(tab, response.errorCode, response.error);
}
return undefined;
};
protocolClient.verifyNonce = function(response, nonce) {
if (!response.nonce) {
logError('No nonce in reponse');
return false;
}
if (!keepassClient.checkNonceLength(response.nonce)) {
logError('Incorrect nonce length');
return false;
}
if (response.nonce !== nonce) {
logError('Nonce compare failed');
return false;
}
return true;
};
//--------------------------------------------------------------------------
// Utils
//--------------------------------------------------------------------------
protocolClient.getNonce = function() {
return nacl.util.encodeBase64(nacl.randomBytes(protocolClient.keySize));
};
// Creates a random 8 character string for Request ID
protocolClient.getRequestId = function() {
return Math.random().toString(16).substring(2, 10);
};
protocolClient.incrementedNonce = function(nonce) {
const oldNonce = nacl.util.decodeBase64(nonce);
const newNonce = oldNonce.slice(0);
// from libsodium/utils.c
let i = 0;
let c = 1;
for (; i < newNonce.length; ++i) {
c += newNonce[i];
newNonce[i] = c;
c >>= 8;
}
return nacl.util.encodeBase64(newNonce);
};
protocolClient.getNonces = function() {
const nonce = protocolClient.getNonce();
const incrementedNonce = protocolClient.incrementedNonce(nonce);
return [ nonce, incrementedNonce ];
};
protocolClient.checkNonceLength = function(nonce) {
return nacl.util.decodeBase64(nonce).length === nacl.secretbox.nonceLength;
};
protocolClient.generateNewKeyPair = function() {
keepass.keyPair = nacl.box.keyPair();
};
//--------------------------------------------------------------------------
// Encrypt/Decrypt
//--------------------------------------------------------------------------
protocolClient.encrypt = function(input, nonce) {
const messageData = nacl.util.decodeUTF8(JSON.stringify(input));
const messageNonce = nacl.util.decodeBase64(nonce);
if (keepass.serverPublicKey) {
const message = nacl.box(messageData, messageNonce, keepass.serverPublicKey, keepass.keyPair.secretKey);
if (message) {
return nacl.util.encodeBase64(message);
}
}
return '';
};
protocolClient.decrypt = function(input, nonce) {
const m = nacl.util.decodeBase64(input);
const n = nacl.util.decodeBase64(nonce);
const res = nacl.box.open(m, n, keepass.serverPublicKey, keepass.keyPair.secretKey);
return res;
};
//--------------------------------------------------------------------------
// Native Messaging related
//--------------------------------------------------------------------------
protocolClient.connectToNative = function() {
if (protocolClient.nativePort) {
protocolClient.nativePort.disconnect();
}
protocolClient.nativeConnect();
};
protocolClient.nativeConnect = function() {
console.log(`${EXTENSION_NAME}: Connecting to native messaging host ${protocolClient.nativeHostName}`);
protocolClient.nativePort = browser.runtime.connectNative(protocolClient.nativeHostName);
protocolClient.nativePort.onMessage.addListener(protocolClient.onNativeMessage);
protocolClient.nativePort.onDisconnect.addListener(onDisconnected);
keepass.isConnected = true;
return protocolClient.nativePort;
};
function onDisconnected() {
protocolClient.nativePort = null;
keepass.isConnected = false;
keepass.isDatabaseClosed = true;
keepass.isKeePassXCAvailable = false;
keepass.associated.value = false;
keepass.associated.hash = null;
keepass.databaseHash = '';
page.clearAllLogins();
keepass.updatePopup('cross');
keepass.updateDatabaseHashToContent();
logError(`Failed to connect: ${(browser.runtime.lastError === null ? 'Unknown error' : browser.runtime.lastError.message)}`);
}
protocolClient.onNativeMessage = function(response) {
// Handle database lock/unlock status
if (response.action === kpActions.DATABASE_LOCKED || response.action === kpActions.DATABASE_UNLOCKED) {
keepass.updateDatabase();
}
};

View file

@ -140,7 +140,7 @@ kpxcBanner.create = async function(credentials = {}) {
kpxcBanner.saveNewCredentials = async function(credentials = {}) {
const saveToDefaultGroup = async function(creds) {
const args = [ creds.username, creds.password, creds.url ];
const res = await sendMessage('add_credentials', args);
const res = await sendMessage('create_credentials', args);
kpxcBanner.verifyResult(res);
};
@ -170,7 +170,7 @@ kpxcBanner.saveNewCredentials = async function(credentials = {}) {
// Create a new group
const newGroup = await sendMessage('create_new_group', [ result.defaultGroup ]);
if (newGroup.name && newGroup.uuid) {
const res = await sendMessage('add_credentials', [ credentials.username, credentials.password, credentials.url, newGroup.name, newGroup.uuid ]);
const res = await sendMessage('create_credentials', [ credentials.username, credentials.password, credentials.url, newGroup.name, newGroup.uuid ]);
kpxcBanner.verifyResult(res);
} else {
kpxcUI.createNotification('error', tr('rememberErrorCreatingNewGroup'));
@ -180,7 +180,7 @@ kpxcBanner.saveNewCredentials = async function(credentials = {}) {
}
}
const res = await sendMessage('add_credentials', [ credentials.username, credentials.password, credentials.url, gname, guuid ]);
const res = await sendMessage('create_credentials', [ credentials.username, credentials.password, credentials.url, gname, guuid ]);
kpxcBanner.verifyResult(res);
return;
}
@ -212,7 +212,7 @@ kpxcBanner.saveNewCredentials = async function(credentials = {}) {
return;
}
const res = await sendMessage('add_credentials', [ credentials.username, credentials.password, credentials.url, group, groupUuid ]);
const res = await sendMessage('create_credentials', [ credentials.username, credentials.password, credentials.url, group, groupUuid ]);
kpxcBanner.verifyResult(res);
});

View file

@ -102,12 +102,23 @@ kpxc.detectDatabaseChange = async function(response) {
kpxc.clearAllFromPage();
kpxcIcons.switchIcons();
// TODO: This doesn't work well anymore.
if (document.visibilityState !== 'hidden') {
if (response.hash.new !== '') {
_called.retrieveCredentials = false;
// Why is this needed? For the Connection Keys?
const settings = await sendMessage('load_settings');
kpxc.settings = settings;
kpxc.databaseState = DatabaseState.UNLOCKED;
// TODO: Cleanup this..
if (response.associateResult) {
if (!response.associateResult.areAllLocked) {
kpxc.databaseState = DatabaseState.UNLOCKED;
}
} else {
kpxc.databaseState = DatabaseState.UNLOCKED; // This is important to set correctly!
}
await kpxc.initCredentialFields();
kpxcIcons.switchIcons();
@ -115,7 +126,7 @@ kpxc.detectDatabaseChange = async function(response) {
// If user has requested a manual fill through context menu the actual credential filling
// is handled here when the opened database has been regognized. It's not a pretty hack.
const manualFill = await sendMessage('page_get_manual_fill');
if (manualFill !== ManualFill.NONE && kpxc.combinations.length > 0) {
if (manualFill !== ManualFill.NONE) {
await kpxcFill.fillInFromActiveElement(manualFill === ManualFill.PASSWORD);
await sendMessage('page_set_manual_fill', ManualFill.NONE);
}

View file

@ -134,7 +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?
// TODO: Replace with open-database or get-database-statuses? With legacyProtocol keepass.getDatabaseHash must be used.
await sendMessage('get_database_hash', [ false, true ]); // Set triggerUnlock to true
field.focus();
}

View file

@ -37,14 +37,16 @@
"common/sites.js",
"background/nacl.min.js",
"background/nacl-util.min.js",
"background/client.js",
"background/keepass.js",
"background/httpauth.js",
"background/browserAction.js",
"background/page.js",
"background/event.js",
"background/init.js",
"background/protocol.js"
"background/protocol.js",
"background/protocolClient.js",
"background/legacyProtocol.js",
"background/legacyProtocolClient.js"
]
},
"content_scripts": [

View file

@ -10,7 +10,7 @@ HTMLElement.prototype.hide = function() {
this.style.display = 'none';
};
function statusResponse(r) {
function statusResponse(status) {
$('#initial-state').hide();
$('#error-encountered').hide();
$('#need-reconfigure').hide();
@ -20,39 +20,70 @@ function statusResponse(r) {
$('#lock-database-button').hide();
$('#getting-started-guide').hide();
if (!r.keePassXCAvailable) {
$('#error-message').textContent = r.error;
if (!status.keePassXCAvailable) {
$('#error-message').textContent = status.error;
$('#error-encountered').show();
if (r.showGettingStartedGuideAlert) {
if (status.showGettingStartedGuideAlert) {
$('#getting-started-guide').show();
}
if (r.showTroubleshootingGuideAlert && reloadCount >= 2) {
if (status.showTroubleshootingGuideAlert && reloadCount >= 2) {
$('#troubleshooting-guide').show();
} else {
$('#troubleshooting-guide').hide();
}
} else if (r.keePassXCAvailable && r.databaseClosed) {
$('#database-error-message').textContent = r.error;
return;
}
// Only supported with Protocol V2
if (status.databaseAssociationStatuses) {
// This can be also shown when isAnyAssociated is true?
if (status.databaseAssociationStatuses.associationNeeded) {
$('#not-configured').show();
}
if (status.keePassXCAvailable && status.databaseAssociationStatuses.areAllLocked) {
$('#database-error-message').textContent = status.error;
$('#database-not-opened').show();
}
if (status.databaseAssociationStatuses.isAnyAssociated) {
$('#configured-and-associated').show();
$('#associated-identifier').textContent = status.identifier;
$('#lock-database-button').show();
if (status.usernameFieldDetected) {
$('#username-field-detected').show();
}
reloadCount = 0;
}
return;
}
if (status.keePassXCAvailable && status.databaseClosed) {
$('#database-error-message').textContent = status.error;
$('#database-not-opened').show();
} else if (!r.configured) {
} else if (!status.configured) {
$('#not-configured').show();
} else if (r.encryptionKeyUnrecognized) {
} else if (status.encryptionKeyUnrecognized) {
$('#need-reconfigure').show();
$('#need-reconfigure-message').textContent = r.error;
} else if (!r.associated) {
$('#need-reconfigure-message').textContent = status.error;
} else if (!status.associated) {
$('#need-reconfigure').show();
$('#need-reconfigure-message').textContent = r.error;
} else if (r.error) {
$('#need-reconfigure-message').textContent = status.error;
} else if (status.error !== null) {
$('#error-encountered').show();
$('#error-message').textContent = r.error;
$('#error-message').textContent = status.error;
} else {
$('#configured-and-associated').show();
$('#associated-identifier').textContent = r.identifier;
$('#associated-identifier').textContent = status.identifier;
$('#lock-database-button').show();
if (r.usernameFieldDetected) {
if (status.usernameFieldDetected) {
$('#username-field-detected').show();
}