New draft

This commit is contained in:
varjolintu 2025-07-17 11:57:51 +03:00
parent a6cbf580b9
commit db8cd1b1f0
28 changed files with 2252 additions and 1168 deletions

View file

@ -88,6 +88,7 @@
"assertSearchField": "readonly", "assertSearchField": "readonly",
"assertSearchForm": "readonly", "assertSearchForm": "readonly",
"assertTOTPField": "readonly", "assertTOTPField": "readonly",
"AddCredentials": "readonly",
"AssociatedAction": "readonly", "AssociatedAction": "readonly",
"AuthenticatorAssertionResponse": "readonly", "AuthenticatorAssertionResponse": "readonly",
"AuthenticatorAttestationResponse": "readonly", "AuthenticatorAttestationResponse": "readonly",
@ -126,6 +127,7 @@
"isFirefox": "readonly", "isFirefox": "readonly",
"keepass": "readonly", "keepass": "readonly",
"keepassClient": "readonly", "keepassClient": "readonly",
"keepassProtocol": "readonly",
"kpActions": "readonly", "kpActions": "readonly",
"kpErrors": "readonly", "kpErrors": "readonly",
"kpxc": "readonly", "kpxc": "readonly",
@ -163,6 +165,8 @@
"nacl": "readonly", "nacl": "readonly",
"ORANGE_BUTTON": "readonly", "ORANGE_BUTTON": "readonly",
"page": "readonly", "page": "readonly",
"protocol": "readonly",
"protocolClient": "readonly",
"Pixels": "readonly", "Pixels": "readonly",
"PublicKeyCredential": "readonly", "PublicKeyCredential": "readonly",
"PREDEFINED_SITELIST": "readonly", "PREDEFINED_SITELIST": "readonly",

View file

@ -1,5 +1,5 @@
# This repository does not use prettier for formatting but if you want to use it for testing then you can comment out the following line: # This repository does not use prettier for formatting but if you want to use it for testing then you can comment out the following line:
* #*
# Should never format these files: # Should never format these files:
*.min.js *.min.js

View file

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

View file

@ -159,6 +159,10 @@
"message": "No logins found.", "message": "No logins found.",
"description": "No logins found." "description": "No logins found."
}, },
"errorActionTimeout": {
"message": "Action timeout.",
"description": "Action timeout."
},
"errorMessagePaswordLengthExceeded": { "errorMessagePaswordLengthExceeded": {
"message": "Filled password is longer than field's allowed max length.", "message": "Filled password is longer than field's allowed max length.",
"description": "Error notification text shown when filled password is longer than defined maxLength of the input field." "description": "Error notification text shown when filled password is longer than defined maxLength of the input field."
@ -1393,6 +1397,14 @@
"lockDatabase": { "lockDatabase": {
"message": "Lock database", "message": "Lock database",
"description": "Lock database button title text." "description": "Lock database button title text."
},
"lockAllDatabases": {
"message": "Lock all databases",
"description": "Lock all databases button title text."
},
"noAction": {
"message": "No action",
"description": "No action text."
}, },
"welcomeText": { "welcomeText": {
"message": "Welcome to KeePassXC-Browser!", "message": "Welcome to KeePassXC-Browser!",

View file

@ -7,14 +7,17 @@ try {
'../common/sites.js', '../common/sites.js',
'nacl.min.js', 'nacl.min.js',
'nacl-util.min.js', 'nacl-util.min.js',
'client.js',
'keepass.js', 'keepass.js',
'httpauth.js', 'httpauth.js',
'offscreen.js', 'offscreen.js',
'browserAction.js', 'browserAction.js',
'page.js', 'page.js',
'event.js', 'event.js',
'init.js' 'init.js',
'protocol.js',
'protocolClient.js',
'legacyProtocol.js',
'legacyProtocolClient.js'
); );
} catch (e) { } catch (e) {
console.log('Cannot import background scripts: ', e); console.log('Cannot import background scripts: ', e);

View file

@ -48,7 +48,7 @@ browserAction.showDefault = async function(tab) {
// Get the current tab if no tab given // Get the current tab if no tab given
tab ??= await getCurrentTab(); tab ??= await getCurrentTab();
if (!tab) { if (!tab || tab.id < 0) {
return; return;
} }

View file

@ -1,415 +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,
NO_GROUPS_FOUND: 16,
CANNOT_CREATE_NEW_GROUP: 17,
NO_VALID_UUID_PROVIDED: 18,
ACCESS_TO_ALL_ENTRIES_DENIED: 19,
PASSKEYS_ATTESTATION_NOT_SUPPORTED: 20,
PASSKEYS_CREDENTIAL_IS_EXCLUDED: 21,
PASSKEYS_REQUEST_CANCELED: 22,
PASSKEYS_INVALID_USER_VERIFICATION: 23,
PASSKEYS_EMPTY_PUBLIC_KEY: 24,
PASSKEYS_INVALID_URL_PROVIDED: 25,
PASSKEYS_ORIGIN_NOT_ALLOWED: 26,
PASSKEYS_DOMAIN_IS_NOT_VALID: 27,
PASSKEYS_DOMAIN_RPID_MISMATCH: 28,
PASSKEYS_NO_SUPPORTED_ALGORITHMS: 29,
PASSKEYS_WAIT_FOR_LIFETIMER: 30,
PASSKEYS_UNKNOWN_ERROR: 31,
PASSKEYS_INVALID_CHALLENGE: 32,
PASSKEYS_INVALID_USER_ID: 33,
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') },
16: { msg: tr('errorMessageNoGroupsFound') },
17: { msg: tr('errorMessageCannotCreateNewGroup') },
18: { msg: tr('errorMessageNoValidUuidProvided') },
19: { msg: tr('errorMessageAccessToAllEntriesDenied') },
20: { msg: tr('errorMessagePasskeysAttestationNotSupported') },
21: { msg: tr('errorMessagePasskeysCredentialIsExcluded') },
22: { msg: tr('errorMessagePasskeysRequestCanceled') },
23: { msg: tr('errorMessagePasskeysInvalidUserVerification') },
24: { msg: tr('errorMessagePasskeysEmptyPublicKey') },
25: { msg: tr('errorMessagePasskeysInvalidUrlProvided') },
26: { msg: tr('errorMessagePasskeysOriginNotAllowed') },
27: { msg: tr('errorMessagePasskeysDomainNotValid') },
28: { msg: tr('errorMessagePasskeysDomainRpIdMismatch') },
29: { msg: tr('errorMessagePasskeysNoSupportedAlgorithms') },
30: { msg: tr('errorMessagePasskeysWaitforLifeTimer') },
31: { msg: tr('errorMessagePasskeysUnknownError') },
32: { msg: tr('errorMessagePasskeysInvalidChallenge') },
33: { msg: tr('errorMessagePasskeysInvalidUserId') },
},
getError(errorCode) {
return this.errorMessages[errorCode].msg;
}
};
const messageBuffer = {
buffer: [],
addMessage(message) {
this.buffer.push(message);
},
// Returns corresponding message from the response. If the response is an error,
// return the first matching action from the buffer.
getMessage(response) {
const isError = Boolean(!response.nonce && response.error && response.errorCode);
return this.buffer.find(message => {
if (keepassClient.incrementedNonce(message.request.nonce) === response.nonce
|| (isError && message.request?.action === response?.action)) {
// Cancel timeout
if (message.enableTimeout) {
message.cancelTimeout();
}
return message;
}
});
},
removeMessage(message) {
const index = this.buffer.indexOf(message);
if (index >= 0 && index < this.buffer.length) {
this.buffer.splice(index, 1);
}
},
};
// Basic class for a message to be sent. The Promise inside the class will be resolved when
// the response to the message is received.
class Message {
constructor(request, enableTimeout, timeoutValue) {
this.enableTimeout = enableTimeout;
this.request = request;
this.timeout = undefined;
this.promise = new Promise((resolve, reject) => {
this.reject = reject;
this.resolve = resolve;
const messageTimeout = timeoutValue || keepassClient.messageTimeout;
// Handle timeout
if (this.enableTimeout) {
this.timeout = setTimeout(() => {
const errorMessage = {
action: request.action,
error: kpErrors.getError(kpErrors.TIMEOUT_OR_NOT_CONNECTED),
errorCode: kpErrors.TIMEOUT_OR_NOT_CONNECTED
};
keepass.isKeePassXCAvailable = false;
resolve(errorMessage);
}, messageTimeout);
}
});
}
cancelTimeout() {
this.enableTimeout = false;
clearTimeout(this.timeout);
}
}
//--------------------------------------------------------------------------
// Messaging
//--------------------------------------------------------------------------
keepassClient.sendNativeMessage = async function(request, enableTimeout = false, timeoutValue) {
if (!keepassClient.nativePort) {
logError('No native messaging port defined.');
return;
}
const message = new Message(request, enableTimeout, timeoutValue);
await navigator.locks.request('messageBuffer', async (lock) => {
messageBuffer.addMessage(message);
});
keepassClient.nativePort.postMessage(request);
const response = await message.promise;
// Remove a timeouted message
if (response.error && response?.errorCode === kpErrors.TIMEOUT_OR_NOT_CONNECTED) {
messageBuffer.removeMessage(message);
}
return response;
};
keepassClient.handleNativeMessage = async function(response) {
// Parse through the message buffer to find the corresponding Promise.
await navigator.locks.request('messageBuffer', async (lock) => {
const message = messageBuffer.getMessage(response);
if (message) {
message.resolve(response);
messageBuffer.removeMessage(message);
return;
}
debugLogMessage('Corresponding request not found in the message buffer for response: ', response);
});
};
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);
};
//--------------------------------------------------------------------------
// 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();
return;
}
// Generic response handling
keepassClient.handleNativeMessage(response);
};

View file

