diff --git a/.eslintrc b/.eslintrc index 68baaa4..15b2bf7 100644 --- a/.eslintrc +++ b/.eslintrc @@ -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", diff --git a/.prettierignore b/.prettierignore index a1fc10d..d018a53 100644 --- a/.prettierignore +++ b/.prettierignore @@ -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 diff --git a/dist/manifest_firefox.json b/dist/manifest_firefox.json index 81e8fa8..18b2493 100644 --- a/dist/manifest_firefox.json +++ b/dist/manifest_firefox.json @@ -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": [ diff --git a/keepassxc-browser/_locales/en/messages.json b/keepassxc-browser/_locales/en/messages.json index 715e50f..c4615d4 100644 --- a/keepassxc-browser/_locales/en/messages.json +++ b/keepassxc-browser/_locales/en/messages.json @@ -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!", diff --git a/keepassxc-browser/background/background_service.js b/keepassxc-browser/background/background_service.js index e38e1db..e93f898 100644 --- a/keepassxc-browser/background/background_service.js +++ b/keepassxc-browser/background/background_service.js @@ -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); diff --git a/keepassxc-browser/background/browserAction.js b/keepassxc-browser/background/browserAction.js index 1a28354..4b9d540 100755 --- a/keepassxc-browser/background/browserAction.js +++ b/keepassxc-browser/background/browserAction.js @@ -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; } diff --git a/keepassxc-browser/background/client.js b/keepassxc-browser/background/client.js deleted file mode 100644 index c98b01d..0000000 --- a/keepassxc-browser/background/client.js +++ /dev/null @@ -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); -}; diff --git a/keepassxc-browser/background/event.js b/keepassxc-browser/background/event.js index a6d3055..54507f7 100755 --- a/keepassxc-browser/background/event.js +++ b/keepassxc-browser/background/event.js @@ -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 }; diff --git a/keepassxc-browser/background/httpauth.js b/keepassxc-browser/background/httpauth.js index 4b5e61c..e2339cd 100755 --- a/keepassxc-browser/background/httpauth.js +++ b/keepassxc-browser/background/httpauth.js @@ -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.'); diff --git a/keepassxc-browser/background/keepass.js b/keepassxc-browser/background/keepass.js index c1d1367..5a5c228 100755 --- a/keepassxc-browser/background/keepass.js +++ b/keepassxc-browser/background/keepass.js @@ -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) { diff --git a/keepassxc-browser/background/legacyProtocol.js b/keepassxc-browser/background/legacyProtocol.js new file mode 100644 index 0000000..86273c0 --- /dev/null +++ b/keepassxc-browser/background/legacyProtocol.js @@ -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; + } +}; + diff --git a/keepassxc-browser/background/legacyProtocolClient.js b/keepassxc-browser/background/legacyProtocolClient.js new file mode 100644 index 0000000..43ddfae --- /dev/null +++ b/keepassxc-browser/background/legacyProtocolClient.js @@ -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'; +}; diff --git a/keepassxc-browser/background/page.js b/keepassxc-browser/background/page.js index 06d7880..e404976 100755 --- a/keepassxc-browser/background/page.js +++ b/keepassxc-browser/background/page.js @@ -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; }; diff --git a/keepassxc-browser/background/protocol.js b/keepassxc-browser/background/protocol.js new file mode 100644 index 0000000..7f53fb5 --- /dev/null +++ b/keepassxc-browser/background/protocol.js @@ -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 + } + ]; +}; diff --git a/keepassxc-browser/background/protocolClient.js b/keepassxc-browser/background/protocolClient.js new file mode 100644 index 0000000..fe4f8cc --- /dev/null +++ b/keepassxc-browser/background/protocolClient.js @@ -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); + } +}; diff --git a/keepassxc-browser/common/global.js b/keepassxc-browser/common/global.js index 55de510..abdfc1d 100755 --- a/keepassxc-browser/common/global.js +++ b/keepassxc-browser/common/global.js @@ -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, diff --git a/keepassxc-browser/content/banner.js b/keepassxc-browser/content/banner.js index eb732a3..7f1261a 100644 --- a/keepassxc-browser/content/banner.js +++ b/keepassxc-browser/content/banner.js @@ -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')); diff --git a/keepassxc-browser/content/fill.js b/keepassxc-browser/content/fill.js index b7846cb..60ed8d4 100644 --- a/keepassxc-browser/content/fill.js +++ b/keepassxc-browser/content/fill.js @@ -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 diff --git a/keepassxc-browser/content/keepassxc-browser.js b/keepassxc-browser/content/keepassxc-browser.js index 881c113..7ff3411 100755 --- a/keepassxc-browser/content/keepassxc-browser.js +++ b/keepassxc-browser/content/keepassxc-browser.js @@ -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) { diff --git a/keepassxc-browser/content/username-field.js b/keepassxc-browser/content/username-field.js index 6f3afb1..d8fb19f 100644 --- a/keepassxc-browser/content/username-field.js +++ b/keepassxc-browser/content/username-field.js @@ -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(); } diff --git a/keepassxc-browser/popups/popup.css b/keepassxc-browser/popups/popup.css index 3f0929f..c1b6ee2 100644 --- a/keepassxc-browser/popups/popup.css +++ b/keepassxc-browser/popups/popup.css @@ -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; diff --git a/keepassxc-browser/popups/popup.html b/keepassxc-browser/popups/popup.html index 5c8d5cb..d64820e 100644 --- a/keepassxc-browser/popups/popup.html +++ b/keepassxc-browser/popups/popup.html @@ -24,10 +24,14 @@ -
diff --git a/keepassxc-browser/popups/popup.js b/keepassxc-browser/popups/popup.js index 2823284..16deb48 100644 --- a/keepassxc-browser/popups/popup.js +++ b/keepassxc-browser/popups/popup.js @@ -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 () => { diff --git a/keepassxc-browser/popups/popup_functions.js b/keepassxc-browser/popups/popup_functions.js index 82ad454..4a7b011 100644 --- a/keepassxc-browser/popups/popup_functions.js +++ b/keepassxc-browser/popups/popup_functions.js @@ -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(); + }); })(); diff --git a/keepassxc-browser/popups/popup_httpauth.html b/keepassxc-browser/popups/popup_httpauth.html index c494a0d..b057014 100644 --- a/keepassxc-browser/popups/popup_httpauth.html +++ b/keepassxc-browser/popups/popup_httpauth.html @@ -24,10 +24,14 @@ - diff --git a/keepassxc-browser/popups/popup_httpauth.js b/keepassxc-browser/popups/popup_httpauth.js index 3369f85..8afa6cc 100644 --- a/keepassxc-browser/popups/popup_httpauth.js +++ b/keepassxc-browser/popups/popup_httpauth.js @@ -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 () => { diff --git a/keepassxc-browser/popups/popup_login.html b/keepassxc-browser/popups/popup_login.html index 57dad2f..18418d3 100644 --- a/keepassxc-browser/popups/popup_login.html +++ b/keepassxc-browser/popups/popup_login.html @@ -24,10 +24,14 @@ - diff --git a/keepassxc-browser/popups/popup_login.js b/keepassxc-browser/popups/popup_login.js index 4703b9a..f40befe 100644 --- a/keepassxc-browser/popups/popup_login.js +++ b/keepassxc-browser/popups/popup_login.js @@ -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 }); }); })();