From 8639ab2288ca238c53cd3d9d5c1b7c908eb559d3 Mon Sep 17 00:00:00 2001 From: varjolintu Date: Mon, 24 Apr 2023 14:19:42 +0300 Subject: [PATCH] New draft --- .eslintrc | 3 + keepassxc-browser/background/client.js | 468 ------------ keepassxc-browser/background/event.js | 45 +- keepassxc-browser/background/keepass.js | 696 +++--------------- .../background/legacyProtocol.js | 576 +++++++++++++++ .../background/legacyProtocolClient.js | 201 +++++ keepassxc-browser/background/page.js | 3 +- keepassxc-browser/background/protocol.js | 201 +++-- .../background/protocolClient.js | 258 +++++++ keepassxc-browser/content/banner.js | 8 +- .../content/keepassxc-browser.js | 15 +- keepassxc-browser/content/username-field.js | 2 +- keepassxc-browser/manifest.json | 6 +- keepassxc-browser/popups/popup.js | 63 +- 14 files changed, 1392 insertions(+), 1153 deletions(-) delete mode 100644 keepassxc-browser/background/client.js create mode 100644 keepassxc-browser/background/legacyProtocol.js create mode 100644 keepassxc-browser/background/legacyProtocolClient.js create mode 100644 keepassxc-browser/background/protocolClient.js diff --git a/.eslintrc b/.eslintrc index cb4c5c4..d54feef 100644 --- a/.eslintrc +++ b/.eslintrc @@ -115,6 +115,7 @@ "jQuery": true, "keepass": true, "keepassClient": true, + "keepassProtocol": true, "kpActions": true, "kpErrors": true, "kpxc": true, @@ -149,6 +150,8 @@ "MIN_TOTP_INPUT_LENGTH": true, "nacl": true, "page": true, + "protocol": true, + "protocolClient": true, "Pixels": true, "PREDEFINED_SITELIST": true, "resizePopup": true, diff --git a/keepassxc-browser/background/client.js b/keepassxc-browser/background/client.js deleted file mode 100644 index 4a5352d..0000000 --- a/keepassxc-browser/background/client.js +++ /dev/null @@ -1,468 +0,0 @@ -'use strict'; - -const keepassClient = {}; -keepassClient.keySize = 24; -keepassClient.messageTimeout = 500; // Milliseconds -keepassClient.nativeHostName = 'org.keepassxc.keepassxc_browser'; -keepassClient.nativePort = null; - -const kpErrors = { - UNKNOWN_ERROR: 0, - DATABASE_NOT_OPENED: 1, - DATABASE_HASH_NOT_RECEIVED: 2, - CLIENT_PUBLIC_KEY_NOT_RECEIVED: 3, - CANNOT_DECRYPT_MESSAGE: 4, - TIMEOUT_OR_NOT_CONNECTED: 5, - ACTION_CANCELLED_OR_DENIED: 6, - PUBLIC_KEY_NOT_FOUND: 7, - ASSOCIATION_FAILED: 8, - KEY_CHANGE_FAILED: 9, - ENCRYPTION_KEY_UNRECOGNIZED: 10, - NO_SAVED_DATABASES_FOUND: 11, - INCORRECT_ACTION: 12, - EMPTY_MESSAGE_RECEIVED: 13, - NO_URL_PROVIDED: 14, - NO_LOGINS_FOUND: 15, - - errorMessages: { - 0: { msg: tr('errorMessageUnknown') }, - 1: { msg: tr('errorMessageDatabaseNotOpened') }, - 2: { msg: tr('errorMessageDatabaseHash') }, - 3: { msg: tr('errorMessageClientPublicKey') }, - 4: { msg: tr('errorMessageDecrypt') }, - 5: { msg: tr('errorMessageTimeout') }, - 6: { msg: tr('errorMessageCanceled') }, - 7: { msg: tr('errorMessageEncrypt') }, - 8: { msg: tr('errorMessageAssociate') }, - 9: { msg: tr('errorMessageKeyExchange') }, - 10: { msg: tr('errorMessageEncryptionKey') }, - 11: { msg: tr('errorMessageSavedDatabases') }, - 12: { msg: tr('errorMessageIncorrectAction') }, - 13: { msg: tr('errorMessageEmptyMessage') }, - 14: { msg: tr('errorMessageNoURL') }, - 15: { msg: tr('errorMessageNoLogins') } - }, - - getError(errorCode) { - return this.errorMessages[errorCode].msg; - } -}; - -const messageBuffer = { - buffer: [], - - addMessage(msg) { - if (!this.buffer.includes(msg)) { - this.buffer.push(msg); - } - }, - - matchAndRemove(msg) { - for (let i = 0; i < this.buffer.length; ++i) { - if (msg.nonce && msg.nonce === keepassClient.incrementedNonce(this.buffer[i].nonce)) { - this.buffer.splice(i, 1); - return true; - } - } - - return false; - }, - - matchAndRemoveV2(msg) { - for (let i = 0; i < this.buffer.length; ++i) { - if (msg?.requestID === this.buffer[i].requestID) { - this.buffer.splice(i, 1); - return true; - } - } - - return false; - } -}; - -//-------------------------------------------------------------------------- -// Messaging -//-------------------------------------------------------------------------- - -keepassClient.sendNativeMessage = function(request, enableTimeout = false, timeoutValue) { - return new Promise((resolve, reject) => { - let timeout; - const requestAction = request.action; - const ev = keepassClient.nativePort.onMessage; - - const listener = ((port, action) => { - const handler = (msg) => { - if (msg && msg?.action === action) { - // If the request has a separate requestID, check if it matches when there's no nonce (an error message) - const isNotificationOrError = !msg.nonce && request.requestID === msg.requestID; - - // Only resolve a matching response or a notification (without nonce) - if (isNotificationOrError || messageBuffer.matchAndRemoveV2(msg)) { - port.removeListener(handler); - if (enableTimeout) { - clearTimeout(timeout); - } - - resolve(msg); - return; - } - } - }; - return handler; - })(ev, requestAction); - ev.addListener(listener); - - const messageTimeout = timeoutValue || keepassClient.messageTimeout; - - // Handle timeouts - if (enableTimeout) { - timeout = setTimeout(() => { - const errorMessage = { - action: requestAction, - error: kpErrors.getError(kpErrors.TIMEOUT_OR_NOT_CONNECTED), - errorCode: kpErrors.TIMEOUT_OR_NOT_CONNECTED - }; - keepass.isKeePassXCAvailable = false; - ev.removeListener(listener.handler); - resolve(errorMessage); - }, messageTimeout); - } - - // Store the request to the buffer - messageBuffer.addMessage(request); - - // Send the request - if (keepassClient.nativePort) { - keepassClient.nativePort.postMessage(request); - } - }); -}; - -keepassClient.handleResponse = function(response, incrementedNonce, tab) { - if (response.message && response.nonce) { - const res = keepassClient.decrypt(response.message, response.nonce); - if (!res) { - keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE); - return undefined; - } - - const message = nacl.util.encodeUTF8(res); - const parsed = JSON.parse(message); - - if (keepassClient.verifyResponse(parsed, incrementedNonce)) { - return parsed; - } - } else if (response.error && response.errorCode) { - keepass.handleError(tab, response.errorCode, response.error); - } - - return undefined; -}; - -keepassClient.buildRequest = function(action, encrypted, nonce, clientID, triggerUnlock = false) { - const request = { - action: action, - message: encrypted, - nonce: nonce, - clientID: clientID - }; - - if (triggerUnlock) { - request.triggerUnlock = 'true'; - } - - return request; -}; - -keepassClient.sendMessage = async function(kpAction, tab, messageData, nonce, enableTimeout = false, triggerUnlock = false) { - const request = keepassClient.buildRequest(kpAction, keepassClient.encrypt(messageData, nonce), nonce, keepass.clientID, triggerUnlock); - if (messageData.requestID) { - request['requestID'] = messageData.requestID; - } - - const response = await keepassClient.sendNativeMessage(request, enableTimeout); - const incrementedNonce = keepassClient.incrementedNonce(nonce); - - return keepassClient.handleResponse(response, incrementedNonce, tab); -}; - -//-------------------------------------------------------------------------- -// Protocol V2 -//-------------------------------------------------------------------------- - -keepassClient.sendNativeMessageV2 = function(requestAction, request, enableTimeout = false, timeoutValue) { - return new Promise((resolve, reject) => { - let timeout; - const ev = keepassClient.nativePort.onMessage; - - const listener = ((port) => { - const handler = (msg) => { - if (msg && msg?.requestID === request.requestID) { - // Only resolve a matching response - if (messageBuffer.matchAndRemove(msg)) { - port.removeListener(handler); - if (enableTimeout) { - clearTimeout(timeout); - } - - resolve(msg); - return; - } - } - }; - return handler; - })(ev); - ev.addListener(listener); - - const messageTimeout = timeoutValue || keepassClient.messageTimeout; - - // Handle timeouts - if (enableTimeout) { - timeout = setTimeout(() => { - const errorMessage = { - action: requestAction, - error: kpErrors.getError(kpErrors.TIMEOUT_OR_NOT_CONNECTED), - errorCode: kpErrors.TIMEOUT_OR_NOT_CONNECTED - }; - keepass.isKeePassXCAvailable = false; - ev.removeListener(listener.handler); - resolve(errorMessage); - }, messageTimeout); - } - - // Store the request to the buffer - messageBuffer.addMessage(request); - - // Send the request - if (keepassClient.nativePort) { - keepassClient.nativePort.postMessage(request); - } - }); -}; - -keepassClient.sendMessageV2 = async function(kpAction, tab, messageData, nonce, enableTimeout = false, triggerUnlock = false) { - const request = keepassClient.buildRequestV2(keepassClient.encrypt(messageData, nonce), nonce, keepass.clientID, messageData.requestID, triggerUnlock); - const response = await keepassClient.sendNativeMessageV2(kpAction, request, enableTimeout); - const incrementedNonce = keepassClient.incrementedNonce(nonce); - - return keepassClient.handleResponseV2(response, incrementedNonce, tab); -}; - -keepassClient.buildRequestV2 = function(encryptedMessage, nonce, clientID, requestID, triggerUnlock = false) { - const request = { - message: encryptedMessage, - nonce: nonce, - clientID: clientID, - requestID: requestID - }; - - if (triggerUnlock) { - request.triggerUnlock = 'true'; - } - - return request; -}; - -keepassClient.handleResponseV2 = function(response, incrementedNonce, tab) { - if (response.message && response.nonce) { - const res = keepassClient.decrypt(response.message, response.nonce); - if (!res) { - keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE); - return undefined; - } - - const message = nacl.util.encodeUTF8(res); - const parsed = JSON.parse(message); - - if (keepassClient.verifyResponseV2(parsed, incrementedNonce)) { - return parsed; - } - } else if (response.error && response.errorCode) { - keepass.handleError(tab, response.errorCode, response.error); - } - - return undefined; -}; - -keepassClient.verifyResponseV2 = function(response, nonce) { - if (!keepassClient.checkNonceLength(response.nonce)) { - logError('Incorrect nonce length'); - return false; - } - - if (response.nonce !== nonce) { - logError('Nonce compare failed'); - return false; - } - - return true; -}; - -//-------------------------------------------------------------------------- -// Utils -//-------------------------------------------------------------------------- - -keepassClient.getNonce = function() { - return nacl.util.encodeBase64(nacl.randomBytes(keepassClient.keySize)); -}; - -// Creates a random 8 character string for Request ID -keepassClient.getRequestId = function() { - return Math.random().toString(16).substring(2, 10); -}; - -keepassClient.incrementedNonce = function(nonce) { - const oldNonce = nacl.util.decodeBase64(nonce); - const newNonce = oldNonce.slice(0); - - // from libsodium/utils.c - let i = 0; - let c = 1; - for (; i < newNonce.length; ++i) { - c += newNonce[i]; - newNonce[i] = c; - c >>= 8; - } - - return nacl.util.encodeBase64(newNonce); -}; - -keepassClient.getNonces = function() { - const nonce = keepassClient.getNonce(); - const incrementedNonce = keepassClient.incrementedNonce(nonce); - return [ nonce, incrementedNonce ]; -}; - -keepassClient.verifyKeyResponse = function(response, key, nonce) { - if (!response.success || !response.publicKey) { - keepass.associated.hash = null; - return false; - } - - if (!keepassClient.checkNonceLength(response.nonce)) { - logError('Invalid nonce length.'); - return false; - } - - const reply = (response.nonce === nonce); - if (response.publicKey && reply) { - keepass.serverPublicKey = nacl.util.decodeBase64(response.publicKey); - return true; - } - - return reply; -}; - -keepassClient.verifyResponse = function(response, nonce, id) { - keepass.associated.value = response.success; - if (response.success !== 'true') { - keepass.associated.hash = null; - return false; - } - - keepass.associated.hash = keepass.databaseHash; - - if (!keepassClient.checkNonceLength(response.nonce)) { - return false; - } - - keepass.associated.value = (response.nonce === nonce); - if (keepass.associated.value === false) { - logError('Nonce compare failed'); - return false; - } - - if (id) { - keepass.associated.value = (keepass.associated.value && id === response.id); - } - - keepass.associated.hash = (keepass.associated.value) ? keepass.databaseHash : null; - return keepass.isAssociated(); -}; - -keepassClient.verifyDatabaseResponse = function(response, nonce) { - if (response.success !== 'true') { - keepass.associated.hash = null; - return false; - } - - if (!keepassClient.checkNonceLength(response.nonce)) { - logError('Invalid nonce length.'); - return false; - } - - if (response.nonce !== nonce) { - logError('Nonce compare failed.'); - return false; - } - - keepass.associated.hash = response.hash; - return response.hash !== '' && response.success === 'true'; -}; - -keepassClient.checkNonceLength = function(nonce) { - return nacl.util.decodeBase64(nonce).length === nacl.secretbox.nonceLength; -}; - -keepassClient.encrypt = function(input, nonce) { - const messageData = nacl.util.decodeUTF8(JSON.stringify(input)); - const messageNonce = nacl.util.decodeBase64(nonce); - - if (keepass.serverPublicKey) { - const message = nacl.box(messageData, messageNonce, keepass.serverPublicKey, keepass.keyPair.secretKey); - if (message) { - return nacl.util.encodeBase64(message); - } - } - - return ''; -}; - -keepassClient.decrypt = function(input, nonce) { - const m = nacl.util.decodeBase64(input); - const n = nacl.util.decodeBase64(nonce); - const res = nacl.box.open(m, n, keepass.serverPublicKey, keepass.keyPair.secretKey); - return res; -}; - -//-------------------------------------------------------------------------- -// Native Messaging related -//-------------------------------------------------------------------------- - -keepassClient.connectToNative = function() { - if (keepassClient.nativePort) { - keepassClient.nativePort.disconnect(); - } - keepassClient.nativeConnect(); -}; - -keepassClient.nativeConnect = function() { - console.log(`${EXTENSION_NAME}: Connecting to native messaging host ${keepassClient.nativeHostName}`); - keepassClient.nativePort = browser.runtime.connectNative(keepassClient.nativeHostName); - keepassClient.nativePort.onMessage.addListener(keepassClient.onNativeMessage); - keepassClient.nativePort.onDisconnect.addListener(onDisconnected); - keepass.isConnected = true; - return keepassClient.nativePort; -}; - -function onDisconnected() { - keepassClient.nativePort = null; - keepass.isConnected = false; - keepass.isDatabaseClosed = true; - keepass.isKeePassXCAvailable = false; - keepass.associated.value = false; - keepass.associated.hash = null; - keepass.databaseHash = ''; - - page.clearAllLogins(); - keepass.updatePopup('cross'); - keepass.updateDatabaseHashToContent(); - logError(`Failed to connect: ${(browser.runtime.lastError === null ? 'Unknown error' : browser.runtime.lastError.message)}`); -} - -keepassClient.onNativeMessage = function(response) { - // Handle database lock/unlock status - if (response.action === kpActions.DATABASE_LOCKED || response.action === kpActions.DATABASE_UNLOCKED) { - keepass.updateDatabase(); - } -}; diff --git a/keepassxc-browser/background/event.js b/keepassxc-browser/background/event.js index 14bd0b2..e9e33fb 100755 --- a/keepassxc-browser/background/event.js +++ b/keepassxc-browser/background/event.js @@ -15,7 +15,7 @@ kpxcEvent.onMessage = async function(request, sender) { kpxcEvent.showStatus = async function(tab, configured, internalPoll) { let keyId = null; - if (configured && keepass.databaseHash !== '') { + if (configured && keepass.databaseHash !== '' && keepass.keyRing[keepass.databaseHash]) { keyId = keepass.keyRing[keepass.databaseHash].id; } @@ -27,16 +27,17 @@ kpxcEvent.showStatus = async function(tab, configured, internalPoll) { const usernameFieldDetected = page.tabs[tab.id]?.usernameFieldDetected ?? false; return { - identifier: keyId, + associated: keepass.isAssociated(), configured: configured, databaseClosed: keepass.isDatabaseClosed, - keePassXCAvailable: keepass.isKeePassXCAvailable, + databaseAssociationStatuses: keepass.databaseAssosiationStatuses, encryptionKeyUnrecognized: keepass.isEncryptionKeyUnrecognized, - associated: keepass.isAssociated(), - error: errorMessage, - usernameFieldDetected: usernameFieldDetected, + error: errorMessage || null, + identifier: keyId, + keePassXCAvailable: keepass.isKeePassXCAvailable, showGettingStartedGuideAlert: page.settings.showGettingStartedGuideAlert, - showTroubleshootingGuideAlert: page.settings.showTroubleshootingGuideAlert + showTroubleshootingGuideAlert: page.settings.showTroubleshootingGuideAlert, + usernameFieldDetected: usernameFieldDetected }; }; @@ -54,6 +55,7 @@ kpxcEvent.onLoadKeyRing = async function() { }); keepass.keyRing = item.keyRing; + // TODO: What to do here? if (keepass.isAssociated() && !keepass.keyRing[keepass.associated.hash]) { keepass.associated = { value: false, @@ -73,14 +75,22 @@ kpxcEvent.onGetStatus = async function(tab, args = []) { // When internalPoll is true the event is triggered from content script in intervals -> don't poll KeePassXC try { const [ internalPoll = false, triggerUnlock = false ] = args; - if (!internalPoll) { - const response = await keepass.testAssociation(tab, [ true, triggerUnlock ]); - if (!response) { - return kpxcEvent.showStatus(tab, false); + let configured = false; + + if (keepass.protocolV2) { + const response = await protocol.testAssociationFromDatabaseStatuses(tab, [ true, triggerUnlock ]); + configured = response.isAnyAssociated; + } else { + if (!internalPoll) { + const response = await keepassProtocol.testAssociation(tab, [ true, triggerUnlock ]); + if (!response) { + return kpxcEvent.showStatus(tab, false); + } } + + configured = await keepass.isConfigured(); } - const configured = await keepass.isConfigured(); return kpxcEvent.showStatus(tab, configured, internalPoll); } catch (err) { logError('No status shown: ' + err); @@ -125,10 +135,11 @@ kpxcEvent.onGetConnectedDatabase = async function() { }; kpxcEvent.onGetKeePassXCVersions = async function(tab) { - if (keepass.currentKeePassXC === '') { - await keepass.getDatabaseHash(tab); + // TODO: Maybe this is not needed? + /*if (keepass.currentKeePassXC === '') { + await keepass.getDatabaseHash(tab); // TODO: How to get just the version? A separate API call? return { 'current': keepass.currentKeePassXC, 'latest': keepass.latestKeePassXC.version }; - } + }*/ return { 'current': keepass.currentKeePassXC, 'latest': keepass.latestKeePassXC.version }; }; @@ -227,11 +238,11 @@ kpxcEvent.sendBackToTabs = async function(tab, args = []) { // All methods named in this object have to be declared BEFORE this! kpxcEvent.messageHandlers = { - 'add_credentials': keepass.addCredentials, 'associate': keepass.associate, 'check_database_hash': keepass.checkDatabaseHash, 'check_update_keepassxc': kpxcEvent.onCheckUpdateKeePassXC, 'compare_version': kpxcEvent.compareVersion, + 'create_credentials': keepass.createCredentials, 'create_new_group': keepass.createNewGroup, 'enable_automatic_reconnect': keepass.enableAutomaticReconnect, 'disable_automatic_reconnect': keepass.disableAutomaticReconnect, @@ -240,7 +251,7 @@ kpxcEvent.messageHandlers = { 'generate_password': keepass.generatePassword, 'get_color_theme': kpxcEvent.getColorTheme, 'get_connected_database': kpxcEvent.onGetConnectedDatabase, - 'get_database_hash': keepass.getDatabaseHash, + 'get_database_hash': keepass.getDatabaseHash, // TODO ? 'get_database_groups': keepass.getDatabaseGroups, 'get_keepassxc_versions': kpxcEvent.onGetKeePassXCVersions, 'get_login_list': page.getLoginList, diff --git a/keepassxc-browser/background/keepass.js b/keepassxc-browser/background/keepass.js index b74b7fe..a24833d 100755 --- a/keepassxc-browser/background/keepass.js +++ b/keepassxc-browser/background/keepass.js @@ -33,583 +33,98 @@ const kpActions = { CREATE_NEW_GROUP: 'create-new-group', GET_TOTP: 'get-totp', REQUEST_AUTOTYPE: 'request-autotype', - // V2 + // Protocol V2 CREATE_CREDENTIALS: 'create-credentials', - GET_CREDENTIALS: 'get-credentials' + GET_CREDENTIALS: 'get-credentials', + GET_DATABASE_STATUSES: 'get-database-statuses' }; +const kpErrors = { + UNKNOWN_ERROR: 0, + DATABASE_NOT_OPENED: 1, + DATABASE_HASH_NOT_RECEIVED: 2, + CLIENT_PUBLIC_KEY_NOT_RECEIVED: 3, + CANNOT_DECRYPT_MESSAGE: 4, + TIMEOUT_OR_NOT_CONNECTED: 5, + ACTION_CANCELLED_OR_DENIED: 6, + PUBLIC_KEY_NOT_FOUND: 7, + ASSOCIATION_FAILED: 8, + KEY_CHANGE_FAILED: 9, + ENCRYPTION_KEY_UNRECOGNIZED: 10, + NO_SAVED_DATABASES_FOUND: 11, + INCORRECT_ACTION: 12, + EMPTY_MESSAGE_RECEIVED: 13, + NO_URL_PROVIDED: 14, + NO_LOGINS_FOUND: 15, + + errorMessages: { + 0: { msg: tr('errorMessageUnknown') }, + 1: { msg: tr('errorMessageDatabaseNotOpened') }, + 2: { msg: tr('errorMessageDatabaseHash') }, + 3: { msg: tr('errorMessageClientPublicKey') }, + 4: { msg: tr('errorMessageDecrypt') }, + 5: { msg: tr('errorMessageTimeout') }, + 6: { msg: tr('errorMessageCanceled') }, + 7: { msg: tr('errorMessageEncrypt') }, + 8: { msg: tr('errorMessageAssociate') }, + 9: { msg: tr('errorMessageKeyExchange') }, + 10: { msg: tr('errorMessageEncryptionKey') }, + 11: { msg: tr('errorMessageSavedDatabases') }, + 12: { msg: tr('errorMessageIncorrectAction') }, + 13: { msg: tr('errorMessageEmptyMessage') }, + 14: { msg: tr('errorMessageNoURL') }, + 15: { msg: tr('errorMessageNoLogins') } + }, + + getError(errorCode) { + return this.errorMessages[errorCode].msg; + } +}; + + browser.storage.local.get({ 'latestKeePassXC': { 'version': '', 'lastChecked': null }, 'keyRing': {} }).then((item) => { keepass.latestKeePassXC = item.latestKeePassXC; keepass.keyRing = item.keyRing; }); //-------------------------------------------------------------------------- -// Commands +// Command wrappers //-------------------------------------------------------------------------- -keepass.addCredentials = async function(tab, args = []) { - const [ username, password, url, group, groupUuid ] = args; - return keepass.updateCredentials(tab, [ null, username, password, url, group, groupUuid ]); +keepass.associate = async function(tab, args = []) { + return keepass.protocolV2 ? await protocol.associate(tab, args) : await keepassProtocol.associate(tab, args); }; -keepass.updateCredentials = async function(tab, args = []) { - try { - const [ entryId, username, password, url, group, groupUuid ] = args; - const taResponse = await keepass.testAssociation(tab); - if (!taResponse) { - browserAction.showDefault(tab); - return []; - } - - const kpAction = kpActions.SET_LOGIN; - const [ dbid ] = keepass.getCryptoKey(); - const nonce = keepassClient.getNonce(); - - const messageData = { - action: kpAction, - id: dbid, - login: username, - password: password, - url: url, - submitUrl: url - }; - - if (entryId) { - messageData.uuid = entryId; - } - - if (!entryId && page.settings.downloadFaviconAfterSave) { - messageData.downloadFavicon = 'true'; - } - - if (group && groupUuid) { - messageData.group = group; - messageData.groupUuid = groupUuid; - } - - const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce); - if (response) { - // KeePassXC versions lower than 2.5.0 will have an empty parsed.error - let successMessage = response.error; - if (response.error === 'success' || response.error === '') { - successMessage = entryId ? 'updated' : 'created'; - } - - return successMessage; - } else { - return 'error'; - } - } catch (err) { - logError(`updateCredentials failed: ${err}`); - return []; - } -}; - -keepass.retrieveCredentials = async function(tab, args = []) { - try { - const [ url, submiturl, triggerUnlock = false, httpAuth = false ] = args; - const taResponse = await keepass.testAssociation(tab, [ false, triggerUnlock ]); - if (!taResponse) { - browserAction.showDefault(tab); - return []; - } - - keepass.clearErrorMessage(tab); - - if (!keepass.isConnected) { - return []; - } - - let entries = []; - const keys = []; - const kpAction = kpActions.GET_LOGINS; - const nonce = keepassClient.getNonce(); - const [ dbid ] = keepass.getCryptoKey(); - - for (const keyHash in keepass.keyRing) { - keys.push({ - id: keepass.keyRing[keyHash].id, - key: keepass.keyRing[keyHash].key - }); - } - - const messageData = { - action: kpAction, - id: dbid, - url: url, - keys: keys - }; - - if (submiturl) { - messageData.submitUrl = submiturl; - } - - if (httpAuth) { - messageData.httpAuth = 'true'; - } - - const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce); - if (response) { - entries = keepass.removeDuplicateEntries(response.entries); - keepass.updateLastUsed(keepass.databaseHash); - - if (entries.length === 0) { - // Questionmark-icon is not triggered, so we have to trigger for the normal symbol - browserAction.showDefault(tab); - } - - logDebug(`Found ${entries.length} entries for url ${url}`); - return entries; - } - - browserAction.showDefault(tab); - return []; - } catch (err) { - logError(`retrieveCredentials failed: ${err}`); - return []; - } -}; - -keepass.generatePassword = async function(tab) { - if (!keepass.isConnected) { - return []; - } - - try { - const taResponse = await keepass.testAssociation(tab); - if (!taResponse) { - browserAction.showDefault(tab); - return []; - } - - if (!keepass.compareVersion(keepass.requiredKeePassXC, keepass.currentKeePassXC)) { - return []; - } - - let password; - const kpAction = kpActions.GENERATE_PASSWORD; - const nonce = keepassClient.getNonce(); - - const messageData = { - action: kpAction, - nonce: nonce, - clientID: keepass.clientID, - requestID: keepassClient.getRequestId() - }; - - const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce); - if (response) { - password = response.entries ?? response.password; - keepass.updateLastUsed(keepass.databaseHash); - } else { - logError('generatePassword rejected'); - } - - return password; - } catch (err) { - logError(`generatePassword failed: ${err}`); - return []; - } -}; - -keepass.associate = async function(tab) { - if (keepass.isAssociated()) { - return AssociatedAction.ASSOCIATED; - } - - try { - await keepass.getDatabaseHash(tab); - if (keepass.isDatabaseClosed || !keepass.isKeePassXCAvailable) { - return AssociatedAction.NOT_ASSOCIATED; - } - - keepass.clearErrorMessage(tab); - - const kpAction = kpActions.ASSOCIATE; - const key = nacl.util.encodeBase64(keepass.keyPair.publicKey); - const nonce = keepassClient.getNonce(); - const idKeyPair = nacl.box.keyPair(); - const idKey = nacl.util.encodeBase64(idKeyPair.publicKey); - - const messageData = { - action: kpAction, - key: key, - idKey: idKey - }; - - const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce, false, true); - if (response) { - // Use public key as identification key with older KeePassXC releases - const savedKey = keepass.compareVersion('2.3.4', keepass.currentKeePassXC) ? idKey : key; - keepass.setCryptoKey(response.id, savedKey); // Save the new identification public key as id key for the database - keepass.associated.value = true; - keepass.associated.hash = response.hash || 0; - - browserAction.show(tab); - return AssociatedAction.NEW_ASSOCIATION; - } - - keepass.handleError(tab, kpErrors.ASSOCIATION_FAILED); - return AssociatedAction.NOT_ASSOCIATED; - } catch (err) { - logError(`associate failed: ${err}`); - } - - return AssociatedAction.NOT_ASSOCIATED; -}; - -keepass.testAssociation = async function(tab, args = []) { - keepass.clearErrorMessage(tab); - - try { - const [ enableTimeout = false, triggerUnlock = false ] = args; - const dbHash = await keepass.getDatabaseHash(tab, [ enableTimeout, triggerUnlock ]); - if (!dbHash) { - return false; - } - - if (keepass.isDatabaseClosed || !keepass.isKeePassXCAvailable) { - return false; - } - - if (keepass.isAssociated()) { - return true; - } - - if (!keepass.serverPublicKey) { - if (tab && page.tabs[tab.id]) { - keepass.handleError(tab, kpErrors.PUBLIC_KEY_NOT_FOUND); - } - return false; - } - - const kpAction = kpActions.TEST_ASSOCIATE; - const nonce = keepassClient.getNonce(); - const [ dbid, dbkey ] = keepass.getCryptoKey(); - - if (dbkey === null || dbid === null) { - if (tab && page.tabs[tab.id]) { - keepass.handleError(tab, kpErrors.NO_SAVED_DATABASES_FOUND); - } - return false; - } - - const messageData = { - action: kpAction, - id: dbid, - key: dbkey - }; - - const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce, enableTimeout); - if (!response) { - const hash = response.hash || 0; - keepass.deleteKey(hash); - keepass.isEncryptionKeyUnrecognized = true; - keepass.handleError(tab, kpErrors.ENCRYPTION_KEY_UNRECOGNIZED); - keepass.associated.value = false; - keepass.associated.hash = null; - } else if (!keepass.isAssociated()) { - keepass.handleError(tab, kpErrors.ASSOCIATION_FAILED); - } else { - keepass.isEncryptionKeyUnrecognized = false; - keepass.clearErrorMessage(tab); - } - - return keepass.isAssociated(); - } catch (err) { - logError(`testAssociation failed: ${err}`); - return false; - } -}; - -keepass.getDatabaseHash = async function(tab, args = []) { - if (!keepass.isConnected) { - keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED); - return ''; - } - - if (!keepass.serverPublicKey) { - keepass.changePublicKeys(tab); - } - - const [ enableTimeout = false, triggerUnlock = false ] = args; - const kpAction = kpActions.GET_DATABASE_HASH; - const [ nonce, incrementedNonce ] = keepassClient.getNonces(); - - const messageData = { - action: kpAction, - connectedKeys: Object.keys(keepass.keyRing) // This will be removed in the future - }; - - const encrypted = keepassClient.encrypt(messageData, nonce); - if (encrypted.length <= 0) { - keepass.handleError(tab, kpErrors.PUBLIC_KEY_NOT_FOUND); - keepass.updateDatabaseHashToContent(); - return keepass.databaseHash; - } - - try { - const request = keepassClient.buildRequest(kpAction, keepassClient.encrypt(messageData, nonce), nonce, keepass.clientID, triggerUnlock); - const response = await keepassClient.sendNativeMessage(request, enableTimeout); - if (response.message && response.nonce) { - const res = keepassClient.decrypt(response.message, response.nonce); - if (!res) { - keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE); - return ''; - } - - const message = nacl.util.encodeUTF8(res); - const parsed = JSON.parse(message); - if (keepassClient.verifyDatabaseResponse(parsed, incrementedNonce) && parsed.hash) { - const oldDatabaseHash = keepass.databaseHash; - keepass.setcurrentKeePassXCVersion(parsed.version); - keepass.databaseHash = parsed.hash || ''; - - if (oldDatabaseHash && oldDatabaseHash !== keepass.databaseHash) { - keepass.associated.value = false; - keepass.associated.hash = null; - } - - keepass.isDatabaseClosed = false; - keepass.isKeePassXCAvailable = true; - - // Update the databaseHash from legacy hash - if (parsed.oldHash) { - keepass.updateDatabaseHash(parsed.oldHash, parsed.hash); - } - - return parsed.hash; - } else if (parsed.errorCode) { - keepass.databaseHash = ''; - keepass.isDatabaseClosed = true; - keepass.handleError(tab, kpErrors.DATABASE_NOT_OPENED); - return keepass.databaseHash; - } - - return keepass.databaseHash; - } - - keepass.databaseHash = ''; - keepass.isDatabaseClosed = true; - if (response.message && response.message === '') { - keepass.isKeePassXCAvailable = false; - keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED); - } else { - keepass.handleError(tab, response.errorCode, response.error); - } - return keepass.databaseHash; - } catch (err) { - logError(`getDatabaseHash failed: ${err}`); - return keepass.databaseHash; - } -}; - -keepass.changePublicKeys = async function(tab, enableTimeout = false, connectionTimeout) { - if (!keepass.isConnected) { - keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED); - return false; - } - - const kpAction = kpActions.CHANGE_PUBLIC_KEYS; - const key = nacl.util.encodeBase64(keepass.keyPair.publicKey); - const [ nonce, incrementedNonce ] = keepassClient.getNonces(); - keepass.clientID = nacl.util.encodeBase64(nacl.randomBytes(keepassClient.keySize)); - - const request = { - action: kpAction, - publicKey: key, - nonce: nonce, - clientID: keepass.clientID - }; - - try { - const response = await keepassClient.sendNativeMessage(request, enableTimeout, connectionTimeout); - keepass.setcurrentKeePassXCVersion(response.version); - - if (!keepassClient.verifyKeyResponse(response, key, incrementedNonce)) { - if (tab && page.tabs[tab.id]) { - keepass.handleError(tab, kpErrors.KEY_CHANGE_FAILED); - } - - keepass.updateDatabaseHashToContent(); - return false; - } - - keepass.isKeePassXCAvailable = true; - console.log(`${EXTENSION_NAME}: Server public key: ${nacl.util.encodeBase64(keepass.serverPublicKey)}`); - return true; - } catch (err) { - logError(`changePublicKeys failed: ${err}`); - return false; - } -}; - -keepass.lockDatabase = async function(tab) { - if (!keepass.isConnected) { - keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED); - return false; - } - - const kpAction = kpActions.LOCK_DATABASE; - const nonce = keepassClient.getNonce(); - - const messageData = { - action: kpAction - }; - - - try { - const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce); - if (response) { - keepass.isDatabaseClosed = true; - keepass.updateDatabase(); - - // Display error message in the popup - keepass.handleError(tab, kpErrors.DATABASE_NOT_OPENED); - return true; - } else { - keepass.isDatabaseClosed = true; - } - - return false; - } catch (err) { - logError(`ockDatabase failed: ${err}`); - return false; - } -}; - -keepass.getDatabaseGroups = async function(tab) { - try { - const taResponse = await keepass.testAssociation(tab, [ false ]); - if (!taResponse) { - browserAction.showDefault(tab); - return []; - } - - keepass.clearErrorMessage(tab); - - if (!keepass.isConnected) { - return []; - } - - let groups = []; - const kpAction = kpActions.GET_DATABASE_GROUPS; - const nonce = keepassClient.getNonce(); - - const messageData = { - action: kpAction - }; - - const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce); - if (response) { - groups = response.groups; - groups.defaultGroup = page.settings.defaultGroup; - groups.defaultGroupAlwaysAsk = page.settings.defaultGroupAlwaysAsk; - keepass.updateLastUsed(keepass.databaseHash); - return groups; - } - - browserAction.showDefault(tab); - return []; - } catch (err) { - logError(`getDatabaseGroups failed: ${err}`); - return []; - } +keepass.createCredentials = async function(tab, args = []) { + return keepass.protocolV2 ? await protocol.createCredentials(tab, args) : await keepassProtocol.addCredentials(tab, args); }; keepass.createNewGroup = async function(tab, args = []) { - try { - const [ groupName ] = args; - const taResponse = await keepass.testAssociation(tab, [ false ]); - if (!taResponse) { - browserAction.showDefault(tab); - return []; - } + return keepass.protocolV2 ? await protocol.createNewGroup(tab, args) : await keepassProtocol.createNewGroup(tab, args); +}; - keepass.clearErrorMessage(tab); +keepass.getCredentials = async function(tab, args = []) { + return keepass.protocolV2 ? await protocol.getCredentials(tab, args) : await keepassProtocol.retrieveCredentials(tab, args); +}; - if (!keepass.isConnected) { - return []; - } +keepass.getDatabaseGroups = async function(tab, args = []) { + return keepass.protocolV2 ? await protocol.getDatabaseGroups(tab, args) : await keepassProtocol.getDatabaseGroups(tab, args); +}; - const kpAction = kpActions.CREATE_NEW_GROUP; - const nonce = keepassClient.getNonce(); - - const messageData = { - action: kpAction, - groupName: groupName - }; - - const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce); - if (response) { - keepass.updateLastUsed(keepass.databaseHash); - return response; - } else { - logError('getDatabaseGroups rejected'); - } - - browserAction.showDefault(tab); - return []; - } catch (err) { - logError(`createNewGroup failed: ${err}`); - return []; - } +keepass.getDatabaseHash = async function(tab, args = []) { + return keepass.protocolV2 ? await protocol.getDatabaseStatuses(tab, args) : await keepassProtocol.getDatabaseHash(tab, args); }; keepass.getTotp = async function(tab, args = []) { - const [ uuid, oldTotp ] = args; - if (!keepass.compareVersion('2.6.1', keepass.currentKeePassXC, true)) { - return oldTotp; - } - - const taResponse = await keepass.testAssociation(tab, [ false ]); - if (!taResponse || !keepass.isConnected) { - return; - } - - const kpAction = kpActions.GET_TOTP; - const nonce = keepassClient.getNonce(); - - const messageData = { - action: kpAction, - uuid: uuid - }; - - try { - const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce); - if (response) { - keepass.updateLastUsed(keepass.databaseHash); - return response.totp; - } - - return; - } catch (err) { - logError(`getTotp failed: ${err}`); - } + return keepass.protocolV2 ? await protocol.getTotp(tab, args) : await keepassProtocol.getTotp(tab, args); }; -keepass.requestAutotype = async function(tab, args = []) { - if (!keepass.isConnected) { - keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED); - return false; - } +keepass.lockDatabase = async function(tab, args = []) { + return keepass.protocolV2 ? await protocol.lockDatabase(tab, args) : await keepassProtocol.lockDatabase(tab, args); +}; - const kpAction = kpActions.REQUEST_AUTOTYPE; - const nonce = keepassClient.getNonce(); - const search = getTopLevelDomainFromUrl(args[0]); - - const messageData = { - action: kpAction, - search: search - }; - - try { - const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce); - return response; - } catch (err) { - logError(`requestAutotype failed: ${err}`); - return false; - } +keepass.updateCredentials = async function(tab, args = []) { + return keepass.protocolV2 ? await protocol.updateCredentials(tab, args) : await keepassProtocol.updateCredentials(tab, args); }; //-------------------------------------------------------------------------- @@ -739,34 +254,55 @@ keepass.disableAutomaticReconnect = function() { }; keepass.reconnect = async function(tab, connectionTimeout) { - keepassClient.connectToNative(); - keepass.generateNewKeyPair(); - const keyChangeResult = await keepass.changePublicKeys(tab, true, connectionTimeout).catch(() => false); + protocolClient.connectToNative(); + protocolClient.generateNewKeyPair(); + const keyChangeResult = await protocol.changePublicKeys(tab, true, connectionTimeout).catch(() => false); // Change public keys timeout if (!keyChangeResult) { return false; } - const hash = await keepass.getDatabaseHash(tab); - if (hash !== '') { - keepass.clearErrorMessage(tab); - } + // What to do here? + if (!keepass.protocolV2) { + // Needed? + const hash = await keepass.getDatabaseHash(tab); + if (hash !== '') { + keepass.clearErrorMessage(tab); + } - await keepass.testAssociation(); - await keepass.isConfigured(); + await keepass.testAssociation(); + await keepass.isConfigured(); + } keepass.updateDatabaseHashToContent(); return true; }; //-------------------------------------------------------------------------- -// Utils +// Error handling //-------------------------------------------------------------------------- -keepass.generateNewKeyPair = function() { - keepass.keyPair = nacl.box.keyPair(); +keepass.clearErrorMessage = function(tab) { + if (tab && page.tabs[tab.id]) { + page.tabs[tab.id].errorMessage = undefined; + } }; +keepass.handleError = function(tab, errorCode, errorMessage = '') { + if (errorMessage.length === 0) { + errorMessage = kpErrors.getError(errorCode); + } + + logError(`${errorCode}: ${errorMessage}`); + if (tab && page.tabs[tab.id]) { + page.tabs[tab.id].errorMessage = errorMessage; + } +}; + +//-------------------------------------------------------------------------- +// Utils +//-------------------------------------------------------------------------- + keepass.isConfigured = async function() { if (typeof(keepass.databaseHash) === 'undefined') { const hash = keepass.getDatabaseHash(); @@ -787,7 +323,6 @@ keepass.isAssociated = function() { keepass.setcurrentKeePassXCVersion = function(version) { if (version) { keepass.currentKeePassXC = version; - keepass.protocolV2 = keepass.compareVersion('2.8.0', version); } }; @@ -837,23 +372,6 @@ keepass.checkForNewKeePassXCVersion = function() { keepass.latestKeePassXC.lastChecked = new Date().valueOf(); }; -keepass.clearErrorMessage = function(tab) { - if (tab && page.tabs[tab.id]) { - page.tabs[tab.id].errorMessage = undefined; - } -}; - -keepass.handleError = function(tab, errorCode, errorMessage = '') { - if (errorMessage.length === 0) { - errorMessage = kpErrors.getError(errorCode); - } - - logError(`${errorCode}: ${errorMessage}`); - if (tab && page.tabs[tab.id]) { - page.tabs[tab.id].errorMessage = errorMessage; - } -}; - keepass.updatePopup = function() { if (page && page.tabs.length > 0) { browserAction.showDefault(); @@ -866,19 +384,29 @@ keepass.updateDatabase = async function() { keepass.associated.hash = null; page.clearAllLogins(); - await keepass.testAssociation(null, [ true ]); + if (keepass.protocolV2) { + // TODO: Only show "Connect" if the active database is not connected? + // TODO: What if there are credentials from another database but the selected one is not connected? + const result = await protocol.testAssociationFromDatabaseStatuses(); + keepass.updatePopup(); + keepass.updateDatabaseHashToContent(result); + return; + } + // Legacy protocol + await keepassProtocol.testAssociation(null, [ true ]); keepass.updatePopup(); keepass.updateDatabaseHashToContent(); }; -keepass.updateDatabaseHashToContent = async function() { +keepass.updateDatabaseHashToContent = async function(associateResult = {}) { try { const tab = await getCurrentTab(); if (tab) { // Send message to content script browser.tabs.sendMessage(tab.id, { action: 'check_database_hash', + associateResult: associateResult, hash: { old: keepass.previousDatabaseHash, new: keepass.databaseHash }, connected: keepass.isKeePassXCAvailable }).catch((err) => { diff --git a/keepassxc-browser/background/legacyProtocol.js b/keepassxc-browser/background/legacyProtocol.js new file mode 100644 index 0000000..568188a --- /dev/null +++ b/keepassxc-browser/background/legacyProtocol.js @@ -0,0 +1,576 @@ +'use strict'; + +// Legacy protocol, "old" client for KeePassXC 2.7.5 and older. +const keepassProtocol = {}; + +//-------------------------------------------------------------------------- +// Commands +//-------------------------------------------------------------------------- + +keepassProtocol.addCredentials = async function(tab, args = []) { + const [ username, password, url, group, groupUuid ] = args; + return keepass.updateCredentials(tab, [ null, username, password, url, group, groupUuid ]); +}; + +keepassProtocol.associate = async function(tab) { + if (keepass.isAssociated()) { + return AssociatedAction.ASSOCIATED; + } + + try { + await keepassProtocol.getDatabaseHash(tab); + if (keepass.isDatabaseClosed || !keepass.isKeePassXCAvailable) { + return AssociatedAction.NOT_ASSOCIATED; + } + + keepass.clearErrorMessage(tab); + + const kpAction = kpActions.ASSOCIATE; + const key = nacl.util.encodeBase64(keepass.keyPair.publicKey); + const nonce = protocolClient.getNonce(); + const idKeyPair = nacl.box.keyPair(); + const idKey = nacl.util.encodeBase64(idKeyPair.publicKey); + + const messageData = { + action: kpAction, + key: key, + idKey: idKey + }; + + const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce, false, true); + if (response) { + // Use public key as identification key with older KeePassXC releases + const savedKey = keepass.compareVersion('2.3.4', keepass.currentKeePassXC) ? idKey : key; + keepass.setCryptoKey(response.id, savedKey); // Save the new identification public key as id key for the database + keepass.associated.value = true; + keepass.associated.hash = response.hash || 0; + + browserAction.show(tab); + return AssociatedAction.NEW_ASSOCIATION; + } + + keepass.handleError(tab, kpErrors.ASSOCIATION_FAILED); + return AssociatedAction.NOT_ASSOCIATED; + } catch (err) { + logError(`associate failed: ${err}`); + } + + return AssociatedAction.NOT_ASSOCIATED; +}; + +keepassProtocol.createNewGroup = async function(tab, args = []) { + try { + const [ groupName ] = args; + const taResponse = await keepassProtocol.testAssociation(tab, [ false ]); + if (!taResponse) { + browserAction.showDefault(tab); + return []; + } + + keepass.clearErrorMessage(tab); + + if (!keepass.isConnected) { + return []; + } + + const kpAction = kpActions.CREATE_NEW_GROUP; + const nonce = protocolClient.getNonce(); + + const messageData = { + action: kpAction, + groupName: groupName + }; + + const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce); + if (response) { + keepass.updateLastUsed(keepass.databaseHash); + return response; + } else { + logError('getDatabaseGroups rejected'); + } + + browserAction.showDefault(tab); + return []; + } catch (err) { + logError(`createNewGroup failed: ${err}`); + return []; + } +}; + + +keepassProtocol.changePublicKeys = async function(tab, enableTimeout = false, connectionTimeout) { + if (!keepass.isConnected) { + keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED); + return false; + } + + const kpAction = kpActions.CHANGE_PUBLIC_KEYS; + const key = nacl.util.encodeBase64(keepass.keyPair.publicKey); + const [ nonce, incrementedNonce ] = protocolClient.getNonces(); + keepass.clientID = nacl.util.encodeBase64(nacl.randomBytes(protocolClient.keySize)); + + const request = { + action: kpAction, + publicKey: key, + nonce: nonce, + clientID: keepass.clientID + }; + + try { + const response = await keepassClient.sendNativeMessage(request, enableTimeout, connectionTimeout); + keepass.setcurrentKeePassXCVersion(response.version); + + if (!keepassClient.verifyKeyResponse(response, key, incrementedNonce)) { + if (tab && page.tabs[tab.id]) { + keepass.handleError(tab, kpErrors.KEY_CHANGE_FAILED); + } + + keepass.updateDatabaseHashToContent(); + return false; + } + + keepass.isKeePassXCAvailable = true; + console.log(`${EXTENSION_NAME}: Server public key: ${nacl.util.encodeBase64(keepass.serverPublicKey)}`); + return true; + } catch (err) { + logError(`changePublicKeys failed: ${err}`); + return false; + } +}; + +keepassProtocol.generatePassword = async function(tab) { + if (!keepass.isConnected) { + return []; + } + + try { + const taResponse = await keepassProtocol.testAssociation(tab); + if (!taResponse) { + browserAction.showDefault(tab); + return []; + } + + if (!keepass.compareVersion(keepass.requiredKeePassXC, keepass.currentKeePassXC)) { + return []; + } + + let password; + const kpAction = kpActions.GENERATE_PASSWORD; + const nonce = protocolClient.getNonce(); + + const messageData = { + action: kpAction, + nonce: nonce, + clientID: keepass.clientID, + requestID: protocolClient.getRequestId() + }; + + const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce); + if (response) { + password = response.entries ?? response.password; + keepass.updateLastUsed(keepass.databaseHash); + } else { + logError('generatePassword rejected'); + } + + return password; + } catch (err) { + logError(`generatePassword failed: ${err}`); + return []; + } +}; + +keepassProtocol.getDatabaseGroups = async function(tab) { + try { + const taResponse = await keepassProtocol.testAssociation(tab, [ false ]); + if (!taResponse) { + browserAction.showDefault(tab); + return []; + } + + keepass.clearErrorMessage(tab); + + if (!keepass.isConnected) { + return []; + } + + let groups = []; + const kpAction = kpActions.GET_DATABASE_GROUPS; + const nonce = protocolClient.getNonce(); + + const messageData = { + action: kpAction + }; + + const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce); + if (response) { + groups = response.groups; + groups.defaultGroup = page.settings.defaultGroup; + groups.defaultGroupAlwaysAsk = page.settings.defaultGroupAlwaysAsk; + keepass.updateLastUsed(keepass.databaseHash); + return groups; + } + + browserAction.showDefault(tab); + return []; + } catch (err) { + logError(`getDatabaseGroups failed: ${err}`); + return []; + } +}; + + +keepassProtocol.getDatabaseHash = async function(tab, args = []) { + if (!keepass.isConnected) { + keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED); + return ''; + } + + if (!keepass.serverPublicKey) { + keepassProtocol.changePublicKeys(tab); + } + + const [ enableTimeout = false, triggerUnlock = false ] = args; + const kpAction = kpActions.GET_DATABASE_HASH; + const [ nonce, incrementedNonce ] = protocolClient.getNonces(); + + const messageData = { + action: kpAction, + connectedKeys: Object.keys(keepass.keyRing) // This will be removed in the future + }; + + // Why is this here? + /*const encrypted = protocolClient.encrypt(messageData, nonce); + if (encrypted.length <= 0) { + keepass.handleError(tab, kpErrors.PUBLIC_KEY_NOT_FOUND); + keepass.updateDatabaseHashToContent(); + return keepass.databaseHash; + }*/ + + try { + const request = keepassClient.buildRequest(kpAction, protocolClient.encrypt(messageData, nonce), nonce, keepass.clientID, triggerUnlock); + const response = await keepassClient.sendNativeMessage(request, enableTimeout); + if (response.message && response.nonce) { + const res = protocolClient.decrypt(response.message, response.nonce); + if (!res) { + keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE); + return ''; + } + + const message = nacl.util.encodeUTF8(res); + const parsed = JSON.parse(message); + if (keepassClient.verifyDatabaseResponse(parsed, incrementedNonce) && parsed.hash) { + const oldDatabaseHash = keepass.databaseHash; + keepass.setcurrentKeePassXCVersion(parsed.version); + keepass.databaseHash = parsed.hash || ''; + + if (oldDatabaseHash && oldDatabaseHash !== keepass.databaseHash) { + keepass.associated.value = false; + keepass.associated.hash = null; + } + + keepass.isDatabaseClosed = false; + keepass.isKeePassXCAvailable = true; + + // Update the databaseHash from legacy hash + if (parsed.oldHash) { + keepass.updateDatabaseHash(parsed.oldHash, parsed.hash); + } + + return parsed.hash; + } else if (parsed.errorCode) { + keepass.databaseHash = ''; + keepass.isDatabaseClosed = true; + keepass.handleError(tab, kpErrors.DATABASE_NOT_OPENED); + return keepass.databaseHash; + } + + return keepass.databaseHash; + } + + keepass.databaseHash = ''; + keepass.isDatabaseClosed = true; + if (response.message && response.message === '') { + keepass.isKeePassXCAvailable = false; + keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED); + } else { + keepass.handleError(tab, response.errorCode, response.error); + } + return keepass.databaseHash; + } catch (err) { + logError(`getDatabaseHash failed: ${err}`); + return keepass.databaseHash; + } +}; + +keepassProtocol.getTotp = async function(tab, args = []) { + const [ uuid, oldTotp ] = args; + if (!keepass.compareVersion('2.6.1', keepass.currentKeePassXC, true)) { + return oldTotp; + } + + const taResponse = await keepassProtocol.testAssociation(tab, [ false ]); + if (!taResponse || !keepass.isConnected) { + return; + } + + const kpAction = kpActions.GET_TOTP; + const nonce = protocolClient.getNonce(); + + const messageData = { + action: kpAction, + uuid: uuid + }; + + try { + const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce); + if (response) { + keepass.updateLastUsed(keepass.databaseHash); + return response.totp; + } + + return; + } catch (err) { + logError(`getTotp failed: ${err}`); + } +}; + +keepassProtocol.lockDatabase = async function(tab) { + if (!keepass.isConnected) { + keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED); + return false; + } + + const kpAction = kpActions.LOCK_DATABASE; + const nonce = protocolClient.getNonce(); + + const messageData = { + action: kpAction + }; + + + try { + const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce); + if (response) { + keepass.isDatabaseClosed = true; + keepass.updateDatabase(); + + // Display error message in the popup + keepass.handleError(tab, kpErrors.DATABASE_NOT_OPENED); + return true; + } else { + keepass.isDatabaseClosed = true; + } + + return false; + } catch (err) { + logError(`ockDatabase failed: ${err}`); + return false; + } +}; + +keepassProtocol.requestAutotype = async function(tab, args = []) { + if (!keepass.isConnected) { + keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED); + return false; + } + + const kpAction = kpActions.REQUEST_AUTOTYPE; + const nonce = protocolClient.getNonce(); + const search = getTopLevelDomainFromUrl(args[0]); + + const messageData = { + action: kpAction, + search: search + }; + + try { + const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce); + return response; + } catch (err) { + logError(`requestAutotype failed: ${err}`); + return false; + } +}; + +keepassProtocol.retrieveCredentials = async function(tab, args = []) { + try { + const [ url, submiturl, triggerUnlock = false, httpAuth = false ] = args; + const taResponse = await keepassProtocol.testAssociation(tab, [ false, triggerUnlock ]); + if (!taResponse) { + browserAction.showDefault(tab); + return []; + } + + keepass.clearErrorMessage(tab); + + if (!keepass.isConnected) { + return []; + } + + let entries = []; + const keys = []; + const kpAction = kpActions.GET_LOGINS; + const nonce = protocolClient.getNonce(); + const [ dbid ] = keepass.getCryptoKey(); + + for (const keyHash in keepass.keyRing) { + keys.push({ + id: keepass.keyRing[keyHash].id, + key: keepass.keyRing[keyHash].key + }); + } + + const messageData = { + action: kpAction, + id: dbid, + url: url, + keys: keys + }; + + if (submiturl) { + messageData.submitUrl = submiturl; + } + + if (httpAuth) { + messageData.httpAuth = 'true'; + } + + const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce); + if (response) { + entries = keepass.removeDuplicateEntries(response.entries); + keepass.updateLastUsed(keepass.databaseHash); + + if (entries.length === 0) { + // Questionmark-icon is not triggered, so we have to trigger for the normal symbol + browserAction.showDefault(tab); + } + + logDebug(`Found ${entries.length} entries for url ${url}`); + return entries; + } + + browserAction.showDefault(tab); + return []; + } catch (err) { + logError(`retrieveCredentials failed: ${err}`); + return []; + } +}; + +keepassProtocol.testAssociation = async function(tab, args = []) { + keepass.clearErrorMessage(tab); + + try { + const [ enableTimeout = false, triggerUnlock = false ] = args; + const dbHash = await keepassProtocol.getDatabaseHash(tab, [ enableTimeout, triggerUnlock ]); + if (!dbHash) { + return false; + } + + if (keepass.isDatabaseClosed || !keepass.isKeePassXCAvailable) { + return false; + } + + if (keepass.isAssociated()) { + return true; + } + + if (!keepass.serverPublicKey) { + if (tab && page.tabs[tab.id]) { + keepass.handleError(tab, kpErrors.PUBLIC_KEY_NOT_FOUND); + } + return false; + } + + const kpAction = kpActions.TEST_ASSOCIATE; + const nonce = protocolClient.getNonce(); + const [ dbid, dbkey ] = keepass.getCryptoKey(); + + if (dbkey === null || dbid === null) { + if (tab && page.tabs[tab.id]) { + keepass.handleError(tab, kpErrors.NO_SAVED_DATABASES_FOUND); + } + return false; + } + + const messageData = { + action: kpAction, + id: dbid, + key: dbkey + }; + + const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce, enableTimeout); + if (!response) { + const hash = response.hash || 0; + keepass.deleteKey(hash); + keepass.isEncryptionKeyUnrecognized = true; + keepass.handleError(tab, kpErrors.ENCRYPTION_KEY_UNRECOGNIZED); + keepass.associated.value = false; + keepass.associated.hash = null; + } else if (!keepass.isAssociated()) { + keepass.handleError(tab, kpErrors.ASSOCIATION_FAILED); + } else { + keepass.isEncryptionKeyUnrecognized = false; + keepass.clearErrorMessage(tab); + } + + return keepass.isAssociated(); + } catch (err) { + logError(`testAssociation failed: ${err}`); + return false; + } +}; + +keepassProtocol.updateCredentials = async function(tab, args = []) { + try { + const [ entryId, username, password, url, group, groupUuid ] = args; + const taResponse = await keepassProtocol.testAssociation(tab); + if (!taResponse) { + browserAction.showDefault(tab); + return []; + } + + const kpAction = kpActions.SET_LOGIN; + const [ dbid ] = keepass.getCryptoKey(); + const nonce = protocolClient.getNonce(); + + const messageData = { + action: kpAction, + id: dbid, + login: username, + password: password, + url: url, + submitUrl: url + }; + + if (entryId) { + messageData.uuid = entryId; + } + + if (!entryId && page.settings.downloadFaviconAfterSave) { + messageData.downloadFavicon = 'true'; + } + + if (group && groupUuid) { + messageData.group = group; + messageData.groupUuid = groupUuid; + } + + const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce); + if (response) { + // KeePassXC versions lower than 2.5.0 will have an empty parsed.error + let successMessage = response.error; + if (response.error === 'success' || response.error === '') { + successMessage = entryId ? 'updated' : 'created'; + } + + return successMessage; + } else { + return 'error'; + } + } catch (err) { + logError(`updateCredentials failed: ${err}`); + return []; + } +}; diff --git a/keepassxc-browser/background/legacyProtocolClient.js b/keepassxc-browser/background/legacyProtocolClient.js new file mode 100644 index 0000000..69ff16a --- /dev/null +++ b/keepassxc-browser/background/legacyProtocolClient.js @@ -0,0 +1,201 @@ +'use strict'; + +const messageBuffer = { + buffer: [], + + addMessage(msg) { + if (!this.buffer.includes(msg)) { + this.buffer.push(msg); + } + }, + + matchAndRemove(msg) { + for (let i = 0; i < this.buffer.length; ++i) { + if (msg.nonce && msg.nonce === protocolClient.incrementedNonce(this.buffer[i].nonce)) { + this.buffer.splice(i, 1); + return true; + } + } + + return false; + } +}; + +const keepassClient = {}; + +//-------------------------------------------------------------------------- +// Messaging +//-------------------------------------------------------------------------- + +keepassClient.sendNativeMessage = function(request, enableTimeout = false, timeoutValue) { + return new Promise((resolve, reject) => { + let timeout; + const requestAction = request.action; + const ev = protocolClient.nativePort.onMessage; + + const listener = ((port, action) => { + const handler = (msg) => { + if (msg && msg?.action === action) { + // If the request has a separate requestID, check if it matches when there's no nonce (an error message) + const isNotificationOrError = !msg.nonce && request.requestID === msg.requestID; + + // Only resolve a matching response or a notification (without nonce) + if (isNotificationOrError || messageBuffer.matchAndRemove(msg)) { + port.removeListener(handler); + if (enableTimeout) { + clearTimeout(timeout); + } + + resolve(msg); + return; + } + } + }; + return handler; + })(ev, requestAction); + ev.addListener(listener); + + const messageTimeout = timeoutValue || protocolClient.messageTimeout; + + // Handle timeouts + if (enableTimeout) { + timeout = setTimeout(() => { + const errorMessage = { + action: requestAction, + error: kpErrors.getError(kpErrors.TIMEOUT_OR_NOT_CONNECTED), + errorCode: kpErrors.TIMEOUT_OR_NOT_CONNECTED + }; + keepass.isKeePassXCAvailable = false; + ev.removeListener(listener.handler); + resolve(errorMessage); + }, messageTimeout); + } + + // Store the request to the buffer + messageBuffer.addMessage(request); + + // Send the request + if (protocolClient.nativePort) { + protocolClient.nativePort.postMessage(request); + } + }); +}; + +keepassClient.handleResponse = function(response, incrementedNonce, tab) { + if (response.message && response.nonce) { + const res = protocolClient.decrypt(response.message, response.nonce); + if (!res) { + keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE); + return undefined; + } + + const message = nacl.util.encodeUTF8(res); + const parsed = JSON.parse(message); + + if (keepassClient.verifyResponse(parsed, incrementedNonce)) { + return parsed; + } + } else if (response.error && response.errorCode) { + keepass.handleError(tab, response.errorCode, response.error); + } + + return undefined; +}; + +keepassClient.buildRequest = function(action, encrypted, nonce, clientID, triggerUnlock = false) { + const request = { + action: action, + message: encrypted, + nonce: nonce, + clientID: clientID + }; + + if (triggerUnlock) { + request.triggerUnlock = 'true'; + } + + return request; +}; + +keepassClient.sendMessage = async function(kpAction, tab, messageData, nonce, enableTimeout = false, triggerUnlock = false) { + const request = keepassClient.buildRequest(kpAction, protocolClient.encrypt(messageData, nonce), nonce, keepass.clientID, triggerUnlock); + if (messageData.requestID) { + request['requestID'] = messageData.requestID; + } + + const response = await keepassClient.sendNativeMessage(request, enableTimeout); + const incrementedNonce = protocolClient.incrementedNonce(nonce); + + return keepassClient.handleResponse(response, incrementedNonce, tab); +}; + +//-------------------------------------------------------------------------- +// Utils +//-------------------------------------------------------------------------- + +keepassClient.verifyKeyResponse = function(response, key, nonce) { + if (!response.success || !response.publicKey) { + keepass.associated.hash = null; + return false; + } + + if (!protocolClient.checkNonceLength(response.nonce)) { + logError('Invalid nonce length.'); + return false; + } + + const reply = (response.nonce === nonce); + if (response.publicKey && reply) { + keepass.serverPublicKey = nacl.util.decodeBase64(response.publicKey); + return true; + } + + return reply; +}; + +keepassClient.verifyResponse = function(response, nonce, id) { + keepass.associated.value = response.success; + if (response.success !== 'true') { + keepass.associated.hash = null; + return false; + } + + keepass.associated.hash = keepass.databaseHash; + + if (!protocolClient.checkNonceLength(response.nonce)) { + return false; + } + + keepass.associated.value = (response.nonce === nonce); + if (keepass.associated.value === false) { + logError('Nonce compare failed'); + return false; + } + + if (id) { + keepass.associated.value = (keepass.associated.value && id === response.id); + } + + keepass.associated.hash = (keepass.associated.value) ? keepass.databaseHash : null; + return keepass.isAssociated(); +}; + +keepassClient.verifyDatabaseResponse = function(response, nonce) { + if (response.success !== 'true') { + keepass.associated.hash = null; + return false; + } + + if (!protocolClient.checkNonceLength(response.nonce)) { + logError('Invalid nonce length.'); + return false; + } + + if (response.nonce !== nonce) { + logError('Nonce compare failed.'); + return false; + } + + keepass.associated.hash = response.hash; + return response.hash !== '' && response.success === 'true'; +}; diff --git a/keepassxc-browser/background/page.js b/keepassxc-browser/background/page.js index d787818..571a769 100755 --- a/keepassxc-browser/background/page.js +++ b/keepassxc-browser/background/page.js @@ -209,7 +209,8 @@ page.retrieveCredentials = async function(tab, args = []) { page.currentRequest.submitUrl = submitUrl; } - const credentials = await keepass.retrieveCredentials(tab, args); + // TODO: Make keepass.js to handle protocol/protocolClient and legacyProtocol/legacyProcotolClient + const credentials = await keepass.getCredentials(tab, args); page.tabs[tab.id].credentials = credentials; return credentials; }; diff --git a/keepassxc-browser/background/protocol.js b/keepassxc-browser/background/protocol.js index 1d1cf76..21608ea 100644 --- a/keepassxc-browser/background/protocol.js +++ b/keepassxc-browser/background/protocol.js @@ -1,10 +1,12 @@ 'use strict'; +//-------------------------------------------------------------------------- +// Protocol V2 +//-------------------------------------------------------------------------- + const protocol = {}; protocol.associate = async function(tab, args = []) { - console.log('associate'); - if (!keepass.isKeePassXCAvailable) { return AssociatedAction.NOT_ASSOCIATED; } @@ -14,7 +16,6 @@ protocol.associate = async function(tab, args = []) { const kpAction = kpActions.ASSOCIATE; const key = nacl.util.encodeBase64(keepass.keyPair.publicKey); - const nonce = keepassClient.getNonce(); const idKeyPair = nacl.box.keyPair(); const idKey = nacl.util.encodeBase64(idKeyPair.publicKey); @@ -24,7 +25,7 @@ protocol.associate = async function(tab, args = []) { idKey: idKey }; - const response = await keepassClient.sendMessageV2(kpAction, tab, messageData, nonce, false, true); + const response = await protocolClient.sendMessage(kpAction, tab, messageData, false, true); if (response && response.id && response.hash) { keepass.setCryptoKey(response.id, key); // Save the new identification public key as id key for the database @@ -42,8 +43,6 @@ protocol.associate = async function(tab, args = []) { }; protocol.changePublicKeys = async function(tab, enableTimeout = false, connectionTimeout) { - console.log('change-public-keys'); - if (!keepass.isConnected) { keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED); return false; @@ -58,15 +57,17 @@ protocol.changePublicKeys = async function(tab, enableTimeout = false, connectio action: kpAction, publicKey: key, nonce: nonce, - clientID: keepass.clientID + clientID: keepass.clientID, + requestID: keepassClient.getRequestId() }; try { - const response = await keepassClient.sendNativeMessageV2(kpAction, request, enableTimeout, connectionTimeout); + const response = await protocolClient.sendNativeMessage(kpAction, request, enableTimeout, connectionTimeout); keepass.setcurrentKeePassXCVersion(response.version); + keepass.protocolV2 = response?.protocolVersion === 2; const verified = keepass.protocolV2 - ? keepassClient.verifyResponseV2(response, incrementedNonce) + ? protocolClient.verifyNonce(response, incrementedNonce) : keepassClient.verifyKeyResponse(response, key, incrementedNonce); if (!response?.publicKey || !verified) { if (tab && page.tabs[tab.id]) { @@ -88,33 +89,30 @@ protocol.changePublicKeys = async function(tab, enableTimeout = false, connectio }; protocol.createCredentials = async function(tab, args = []) { - console.log('create-credentials'); - const [ username, password, url, group, groupUuid ] = args; return protocol.updateCredentials(tab, [ null, username, password, url, group, groupUuid ]); }; protocol.createNewGroup = async function(tab, args = []) { - console.log('create-new-group'); - if (!keepass.isConnected) { return []; } keepass.clearErrorMessage(tab); - const [ groupName ] = args; const kpAction = kpActions.CREATE_NEW_GROUP; - const nonce = keepassClient.getNonce(); + const [ groupName ] = args; + const [ dbid ] = keepass.getCryptoKey(); const messageData = { action: kpAction, - groupName: groupName + id: dbid, + groupName: groupName, }; try { // TODO: Handle errors - const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce); + const response = await protocolClient.sendMessage(kpAction, tab, messageData); if (response) { keepass.updateLastUsed(keepass.databaseHash); // ? return response; @@ -131,8 +129,6 @@ protocol.createNewGroup = async function(tab, args = []) { }; protocol.generatePassword = async function(tab, args = []) { - console.log('generate-password'); - if (!keepass.isConnected) { return []; } @@ -141,19 +137,17 @@ protocol.generatePassword = async function(tab, args = []) { return []; } - let password; const kpAction = kpActions.GENERATE_PASSWORD; - const nonce = keepassClient.getNonce(); + let password; const messageData = { action: kpAction, - nonce: nonce, clientID: keepass.clientID, - requestID: keepassClient.getRequestId() + requestID: keepassClient.getRequestId() // Needed? }; try { - const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce); + const response = await protocolClient.sendMessage(kpAction, tab, messageData); if (response) { password = response.entries ?? response.password; keepass.updateLastUsed(keepass.databaseHash); // ? @@ -169,25 +163,20 @@ protocol.generatePassword = async function(tab, args = []) { }; protocol.getCredentials = async function(tab, args = []) { - console.log('get-credentials'); - if (!keepass.isConnected) { return []; } keepass.clearErrorMessage(tab); + const kpAction = kpActions.GET_CREDENTIALS; const [ url, submiturl, triggerUnlock = false, httpAuth = false ] = args; let entries = []; - const kpAction = kpActions.GET_CREDENTIALS; - const nonce = keepassClient.getNonce(); - const [ dbid ] = keepass.getCryptoKey(); const messageData = { action: kpAction, - id: dbid, url: url, - keys: keepass.getKeys() + keys: protocol.getKeys() }; if (submiturl) { @@ -200,7 +189,7 @@ protocol.getCredentials = async function(tab, args = []) { try { // TODO: Handle errors - const response = await keepassClient.sendMessageV2(kpAction, tab, messageData, nonce, false, triggerUnlock); + const response = await protocolClient.sendMessage(kpAction, tab, messageData, false, triggerUnlock); if (response) { entries = keepass.removeDuplicateEntries(response.entries); keepass.updateLastUsed(keepass.databaseHash); // What about this? @@ -223,25 +212,24 @@ protocol.getCredentials = async function(tab, args = []) { }; protocol.getDatabaseGroups = async function(tab, args = []) { - console.log('get-database-groups'); - if (!keepass.isConnected) { return []; } keepass.clearErrorMessage(tab); - let groups = []; const kpAction = kpActions.GET_DATABASE_GROUPS; - const nonce = keepassClient.getNonce(); + const [ dbid ] = keepass.getCryptoKey(); + let groups = []; const messageData = { - action: kpAction + action: kpAction, + id: dbid }; try { // TODO: Handle errors - const response = await keepassClient.sendMessageV2(kpAction, tab, messageData, nonce); + const response = await protocolClient.sendMessage(kpAction, tab, messageData); if (response) { groups = response.groups; groups.defaultGroup = page.settings.defaultGroup; @@ -258,33 +246,72 @@ protocol.getDatabaseGroups = async function(tab, args = []) { } }; -// Obsolete? This should be checked inside KeePassXC protocol.getDatabaseStatuses = async function(tab, args = []) { - console.log('get-database-statuses'); + if (!keepass.isConnected) { + keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED); + return; + } + + if (!keepass.serverPublicKey) { + await protocol.changePublicKeys(tab); + } + + const kpAction = kpActions.GET_DATABASE_STATUSES; + const [ enableTimeout = false, triggerUnlock = false ] = args; + + const messageData = { + action: kpAction, + keys: protocol.getKeys() + }; + + try { + const response = await protocolClient.sendMessage(kpAction, tab, messageData, enableTimeout, triggerUnlock); + if (response) { + keepass.databaseHash = response.hash; + + // Return this error only if all databases are closed + if (response?.statuses.every(s => s.locked)) { + keepass.databaseHash = ''; + keepass.isDatabaseClosed = true; + keepass.handleError(tab, kpErrors.DATABASE_NOT_OPENED); + } + + return response; + } + + // TODO: Check if these are even possible ..? + keepass.databaseHash = ''; + keepass.isDatabaseClosed = true; + if (response.message && response.message === '') { + // ..? + keepass.isKeePassXCAvailable = false; + keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED); + } else { + keepass.handleError(tab, response.errorCode, response.error); + } + } catch (err) { + logError(`getDatabaseStatuses failed: ${err}`); + } }; protocol.getTotp = async function(tab, args = []) { - console.log('get-totp'); - if (!keepass.isConnected) { return []; } + const kpAction = kpActions.GET_TOTP; const [ uuid, oldTotp ] = args; if (!keepass.compareVersion('2.6.1', keepass.currentKeePassXC, true)) { return oldTotp; } - const kpAction = kpActions.GET_TOTP; - const nonce = keepassClient.getNonce(); - const messageData = { action: kpAction, uuid: uuid }; try { - const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce); + const response = await protocolClient.sendMessage(kpAction, tab, messageData); if (response) { keepass.updateLastUsed(keepass.databaseHash); return response.totp; @@ -297,22 +324,18 @@ protocol.getTotp = async function(tab, args = []) { }; protocol.lockDatabase = async function(tab, args = []) { - console.log('lock-database'); - if (!keepass.isConnected) { keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED); return false; } const kpAction = kpActions.LOCK_DATABASE; - const nonce = keepassClient.getNonce(); - const messageData = { action: kpAction }; try { - const response = await keepassClient.sendMessageV2(kpAction, tab, messageData, nonce); + const response = await protocolClient.sendMessage(kpAction, tab, messageData); if (response) { //keepass.isDatabaseClosed = true; // ? keepass.updateDatabase(); @@ -332,15 +355,12 @@ protocol.lockDatabase = async function(tab, args = []) { }; protocol.requestAutotype = async function(tab, args = []) { - console.log('request-autotype'); - if (!keepass.isConnected) { keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED); return false; } const kpAction = kpActions.REQUEST_AUTOTYPE; - const nonce = keepassClient.getNonce(); const search = getTopLevelDomainFromUrl(args[0]); const messageData = { @@ -349,7 +369,7 @@ protocol.requestAutotype = async function(tab, args = []) { }; try { - const response = await keepassClient.sendMessageV2(kpAction, tab, messageData, nonce); + const response = await protocolClient.sendMessage(kpAction, tab, messageData); return response?.result; } catch (err) { logError(`requestAutotype failed: ${err}`); @@ -357,6 +377,55 @@ protocol.requestAutotype = async function(tab, args = []) { } }; +protocol.testAssociationFromDatabaseStatuses = async function(tab, args = []) { + const databaseStatuses = await protocol.getDatabaseStatuses(tab, args); + console.log(databaseStatuses); + + const result = { + areAllLocked: true, + associationNeeded: false, + databaseHash: undefined, + isAnyAssociated: false + }; + + // TODO: Handle this already in getDatabaseStatuses? + if (!databaseStatuses || databaseStatuses.statuses.length === 0) { + keepass.handleError(tab, kpErrors.DATABASE_NOT_OPENED); + return result; + } + + const currentDatabaseStatus = databaseStatuses.statuses.filter(s => s.hash === databaseStatuses.hash); + const isCurrentAssociated = currentDatabaseStatus[0]?.associated; + const isCurrentLocked = currentDatabaseStatus[0]?.locked; + + const isAnyAssociated = databaseStatuses.statuses.some(s => s.associated); + const areAllLocked = databaseStatuses.statuses.every(s => s.locked); + + // TODO: Add a warning notification if two databases with identical hashes are regognized. + // To where? DOM? KeePassXC? Popup? Maybe this feature should be in KeePassXC instead when making the request and not here. + if (currentDatabaseStatus.length > 1) { + console.log('Identical databases found.'); + } + + // TODO: If the current one is not associated, activate the Connect button in the popup? + // But only if the current database is not locked.. + if (!isCurrentAssociated && !isCurrentLocked) { + console.log('Current one is not associated'); + + } + + // This should be true only if all databases are locked + keepass.isDatabaseClosed = areAllLocked; // ? + + result.areAllLocked = areAllLocked; + result.associationNeeded = !isCurrentAssociated && !isCurrentLocked; + result.databaseHash = databaseStatuses.hash; + result.isAnyAssociated = isAnyAssociated; + + keepass.databaseAssosiationStatuses = result; + return result; +}; + protocol.updateCredentials = async function(tab, args = []) { console.log('update-credentials'); @@ -364,10 +433,9 @@ protocol.updateCredentials = async function(tab, args = []) { return []; } - const [ entryId, username, password, url, group, groupUuid ] = args; const kpAction = kpActions.CREATE_CREDENTIALS; + const [ entryId, username, password, url, group, groupUuid ] = args; const [ dbid ] = keepass.getCryptoKey(); - const nonce = keepassClient.getNonce(); const messageData = { action: kpAction, @@ -393,7 +461,7 @@ protocol.updateCredentials = async function(tab, args = []) { try { // TODO: Check response messages - const response = await keepassClient.sendMessageV2(kpAction, tab, messageData, nonce); + const response = await protocolClient.sendMessage(kpAction, tab, messageData); if (response) { // KeePassXC versions lower than 2.5.0 will have an empty parsed.error let successMessage = response.error; @@ -410,3 +478,20 @@ protocol.updateCredentials = async function(tab, args = []) { return []; } }; + +//-------------------------------------------------------------------------- +// Utils +//-------------------------------------------------------------------------- + +protocol.getKeys = function() { + const keys = []; + + for (const keyHash in keepass.keyRing) { + keys.push({ + id: keepass.keyRing[keyHash].id, + key: keepass.keyRing[keyHash].key + }); + } + + return keys; +}; diff --git a/keepassxc-browser/background/protocolClient.js b/keepassxc-browser/background/protocolClient.js new file mode 100644 index 0000000..dda60b9 --- /dev/null +++ b/keepassxc-browser/background/protocolClient.js @@ -0,0 +1,258 @@ +'use strict'; + +// Stores 'requestID' and 'action' to internal buffer +const protocolBuffer = { + buffer: [], + + addMessage(msg) { + if (!this.buffer.includes(msg)) { + this.buffer.push({ + action: msg?.action, + requestID: msg?.requestID + }); + } + }, + + matchAndRemove(msg) { + for (let i = 0; i < this.buffer.length; ++i) { + if (msg?.requestID === this.buffer[i].requestID + || (msg.action === 'change-public-keys' && msg?.action === this.buffer[i].action)) { + this.buffer.splice(i, 1); + return true; + } + } + + return false; + } +}; + +//-------------------------------------------------------------------------- +// Protocol V2 +//-------------------------------------------------------------------------- + +const protocolClient = {}; +protocolClient.keySize = 24; +protocolClient.messageTimeout = 500; // Milliseconds +protocolClient.nativeHostName = 'org.keepassxc.keepassxc_browser'; +protocolClient.nativePort = null; + +protocolClient.sendNativeMessage = function(requestAction, request, enableTimeout = false, timeoutValue) { + return new Promise((resolve, reject) => { + let timeout; + const ev = protocolClient.nativePort.onMessage; + + const listener = ((port) => { + const handler = (msg) => { + if (msg && (msg?.requestID === request.requestID || msg?.action === 'change-public-keys')) { + // Only resolve a matching response + if (protocolBuffer.matchAndRemove(msg)) { + port.removeListener(handler); + if (enableTimeout) { + clearTimeout(timeout); + } + + resolve(msg); + return; + } + } + }; + return handler; + })(ev); + ev.addListener(listener); + + const messageTimeout = timeoutValue || protocolClient.messageTimeout; + + // Handle timeouts + if (enableTimeout) { + timeout = setTimeout(() => { + const errorMessage = { + action: requestAction, + error: kpErrors.getError(kpErrors.TIMEOUT_OR_NOT_CONNECTED), + errorCode: kpErrors.TIMEOUT_OR_NOT_CONNECTED + }; + keepass.isKeePassXCAvailable = false; + ev.removeListener(listener.handler); + resolve(errorMessage); + }, messageTimeout); + } + + // Store the request to the buffer + protocolBuffer.addMessage(request); + + // Send the request + if (protocolClient.nativePort) { + protocolClient.nativePort.postMessage(request); + } + }); +}; + +protocolClient.sendMessage = async function(kpAction, tab, messageData, enableTimeout = false, triggerUnlock = false) { + const nonce = protocolClient.getNonce(); + const request = protocolClient.buildRequest(protocolClient.encrypt(messageData, nonce), nonce, keepass.clientID, triggerUnlock); + const response = await protocolClient.sendNativeMessage(kpAction, request, enableTimeout); + const incrementedNonce = keepassClient.incrementedNonce(nonce); + + return protocolClient.handleResponse(response, incrementedNonce, tab); +}; + +protocolClient.buildRequest = function(encryptedMessage, nonce, clientID, triggerUnlock = false) { + const request = { + message: encryptedMessage, + nonce: nonce, + clientID: clientID, + requestID: keepassClient.getRequestId() + }; + + if (triggerUnlock) { + request.triggerUnlock = true; + } + + return request; +}; + +protocolClient.handleResponse = function(response, incrementedNonce, tab) { + if (response.message && protocolClient.verifyNonce(response, incrementedNonce)) { + const res = keepassClient.decrypt(response.message, response.nonce); + if (!res) { + keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE); + return undefined; + } + + const message = nacl.util.encodeUTF8(res); + const parsed = JSON.parse(message); + return parsed; + } else if (response.error && response.errorCode) { + keepass.handleError(tab, response.errorCode, response.error); + } + + return undefined; +}; + +protocolClient.verifyNonce = function(response, nonce) { + if (!response.nonce) { + logError('No nonce in reponse'); + return false; + } + + if (!keepassClient.checkNonceLength(response.nonce)) { + logError('Incorrect nonce length'); + return false; + } + + if (response.nonce !== nonce) { + logError('Nonce compare failed'); + return false; + } + + return true; +}; + +//-------------------------------------------------------------------------- +// Utils +//-------------------------------------------------------------------------- + +protocolClient.getNonce = function() { + return nacl.util.encodeBase64(nacl.randomBytes(protocolClient.keySize)); +}; + +// Creates a random 8 character string for Request ID +protocolClient.getRequestId = function() { + return Math.random().toString(16).substring(2, 10); +}; + +protocolClient.incrementedNonce = function(nonce) { + const oldNonce = nacl.util.decodeBase64(nonce); + const newNonce = oldNonce.slice(0); + + // from libsodium/utils.c + let i = 0; + let c = 1; + for (; i < newNonce.length; ++i) { + c += newNonce[i]; + newNonce[i] = c; + c >>= 8; + } + + return nacl.util.encodeBase64(newNonce); +}; + +protocolClient.getNonces = function() { + const nonce = protocolClient.getNonce(); + const incrementedNonce = protocolClient.incrementedNonce(nonce); + return [ nonce, incrementedNonce ]; +}; + +protocolClient.checkNonceLength = function(nonce) { + return nacl.util.decodeBase64(nonce).length === nacl.secretbox.nonceLength; +}; + +protocolClient.generateNewKeyPair = function() { + keepass.keyPair = nacl.box.keyPair(); +}; + +//-------------------------------------------------------------------------- +// Encrypt/Decrypt +//-------------------------------------------------------------------------- + +protocolClient.encrypt = function(input, nonce) { + const messageData = nacl.util.decodeUTF8(JSON.stringify(input)); + const messageNonce = nacl.util.decodeBase64(nonce); + + if (keepass.serverPublicKey) { + const message = nacl.box(messageData, messageNonce, keepass.serverPublicKey, keepass.keyPair.secretKey); + if (message) { + return nacl.util.encodeBase64(message); + } + } + + return ''; +}; + +protocolClient.decrypt = function(input, nonce) { + const m = nacl.util.decodeBase64(input); + const n = nacl.util.decodeBase64(nonce); + const res = nacl.box.open(m, n, keepass.serverPublicKey, keepass.keyPair.secretKey); + return res; +}; + +//-------------------------------------------------------------------------- +// Native Messaging related +//-------------------------------------------------------------------------- + +protocolClient.connectToNative = function() { + if (protocolClient.nativePort) { + protocolClient.nativePort.disconnect(); + } + protocolClient.nativeConnect(); +}; + +protocolClient.nativeConnect = function() { + console.log(`${EXTENSION_NAME}: Connecting to native messaging host ${protocolClient.nativeHostName}`); + protocolClient.nativePort = browser.runtime.connectNative(protocolClient.nativeHostName); + protocolClient.nativePort.onMessage.addListener(protocolClient.onNativeMessage); + protocolClient.nativePort.onDisconnect.addListener(onDisconnected); + keepass.isConnected = true; + return protocolClient.nativePort; +}; + +function onDisconnected() { + protocolClient.nativePort = null; + keepass.isConnected = false; + keepass.isDatabaseClosed = true; + keepass.isKeePassXCAvailable = false; + keepass.associated.value = false; + keepass.associated.hash = null; + keepass.databaseHash = ''; + + page.clearAllLogins(); + keepass.updatePopup('cross'); + keepass.updateDatabaseHashToContent(); + logError(`Failed to connect: ${(browser.runtime.lastError === null ? 'Unknown error' : browser.runtime.lastError.message)}`); +} + +protocolClient.onNativeMessage = function(response) { + // Handle database lock/unlock status + if (response.action === kpActions.DATABASE_LOCKED || response.action === kpActions.DATABASE_UNLOCKED) { + keepass.updateDatabase(); + } +}; diff --git a/keepassxc-browser/content/banner.js b/keepassxc-browser/content/banner.js index ce1e799..f860068 100644 --- a/keepassxc-browser/content/banner.js +++ b/keepassxc-browser/content/banner.js @@ -140,7 +140,7 @@ kpxcBanner.create = async function(credentials = {}) { kpxcBanner.saveNewCredentials = async function(credentials = {}) { const saveToDefaultGroup = async function(creds) { const args = [ creds.username, creds.password, creds.url ]; - const res = await sendMessage('add_credentials', args); + const res = await sendMessage('create_credentials', args); kpxcBanner.verifyResult(res); }; @@ -170,7 +170,7 @@ kpxcBanner.saveNewCredentials = async function(credentials = {}) { // Create a new group const newGroup = await sendMessage('create_new_group', [ result.defaultGroup ]); if (newGroup.name && newGroup.uuid) { - const res = await sendMessage('add_credentials', [ credentials.username, credentials.password, credentials.url, newGroup.name, newGroup.uuid ]); + const res = await sendMessage('create_credentials', [ credentials.username, credentials.password, credentials.url, newGroup.name, newGroup.uuid ]); kpxcBanner.verifyResult(res); } else { kpxcUI.createNotification('error', tr('rememberErrorCreatingNewGroup')); @@ -180,7 +180,7 @@ kpxcBanner.saveNewCredentials = async function(credentials = {}) { } } - const res = await sendMessage('add_credentials', [ credentials.username, credentials.password, credentials.url, gname, guuid ]); + const res = await sendMessage('create_credentials', [ credentials.username, credentials.password, credentials.url, gname, guuid ]); kpxcBanner.verifyResult(res); return; } @@ -212,7 +212,7 @@ kpxcBanner.saveNewCredentials = async function(credentials = {}) { return; } - const res = await sendMessage('add_credentials', [ credentials.username, credentials.password, credentials.url, group, groupUuid ]); + const res = await sendMessage('create_credentials', [ credentials.username, credentials.password, credentials.url, group, groupUuid ]); kpxcBanner.verifyResult(res); }); diff --git a/keepassxc-browser/content/keepassxc-browser.js b/keepassxc-browser/content/keepassxc-browser.js index bcf780e..a89e465 100755 --- a/keepassxc-browser/content/keepassxc-browser.js +++ b/keepassxc-browser/content/keepassxc-browser.js @@ -102,12 +102,23 @@ kpxc.detectDatabaseChange = async function(response) { kpxc.clearAllFromPage(); kpxcIcons.switchIcons(); + // TODO: This doesn't work well anymore. if (document.visibilityState !== 'hidden') { if (response.hash.new !== '') { _called.retrieveCredentials = false; + + // Why is this needed? For the Connection Keys? const settings = await sendMessage('load_settings'); kpxc.settings = settings; - kpxc.databaseState = DatabaseState.UNLOCKED; + + // TODO: Cleanup this.. + if (response.associateResult) { + if (!response.associateResult.areAllLocked) { + kpxc.databaseState = DatabaseState.UNLOCKED; + } + } else { + kpxc.databaseState = DatabaseState.UNLOCKED; // This is important to set correctly! + } await kpxc.initCredentialFields(); kpxcIcons.switchIcons(); @@ -115,7 +126,7 @@ kpxc.detectDatabaseChange = async function(response) { // If user has requested a manual fill through context menu the actual credential filling // is handled here when the opened database has been regognized. It's not a pretty hack. const manualFill = await sendMessage('page_get_manual_fill'); - if (manualFill !== ManualFill.NONE && kpxc.combinations.length > 0) { + if (manualFill !== ManualFill.NONE) { await kpxcFill.fillInFromActiveElement(manualFill === ManualFill.PASSWORD); await sendMessage('page_set_manual_fill', ManualFill.NONE); } diff --git a/keepassxc-browser/content/username-field.js b/keepassxc-browser/content/username-field.js index 377b181..f713d72 100644 --- a/keepassxc-browser/content/username-field.js +++ b/keepassxc-browser/content/username-field.js @@ -134,7 +134,7 @@ const iconClicked = async function(field, icon) { if (kpxc.databaseState !== DatabaseState.UNLOCKED) { // Triggers database unlock await sendMessage('page_set_manual_fill', ManualFill.BOTH); - // TODO: Replace with open-database? + // TODO: Replace with open-database or get-database-statuses? With legacyProtocol keepass.getDatabaseHash must be used. await sendMessage('get_database_hash', [ false, true ]); // Set triggerUnlock to true field.focus(); } diff --git a/keepassxc-browser/manifest.json b/keepassxc-browser/manifest.json index 9047dbb..7f5c9e8 100755 --- a/keepassxc-browser/manifest.json +++ b/keepassxc-browser/manifest.json @@ -37,14 +37,16 @@ "common/sites.js", "background/nacl.min.js", "background/nacl-util.min.js", - "background/client.js", "background/keepass.js", "background/httpauth.js", "background/browserAction.js", "background/page.js", "background/event.js", "background/init.js", - "background/protocol.js" + "background/protocol.js", + "background/protocolClient.js", + "background/legacyProtocol.js", + "background/legacyProtocolClient.js" ] }, "content_scripts": [ diff --git a/keepassxc-browser/popups/popup.js b/keepassxc-browser/popups/popup.js index af022a8..c78bf6c 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(); @@ -20,39 +20,70 @@ function statusResponse(r) { $('#lock-database-button').hide(); $('#getting-started-guide').hide(); - if (!r.keePassXCAvailable) { - $('#error-message').textContent = r.error; + if (!status.keePassXCAvailable) { + $('#error-message').textContent = status.error; $('#error-encountered').show(); - if (r.showGettingStartedGuideAlert) { + if (status.showGettingStartedGuideAlert) { $('#getting-started-guide').show(); } - if (r.showTroubleshootingGuideAlert && reloadCount >= 2) { + if (status.showTroubleshootingGuideAlert && reloadCount >= 2) { $('#troubleshooting-guide').show(); } else { $('#troubleshooting-guide').hide(); } - } else if (r.keePassXCAvailable && r.databaseClosed) { - $('#database-error-message').textContent = r.error; + + return; + } + + // Only supported with Protocol V2 + if (status.databaseAssociationStatuses) { + // This can be also shown when isAnyAssociated is true? + if (status.databaseAssociationStatuses.associationNeeded) { + $('#not-configured').show(); + } + + if (status.keePassXCAvailable && status.databaseAssociationStatuses.areAllLocked) { + $('#database-error-message').textContent = status.error; + $('#database-not-opened').show(); + } + + if (status.databaseAssociationStatuses.isAnyAssociated) { + $('#configured-and-associated').show(); + $('#associated-identifier').textContent = status.identifier; + $('#lock-database-button').show(); + + if (status.usernameFieldDetected) { + $('#username-field-detected').show(); + } + + reloadCount = 0; + } + + return; + } + + if (status.keePassXCAvailable && status.databaseClosed) { + $('#database-error-message').textContent = status.error; $('#database-not-opened').show(); - } else if (!r.configured) { + } else if (!status.configured) { $('#not-configured').show(); - } else if (r.encryptionKeyUnrecognized) { + } else if (status.encryptionKeyUnrecognized) { $('#need-reconfigure').show(); - $('#need-reconfigure-message').textContent = r.error; - } else if (!r.associated) { + $('#need-reconfigure-message').textContent = status.error; + } else if (!status.associated) { $('#need-reconfigure').show(); - $('#need-reconfigure-message').textContent = r.error; - } else if (r.error) { + $('#need-reconfigure-message').textContent = status.error; + } else if (status.error !== null) { $('#error-encountered').show(); - $('#error-message').textContent = r.error; + $('#error-message').textContent = status.error; } else { $('#configured-and-associated').show(); - $('#associated-identifier').textContent = r.identifier; + $('#associated-identifier').textContent = status.identifier; $('#lock-database-button').show(); - if (r.usernameFieldDetected) { + if (status.usernameFieldDetected) { $('#username-field-detected').show(); }