@ -30,34 +30,40 @@ kpxcEvent.showStatus = async function(tab, configured, internalPoll) {
return { return {
associated: keepass.isAssociated(), associated: keepass.isAssociated(),
configured: configured, configured: configured,
databaseClosed: keepass.isDatabaseClosed, databaseClosed: keepass.isDatabaseClosed,
databaseAssociationStatuses: keepass.databaseAssociationStatuses,
encryptionKeyUnrecognized: keepass.isEncryptionKeyUnrecognized, encryptionKeyUnrecognized: keepass.isEncryptionKeyUnrecognized,
error: errorMessage, error: errorMessage,
iframeDetected: iframeDetected, iframeDetected: iframeDetected,
identifier: keyId, identifier: keyId,
keePassXCAvailable: keepass.isKeePassXCAvailable, keePassXCAvailable: keepass.isKeePassXCAvailable,
protocolV2: keepass.protocolV2,
showGettingStartedGuideAlert: page.settings.showGettingStartedGuideAlert, showGettingStartedGuideAlert: page.settings.showGettingStartedGuideAlert,
showTroubleshootingGuideAlert: page.settings.showTroubleshootingGuideAlert, showTroubleshootingGuideAlert: page.settings.showTroubleshootingGuideAlert,
usernameFieldDetected: usernameFieldDetected usernameFieldDetected: usernameFieldDetected
}; };
}; };
kpxcEvent.onLoadSettings = async function() { kpxcEvent.loadSettings = async function() {
return await page.initSettings().catch((err) => { return await page.initSettings().catch((err) => {
logError('onLoadSettings error: ' + err); logError('loadSettings error: ' + err);
return Promise.reject(); return Promise.reject();
}); });
}; };
kpxcEvent.onLoadKeyRing = async function() { kpxcEvent.isProtocolV2 = async function(tab) {
return keepass.protocolV2;
};
kpxcEvent.loadKeyRing = async function() {
const item = await browser.storage.local.get({ 'keyRing': {} }).catch((err) => { const item = await browser.storage.local.get({ 'keyRing': {} }).catch((err) => {
logError('kpxcEvent.onLoadKeyRing error: ' + err); logError('kpxcEvent.loadKeyRing error: ' + err);
return Promise.reject(); return Promise.reject();
}); });
keepass.keyRing = item.keyRing; keepass.keyRing = item.keyRing;
// TODO: What to do here?
if (keepass.isAssociated() && !keepass.keyRing[keepass.associated.hash]) { if (keepass.isAssociated() && !keepass.keyRing[keepass.associated.hash]) {
keepass.associated = { keepass.associated = {
value: false, value: false,
@ -68,23 +74,33 @@ kpxcEvent.onLoadKeyRing = async function() {
return item.keyRing; return item.keyRing;
}; };
kpxcEvent.onSaveSettings = async function(tab, settings) { kpxcEvent.saveSettings = async function(tab, settings) {
browser.storage.local.set({ 'settings': settings }); browser.storage.local.set({ 'settings': settings });
kpxcEvent.onLoadSettings(tab); kpxcEvent.loadSettings(tab);
}; };
kpxcEvent.onGetStatus = async function(tab, args = []) { kpxcEvent.getStatus = async function(tab, args = []) {
// When internalPoll is true the event is triggered from content script in intervals -> don't poll KeePassXC // When internalPoll is true the event is triggered from content script in intervals -> don't poll KeePassXC
try { try {
const [ internalPoll = false, triggerUnlock = false ] = args; const [ internalPoll = false, triggerUnlock = false ] = args;
let configured = false;
if (keepass.protocolV2) {
configured = internalPoll
? keepass.databaseAssociationStatuses?.isAnyAssociated
: await protocol.testAssociationFromDatabaseStatuses(tab, [ true, triggerUnlock ])?.isAnyAssociated;
return kpxcEvent.showStatus(tab, configured, internalPoll);
}
// Protocol V1
if (!internalPoll) { if (!internalPoll) {
const response = await keepass.testAssociation(tab, [ true, triggerUnlock ]); const response = await keepassProtocol.testAssociation(tab, [ true, triggerUnlock ]);
if (!response) { if (!response) {
return kpxcEvent.showStatus(tab, false); return kpxcEvent.showStatus(tab, false);
} }
} }
const configured = await keepass.isConfigured(); configured = await keepass.isConfigured();
return kpxcEvent.showStatus(tab, configured, internalPoll); return kpxcEvent.showStatus(tab, configured, internalPoll);
} catch (err) { } catch (err) {
logError('No status shown: ' + err); logError('No status shown: ' + err);
@ -92,7 +108,7 @@ kpxcEvent.onGetStatus = async function(tab, args = []) {
} }
}; };
kpxcEvent.onReconnect = async function(tab) { kpxcEvent.reconnect = async function(tab) {
const configured = await keepass.reconnect(tab); const configured = await keepass.reconnect(tab);
if (configured) { if (configured) {
browser.tabs.sendMessage(tab?.id, { browser.tabs.sendMessage(tab?.id, {
@ -116,19 +132,19 @@ kpxcEvent.lockDatabase = async function(tab) {
} }
}; };
kpxcEvent.onGetTabInformation = async function(tab) { kpxcEvent.getTabInformation = async function(tab) {
const id = tab?.id || page.currentTabId; const id = tab?.id || page.currentTabId;
return page.tabs[id]; return page.tabs[id];
}; };
kpxcEvent.onGetConnectedDatabase = async function() { kpxcEvent.getConnectedDatabase = async function() {
return Promise.resolve({ return Promise.resolve({
count: Object.keys(keepass.keyRing).length, count: Object.keys(keepass.keyRing).length,
identifier: (keepass.keyRing[keepass.associated.hash]) ? keepass.keyRing[keepass.associated.hash].id : null identifier: (keepass.keyRing[keepass.associated.hash]) ? keepass.keyRing[keepass.associated.hash].id : null
}); });
}; };
kpxcEvent.onGetKeePassXCVersions = async function(tab) { kpxcEvent.getKeePassXCVersions = async function(tab) {
if (keepass.currentKeePassXC === '') { if (keepass.currentKeePassXC === '') {
await keepass.getDatabaseHash(tab); await keepass.getDatabaseHash(tab);
return { 'current': keepass.currentKeePassXC, 'latest': keepass.latestKeePassXC.version }; return { 'current': keepass.currentKeePassXC, 'latest': keepass.latestKeePassXC.version };
@ -137,22 +153,22 @@ kpxcEvent.onGetKeePassXCVersions = async function(tab) {
return { 'current': keepass.currentKeePassXC, 'latest': keepass.latestKeePassXC.version }; return { 'current': keepass.currentKeePassXC, 'latest': keepass.latestKeePassXC.version };
}; };
kpxcEvent.onCheckUpdateKeePassXC = async function() { kpxcEvent.checkUpdateKeePassXC = async function() {
await keepass.checkForNewKeePassXCVersion(); await keepass.checkForNewKeePassXCVersion();
return { current: keepass.currentKeePassXC, latest: keepass.latestKeePassXC.version }; return { current: keepass.currentKeePassXC, latest: keepass.latestKeePassXC.version };
}; };
kpxcEvent.onUpdateAvailableKeePassXC = async function() { kpxcEvent.updateAvailableKeePassXC = async function() {
return (Number(page.settings.checkUpdateKeePassXC) !== CHECK_UPDATE_NEVER) ? await keepass.keePassXCUpdateAvailable() : false; return (Number(page.settings.checkUpdateKeePassXC) !== CHECK_UPDATE_NEVER) ? await keepass.keePassXCUpdateAvailable() : false;
}; };
kpxcEvent.onRemoveCredentialsFromTabInformation = async function(tab) { kpxcEvent.removeCredentialsFromTabInformation = async function(tab) {
const id = tab?.id || page.currentTabId; const id = tab?.id || page.currentTabId;
page.clearCredentials(id); page.clearCredentials(id);
page.clearSubmittedCredentials(); page.clearSubmittedCredentials();
}; };
kpxcEvent.onLoginPopup = async function(tab, logins) { kpxcEvent.initLoginPopup = async function(tab, logins) {
const popupData = { const popupData = {
iconType: 'normal', iconType: 'normal',
popup: 'popup_login' popup: 'popup_login'
@ -168,7 +184,7 @@ kpxcEvent.initHttpAuth = async function() {
httpAuth.init(); httpAuth.init();
}; };
kpxcEvent.onHTTPAuthPopup = async function(tab, data) { kpxcEvent.initHttpAuthPopup = async function(tab, data) {
const popupData = { const popupData = {
iconType: 'normal', iconType: 'normal',
popup: 'popup_httpauth' popup: 'popup_httpauth'
@ -178,7 +194,7 @@ kpxcEvent.onHTTPAuthPopup = async function(tab, data) {
await browserAction.show(tab, popupData); await browserAction.show(tab, popupData);
}; };
kpxcEvent.onUsernameFieldDetected = async function(tab, detected) { kpxcEvent.usernameFieldDetected = async function(tab, detected) {
if (tab?.id) { if (tab?.id) {
page.tabs[tab.id].usernameFieldDetected = detected; page.tabs[tab.id].usernameFieldDetected = detected;
} }
@ -221,17 +237,17 @@ kpxcEvent.getIsKeePassXCAvailable = async function() {
}; };
kpxcEvent.hideGettingStartedGuideAlert = async function(tab) { kpxcEvent.hideGettingStartedGuideAlert = async function(tab) {
const settings = await kpxcEvent.onLoadSettings(); const settings = await kpxcEvent.loadSettings();
settings.showGettingStartedGuideAlert = false; settings.showGettingStartedGuideAlert = false;
await kpxcEvent.onSaveSettings(tab, settings); await kpxcEvent.saveSettings(tab, settings);
}; };
kpxcEvent.hideTroubleshootingGuideAlert = async function(tab) { kpxcEvent.hideTroubleshootingGuideAlert = async function(tab) {
const settings = await kpxcEvent.onLoadSettings(); const settings = await kpxcEvent.loadSettings();
settings.showTroubleshootingGuideAlert = false; settings.showTroubleshootingGuideAlert = false;
await kpxcEvent.onSaveSettings(tab, settings); await kpxcEvent.saveSettings(tab, settings);
}; };
// Bounce message back to all frames // Bounce message back to all frames
@ -243,13 +259,13 @@ kpxcEvent.sendBackToTabs = async function(tab, args = []) {
// All methods named in this object have to be declared BEFORE this! // All methods named in this object have to be declared BEFORE this!
kpxcEvent.messageHandlers = { kpxcEvent.messageHandlers = {
'add_credentials': keepass.addCredentials,
'associate': keepass.associate, 'associate': keepass.associate,
'banner_get_position': page.getBannerPosition, 'banner_get_position': page.getBannerPosition,
'banner_set_position': page.setBannerPosition, 'banner_set_position': page.setBannerPosition,
'check_database_hash': keepass.checkDatabaseHash, 'check_database_hash': keepass.checkDatabaseHash,
'check_update_keepassxc': kpxcEvent.onCheckUpdateKeePassXC, 'check_update_keepassxc': kpxcEvent.checkUpdateKeePassXC,
'compare_versions': kpxcEvent.compareMultipleVersions, 'compare_versions': kpxcEvent.compareMultipleVersions,
'create_credentials': keepass.createCredentials,
'create_new_group': keepass.createNewGroup, 'create_new_group': keepass.createNewGroup,
'enable_automatic_reconnect': keepass.enableAutomaticReconnect, 'enable_automatic_reconnect': keepass.enableAutomaticReconnect,
'disable_automatic_reconnect': keepass.disableAutomaticReconnect, 'disable_automatic_reconnect': keepass.disableAutomaticReconnect,
@ -257,14 +273,14 @@ kpxcEvent.messageHandlers = {
'frame_message': kpxcEvent.sendBackToTabs, 'frame_message': kpxcEvent.sendBackToTabs,
'generate_password': keepass.generatePassword, 'generate_password': keepass.generatePassword,
'get_color_theme': kpxcEvent.getColorTheme, 'get_color_theme': kpxcEvent.getColorTheme,
'get_connected_database': kpxcEvent.onGetConnectedDatabase, 'get_connected_database': kpxcEvent.getConnectedDatabase,
'get_database_hash': keepass.getDatabaseHash, 'get_database_hash': keepass.getDatabaseHash, // TODO ?
'get_database_groups': keepass.getDatabaseGroups, 'get_database_groups': keepass.getDatabaseGroups,
'get_error_message': keepass.getErrorMessage, 'get_error_message': keepass.getErrorMessage,
'get_keepassxc_versions': kpxcEvent.onGetKeePassXCVersions, 'get_keepassxc_versions': kpxcEvent.getKeePassXCVersions,
'get_login_list': page.getLoginList, 'get_login_list': page.getLoginList,
'get_status': kpxcEvent.onGetStatus, 'get_status': kpxcEvent.getStatus,
'get_tab_information': kpxcEvent.onGetTabInformation, 'get_tab_information': kpxcEvent.getTabInformation,
'get_totp': keepass.getTotp, 'get_totp': keepass.getTotp,
'hide_getting_started_guide_alert': kpxcEvent.hideGettingStartedGuideAlert, 'hide_getting_started_guide_alert': kpxcEvent.hideGettingStartedGuideAlert,
'hide_troubleshooting_guide_alert': kpxcEvent.hideTroubleshootingGuideAlert, 'hide_troubleshooting_guide_alert': kpxcEvent.hideTroubleshootingGuideAlert,
@ -272,12 +288,15 @@ kpxcEvent.messageHandlers = {
'init_http_auth': kpxcEvent.initHttpAuth, 'init_http_auth': kpxcEvent.initHttpAuth,
'is_connected': kpxcEvent.getIsKeePassXCAvailable, 'is_connected': kpxcEvent.getIsKeePassXCAvailable,
'is_iframe_allowed': page.isIframeAllowed, 'is_iframe_allowed': page.isIframeAllowed,
'is_protocol_v2': kpxcEvent.isProtocolV2,
'is_site_ignored': page.isSiteIgnored, 'is_site_ignored': page.isSiteIgnored,
'load_keyring': kpxcEvent.onLoadKeyRing, 'load_keyring': kpxcEvent.loadKeyRing,
'load_settings': kpxcEvent.onLoadSettings, 'load_settings': kpxcEvent.loadSettings,
'lock_database': kpxcEvent.lockDatabase, 'lock_database': kpxcEvent.lockDatabase,
'page_clear_auto_lock_requested': page.clearAutoLockRequested,
'page_clear_logins': kpxcEvent.pageClearLogins, 'page_clear_logins': kpxcEvent.pageClearLogins,
'page_clear_submitted': page.clearSubmittedCredentials, 'page_clear_submitted': page.clearSubmittedCredentials,
'page_get_auto_lock_requested': page.getAutoLockRequested,
'page_get_autosubmit_performed': page.getAutoSubmitPerformed, 'page_get_autosubmit_performed': page.getAutoSubmitPerformed,
'page_get_login_id': page.getLoginId, 'page_get_login_id': page.getLoginId,
'page_get_manual_fill': page.getManualFill, 'page_get_manual_fill': page.getManualFill,
@ -292,16 +311,16 @@ kpxcEvent.messageHandlers = {
'passkeys_register': keepass.passkeysRegister, 'passkeys_register': keepass.passkeysRegister,
'password_get_filled': kpxcEvent.passwordGetFilled, 'password_get_filled': kpxcEvent.passwordGetFilled,
'password_set_filled': kpxcEvent.passwordSetFilled, 'password_set_filled': kpxcEvent.passwordSetFilled,
'popup_login': kpxcEvent.onLoginPopup, 'popup_login': kpxcEvent.initLoginPopup,
'reconnect': kpxcEvent.onReconnect, 'reconnect': kpxcEvent.reconnect,
'remove_credentials_from_tab_information': kpxcEvent.onRemoveCredentialsFromTabInformation, 'remove_credentials_from_tab_information': kpxcEvent.removeCredentialsFromTabInformation,
'request_autotype': keepass.requestAutotype, 'request_autotype': keepass.requestAutotype,
'retrieve_credentials': page.retrieveCredentials, 'retrieve_credentials': page.retrieveCredentials,
'show_default_browseraction': browserAction.showDefault, 'show_default_browseraction': browserAction.showDefault,
'update_credentials': keepass.updateCredentials, 'update_credentials': keepass.updateCredentials,
'username_field_detected': kpxcEvent.onUsernameFieldDetected, 'username_field_detected': kpxcEvent.usernameFieldDetected,
'save_settings': kpxcEvent.onSaveSettings, 'save_settings': kpxcEvent.saveSettings,
'update_available_keepassxc': kpxcEvent.onUpdateAvailableKeePassXC, 'update_available_keepassxc': kpxcEvent.updateAvailableKeePassXC,
'update_context_menu': page.updateContextMenu, 'update_context_menu': page.updateContextMenu,
'update_popup': page.updatePopup 'update_popup': page.updatePopup
}; };

View file

@ -47,9 +47,9 @@ httpAuth.handleRequestCallback = function(details, callback) {
httpAuth.processPendingCallbacks(details, callback, callback); httpAuth.processPendingCallbacks(details, callback, callback);
}; };
httpAuth.retrieveCredentials = async function(tabId, url, submitUrl) { httpAuth.getCredentials = async function(tabId, url, submitUrl) {
return await keepass.retrieveCredentials(tabId, [ url, submitUrl, false, true ]).catch((err) => { return await keepass.getCredentials(tabId, [ url, submitUrl, false, true ]).catch((err) => {
logError('httpAuth.retrieveCredentials error: ' + err); logError('httpAuth.getCredentials error: ' + err);
return Promise.reject(); return Promise.reject();
}); });
}; };
@ -72,7 +72,7 @@ httpAuth.processPendingCallbacks = async function(details, resolve, reject) {
details.searchUrl = (details.isProxy && details.proxyUrl) ? details.proxyUrl : details.url; details.searchUrl = (details.isProxy && details.proxyUrl) ? details.proxyUrl : details.url;
const logins = await httpAuth.retrieveCredentials({ 'id': details.tabId }, details.searchUrl, details.searchUrl); const logins = await httpAuth.getCredentials({ 'id': details.tabId }, details.searchUrl, details.searchUrl);
httpAuth.loginOrShowCredentials(logins, details, resolve, reject); httpAuth.loginOrShowCredentials(logins, details, resolve, reject);
}; };
@ -90,7 +90,10 @@ httpAuth.loginOrShowCredentials = function(logins, details, resolve, reject) {
if (page.settings.showNotifications) { if (page.settings.showNotifications) {
showNotification(tr('multipleCredentialsDetected')); showNotification(tr('multipleCredentialsDetected'));
} }
kpxcEvent.onHTTPAuthPopup({ 'id': details.tabId }, { 'logins': logins, 'url': details.searchUrl, 'resolve': resolve }); kpxcEvent.initHttpAuthPopup(
{ id: details.tabId },
{ logins: logins, url: details.searchUrl, resolve: resolve },
);
} }
} else { } else {
logError('No logins found for HTTP Basic Auth.'); logError('No logins found for HTTP Basic Auth.');

View file

@ -1,21 +1,24 @@
'use strict'; 'use strict';
const keepass = {}; const keepass = {};
keepass.associated = { 'value': false, 'hash': null }; keepass.associated = { value: false, hash: null };
keepass.keyPair = { publicKey: null, secretKey: null }; keepass.cacheTimeout = 30 * 1000; // Milliseconds
keepass.serverPublicKey = '';
keepass.clientID = ''; keepass.clientID = '';
keepass.currentKeePassXC = '';
keepass.databaseAssociationStatuses = {};
keepass.databaseHash = ''; // Hash of the active database
keepass.databaseStatuses = [];
keepass.isConnected = false; keepass.isConnected = false;
keepass.isDatabaseClosed = true; keepass.isDatabaseClosed = true;
keepass.isKeePassXCAvailable = false;
keepass.isEncryptionKeyUnrecognized = false; keepass.isEncryptionKeyUnrecognized = false;
keepass.currentKeePassXC = ''; keepass.isKeePassXCAvailable = false;
keepass.requiredKeePassXC = '2.3.1'; keepass.keyPair = { publicKey: null, secretKey: null };
keepass.latestVersionUrl = 'https://api.github.com/repos/keepassxreboot/keepassxc/releases/latest'; keepass.latestVersionUrl = 'https://api.github.com/repos/keepassxreboot/keepassxc/releases/latest';
keepass.cacheTimeout = 30 * 1000; // Milliseconds
keepass.databaseHash = '';
keepass.previousDatabaseHash = ''; keepass.previousDatabaseHash = '';
keepass.protocolV2 = false;
keepass.reconnectLoop = null; keepass.reconnectLoop = null;
keepass.requiredKeePassXC = '2.3.1';
keepass.serverPublicKey = '';
const kpActions = { const kpActions = {
SET_LOGIN: 'set-login', SET_LOGIN: 'set-login',
@ -33,7 +36,91 @@ const kpActions = {
GET_TOTP: 'get-totp', GET_TOTP: 'get-totp',
REQUEST_AUTOTYPE: 'request-autotype', REQUEST_AUTOTYPE: 'request-autotype',
PASSKEYS_REGISTER: 'passkeys-register', PASSKEYS_REGISTER: 'passkeys-register',
PASSKEYS_GET: 'passkeys-get' PASSKEYS_GET: 'passkeys-get',
// Protocol V2
CREATE_CREDENTIALS: 'create-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,
NO_GROUPS_FOUND: 16,
CANNOT_CREATE_NEW_GROUP: 17,
NO_VALID_UUID_PROVIDED: 18,
ACCESS_TO_ALL_ENTRIES_DENIED: 19,
PASSKEYS_ATTESTATION_NOT_SUPPORTED: 20,
PASSKEYS_CREDENTIAL_IS_EXCLUDED: 21,
PASSKEYS_REQUEST_CANCELED: 22,
PASSKEYS_INVALID_USER_VERIFICATION: 23,
PASSKEYS_EMPTY_PUBLIC_KEY: 24,
PASSKEYS_INVALID_URL_PROVIDED: 25,
PASSKEYS_ORIGIN_NOT_ALLOWED: 26,
PASSKEYS_DOMAIN_IS_NOT_VALID: 27,
PASSKEYS_DOMAIN_RPID_MISMATCH: 28,
PASSKEYS_NO_SUPPORTED_ALGORITHMS: 29,
PASSKEYS_WAIT_FOR_LIFETIMER: 30,
PASSKEYS_UNKNOWN_ERROR: 31,
PASSKEYS_INVALID_CHALLENGE: 32,
PASSKEYS_INVALID_USER_ID: 33,
ACTION_TIMEOUT: 34,
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') },
16: { msg: tr('errorMessageNoGroupsFound') },
17: { msg: tr('errorMessageCannotCreateNewGroup') },
18: { msg: tr('errorMessageNoValidUuidProvided') },
19: { msg: tr('errorMessageAccessToAllEntriesDenied') },
20: { msg: tr('errorMessagePasskeysAttestationNotSupported') },
21: { msg: tr('errorMessagePasskeysCredentialIsExcluded') },
22: { msg: tr('errorMessagePasskeysRequestCanceled') },
23: { msg: tr('errorMessagePasskeysInvalidUserVerification') },
24: { msg: tr('errorMessagePasskeysEmptyPublicKey') },
25: { msg: tr('errorMessagePasskeysInvalidUrlProvided') },
26: { msg: tr('errorMessagePasskeysOriginNotAllowed') },
27: { msg: tr('errorMessagePasskeysDomainNotValid') },
28: { msg: tr('errorMessagePasskeysDomainRpIdMismatch') },
29: { msg: tr('errorMessagePasskeysNoSupportedAlgorithms') },
30: { msg: tr('errorMessagePasskeysWaitforLifeTimer') },
31: { msg: tr('errorMessagePasskeysUnknownError') },
32: { msg: tr('errorMessagePasskeysInvalidChallenge') },
33: { msg: tr('errorMessagePasskeysInvalidUserId') },
34: { msg: tr('errorActionTimeout') },
},
getError(errorCode) {
return this.errorMessages[errorCode].msg;
}
}; };
browser.storage.local.get({ 'latestKeePassXC': { 'version': '', 'lastChecked': null }, 'keyRing': {} }).then((item) => { browser.storage.local.get({ 'latestKeePassXC': { 'version': '', 'lastChecked': null }, 'keyRing': {} }).then((item) => {
@ -42,626 +129,75 @@ browser.storage.local.get({ 'latestKeePassXC': { 'version': '', 'lastChecked': n
}); });
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// Commands // Command wrappers for events
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
keepass.addCredentials = async function(tab, args = []) { keepass.associate = async function(tab, args = []) {
const [ username, password, url, group, groupUuid ] = args; return keepass.protocolV2 ? await protocol.associate(tab, args) : await keepassProtocol.associate(tab, args);
return keepass.updateCredentials(tab, [ null, username, password, url, group, groupUuid ]);
}; };
keepass.updateCredentials = async function(tab, args = []) { keepass.createCredentials = async function(tab, args = []) {
try { return keepass.protocolV2
const [ entryId, username, password, url, group, groupUuid ] = args; ? await protocol.createCredentials(tab, args)
const taResponse = await keepass.testAssociation(tab); : await keepassProtocol.addCredentials(tab, args);
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 kpAction = kpActions.GET_LOGINS;
const nonce = keepassClient.getNonce();
const [ dbid ] = keepass.getCryptoKey();
const messageData = {
action: kpAction,
id: dbid,
url: url,
keys: keepass.getCryptoKeys()
};
if (submiturl) {
messageData.submitUrl = submiturl;
}
if (httpAuth) {
messageData.httpAuth = 'true';
}
const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce);
if (response) {
entries = 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 undefined;
}
try {
const taResponse = await keepass.testAssociation(tab);
if (!taResponse) {
browserAction.showDefault(tab);
return '';
}
if (!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 undefined;
}
};
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 = 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.showDefault(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.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 === '') || response.errorCode === kpErrors.TIMEOUT_OR_NOT_CONNECTED) {
keepass.isKeePassXCAvailable = false;
keepass.isConnected = 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.createNewGroup = async function(tab, args = []) { keepass.createNewGroup = async function(tab, args = []) {
try { return keepass.protocolV2
const [ groupName ] = args; ? await protocol.createNewGroup(tab, args)
const taResponse = await keepass.testAssociation(tab, [ false ]); : await keepassProtocol.createNewGroup(tab, args);
if (!taResponse) { };
browserAction.showDefault(tab);
return [];
}
keepass.clearErrorMessage(tab); keepass.generatePassword = async function(tab, args = []) {
return keepass.protocolV2
? await protocol.generatePassword(tab, args)
: await keepassProtocol.generatePassword(tab, args);
};
if (!keepass.isConnected) { keepass.getCredentials = async function(tab, args = []) {
return []; return keepass.protocolV2
} ? await protocol.getCredentials(tab, args)
: await keepassProtocol.retrieveCredentials(tab, args);
};
const kpAction = kpActions.CREATE_NEW_GROUP; keepass.getDatabaseGroups = async function(tab, args = []) {
const nonce = keepassClient.getNonce(); return keepass.protocolV2
? await protocol.getDatabaseGroups(tab, args)
: await keepassProtocol.getDatabaseGroups(tab, args);
};
const messageData = { keepass.getDatabaseHash = async function(tab, args = []) {
action: kpAction, return keepass.protocolV2
groupName: groupName ? await protocol.getDatabaseStatuses(tab, args)
}; : await keepassProtocol.getDatabaseHash(tab, args);
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.getTotp = async function(tab, args = []) { keepass.getTotp = async function(tab, args = []) {
const [ uuid, oldTotp ] = args; return keepass.protocolV2 ? await protocol.getTotp(tab, args) : await keepassProtocol.getTotp(tab, args);
if (!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}`);
}
}; };
keepass.requestAutotype = async function(tab, args = []) { keepass.lockDatabase = async function(tab, args = []) {
if (!keepass.isConnected) { return keepass.protocolV2 ? await protocol.lockDatabase(tab, args) : await keepassProtocol.lockDatabase(tab, args);
keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED);
return false;
}
const kpAction = kpActions.REQUEST_AUTOTYPE;
const nonce = keepassClient.getNonce();
const search = await page.getBaseDomainFromUrl(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.passkeysRegister = async function(tab, args = []) {
try {
const taResponse = await keepass.testAssociation(tab, [ false ]);
if (!taResponse || !keepass.isConnected || args.length < 2) {
browserAction.showDefault(tab);
return [];
}
const kpAction = kpActions.PASSKEYS_REGISTER;
const nonce = keepassClient.getNonce();
const [ publicKey, origin ] = args;
const messageData = {
action: kpAction,
publicKey: JSON.parse(JSON.stringify(publicKey)),
origin: origin,
groupName: page?.settings?.defaultPasskeyGroup,
keys: keepass.getCryptoKeys()
};
const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce);
if (response) {
return response;
}
browserAction.showDefault(tab);
return [];
} catch (err) {
logError(`passkeysRegister failed: ${err}`);
return [];
}
}; };
keepass.passkeysGet = async function(tab, args = []) { keepass.passkeysGet = async function(tab, args = []) {
try { return keepass.protocolV2 ? await protocol.passkeysGet(tab, args) : await keepassProtocol.passkeysGet(tab, args);
const taResponse = await keepass.testAssociation(tab, [ false ]); };
if (!taResponse || !keepass.isConnected || args.length < 2) {
browserAction.showDefault(tab);
return [];
}
const kpAction = kpActions.PASSKEYS_GET; keepass.passkeysRegister = async function(tab, args = []) {
const nonce = keepassClient.getNonce(); return keepass.protocolV2 ? await protocol.passkeysGet(tab, args) : await keepassProtocol.passkeysGet(tab, args);
const publicKey = args[0]; };
const origin = args[1];
const messageData = { keepass.requestAutotype = async function (tab, args = []) {
action: kpAction, return keepass.protocolV2
publicKey: JSON.parse(JSON.stringify(publicKey)), ? await protocol.requestAutotype(tab, args)
origin: origin, : await keepassProtocol.requestAutotype(tab, args);
keys: keepass.getCryptoKeys() };
};
const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce); keepass.updateCredentials = async function (tab, args = []) {
if (response) { return keepass.protocolV2
return response; ? await protocol.updateCredentials(tab, args)
} : await keepassProtocol.updateCredentials(tab, args);
browserAction.showDefault(tab);
return [];
} catch (err) {
logError(`passkeysGet failed: ${err}`);
return [];
}
}; };
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
@ -805,26 +341,55 @@ keepass.disableAutomaticReconnect = function() {
}; };
keepass.reconnect = async function(tab = null, connectionTimeout = 1500) { keepass.reconnect = async function(tab = null, connectionTimeout = 1500) {
keepassClient.connectToNative(); protocolClient.connectToNative();
keepass.generateNewKeyPair(); protocolClient.generateNewKeyPair();
const keyChangeResult = await keepass.changePublicKeys(tab, !!connectionTimeout, connectionTimeout).catch(() => false);
const keyChangeResult = await protocol
.changePublicKeys(tab, !!connectionTimeout, connectionTimeout)
.catch(() => false);
// Change public keys timeout // Change public keys timeout
if (!keyChangeResult) { if (!keyChangeResult) {
return false; return false;
} }
const hash = await keepass.getDatabaseHash(tab); if (!keepass.protocolV2) {
if (hash !== '') { const hash = await keepass.getDatabaseHash(tab);
keepass.clearErrorMessage(tab); if (hash !== '') {
keepass.clearErrorMessage(tab);
}
await keepassProtocol.testAssociation();
await keepass.isConfigured();
} }
await keepass.testAssociation(); // TODO: What to do with Protocol V2?
await keepass.isConfigured();
keepass.updateDatabaseHashToContent(); keepass.updateDatabaseHashToContent();
return true; return true;
}; };
//--------------------------------------------------------------------------
// Error handling
//--------------------------------------------------------------------------
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 // Utils
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
@ -863,7 +428,9 @@ keepass.setcurrentKeePassXCVersion = function(version) {
keepass.keePassXCUpdateAvailable = async function() { keepass.keePassXCUpdateAvailable = async function() {
const checkUpdate = Number(page.settings.checkUpdateKeePassXC); const checkUpdate = Number(page.settings.checkUpdateKeePassXC);
if (checkUpdate !== CHECK_UPDATE_NEVER) { if (checkUpdate !== CHECK_UPDATE_NEVER) {
const lastChecked = (keepass.latestKeePassXC.lastChecked) ? new Date(keepass.latestKeePassXC.lastChecked) : new Date(1986, 11, 21); const lastChecked = keepass.latestKeePassXC.lastChecked
? new Date(keepass.latestKeePassXC.lastChecked)
: new Date(1986, 11, 21);
const daysSinceLastCheck = Math.floor(((new Date()).getTime() - lastChecked.getTime()) / 86400000); const daysSinceLastCheck = Math.floor(((new Date()).getTime() - lastChecked.getTime()) / 86400000);
if (daysSinceLastCheck >= checkUpdate) { if (daysSinceLastCheck >= checkUpdate) {
await keepass.checkForNewKeePassXCVersion(); await keepass.checkForNewKeePassXCVersion();
@ -891,23 +458,6 @@ keepass.checkForNewKeePassXCVersion = async function() {
keepass.latestKeePassXC.lastChecked = new Date().valueOf(); 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() { keepass.updatePopup = function() {
if (page && page.tabs.length > 0) { if (page && page.tabs.length > 0) {
browserAction.showDefault(); browserAction.showDefault();
@ -915,14 +465,23 @@ keepass.updatePopup = function() {
}; };
// Updates the database hashes to content script // Updates the database hashes to content script
keepass.updateDatabase = async function() { keepass.updateDatabase = async function(tab) {
keepass.associated.value = false; keepass.associated.value = false;
keepass.associated.hash = null; keepass.associated.hash = null;
page.clearAllLogins(); 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(tab);
keepass.updateDatabaseHashToContent(result);
return;
}
keepass.updatePopup(); // Legacy protocol
await keepassProtocol.testAssociation(null, [ true ]);
keepass.updatePopup(tab);
keepass.updateDatabaseHashToContent(); keepass.updateDatabaseHashToContent();
}; };
@ -959,7 +518,7 @@ keepass.compareMultipleVersions = function(versions, current, canBeEqual = true)
return result; return result;
}; };
const removeDuplicateEntries = function(arr) { keepass.removeDuplicateEntries = function(arr) {
const newArray = []; const newArray = [];
for (const a of arr) { for (const a of arr) {

View file

@ -0,0 +1,588 @@
'use strict';
// Legacy protocol for KeePassXC 2.7.x 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 = 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.showDefault(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.generatePassword = async function(tab) {
if (!keepass.isConnected) {
return undefined;
}
try {
const taResponse = await keepassProtocol.testAssociation(tab);
if (!taResponse) {
browserAction.showDefault(tab);
return '';
}
if (!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 undefined;
}
};
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) {
protocol.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
};
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 === '') || response.errorCode === kpErrors.TIMEOUT_OR_NOT_CONNECTED) {
keepass.isKeePassXCAvailable = false;
keepass.isConnected = 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 (!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.passkeysGet = async function(tab, args = []) {
try {
const taResponse = await keepassProtocol.testAssociation(tab, [ false ]);
if (!taResponse || !keepass.isConnected || args.length < 2) {
browserAction.showDefault(tab);
return [];
}
const kpAction = kpActions.PASSKEYS_GET;
const nonce = protocolClient.getNonce();
const publicKey = args[0];
const origin = args[1];
const messageData = {
action: kpAction,
publicKey: JSON.parse(JSON.stringify(publicKey)),
origin: origin,
keys: keepass.getCryptoKeys()
};
const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce);
if (response) {
return response;
}
browserAction.showDefault(tab);
return [];
} catch (err) {
logError(`passkeysGet failed: ${err}`);
return [];
}
};
keepassProtocol.passkeysRegister = async function(tab, args = []) {
try {
const taResponse = await keepassProtocol.testAssociation(tab, [ false ]);
if (!taResponse || !keepass.isConnected || args.length < 2) {
browserAction.showDefault(tab);
return [];
}
const kpAction = kpActions.PASSKEYS_REGISTER;
const nonce = protocolClient.getNonce();
const [ publicKey, origin ] = args;
const messageData = {
action: kpAction,
publicKey: JSON.parse(JSON.stringify(publicKey)),
origin: origin,
groupName: page?.settings?.defaultPasskeyGroup,
keys: keepass.getCryptoKeys()
};
const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce);
if (response) {
return response;
}
browserAction.showDefault(tab);
return [];
} catch (err) {
logError(`passkeysRegister failed: ${err}`);
return [];
}
};
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 = await page.getBaseDomainFromUrl(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 kpAction = kpActions.GET_LOGINS;
const nonce = protocolClient.getNonce();
const [ dbid ] = keepass.getCryptoKey();
const messageData = {
action: kpAction,
id: dbid,
url: url,
keys: keepass.getCryptoKeys()
};
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.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 ? AddCredentials.UPDATED : AddCredentials.CREATED;
}
return successMessage;
} else {
return AddCredentials.ERROR;
}
} catch (err) {
logError(`updateCredentials failed: ${err}`);
return AddCredentials.ERROR;
}
};

View file

@ -0,0 +1,232 @@
'use strict';
const messageBuffer = {
buffer: [],
addMessage(message) {
this.buffer.push(message);
},
// Returns corresponding message from the response. If the response is an error,
// return the first matching action from the buffer.
getMessage(response) {
const isError = Boolean(!response.nonce && response.error && response.errorCode);
return this.buffer.find(message => {
if (protocolClient.incrementedNonce(message.request.nonce) === response.nonce
|| (isError && message.request?.action === response?.action)) {
// Cancel timeout
if (message.enableTimeout) {
message.cancelTimeout();
}
return message;
}
});
},
removeMessage(message) {
const index = this.buffer.indexOf(message);
if (index >= 0 && index < this.buffer.length) {
this.buffer.splice(index, 1);
}
},
};
// Basic class for a message to be sent. The Promise inside the class will be resolved when
// the response to the message is received.
class Message {
constructor(request, enableTimeout, timeoutValue) {
this.enableTimeout = enableTimeout;
this.request = request;
this.timeout = undefined;
this.promise = new Promise((resolve, reject) => {
this.reject = reject;
this.resolve = resolve;
const messageTimeout = timeoutValue || protocolClient.messageTimeout;
// Handle timeout
if (this.enableTimeout) {
this.timeout = setTimeout(() => {
const errorMessage = {
action: request.action,
error: kpErrors.getError(kpErrors.TIMEOUT_OR_NOT_CONNECTED),
errorCode: kpErrors.TIMEOUT_OR_NOT_CONNECTED
};
keepass.isKeePassXCAvailable = false;
resolve(errorMessage);
}, messageTimeout);
}
});
}
cancelTimeout() {
this.enableTimeout = false;
clearTimeout(this.timeout);
}
}
// Legacy client for KeePassXC 2.7.x and older
const keepassClient = {};
//--------------------------------------------------------------------------
// Messaging
//--------------------------------------------------------------------------
keepassClient.sendNativeMessage = async function(request, enableTimeout = false, timeoutValue) {
if (!protocolClient.nativePort) {
logError('No native messaging port defined.');
return;
}
const message = new Message(request, enableTimeout, timeoutValue);
await navigator.locks.request('messageBuffer', async (lock) => {
messageBuffer.addMessage(message);
});
protocolClient.nativePort.postMessage(request);
const response = await message.promise;
// Remove a timeouted message
if (response.error && response?.errorCode === kpErrors.TIMEOUT_OR_NOT_CONNECTED) {
messageBuffer.removeMessage(message);
}
return response;
};
keepassClient.handleNativeMessage = async function(response) {
// Parse through the message buffer to find the corresponding Promise.
await navigator.locks.request('messageBuffer', async (lock) => {
const message = messageBuffer.getMessage(response);
if (message) {
message.resolve(response);
messageBuffer.removeMessage(message);
return;
}
debugLogMessage('Corresponding request not found in the message buffer for response: ', response);
});
};
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

@ -43,6 +43,7 @@ const defaultSettings = {
const AUTO_SUBMIT_TIMEOUT = 5000; const AUTO_SUBMIT_TIMEOUT = 5000;
const page = {}; const page = {};
page.autoLockRequested = false;
page.autoSubmitPerformed = false; page.autoSubmitPerformed = false;
page.attributeMenuItems = []; page.attributeMenuItems = [];
page.blockedTabs = []; page.blockedTabs = [];
@ -214,6 +215,14 @@ page.clearSubmittedCredentials = async function() {
page.submittedCredentials = {}; page.submittedCredentials = {};
}; };
page.clearAutoLockRequested = async function() {
page.autoLockRequested = false;
};
page.getAutoLockRequested = async function() {
return page.autoLockRequested;
};
page.createTabEntry = async function(tabId) { page.createTabEntry = async function(tabId) {
page.tabs[tabId] = { page.tabs[tabId] = {
allowIframes: false, allowIframes: false,
@ -252,8 +261,14 @@ page.retrieveCredentials = async function(tab, args = []) {
page.currentRequest.tabId = tab.id; page.currentRequest.tabId = tab.id;
} }
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; page.tabs[tab.id].credentials = credentials;
if (credentials.autoLockRequested) {
page.autoLockRequested = true;
}
return credentials; return credentials;
}; };

View file

@ -0,0 +1,546 @@
'use strict';
//--------------------------------------------------------------------------
// Protocol V2
//--------------------------------------------------------------------------
const protocol = {};
protocol.associate = async function(tab, args = []) {
if (!keepass.isKeePassXCAvailable) {
return AssociatedAction.NOT_ASSOCIATED;
}
try {
keepass.clearErrorMessage(tab);
const publicKey = protocolClient.getPublicConnectionKey();
const idKey = protocolClient.generateIdKey();
const messageData = {
action: kpActions.ASSOCIATE,
idKey: idKey,
publicKey: publicKey
};
const response = await protocolClient.sendMessage(tab, messageData, false, true);
if (response && response.id && response.hash) {
keepass.setCryptoKey(response.id, idKey);
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;
};
// Unencrypted
protocol.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 = protocolClient.getPublicConnectionKey();
const [ nonce, incrementedNonce ] = protocolClient.getNonces();
keepass.clientID = protocolClient.generateClientId();
const request = {
action: kpAction,
clientID: keepass.clientID,
nonce: nonce,
publicKey: key,
requestID: protocolClient.getRequestId()
};
try {
const response = await keepassClient.sendNativeMessage(request, enableTimeout, connectionTimeout);
if (response.error && response.errorCode) {
keepass.handleError(tab, kpErrors.KEY_CHANGE_FAILED);
return false;
}
keepass.setcurrentKeePassXCVersion(response.version);
keepass.protocolV2 = response?.protocolVersion === 2;
const verified = keepass.protocolV2
? protocolClient.verifyNonce(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 = []) {
const [ username, password, url, group, groupUuid ] = args;
return protocol.updateCredentials(tab, [ null, username, password, url, group, groupUuid ]);
};
protocol.createNewGroup = async function(tab, args = []) {
if (!keepass.isConnected) {
return [];
}
keepass.clearErrorMessage(tab);
const [ groupName ] = args;
const messageData = {
action: kpActions.CREATE_NEW_GROUP,
groupName: groupName,
keys: protocol.getCurrentKey()
};
try {
// TODO: Handle errors
const response = await protocolClient.sendMessage(tab, messageData);
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 = []) {
if (!keepass.isConnected) {
return undefined;
}
if (!compareVersion(keepass.requiredKeePassXC, keepass.currentKeePassXC)) {
return undefined;
}
const messageData = {
action: kpActions.GENERATE_PASSWORD,
};
try {
const response = await protocolClient.sendMessage(tab, messageData);
if (response) {
if (response.error && response.errorCode) {
keepass.handleError(tab, response.errorCode);
return undefined;
}
const password = response.entries ?? response.password;
keepass.updateLastUsed(keepass.databaseHash);
return password;
} else {
logError('generatePassword rejected');
}
return undefined;
} catch (err) {
logError(`generatePassword failed: ${err}`);
return undefined;
}
};
protocol.getCredentials = async function(tab, args = []) {
if (!keepass.isConnected) {
return [];
}
keepass.clearErrorMessage(tab);
const [ url, submiturl, triggerUnlock = false, httpAuth = false ] = args;
let entries = [];
const messageData = {
action: kpActions.GET_CREDENTIALS,
keys: protocol.getKeys(),
url: url
};
if (submiturl) {
messageData.submitUrl = submiturl;
}
if (httpAuth) {
messageData.httpAuth = true;
}
try {
const response = await protocolClient.sendMessage(tab, messageData, false, triggerUnlock);
if (response) {
if (response.error && response.errorCode) {
keepass.handleError(tab, response.errorCode);
return [];
}
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(`getCredentials failed: ${err}`);
return [];
}
};
protocol.getDatabaseGroups = async function(tab, args = []) {
if (!keepass.isConnected) {
return [];
}
keepass.clearErrorMessage(tab);
let groups = [];
const messageData = {
action: kpActions.GET_DATABASE_GROUPS,
keys: protocol.getCurrentKey()
};
try {
const response = await protocolClient.sendMessage(tab, messageData);
if (response) {
if (response.error && response.errorCode) {
keepass.handleError(tab, response.errorCode);
return [];
}
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 [];
}
};
protocol.getDatabaseStatuses = async function(tab, args = []) {
if (!keepass.isKeePassXCAvailable) {
keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED);
return;
}
if (!keepass.serverPublicKey) {
await protocol.changePublicKeys(tab);
}
const [ enableTimeout = false, triggerUnlock = false ] = args;
const messageData = {
action: kpActions.GET_DATABASE_STATUSES,
keys: protocol.getKeys()
};
try {
const response = await protocolClient.sendMessage(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;
}
keepass.handleError(tab, kpErrors.ACTION_TIMEOUT);
} catch (err) {
logError(`getDatabaseStatuses failed: ${err}`);
}
};
protocol.getTotp = async function(tab, args = []) {
if (!keepass.isConnected) {
return [];
}
const messageData = {
action: kpActions.GET_TOTP,
keys: protocol.getKeys(),
uuids: args
};
try {
const response = await protocolClient.sendMessage(tab, messageData);
if (response) {
keepass.updateLastUsed(keepass.databaseHash);
return response.totpList;
}
return;
} catch (err) {
logError(`getTotp failed: ${err}`);
}
};
protocol.lockDatabase = async function(tab, args = []) {
if (!keepass.isConnected) {
keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED);
return false;
}
const [ lockSingle ] = args;
const messageData = {
action: kpActions.LOCK_DATABASE,
lockSingle: lockSingle
};
try {
const response = await protocolClient.sendMessage(tab, messageData);
if (response) {
keepass.updateDatabase(tab);
return true;
}
keepass.isDatabaseClosed = true;
return false;
} catch (err) {
logError(`lockDatabase failed: ${err}`);
return false;
}
};
protocol.passkeysRegister = async function(tab, args = []) {
if (!keepass.isConnected) {
return [];
}
const [ publicKey, origin ] = args;
const messageData = {
action: kpActions.PASSKEYS_REGISTER,
publicKey: JSON.parse(JSON.stringify(publicKey)),
origin: origin,
groupName: page?.settings?.defaultPasskeyGroup,
keys: keepass.getCryptoKeys()
};
const response = await protocolClient.sendMessage(tab, messageData);
if (response) {
return response;
}
browserAction.showDefault(tab);
return [];
};
protocol.passkeysGet = async function(tab, args = []) {
if (!keepass.isConnected) {
return [];
}
const [ publicKey, origin ] = args;
const messageData = {
action: kpActions.PASSKEYS_GET,
publicKey: JSON.parse(JSON.stringify(publicKey)),
origin: origin,
keys: keepass.getCryptoKeys()
};
const response = await keepassClient.sendMessage(tab, messageData);
if (response) {
return response;
}
browserAction.showDefault(tab);
return [];
};
protocol.requestAutotype = async function(tab, args = []) {
if (!keepass.isConnected) {
keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED);
return false;
}
const messageData = {
action: kpActions.REQUEST_AUTOTYPE,
search: getTopLevelDomainFromUrl(args[0])
};
try {
const response = await protocolClient.sendMessage(tab, messageData);
return response?.result;
} catch (err) {
logError(`requestAutotype failed: ${err}`);
return false;
}
};
protocol.testAssociationFromDatabaseStatuses = async function(tab, args = []) {
const databaseStatuses = await protocol.getDatabaseStatuses(tab, args);
console.log(databaseStatuses);
if (!databaseStatuses) {
return {};
}
const result = {
areAllLocked: true,
associationNeeded: false,
databaseHash: undefined,
isAnyAssociated: false,
isCurrentLocked: true
};
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');
}
// Current association status
keepass.associated.hash = currentDatabaseStatus[0]?.hash;
keepass.associated.value = isCurrentAssociated;
// 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;
result.isCurrentLocked = isCurrentLocked;
keepass.databaseStatuses = databaseStatuses;
keepass.databaseAssociationStatuses = result;
return result;
};
protocol.updateCredentials = async function(tab, args = []) {
if (!keepass.isConnected) {
return [];
}
const [ entryId, username, password, url, group, groupUuid ] = args;
const messageData = {
action: kpActions.CREATE_CREDENTIALS,
keys: protocol.getCurrentKey(),
login: username,
password: password,
submitUrl: url,
url: url,
};
if (entryId) {
messageData.uuid = entryId;
}
if (!entryId && page.settings.downloadFaviconAfterSave) {
messageData.downloadFavicon = true;
}
if (group && groupUuid) {
messageData.group = group;
messageData.groupUuid = groupUuid;
}
try {
const response = await protocolClient.sendMessage(tab, messageData);
if (response) {
keepass.updateLastUsed(keepass.databaseHash);
if (response?.result === true) {
return entryId ? AddCredentials.UPDATED : AddCredentials.CREATED;
}
return AddCredentials.CANCELED;
} else {
return AddCredentials.ERROR;
}
} catch (err) {
logError(`updateCredentials failed: ${err}`);
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;
};
// Gets the key only from the current active database
protocol.getCurrentKey = function() {
const [ id, key ] = keepass.getCryptoKey();
return [
{
id: id,
key: key
}
];
};

View file

@ -0,0 +1,316 @@
'use strict';
const protocolBuffer = {
buffer: [],
addMessage(message) {
this.buffer.push(message);
},
// Returns corresponding message from the response. If the response is an error,
// return the first matching action from the buffer.
getMessage(response) {
const isError = Boolean(!response.nonce && response.error && response.errorCode);
return this.buffer.find(message => {
if (message.request.requestID === response.requestID
|| (isError && message.request?.action === response?.action)) {
// Cancel timeout
if (message.enableTimeout) {
message.cancelTimeout();
}
return message;
}
});
},
removeMessage(message) {
const index = this.buffer.indexOf(message);
if (index >= 0 && index < this.buffer.length) {
this.buffer.splice(index, 1);
}
},
};
// Basic class for a message to be sent. The Promise inside the class will be resolved when
// the response to the message is received.
class ProtocolMessage {
constructor(request, enableTimeout, timeoutValue) {
this.enableTimeout = enableTimeout;
this.request = request;
this.timeout = undefined;
this.promise = new Promise((resolve, reject) => {
this.reject = reject;
this.resolve = resolve;
const messageTimeout = timeoutValue || keepassClient.messageTimeout;
// Handle timeout
if (this.enableTimeout) {
this.timeout = setTimeout(() => {
// The error is action timeout if action is not change-public-keys
let error = kpErrors.ACTION_TIMEOUT;
if (request.action === kpActions.CHANGE_PUBLIC_KEYS) {
error = kpErrors.TIMEOUT_OR_NOT_CONNECTED;
keepass.isKeePassXCAvailable = false;
}
resolve({
action: request.action,
error: kpErrors.getError(error),
errorCode: error
});
}, messageTimeout);
}
});
}
cancelTimeout() {
this.enableTimeout = false;
clearTimeout(this.timeout);
}
}
//--------------------------------------------------------------------------
// Protocol V2
//--------------------------------------------------------------------------
const protocolClient = {};
protocolClient.keySize = 24;
protocolClient.messageTimeout = 500; // Milliseconds
protocolClient.nativeHostName = 'org.keepassxc.keepassxc_browser';
protocolClient.nativePort = null;
protocolClient.sendNativeMessage = async function(request, enableTimeout = false, timeoutValue = protocolClient.messageTimeout) {
if (!protocolClient.nativePort) {
logError('No native messaging port defined.');
return;
}
const message = new ProtocolMessage(request, enableTimeout, timeoutValue);
await navigator.locks.request('messageBuffer', async (lock) => {
protocolBuffer.addMessage(message);
});
protocolClient.nativePort.postMessage(request);
const response = await message.promise;
// Remove a timeouted message
if (response.error && response?.errorCode === kpErrors.TIMEOUT_OR_NOT_CONNECTED) {
protocolBuffer.matchAndRemove(message);
}
return response;
};
protocolClient.sendMessage = async function(tab, messageData, enableTimeout = false, triggerUnlock = false) {
const nonce = protocolClient.getNonce();
const encryptedMessage = protocolClient.encrypt(messageData, nonce);
const request = protocolClient.buildRequest(encryptedMessage, nonce, keepass.clientID, triggerUnlock);
const response = await protocolClient.sendNativeMessage(request, enableTimeout);
const incrementedNonce = protocolClient.incrementedNonce(nonce);
return protocolClient.handleResponse(response, incrementedNonce, request.requestID, tab);
};
protocolClient.buildRequest = function(encryptedMessage, nonce, clientID, triggerUnlock = false) {
const request = {
message: encryptedMessage,
nonce: nonce,
clientID: clientID,
requestID: protocolClient.getRequestId()
};
if (triggerUnlock) {
request.triggerUnlock = true;
}
return request;
};
protocolClient.handleNativeMessage = async function(response) {
// Parse through the message buffer to find the corresponding Promise.
await navigator.locks.request('messageBuffer', async (lock) => {
const message = protocolBuffer.getMessage(response);
if (message) {
message.resolve(response);
protocolBuffer.removeMessage(message);
return;
}
debugLogMessage('Corresponding request not found in the message buffer for response: ', response);
});
};
// Verifies nonces, decrypts and parses the response
protocolClient.handleResponse = function(response, incrementedNonce, requestID, tab) {
if (response.message && protocolClient.verifyNonce(response, incrementedNonce)) {
const res = protocolClient.decrypt(response.message, response.nonce);
if (!res) {
keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE);
protocolBuffer.matchAndRemove({ requestID: requestID });
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);
protocolBuffer.matchAndRemove({ requestID: requestID });
}
return undefined;
};
protocolClient.verifyNonce = function(response, nonce) {
if (!response.nonce) {
logError('No nonce in response');
return false;
}
if (!protocolClient.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();
};
protocolClient.getPublicConnectionKey = function() {
return nacl.util.encodeBase64(keepass.keyPair.publicKey);
};
protocolClient.generateIdKey = function() {
const idKeyPair = nacl.box.keyPair();
return nacl.util.encodeBase64(idKeyPair.publicKey);
};
protocolClient.generateClientId = function() {
return nacl.util.encodeBase64(nacl.randomBytes(protocolClient.keySize));
};
//--------------------------------------------------------------------------
// 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();
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();
}
// Generic response handling
if (response.action === kpActions.CHANGE_PUBLIC_KEYS || !keepass.protocolV2) {
keepassClient.handleNativeMessage(response);
} else {
protocolClient.handleNativeMessage(response);
}
};

View file

@ -43,6 +43,13 @@ const showNotification = function(message) {
}); });
}; };
const AddCredentials = {
CANCELED: 0,
CREATED: 1,
ERROR: 2,
UPDATED: 3
};
const AssociatedAction = { const AssociatedAction = {
NOT_ASSOCIATED: 0, NOT_ASSOCIATED: 0,
ASSOCIATED: 1, ASSOCIATED: 1,

View file

@ -172,7 +172,7 @@ kpxcBanner.create = async function(credentials = {}) {
kpxcBanner.saveNewCredentials = async function(credentials = {}) { kpxcBanner.saveNewCredentials = async function(credentials = {}) {
const saveToDefaultGroup = async function(creds) { const saveToDefaultGroup = async function(creds) {
const args = [ creds.username, creds.password, creds.url ]; 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); kpxcBanner.verifyResult(res);
}; };
@ -202,7 +202,7 @@ kpxcBanner.saveNewCredentials = async function(credentials = {}) {
// Create a new group // Create a new group
const newGroup = await sendMessage('create_new_group', [ result.defaultGroup ]); const newGroup = await sendMessage('create_new_group', [ result.defaultGroup ]);
if (newGroup.name && newGroup.uuid) { if (newGroup.name && newGroup.uuid) {
const res = await sendMessage('add_credentials', [ const res = await sendMessage('create_credentials', [
credentials.username, credentials.username,
credentials.password, credentials.password,
credentials.url, credentials.url,
@ -218,7 +218,7 @@ kpxcBanner.saveNewCredentials = async function(credentials = {}) {
} }
} }
const res = await sendMessage('add_credentials', [ const res = await sendMessage('create_credentials', [
credentials.username, credentials.username,
credentials.password, credentials.password,
credentials.url, credentials.url,
@ -256,7 +256,7 @@ kpxcBanner.saveNewCredentials = async function(credentials = {}) {
return; return;
} }
const res = await sendMessage('add_credentials', [ const res = await sendMessage('create_credentials', [
credentials.username, credentials.username,
credentials.password, credentials.password,
credentials.url, credentials.url,
@ -341,7 +341,7 @@ kpxcBanner.updateCredentials = async function(credentials = {}) {
args: [ url, '', true ] // Sets triggerUnlock to true args: [ url, '', true ] // Sets triggerUnlock to true
}).then(async creds => { }).then(async creds => {
if (!creds || creds.length !== credentials.list.length) { if (!creds || creds.length !== credentials.list.length) {
kpxcBanner.verifyResult('error'); kpxcBanner.verifyResult(AddCredentials.ERROR);
return; return;
} }
@ -367,21 +367,21 @@ kpxcBanner.updateCredentials = async function(credentials = {}) {
}; };
kpxcBanner.verifyResult = async function(code) { kpxcBanner.verifyResult = async function(code) {
if (code === 'error') { if (code === AddCredentials.ERROR) {
kpxcUI.createNotification('error', tr('rememberErrorCannotSaveCredentials')); kpxcUI.createNotification('error', tr('rememberErrorCannotSaveCredentials'));
} else if (code === 'created') { } else if (code === AddCredentials.CREATED) {
kpxcUI.createNotification( kpxcUI.createNotification(
'success', 'success',
tr('rememberCredentialsSaved', kpxcBanner.credentials.username || tr('rememberEmptyUsername')), tr('rememberCredentialsSaved', kpxcBanner.credentials.username || tr('rememberEmptyUsername')),
); );
await kpxc.retrieveCredentials(true); // Forced reload await kpxc.retrieveCredentials(true); // Forced reload
} else if (code === 'updated') { } else if (code === AddCredentials.UPDATED) {
kpxcUI.createNotification( kpxcUI.createNotification(
'success', 'success',
tr('rememberCredentialsUpdated', kpxcBanner.credentials.username || tr('rememberEmptyUsername')), tr('rememberCredentialsUpdated', kpxcBanner.credentials.username || tr('rememberEmptyUsername')),
); );
await kpxc.retrieveCredentials(true); // Forced reload await kpxc.retrieveCredentials(true); // Forced reload
} else if (code === 'canceled') { } else if (code === AddCredentials.CANCELED) {
kpxcUI.createNotification('warning', tr('rememberCredentialsNotSaved')); kpxcUI.createNotification('warning', tr('rememberCredentialsNotSaved'));
} else { } else {
kpxcUI.createNotification('error', tr('rememberErrorDatabaseClosed')); kpxcUI.createNotification('error', tr('rememberErrorDatabaseClosed'));

View file

@ -144,24 +144,39 @@ kpxcFill.fillTOTPFromUuid = async function(el, uuid) {
return; return;
} }
let totpFound = false;
if (user.totp?.length > 0) { if (user.totp?.length > 0) {
const protocolV2 = await sendMessage('is_protocol_v2');
// Retrieve a new TOTP value // Retrieve a new TOTP value
const totp = await sendMessage('get_totp', [ user.uuid, user.totp ]); const totp = await sendMessage('get_totp', (protocolV2 ? [ user.uuid ] : [ user.uuid, user.totp ]));
if (!totp) { if (!totp) {
kpxcUI.createNotification('warning', tr('credentialsNoTOTPFound')); kpxcUI.createNotification('warning', tr('credentialsNoTOTPFound'));
return; return;
} }
kpxcFill.setTOTPValue(el, totp); if (protocolV2) {
const result = totp.find(t => t.uuid === uuid);
kpxcFill.setTOTPValue(el, result?.totp);
} else {
kpxcFill.setTOTPValue(el, totp);
}
return;
} else if (user.stringFields?.length > 0) { } else if (user.stringFields?.length > 0) {
const stringFields = user.stringFields; const stringFields = user.stringFields;
for (const s of stringFields) { for (const s of stringFields) {
const val = s['KPH: {TOTP}']; const val = s['KPH: {TOTP}'];
if (val) { if (val) {
kpxcFill.setTOTPValue(el, val); kpxcFill.setTOTPValue(el, val);
totpFound = true;
} }
} }
} }
if (!totpFound) {
kpxcUI.createNotification('warning', tr('credentialsNoTOTPFound'));
}
}; };
// Set normal or segmented TOTP value // Set normal or segmented TOTP value
@ -251,7 +266,7 @@ kpxcFill.fillInCredentials = async function(combination, predefinedUsername, uui
if (combination.password.maxLength if (combination.password.maxLength
&& combination.password.maxLength > 0 && combination.password.maxLength > 0
&& selectedCredentials.password.length > combination.password.maxLength) { && selectedCredentials.password.length > combination.password.maxLength) {
kpxcUI.createNotification('warning', tr('errorMessagePaswordLengthExceeded')); kpxcUI.createNotification('warning', tr('errorMessagePasswordLengthExceeded'));
} }
// Prevent filling password to plain text input field // Prevent filling password to plain text input field
@ -292,6 +307,12 @@ kpxcFill.fillInCredentials = async function(combination, predefinedUsername, uui
await sendMessage('page_set_manual_fill', ManualFill.NONE); await sendMessage('page_set_manual_fill', ManualFill.NONE);
await kpxcFill.performAutoSubmit(combination, skipAutoSubmit); await kpxcFill.performAutoSubmit(combination, skipAutoSubmit);
// Auto-lock database when requested
if (await sendMessage('page_get_auto_lock_requested')) {
await sendMessage('page_clear_auto_lock_requested');
sendMessage('lock_database');
}
}; };
// Fills StringFields defined in Custom Fields // Fills StringFields defined in Custom Fields

View file

@ -15,6 +15,7 @@ const sendMessage = async function(action, args) {
* The main content script object. * The main content script object.
*/ */
const kpxc = {}; const kpxc = {};
kpxc.associationStatus = undefined;
kpxc.combinations = []; kpxc.combinations = [];
kpxc.credentials = []; kpxc.credentials = [];
kpxc.databaseState = DatabaseState.DISCONNECTED; kpxc.databaseState = DatabaseState.DISCONNECTED;
@ -23,8 +24,8 @@ kpxc.improvedFieldDetectionEnabledForPage = false;
kpxc.inputs = []; kpxc.inputs = [];
kpxc.settings = {}; kpxc.settings = {};
kpxc.singleInputEnabledForPage = false; kpxc.singleInputEnabledForPage = false;
kpxc.submitUrl = null; kpxc.submitUrl = undefined;
kpxc.url = null; kpxc.url = undefined;
// Add page to Site Preferences with a selected option enabled. Set from the popup. // Add page to Site Preferences with a selected option enabled. Set from the popup.
kpxc.addToSitePreferences = async function(optionName, addWildcard = false) { kpxc.addToSitePreferences = async function(optionName, addWildcard = false) {
@ -111,6 +112,7 @@ kpxc.createCombination = async function(activeElement, passOnly) {
// Switch credentials if database is changed or closed // Switch credentials if database is changed or closed
kpxc.detectDatabaseChange = async function(response) { kpxc.detectDatabaseChange = async function(response) {
kpxc.associationStatus = response?.associateResult;
kpxc.databaseState = DatabaseState.LOCKED; kpxc.databaseState = DatabaseState.LOCKED;
kpxc.clearAllFromPage(); kpxc.clearAllFromPage();
kpxcIcons.switchIcons(); kpxcIcons.switchIcons();
@ -120,7 +122,8 @@ kpxc.detectDatabaseChange = async function(response) {
_called.retrieveCredentials = false; _called.retrieveCredentials = false;
const settings = await sendMessage('load_settings'); const settings = await sendMessage('load_settings');
kpxc.settings = settings; kpxc.settings = settings;
kpxc.databaseState = DatabaseState.UNLOCKED; kpxc.databaseState = response?.associateResult?.areAllLocked
? DatabaseState.LOCKED : DatabaseState.UNLOCKED;
await kpxc.initCredentialFields(); await kpxc.initCredentialFields();
kpxcIcons.switchIcons(); kpxcIcons.switchIcons();
@ -128,7 +131,7 @@ kpxc.detectDatabaseChange = async function(response) {
// If user has requested a manual fill through context menu the actual credential filling // 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. // is handled here when the opened database has been regognized. It's not a pretty hack.
const manualFill = await sendMessage('page_get_manual_fill'); 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 kpxcFill.fillInFromActiveElement(manualFill === ManualFill.PASSWORD);
await sendMessage('page_set_manual_fill', ManualFill.NONE); await sendMessage('page_set_manual_fill', ManualFill.NONE);
} }
@ -385,6 +388,16 @@ kpxc.initCredentialFields = async function() {
await kpxcIcons.initIcons(kpxc.combinations); await kpxcIcons.initIcons(kpxc.combinations);
// TODO: In optimal case, this should only trigger when a database is opened. How to detect that?
// Protocol V2
if (kpxc.associationStatus) {
if (!kpxc.associationStatus.isCurrentLocked) {
await kpxc.retrieveCredentials();
}
return;
}
// Protocol V1
if (kpxc.databaseState === DatabaseState.UNLOCKED) { if (kpxc.databaseState === DatabaseState.UNLOCKED) {
await kpxc.retrieveCredentials(); await kpxc.retrieveCredentials();
} }
@ -828,6 +841,8 @@ kpxc.updateDatabaseState = async function() {
}; };
// Updates the TOTP Autocomplete Menu // Updates the TOTP Autocomplete Menu
// TODO: Check this against https://github.com/keepassxreboot/keepassxc-browser/compare/develop...feature/protocol_v2
// Which one is the correct implementation?
kpxc.updateTOTPList = async function() { kpxc.updateTOTPList = async function() {
let uuid = await sendMessage('page_get_login_id'); let uuid = await sendMessage('page_get_login_id');
if (uuid === undefined || kpxc.credentials.length === 0) { if (uuid === undefined || kpxc.credentials.length === 0) {

View file

@ -128,6 +128,7 @@ const iconClicked = async function(field, icon) {
if (kpxc.databaseState !== DatabaseState.UNLOCKED) { if (kpxc.databaseState !== DatabaseState.UNLOCKED) {
// Triggers database unlock // Triggers database unlock
await sendMessage('page_set_manual_fill', ManualFill.BOTH); await sendMessage('page_set_manual_fill', ManualFill.BOTH);
// 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 await sendMessage('get_database_hash', [ false, true ]); // Set triggerUnlock to true
field.focus(); field.focus();
} }

View file

@ -98,6 +98,37 @@ code {
justify-content: flex-end; justify-content: flex-end;
} }
.dropdown-toggle {
display: none;
height: 31px;
width: 10px;
}
.kpxc-dropdown-menu {
border: var(--kpxc-card-border-color);
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
border-top-left-radius: 4px;
box-shadow: 0 4px 6px 0 hsla(0, 0%, 0%, 0.2) !important;
overflow: hidden;;
position: absolute;
right: 12px;
top: 40px;
z-index: 2147483646;
}
.kpxc-dropdown-item {
background: var(--kpxc-background-color);
color: var(--kpxc-text-color);
cursor: pointer;
padding: 5px;
}
.kpxc-dropdown-item:hover {
background-color: #28a745 !important;
color: #ffffff !important;
}
#list { #list {
padding-left: 0px; padding-left: 0px;
} }
@ -196,6 +227,15 @@ code {
background-color: var(--kpxc-table-hover-color) !important; background-color: var(--kpxc-table-hover-color) !important;
} }
.kpxc-dropdown-menu {
border: var(--kpxc-card-border-color);
}
.kpxc-dropdown-item {
background: var(--kpxc-background-color);
color: var(--kpxc-text-color);
}
span.bg-success { span.bg-success {
background-color: var(--kpxc-table-hover-color) !important; background-color: var(--kpxc-table-hover-color) !important;
color: var(--kpxc-text-color) !important; color: var(--kpxc-text-color) !important;

View file

@ -24,10 +24,14 @@
</button> </button>
<button id="choose-custom-login-fields-button" class="btn btn-sm btn-warning" data-i18n="[title]popupChooseCredentialsText"></button> <button id="choose-custom-login-fields-button" class="btn btn-sm btn-warning" data-i18n="[title]popupChooseCredentialsText"></button>
</div> </div>
<div class="lock-button-area"> <div class="btn-group lock-button-area">
<button id="lock-database-button" class="btn btn-sm btn-danger" data-i18n="[title]lockDatabase"> <button id="lock-database-button" class="btn btn-sm btn-danger" data-i18n="[title]lockDatabase"><i class="fa fa-lock" aria-hidden="true"></i></button>
<i class="fa fa-lock" aria-hidden="true"></i> <select id="dropdown-button" class="btn btn-sm btn-danger dropdown-toggle" style="display: none"></select>
</button> </div>
<div class="kpxc-dropdown-menu" style="display: none">
<div class="kpxc-dropdown-item">
<span id="kpxc-dropdown-item" data-i18n="lockAllDatabases"></span>
</div>
</div> </div>
</div> </div>

View file

@ -10,7 +10,7 @@ HTMLElement.prototype.hide = function() {
this.style.display = 'none'; this.style.display = 'none';
}; };
function statusResponse(r) { function statusResponse(status) {
$('#initial-state').hide(); $('#initial-state').hide();
$('#error-encountered').hide(); $('#error-encountered').hide();
$('#need-reconfigure').hide(); $('#need-reconfigure').hide();
@ -21,43 +21,77 @@ function statusResponse(r) {
$('#getting-started-guide').hide(); $('#getting-started-guide').hide();
$('#database-not-opened').hide(); $('#database-not-opened').hide();
if (!r.keePassXCAvailable) { if (!status.keePassXCAvailable) {
$('#error-message').textContent = r.error; $('#error-message').textContent = status.error;
$('#error-encountered').show(); $('#error-encountered').show();
if (r.showGettingStartedGuideAlert) { if (status.showGettingStartedGuideAlert) {
$('#getting-started-guide').show(); $('#getting-started-guide').show();
} }
if (r.showTroubleshootingGuideAlert && reloadCount >= 2) { if (status.showTroubleshootingGuideAlert && reloadCount >= 2) {
$('#troubleshooting-guide').show(); $('#troubleshooting-guide').show();
} else { } else {
$('#troubleshooting-guide').hide(); $('#troubleshooting-guide').hide();
} }
} else if (r.keePassXCAvailable && r.databaseClosed) {
$('#database-error-message').textContent = r.error; return;
}
// Only supported with Protocol V2
if (status.protocolV2
&& status.databaseAssociationStatuses
&& Object.keys(status.databaseAssociationStatuses).length > 0) {
// 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();
showDropdownButton(status.protocolV2);
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(); $('#database-not-opened').show();
} else if (!r.configured) { } else if (!status.configured) {
$('#not-configured').show(); $('#not-configured').show();
} else if (r.encryptionKeyUnrecognized) { } else if (status.encryptionKeyUnrecognized) {
$('#need-reconfigure').show(); $('#need-reconfigure').show();
$('#need-reconfigure-message').textContent = r.error; $('#need-reconfigure-message').textContent = status.error;
} else if (!r.associated) { } else if (!status.associated) {
$('#need-reconfigure').show(); $('#need-reconfigure').show();
$('#need-reconfigure-message').textContent = r.error; $('#need-reconfigure-message').textContent = status.error;
} else if (r.error) { } else if (status.error) {
$('#error-encountered').show(); $('#error-encountered').show();
$('#error-message').textContent = r.error; $('#error-message').textContent = status.error;
} else { } else {
$('#configured-and-associated').show(); $('#configured-and-associated').show();
$('#associated-identifier').textContent = r.identifier; $('#associated-identifier').textContent = status.identifier;
$('#lock-database-button').show(); $('#lock-database-button').show();
if (r.usernameFieldDetected) { if (status.usernameFieldDetected) {
$('#username-field-detected').show(); $('#username-field-detected').show();
} }
if (r.iframeDetected) { if (status.iframeDetected) {
$('#iframe-detected').show(); $('#iframe-detected').show();
} }
@ -130,9 +164,11 @@ const sendMessageToTab = async function(message) {
}); });
$('#lock-database-button').addEventListener('click', async () => { $('#lock-database-button').addEventListener('click', async () => {
statusResponse(await browser.runtime.sendMessage({ statusResponse(await lockDatabase());
action: 'lock_database' });
}));
$('.kpxc-dropdown-item').addEventListener('click', async () => {
statusResponse(await lockDatabase(false));
}); });
$('#username-only-button').addEventListener('click', async () => { $('#username-only-button').addEventListener('click', async () => {

View file

@ -53,6 +53,40 @@ async function getLoginData() {
return logins; return logins;
} }
async function lockDatabase(lockSingle = true) {
return await browser.runtime.sendMessage({
action: 'lock_database',
args: [ lockSingle ]
});
}
// Show the dropdown button if protocol is supported
async function showDropdownButton(isV2) {
const isProtocolV2 = isV2 || await browser.runtime.sendMessage({ action: 'is_protocol_v2' });
if (isProtocolV2) {
$('.lock-button-area')?.classList.add('btn-group');
$('#dropdown-button')?.show();
} else {
$('.lock-button-area')?.classList.remove('btn-group');
$('#dropdown-button')?.hide();
}
}
function hideDropdownButton() {
$('.lock-button-area')?.classList.remove('btn-group');
$('#dropdown-button')?.hide();
}
function hideElementsOnDatabaseLock() {
$('.credentials').hide();
$('#database-not-opened').show();
$('#lock-database-button').hide();
$('#dropdown-button').hide();
$('#btn-dismiss')?.hide();
$('#database-error-message').textContent = tr('errorMessageDatabaseNotOpened');
}
(async () => { (async () => {
if (document.readyState === 'complete' || (document.readyState !== 'loading' && !document.documentElement.doScroll)) { if (document.readyState === 'complete' || (document.readyState !== 'loading' && !document.documentElement.doScroll)) {
await initSettings(); await initSettings();
@ -60,7 +94,39 @@ async function getLoginData() {
document.addEventListener('DOMContentLoaded', initSettings); document.addEventListener('DOMContentLoaded', initSettings);
} }
document.addEventListener('mouseup', function(e) {
if (!e.isTrusted) {
return;
}
if (e.target.id !== 'kpxc-dropdown-item' && e.target.id !== 'dropdown-button') {
$('.kpxc-dropdown-menu')?.hide();
$('#dropdown-button').style.borderBottomRightRadius = '4px';
}
});
updateAvailableResponse(await browser.runtime.sendMessage({ updateAvailableResponse(await browser.runtime.sendMessage({
action: 'update_available_keepassxc' action: 'update_available_keepassxc'
})); }));
$('#dropdown-button').addEventListener('click', (e) => {
const dropdownMenu = $('.kpxc-dropdown-menu');
if (!dropdownMenu) {
return;
}
if (dropdownMenu.style.display === 'none') {
dropdownMenu.show();
$('#dropdown-button').style.borderBottomRightRadius = '0px';
} else {
dropdownMenu.hide();
$('#dropdown-button').style.borderBottomRightRadius = '4px';
}
e.target.blur();
});
$('.kpxc-dropdown-item').addEventListener('click', () => {
$('.kpxc-dropdown-menu')?.hide();
});
})(); })();

View file

@ -24,10 +24,14 @@
</button> </button>
<button id="choose-custom-login-fields-button" class="btn btn-sm btn-warning" data-i18n="[title]popupChooseCredentialsText"></button> <button id="choose-custom-login-fields-button" class="btn btn-sm btn-warning" data-i18n="[title]popupChooseCredentialsText"></button>
</div> </div>
<div class="lock-button-area"> <div class="btn-group lock-button-area">
<button id="lock-database-button" class="btn btn-sm btn-danger" data-i18n="[title]lockDatabase"> <button id="lock-database-button" class="btn btn-sm btn-danger" data-i18n="[title]lockDatabase"><i class="fa fa-lock" aria-hidden="true"></i></button>
<i class="fa fa-lock" aria-hidden="true"></i> <select id="dropdown-button" class="btn btn-sm btn-danger dropdown-toggle" style="display: none"></select>
</button> </div>
<div class="kpxc-dropdown-menu" style="display: none">
<div class="kpxc-dropdown-item">
<span id="dropdown-item" data-i18n="lockAllDatabases"></span>
</div>
</div> </div>
</div> </div>

View file

@ -4,6 +4,7 @@
await initColorTheme(); await initColorTheme();
$('#lock-database-button').show(); $('#lock-database-button').show();
await showDropdownButton();
const data = await getLoginData(); const data = await getLoginData();
const ll = document.getElementById('login-list'); const ll = document.getElementById('login-list');
@ -30,16 +31,14 @@
ll.appendChild(a); ll.appendChild(a);
} }
$('#lock-database-button').addEventListener('click', function() { $('#lock-database-button').addEventListener('click', () => {
browser.runtime.sendMessage({ lockDatabase();
action: 'lock_database' hideElementsOnDatabaseLock();
}); });
$('.credentials').hide(); $('.kpxc-dropdown-item').addEventListener('click', () => {
$('#btn-dismiss').hide(); lockDatabase(false);
$('#database-not-opened').show(); hideElementsOnDatabaseLock();
$('#lock-database-button').hide();
$('#database-error-message').textContent = tr('errorMessageDatabaseNotOpened');
}); });
$('#btn-dismiss').addEventListener('click', async () => { $('#btn-dismiss').addEventListener('click', async () => {

View file

@ -24,10 +24,14 @@
</button> </button>
<button id="choose-custom-login-fields-button" class="btn btn-sm btn-warning" data-i18n="[title]popupChooseCredentialsText"></button> <button id="choose-custom-login-fields-button" class="btn btn-sm btn-warning" data-i18n="[title]popupChooseCredentialsText"></button>
</div> </div>
<div class="lock-button-area"> <div class="btn-group lock-button-area">
<button id="lock-database-button" class="btn btn-sm btn-danger" data-i18n="[title]lockDatabase"> <button id="lock-database-button" class="btn btn-sm btn-danger" data-i18n="[title]lockDatabase"><i class="fa fa-lock" aria-hidden="true"></i></button>
<i class="fa fa-lock" aria-hidden="true"></i> <select id="dropdown-button" class="btn btn-sm btn-danger dropdown-toggle" style="display: none"></select>
</button> </div>
<div class="kpxc-dropdown-menu" style="display: none">
<div class="kpxc-dropdown-item">
<span id="dropdown-item" data-i18n="lockAllDatabases"></span>
</div>
</div> </div>
</div> </div>

View file

@ -10,6 +10,9 @@
return []; return [];
} }
$('#lock-database-button').show();
showDropdownButton();
const logins = await getLoginData(); const logins = await getLoginData();
const ll = document.getElementById('login-list'); const ll = document.getElementById('login-list');
@ -61,20 +64,19 @@
} }
$('#lock-database-button').addEventListener('click', (e) => { $('#lock-database-button').addEventListener('click', (e) => {
browser.runtime.sendMessage({ lockDatabase();
action: 'lock_database' hideElementsOnDatabaseLock();
}); });
$('#credentialsList').hide(); $('.kpxc-dropdown-item').addEventListener('click', () => {
$('#database-not-opened').show(); lockDatabase(false);
$('#lock-database-button').hide(); hideElementsOnDatabaseLock();
$('#database-error-message').textContent = tr('errorMessageDatabaseNotOpened');
}); });
$('#reopen-database-button').addEventListener('click', (e) => { $('#reopen-database-button').addEventListener('click', (e) => {
browser.runtime.sendMessage({ browser.runtime.sendMessage({
action: 'get_status', action: 'get_status',
args: [ false, true ] // Set forcePopup to true args: [ false, true ] // Set triggerUnlock to true
}); });
}); });
})(); })();