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",
"assertSearchForm": "readonly",
"assertTOTPField": "readonly",
"AddCredentials": "readonly",
"AssociatedAction": "readonly",
"AuthenticatorAssertionResponse": "readonly",
"AuthenticatorAttestationResponse": "readonly",
@ -126,6 +127,7 @@
"isFirefox": "readonly",
"keepass": "readonly",
"keepassClient": "readonly",
"keepassProtocol": "readonly",
"kpActions": "readonly",
"kpErrors": "readonly",
"kpxc": "readonly",
@ -163,6 +165,8 @@
"nacl": "readonly",
"ORANGE_BUTTON": "readonly",
"page": "readonly",
"protocol": "readonly",
"protocolClient": "readonly",
"Pixels": "readonly",
"PublicKeyCredential": "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:
*
#*
# Should never format these files:
*.min.js

View file

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

View file

@ -159,6 +159,10 @@
"message": "No logins found.",
"description": "No logins found."
},
"errorActionTimeout": {
"message": "Action timeout.",
"description": "Action timeout."
},
"errorMessagePaswordLengthExceeded": {
"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."
@ -1393,6 +1397,14 @@
"lockDatabase": {
"message": "Lock database",
"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": {
"message": "Welcome to KeePassXC-Browser!",

View file

@ -7,14 +7,17 @@ try {
'../common/sites.js',
'nacl.min.js',
'nacl-util.min.js',
'client.js',
'keepass.js',
'httpauth.js',
'offscreen.js',
'browserAction.js',
'page.js',
'event.js',
'init.js'
'init.js',
'protocol.js',
'protocolClient.js',
'legacyProtocol.js',
'legacyProtocolClient.js'
);
} catch (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
tab ??= await getCurrentTab();
if (!tab) {
if (!tab || tab.id < 0) {
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 {
associated: keepass.isAssociated(),
configured: configured,
databaseClosed: keepass.isDatabaseClosed,
databaseAssociationStatuses: keepass.databaseAssociationStatuses,
encryptionKeyUnrecognized: keepass.isEncryptionKeyUnrecognized,
error: errorMessage,
iframeDetected: iframeDetected,
identifier: keyId,
keePassXCAvailable: keepass.isKeePassXCAvailable,
protocolV2: keepass.protocolV2,
showGettingStartedGuideAlert: page.settings.showGettingStartedGuideAlert,
showTroubleshootingGuideAlert: page.settings.showTroubleshootingGuideAlert,
usernameFieldDetected: usernameFieldDetected
};
};
kpxcEvent.onLoadSettings = async function() {
kpxcEvent.loadSettings = async function() {
return await page.initSettings().catch((err) => {
logError('onLoadSettings error: ' + err);
logError('loadSettings error: ' + err);
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) => {
logError('kpxcEvent.onLoadKeyRing error: ' + err);
logError('kpxcEvent.loadKeyRing error: ' + err);
return Promise.reject();
});
keepass.keyRing = item.keyRing;
// TODO: What to do here?
if (keepass.isAssociated() && !keepass.keyRing[keepass.associated.hash]) {
keepass.associated = {
value: false,
@ -68,23 +74,33 @@ kpxcEvent.onLoadKeyRing = async function() {
return item.keyRing;
};
kpxcEvent.onSaveSettings = async function(tab, settings) {
kpxcEvent.saveSettings = async function(tab, 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
try {
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) {
const response = await keepass.testAssociation(tab, [ true, triggerUnlock ]);
const response = await keepassProtocol.testAssociation(tab, [ true, triggerUnlock ]);
if (!response) {
return kpxcEvent.showStatus(tab, false);
}
}
const configured = await keepass.isConfigured();
configured = await keepass.isConfigured();
return kpxcEvent.showStatus(tab, configured, internalPoll);
} catch (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);
if (configured) {
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;
return page.tabs[id];
};
kpxcEvent.onGetConnectedDatabase = async function() {
kpxcEvent.getConnectedDatabase = async function() {
return Promise.resolve({
count: Object.keys(keepass.keyRing).length,
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 === '') {
await keepass.getDatabaseHash(tab);
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 };
};
kpxcEvent.onCheckUpdateKeePassXC = async function() {
kpxcEvent.checkUpdateKeePassXC = async function() {
await keepass.checkForNewKeePassXCVersion();
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;
};
kpxcEvent.onRemoveCredentialsFromTabInformation = async function(tab) {
kpxcEvent.removeCredentialsFromTabInformation = async function(tab) {
const id = tab?.id || page.currentTabId;
page.clearCredentials(id);
page.clearSubmittedCredentials();
};
kpxcEvent.onLoginPopup = async function(tab, logins) {
kpxcEvent.initLoginPopup = async function(tab, logins) {
const popupData = {
iconType: 'normal',
popup: 'popup_login'
@ -168,7 +184,7 @@ kpxcEvent.initHttpAuth = async function() {
httpAuth.init();
};
kpxcEvent.onHTTPAuthPopup = async function(tab, data) {
kpxcEvent.initHttpAuthPopup = async function(tab, data) {
const popupData = {
iconType: 'normal',
popup: 'popup_httpauth'
@ -178,7 +194,7 @@ kpxcEvent.onHTTPAuthPopup = async function(tab, data) {
await browserAction.show(tab, popupData);
};
kpxcEvent.onUsernameFieldDetected = async function(tab, detected) {
kpxcEvent.usernameFieldDetected = async function(tab, detected) {
if (tab?.id) {
page.tabs[tab.id].usernameFieldDetected = detected;
}
@ -221,17 +237,17 @@ kpxcEvent.getIsKeePassXCAvailable = async function() {
};
kpxcEvent.hideGettingStartedGuideAlert = async function(tab) {
const settings = await kpxcEvent.onLoadSettings();
const settings = await kpxcEvent.loadSettings();
settings.showGettingStartedGuideAlert = false;
await kpxcEvent.onSaveSettings(tab, settings);
await kpxcEvent.saveSettings(tab, settings);
};
kpxcEvent.hideTroubleshootingGuideAlert = async function(tab) {
const settings = await kpxcEvent.onLoadSettings();
const settings = await kpxcEvent.loadSettings();
settings.showTroubleshootingGuideAlert = false;
await kpxcEvent.onSaveSettings(tab, settings);
await kpxcEvent.saveSettings(tab, settings);
};
// 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!
kpxcEvent.messageHandlers = {
'add_credentials': keepass.addCredentials,
'associate': keepass.associate,
'banner_get_position': page.getBannerPosition,
'banner_set_position': page.setBannerPosition,
'check_database_hash': keepass.checkDatabaseHash,
'check_update_keepassxc': kpxcEvent.onCheckUpdateKeePassXC,
'check_update_keepassxc': kpxcEvent.checkUpdateKeePassXC,
'compare_versions': kpxcEvent.compareMultipleVersions,
'create_credentials': keepass.createCredentials,
'create_new_group': keepass.createNewGroup,
'enable_automatic_reconnect': keepass.enableAutomaticReconnect,
'disable_automatic_reconnect': keepass.disableAutomaticReconnect,
@ -257,14 +273,14 @@ kpxcEvent.messageHandlers = {
'frame_message': kpxcEvent.sendBackToTabs,
'generate_password': keepass.generatePassword,
'get_color_theme': kpxcEvent.getColorTheme,
'get_connected_database': kpxcEvent.onGetConnectedDatabase,
'get_database_hash': keepass.getDatabaseHash,
'get_connected_database': kpxcEvent.getConnectedDatabase,
'get_database_hash': keepass.getDatabaseHash, // TODO ?
'get_database_groups': keepass.getDatabaseGroups,
'get_error_message': keepass.getErrorMessage,
'get_keepassxc_versions': kpxcEvent.onGetKeePassXCVersions,
'get_keepassxc_versions': kpxcEvent.getKeePassXCVersions,
'get_login_list': page.getLoginList,
'get_status': kpxcEvent.onGetStatus,
'get_tab_information': kpxcEvent.onGetTabInformation,
'get_status': kpxcEvent.getStatus,
'get_tab_information': kpxcEvent.getTabInformation,
'get_totp': keepass.getTotp,
'hide_getting_started_guide_alert': kpxcEvent.hideGettingStartedGuideAlert,
'hide_troubleshooting_guide_alert': kpxcEvent.hideTroubleshootingGuideAlert,
@ -272,12 +288,15 @@ kpxcEvent.messageHandlers = {
'init_http_auth': kpxcEvent.initHttpAuth,
'is_connected': kpxcEvent.getIsKeePassXCAvailable,
'is_iframe_allowed': page.isIframeAllowed,
'is_protocol_v2': kpxcEvent.isProtocolV2,
'is_site_ignored': page.isSiteIgnored,
'load_keyring': kpxcEvent.onLoadKeyRing,
'load_settings': kpxcEvent.onLoadSettings,
'load_keyring': kpxcEvent.loadKeyRing,
'load_settings': kpxcEvent.loadSettings,
'lock_database': kpxcEvent.lockDatabase,
'page_clear_auto_lock_requested': page.clearAutoLockRequested,
'page_clear_logins': kpxcEvent.pageClearLogins,
'page_clear_submitted': page.clearSubmittedCredentials,
'page_get_auto_lock_requested': page.getAutoLockRequested,
'page_get_autosubmit_performed': page.getAutoSubmitPerformed,
'page_get_login_id': page.getLoginId,
'page_get_manual_fill': page.getManualFill,
@ -292,16 +311,16 @@ kpxcEvent.messageHandlers = {
'passkeys_register': keepass.passkeysRegister,
'password_get_filled': kpxcEvent.passwordGetFilled,
'password_set_filled': kpxcEvent.passwordSetFilled,
'popup_login': kpxcEvent.onLoginPopup,
'reconnect': kpxcEvent.onReconnect,
'remove_credentials_from_tab_information': kpxcEvent.onRemoveCredentialsFromTabInformation,
'popup_login': kpxcEvent.initLoginPopup,
'reconnect': kpxcEvent.reconnect,
'remove_credentials_from_tab_information': kpxcEvent.removeCredentialsFromTabInformation,
'request_autotype': keepass.requestAutotype,
'retrieve_credentials': page.retrieveCredentials,
'show_default_browseraction': browserAction.showDefault,
'update_credentials': keepass.updateCredentials,
'username_field_detected': kpxcEvent.onUsernameFieldDetected,
'save_settings': kpxcEvent.onSaveSettings,
'update_available_keepassxc': kpxcEvent.onUpdateAvailableKeePassXC,
'username_field_detected': kpxcEvent.usernameFieldDetected,
'save_settings': kpxcEvent.saveSettings,
'update_available_keepassxc': kpxcEvent.updateAvailableKeePassXC,
'update_context_menu': page.updateContextMenu,
'update_popup': page.updatePopup
};

View file

@ -47,9 +47,9 @@ httpAuth.handleRequestCallback = function(details, callback) {
httpAuth.processPendingCallbacks(details, callback, callback);
};
httpAuth.retrieveCredentials = async function(tabId, url, submitUrl) {
return await keepass.retrieveCredentials(tabId, [ url, submitUrl, false, true ]).catch((err) => {
logError('httpAuth.retrieveCredentials error: ' + err);
httpAuth.getCredentials = async function(tabId, url, submitUrl) {
return await keepass.getCredentials(tabId, [ url, submitUrl, false, true ]).catch((err) => {
logError('httpAuth.getCredentials error: ' + err);
return Promise.reject();
});
};
@ -72,7 +72,7 @@ httpAuth.processPendingCallbacks = async function(details, resolve, reject) {
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);
};
@ -90,7 +90,10 @@ httpAuth.loginOrShowCredentials = function(logins, details, resolve, reject) {
if (page.settings.showNotifications) {
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 {
logError('No logins found for HTTP Basic Auth.');

View file

@ -1,21 +1,24 @@
'use strict';
const keepass = {};
keepass.associated = { 'value': false, 'hash': null };
keepass.keyPair = { publicKey: null, secretKey: null };
keepass.serverPublicKey = '';
keepass.associated = { value: false, hash: null };
keepass.cacheTimeout = 30 * 1000; // Milliseconds
keepass.clientID = '';
keepass.currentKeePassXC = '';
keepass.databaseAssociationStatuses = {};
keepass.databaseHash = ''; // Hash of the active database
keepass.databaseStatuses = [];
keepass.isConnected = false;
keepass.isDatabaseClosed = true;
keepass.isKeePassXCAvailable = false;
keepass.isEncryptionKeyUnrecognized = false;
keepass.currentKeePassXC = '';
keepass.requiredKeePassXC = '2.3.1';
keepass.isKeePassXCAvailable = false;
keepass.keyPair = { publicKey: null, secretKey: null };
keepass.latestVersionUrl = 'https://api.github.com/repos/keepassxreboot/keepassxc/releases/latest';
keepass.cacheTimeout = 30 * 1000; // Milliseconds
keepass.databaseHash = '';
keepass.previousDatabaseHash = '';
keepass.protocolV2 = false;
keepass.reconnectLoop = null;
keepass.requiredKeePassXC = '2.3.1';
keepass.serverPublicKey = '';
const kpActions = {
SET_LOGIN: 'set-login',
@ -33,7 +36,91 @@ const kpActions = {
GET_TOTP: 'get-totp',
REQUEST_AUTOTYPE: 'request-autotype',
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) => {
@ -42,626 +129,75 @@ browser.storage.local.get({ 'latestKeePassXC': { 'version': '', 'lastChecked': n
});
//--------------------------------------------------------------------------
// Commands
// Command wrappers for events
//--------------------------------------------------------------------------
keepass.addCredentials = async function(tab, args = []) {
const [ username, password, url, group, groupUuid ] = args;
return keepass.updateCredentials(tab, [ null, username, password, url, group, groupUuid ]);
keepass.associate = async function(tab, args = []) {
return keepass.protocolV2 ? await protocol.associate(tab, args) : await keepassProtocol.associate(tab, args);
};
keepass.updateCredentials = async function(tab, args = []) {
try {
const [ entryId, username, password, url, group, groupUuid ] = args;
const taResponse = await keepass.testAssociation(tab);
if (!taResponse) {
browserAction.showDefault(tab);
return [];
}
const kpAction = kpActions.SET_LOGIN;
const [ dbid ] = keepass.getCryptoKey();
const nonce = keepassClient.getNonce();
const messageData = {
action: kpAction,
id: dbid,
login: username,
password: password,
url: url,
submitUrl: url
};
if (entryId) {
messageData.uuid = entryId;
}
if (!entryId && page.settings.downloadFaviconAfterSave) {
messageData.downloadFavicon = 'true';
}
if (group && groupUuid) {
messageData.group = group;
messageData.groupUuid = groupUuid;
}
const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce);
if (response) {
// KeePassXC versions lower than 2.5.0 will have an empty parsed.error
let successMessage = response.error;
if (response.error === 'success' || response.error === '') {
successMessage = entryId ? 'updated' : 'created';
}
return successMessage;
} else {
return 'error';
}
} catch (err) {
logError(`updateCredentials failed: ${err}`);
return [];
}
};
keepass.retrieveCredentials = async function(tab, args = []) {
try {
const [ url, submiturl, triggerUnlock = false, httpAuth = false ] = args;
const taResponse = await keepass.testAssociation(tab, [ false, triggerUnlock ]);
if (!taResponse) {
browserAction.showDefault(tab);
return [];
}
keepass.clearErrorMessage(tab);
if (!keepass.isConnected) {
return [];
}
let entries = [];
const 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.createCredentials = async function(tab, args = []) {
return keepass.protocolV2
? await protocol.createCredentials(tab, args)
: await keepassProtocol.addCredentials(tab, args);
};
keepass.createNewGroup = async function(tab, args = []) {
try {
const [ groupName ] = args;
const taResponse = await keepass.testAssociation(tab, [ false ]);
if (!taResponse) {
browserAction.showDefault(tab);
return [];
}
return keepass.protocolV2
? await protocol.createNewGroup(tab, args)
: await keepassProtocol.createNewGroup(tab, args);
};
keepass.clearErrorMessage(tab);
keepass.generatePassword = async function(tab, args = []) {
return keepass.protocolV2
? await protocol.generatePassword(tab, args)
: await keepassProtocol.generatePassword(tab, args);
};
if (!keepass.isConnected) {
return [];
}
keepass.getCredentials = async function(tab, args = []) {
return keepass.protocolV2
? await protocol.getCredentials(tab, args)
: await keepassProtocol.retrieveCredentials(tab, args);
};
const kpAction = kpActions.CREATE_NEW_GROUP;
const nonce = keepassClient.getNonce();
keepass.getDatabaseGroups = async function(tab, args = []) {
return keepass.protocolV2
? await protocol.getDatabaseGroups(tab, args)
: await keepassProtocol.getDatabaseGroups(tab, args);
};
const messageData = {
action: kpAction,
groupName: groupName
};
const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce);
if (response) {
keepass.updateLastUsed(keepass.databaseHash);
return response;
} else {
logError('getDatabaseGroups rejected');
}
browserAction.showDefault(tab);
return [];
} catch (err) {
logError(`createNewGroup failed: ${err}`);
return [];
}
keepass.getDatabaseHash = async function(tab, args = []) {
return keepass.protocolV2
? await protocol.getDatabaseStatuses(tab, args)
: await keepassProtocol.getDatabaseHash(tab, args);
};
keepass.getTotp = async function(tab, args = []) {
const [ uuid, oldTotp ] = args;
if (!compareVersion('2.6.1', keepass.currentKeePassXC, true)) {
return oldTotp;
}
const taResponse = await keepass.testAssociation(tab, [ false ]);
if (!taResponse || !keepass.isConnected) {
return;
}
const kpAction = kpActions.GET_TOTP;
const nonce = keepassClient.getNonce();
const messageData = {
action: kpAction,
uuid: uuid
};
try {
const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce);
if (response) {
keepass.updateLastUsed(keepass.databaseHash);
return response.totp;
}
return;
} catch (err) {
logError(`getTotp failed: ${err}`);
}
return keepass.protocolV2 ? await protocol.getTotp(tab, args) : await keepassProtocol.getTotp(tab, args);
};
keepass.requestAutotype = async function(tab, args = []) {
if (!keepass.isConnected) {
keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED);
return false;
}
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.lockDatabase = async function(tab, args = []) {
return keepass.protocolV2 ? await protocol.lockDatabase(tab, args) : await keepassProtocol.lockDatabase(tab, args);
};
keepass.passkeysGet = async function(tab, args = []) {
try {
const taResponse = await keepass.testAssociation(tab, [ false ]);
if (!taResponse || !keepass.isConnected || args.length < 2) {
browserAction.showDefault(tab);
return [];
}
return keepass.protocolV2 ? await protocol.passkeysGet(tab, args) : await keepassProtocol.passkeysGet(tab, args);
};
const kpAction = kpActions.PASSKEYS_GET;
const nonce = keepassClient.getNonce();
const publicKey = args[0];
const origin = args[1];
keepass.passkeysRegister = async function(tab, args = []) {
return keepass.protocolV2 ? await protocol.passkeysGet(tab, args) : await keepassProtocol.passkeysGet(tab, args);
};
const messageData = {
action: kpAction,
publicKey: JSON.parse(JSON.stringify(publicKey)),
origin: origin,
keys: keepass.getCryptoKeys()
};
keepass.requestAutotype = async function (tab, args = []) {
return keepass.protocolV2
? await protocol.requestAutotype(tab, args)
: await keepassProtocol.requestAutotype(tab, args);
};
const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce);
if (response) {
return response;
}
browserAction.showDefault(tab);
return [];
} catch (err) {
logError(`passkeysGet failed: ${err}`);
return [];
}
keepass.updateCredentials = async function (tab, args = []) {
return keepass.protocolV2
? await protocol.updateCredentials(tab, args)
: await keepassProtocol.updateCredentials(tab, args);
};
//--------------------------------------------------------------------------
@ -805,26 +341,55 @@ keepass.disableAutomaticReconnect = function() {
};
keepass.reconnect = async function(tab = null, connectionTimeout = 1500) {
keepassClient.connectToNative();
keepass.generateNewKeyPair();
const keyChangeResult = await keepass.changePublicKeys(tab, !!connectionTimeout, connectionTimeout).catch(() => false);
protocolClient.connectToNative();
protocolClient.generateNewKeyPair();
const keyChangeResult = await protocol
.changePublicKeys(tab, !!connectionTimeout, connectionTimeout)
.catch(() => false);
// Change public keys timeout
if (!keyChangeResult) {
return false;
}
const hash = await keepass.getDatabaseHash(tab);
if (hash !== '') {
keepass.clearErrorMessage(tab);
if (!keepass.protocolV2) {
const hash = await keepass.getDatabaseHash(tab);
if (hash !== '') {
keepass.clearErrorMessage(tab);
}
await keepassProtocol.testAssociation();
await keepass.isConfigured();
}
await keepass.testAssociation();
await keepass.isConfigured();
// TODO: What to do with Protocol V2?
keepass.updateDatabaseHashToContent();
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
//--------------------------------------------------------------------------
@ -863,7 +428,9 @@ keepass.setcurrentKeePassXCVersion = function(version) {
keepass.keePassXCUpdateAvailable = async function() {
const checkUpdate = Number(page.settings.checkUpdateKeePassXC);
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);
if (daysSinceLastCheck >= checkUpdate) {
await keepass.checkForNewKeePassXCVersion();
@ -891,23 +458,6 @@ keepass.checkForNewKeePassXCVersion = async function() {
keepass.latestKeePassXC.lastChecked = new Date().valueOf();
};
keepass.clearErrorMessage = function(tab) {
if (tab && page.tabs[tab.id]) {
page.tabs[tab.id].errorMessage = undefined;
}
};
keepass.handleError = function(tab, errorCode, errorMessage = '') {
if (errorMessage.length === 0) {
errorMessage = kpErrors.getError(errorCode);
}
logError(`${errorCode}: ${errorMessage}`);
if (tab && page.tabs[tab.id]) {
page.tabs[tab.id].errorMessage = errorMessage;
}
};
keepass.updatePopup = function() {
if (page && page.tabs.length > 0) {
browserAction.showDefault();
@ -915,14 +465,23 @@ keepass.updatePopup = function() {
};
// Updates the database hashes to content script
keepass.updateDatabase = async function() {
keepass.updateDatabase = async function(tab) {
keepass.associated.value = false;
keepass.associated.hash = null;
page.clearAllLogins();
await keepass.testAssociation(null, [ true ]);
if (keepass.protocolV2) {
// TODO: Only show "Connect" if the active database is not connected?
// TODO: What if there are credentials from another database but the selected one is not connected?
const result = await protocol.testAssociationFromDatabaseStatuses();
keepass.updatePopup(tab);
keepass.updateDatabaseHashToContent(result);
return;
}
keepass.updatePopup();
// Legacy protocol
await keepassProtocol.testAssociation(null, [ true ]);
keepass.updatePopup(tab);
keepass.updateDatabaseHashToContent();
};
@ -959,7 +518,7 @@ keepass.compareMultipleVersions = function(versions, current, canBeEqual = true)
return result;
};
const removeDuplicateEntries = function(arr) {
keepass.removeDuplicateEntries = function(arr) {
const newArray = [];
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 page = {};
page.autoLockRequested = false;
page.autoSubmitPerformed = false;
page.attributeMenuItems = [];
page.blockedTabs = [];
@ -214,6 +215,14 @@ page.clearSubmittedCredentials = async function() {
page.submittedCredentials = {};
};
page.clearAutoLockRequested = async function() {
page.autoLockRequested = false;
};
page.getAutoLockRequested = async function() {
return page.autoLockRequested;
};
page.createTabEntry = async function(tabId) {
page.tabs[tabId] = {
allowIframes: false,
@ -252,8 +261,14 @@ page.retrieveCredentials = async function(tab, args = []) {
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;
if (credentials.autoLockRequested) {
page.autoLockRequested = true;
}
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 = {
NOT_ASSOCIATED: 0,
ASSOCIATED: 1,

View file

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

View file

@ -144,24 +144,39 @@ kpxcFill.fillTOTPFromUuid = async function(el, uuid) {
return;
}
let totpFound = false;
if (user.totp?.length > 0) {
const protocolV2 = await sendMessage('is_protocol_v2');
// 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) {
kpxcUI.createNotification('warning', tr('credentialsNoTOTPFound'));
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) {
const stringFields = user.stringFields;
for (const s of stringFields) {
const val = s['KPH: {TOTP}'];
if (val) {
kpxcFill.setTOTPValue(el, val);
totpFound = true;
}
}
}
if (!totpFound) {
kpxcUI.createNotification('warning', tr('credentialsNoTOTPFound'));
}
};
// Set normal or segmented TOTP value
@ -251,7 +266,7 @@ kpxcFill.fillInCredentials = async function(combination, predefinedUsername, uui
if (combination.password.maxLength
&& combination.password.maxLength > 0
&& selectedCredentials.password.length > combination.password.maxLength) {
kpxcUI.createNotification('warning', tr('errorMessagePaswordLengthExceeded'));
kpxcUI.createNotification('warning', tr('errorMessagePasswordLengthExceeded'));
}
// 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 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

View file

@ -15,6 +15,7 @@ const sendMessage = async function(action, args) {
* The main content script object.
*/
const kpxc = {};
kpxc.associationStatus = undefined;
kpxc.combinations = [];
kpxc.credentials = [];
kpxc.databaseState = DatabaseState.DISCONNECTED;
@ -23,8 +24,8 @@ kpxc.improvedFieldDetectionEnabledForPage = false;
kpxc.inputs = [];
kpxc.settings = {};
kpxc.singleInputEnabledForPage = false;
kpxc.submitUrl = null;
kpxc.url = null;
kpxc.submitUrl = undefined;
kpxc.url = undefined;
// Add page to Site Preferences with a selected option enabled. Set from the popup.
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
kpxc.detectDatabaseChange = async function(response) {
kpxc.associationStatus = response?.associateResult;
kpxc.databaseState = DatabaseState.LOCKED;
kpxc.clearAllFromPage();
kpxcIcons.switchIcons();
@ -120,7 +122,8 @@ kpxc.detectDatabaseChange = async function(response) {
_called.retrieveCredentials = false;
const settings = await sendMessage('load_settings');
kpxc.settings = settings;
kpxc.databaseState = DatabaseState.UNLOCKED;
kpxc.databaseState = response?.associateResult?.areAllLocked
? DatabaseState.LOCKED : DatabaseState.UNLOCKED;
await kpxc.initCredentialFields();
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
// is handled here when the opened database has been regognized. It's not a pretty hack.
const manualFill = await sendMessage('page_get_manual_fill');
if (manualFill !== ManualFill.NONE && kpxc.combinations.length > 0) {
if (manualFill !== ManualFill.NONE) {
await kpxcFill.fillInFromActiveElement(manualFill === ManualFill.PASSWORD);
await sendMessage('page_set_manual_fill', ManualFill.NONE);
}
@ -385,6 +388,16 @@ kpxc.initCredentialFields = async function() {
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) {
await kpxc.retrieveCredentials();
}
@ -828,6 +841,8 @@ kpxc.updateDatabaseState = async function() {
};
// 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() {
let uuid = await sendMessage('page_get_login_id');
if (uuid === undefined || kpxc.credentials.length === 0) {

View file

@ -128,6 +128,7 @@ const iconClicked = async function(field, icon) {
if (kpxc.databaseState !== DatabaseState.UNLOCKED) {
// Triggers database unlock
await sendMessage('page_set_manual_fill', ManualFill.BOTH);
// TODO: Replace with open-database or get-database-statuses? With legacyProtocol keepass.getDatabaseHash must be used.
await sendMessage('get_database_hash', [ false, true ]); // Set triggerUnlock to true
field.focus();
}

View file

@ -98,6 +98,37 @@ code {
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 {
padding-left: 0px;
}
@ -196,6 +227,15 @@ code {
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 {
background-color: var(--kpxc-table-hover-color) !important;
color: var(--kpxc-text-color) !important;

View file

@ -24,10 +24,14 @@
</button>
<button id="choose-custom-login-fields-button" class="btn btn-sm btn-warning" data-i18n="[title]popupChooseCredentialsText"></button>
</div>
<div class="lock-button-area">
<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>
<div class="btn-group lock-button-area">
<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>
<select id="dropdown-button" class="btn btn-sm btn-danger dropdown-toggle" style="display: none"></select>
</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>

View file

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

View file

@ -53,6 +53,40 @@ async function getLoginData() {
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 () => {
if (document.readyState === 'complete' || (document.readyState !== 'loading' && !document.documentElement.doScroll)) {
await initSettings();
@ -60,7 +94,39 @@ async function getLoginData() {
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({
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 id="choose-custom-login-fields-button" class="btn btn-sm btn-warning" data-i18n="[title]popupChooseCredentialsText"></button>
</div>
<div class="lock-button-area">
<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>
<div class="btn-group lock-button-area">
<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>
<select id="dropdown-button" class="btn btn-sm btn-danger dropdown-toggle" style="display: none"></select>
</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>

View file

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

View file

@ -24,10 +24,14 @@
</button>
<button id="choose-custom-login-fields-button" class="btn btn-sm btn-warning" data-i18n="[title]popupChooseCredentialsText"></button>
</div>
<div class="lock-button-area">
<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>
<div class="btn-group lock-button-area">
<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>
<select id="dropdown-button" class="btn btn-sm btn-danger dropdown-toggle" style="display: none"></select>
</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>

View file

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