diff --git a/keepassxc-browser/_locales/en/messages.json b/keepassxc-browser/_locales/en/messages.json index f41ce47..77c6fda 100644 --- a/keepassxc-browser/_locales/en/messages.json +++ b/keepassxc-browser/_locales/en/messages.json @@ -21,16 +21,12 @@ }, "contextMenuSaveCredentials": { "message": "Save credentials", - "description": "Save credentials using the extension popup icon." + "description": "Save credentials using the credential banner." }, "contextMenuShowPasswordGenerator": { - "message": "Show password generator", + "message": "Show Password Generator", "description": "Show password generator text." }, - "rememberCredentialsPopup": { - "message": "Create or modify the credentials by clicking on the extension icon.", - "description": "Notification text when saving credentials from the context menu." - }, "multipleCredentialsDetected": { "message": "HTTP authentication with multiple credentials detected. Click on the extension icon to choose the correct one.", "description": "Notification when HTTP Authentication has multiple credentials detected." @@ -99,6 +95,10 @@ "message": "No logins found.", "description": "No logins found." }, + "errorNotConnected": { + "message": "Not connected to KeePassXC.", + "description": "Error notification shown when not connected to KeePassXC" + }, "passwordGeneratorIcon": { "message": "Password generator icon", "description": "Alt attribute text of the password generator icon." @@ -111,10 +111,6 @@ "message": "Also fill in the next password-field", "description": "Checkbox text below the password generator input field." }, - "passwordGeneratorBits": { - "message": "$1 Bits", - "description": "Bits of the generated password." - }, "passwordGeneratorTitle": { "message": "Password Generator", "description": "Password generator dialog title." @@ -131,9 +127,9 @@ "message": "Copy", "description": "Copy button text in password generator." }, - "passwordGeneratorFillAndCopy": { - "message": "Fill and copy", - "description": "Fill and copy button text in password generator." + "passwordGeneratorFill": { + "message": "Fill password", + "description": "Fill a password in password generator." }, "passwordGeneratorErrorTooLong": { "message": "Error: The generated password is longer than the allowed length!", @@ -159,6 +155,18 @@ "message": "Is KeePassXC running and connected?", "description": "Password generator error text when KeePassXC is closed." }, + "usernameFieldText": { + "message": "Fill credentials from KeePassXC", + "description": "Username field icon hover text." + }, + "usernameLockedFieldText": { + "message": "Database is locked", + "description": "Username field icon hover text when database is locked." + }, + "usernameFieldIcon": { + "message": "Username field icon", + "description": "Username field icon text." + }, "defineDismiss": { "message": "Dismiss", "description": "Dismiss button text when choosing custom login fields." @@ -224,11 +232,11 @@ "description": "Shown when no username is set in the credentials." }, "credentialsNoLoginsFound": { - "message": "Error: No logins found.", + "message": "No logins found.", "description": "Shown when no credentials are found for the current page." }, "credentialsMultipleFound": { - "message": "Error: More than one login was found in KeePassXC!", + "message": "More than one login was found in KeePassXC!", "description": "Alert message when trying to fill username and/or password when multiple credentials are found." }, "credentialsMultipleFoundSecondLine": { @@ -236,7 +244,7 @@ "description": "Alert message when trying to fill username and/or password when multiple credentials are found." }, "credentialsNoUsernameFound": { - "message": "Error: No credentials for the given username found.", + "message": "No credentials for the given username found.", "description": "Alert message when no credentials are found for the given username." }, "credentialExpired": { @@ -287,6 +295,10 @@ "message": "Reopen database", "description": "Popup reopen database button text." }, + "popupUsernameButton": { + "message": "Add Username-only option for the site", + "description": "Button text for adding Username-only option." + }, "popupErrorEncountered": { "message": "KeePassXC-Browser has encountered an error:", "description": "A text shown above error message in the popup." @@ -323,46 +335,78 @@ "message": "KeePassXC-Browser is connected to KeePassXC and has been configured using the following identifier: $1", "description": "A popuo message when a connection to KeePassXC is succesful." }, - "popupRememberInfoText": { + "popupUsernameFieldDetected": { + "message": "Only a single username field was detected. Add the URL to Site Preferences with Username-only option enabled?", + "description": "Text shown when page can be added to Site Preferences with Username-only option enabled." + }, + "rememberInfoText": { "message": "Username or password changed! Save it?", - "description": "Popup message when username or password has changed." + "description": "Message when username or password has changed." }, - "popupRememberSaving": { + "rememberSaving": { "message": "Credentials will be saved in connected database with identifier $1", - "description": "A popup message when saving or updating credentials." + "description": "A message when saving or updating credentials." }, - "popupRememberNewUsername": { + "rememberNewUsername": { "message": "The following username is currently not saved: $1", - "description": "A popup message shown when a new username is detected when saving credentials." + "description": "A message shown when a new username is detected when saving credentials." }, - "popupRememberUsernameExists": { + "rememberUsernameExists": { "message": "The credentials with the used username are marked bold: $1", - "description": "A popup message shown when a existing username is detected when saving credentials." + "description": "A message shown when a existing username is detected when saving credentials." }, - "popupRememberChooseCredentials": { + "rememberChooseCredentials": { "message": "Please choose the credentials you want to update.", - "description": "A popup message shown choosing what credentials user wants to update." + "description": "A message shown choosing what credentials user wants to update." }, - "popupRememberChooseGroup": { + "rememberChooseGroup": { "message": "Please choose the group you want to add the new credentials.", - "description": "A popup message shown choosing what group user wants to use for new credentials." + "description": "A message shown choosing what group user wants to use for new credentials." }, - "popupRememberInfoDefaultGroupNotFound": { - "message": "Info: Default group created in connected database.", + "rememberInfoDefaultGroupNotFound": { + "message": "Default group created in connected database.", "description": "Info message shown when default group set cannot be found." }, - "popupRememberErrorCreatingNewGroup": { - "message": "Error: New group(s) cannot be created.", + "rememberErrorCreatingNewGroup": { + "message": "New group(s) cannot be created.", "description": "Error message shown when new groups cannot be created." }, - "popupRememberErrorPasswordNotChanged": { - "message": "Error: Credentials not updated. The password has not been changed.", + "rememberErrorPasswordNotChanged": { + "message": "Credentials not updated. The password has not been changed.", "description": "Error message shown when credential password is not changed." }, - "popupRememberErrorCannotSaveCredentials": { - "message": "Error: Credentials cannot be saved or updated.", + "rememberErrorCannotSaveCredentials": { + "message": "Credentials cannot be saved or updated.", "description": "Error message shown when credentials cannot be saved or updated." }, + "rememberErrorDatabaseClosed": { + "message": "Credentials cannot be saved or updated to a closed database.", + "description": "Error message shown when credentials cannot be saved or updated and the database is closed." + }, + "rememberCredentialsNotSaved": { + "message": "Operation cancelled. Credentials are not updated.", + "description": "Message shown when saving operation was cancelled." + }, + "rememberCredentialsSaved": { + "message": "New credentials with username $1 saved.", + "description": "Message shown when new credentials are saved." + }, + "rememberCredentialsUpdated": { + "message": "Credentials with username $1 updated.", + "description": "Message shown when new credentials are succesfully updated." + }, + "rememberCredentialsExists": { + "message": "Identical credentials already exists. Nothing to update.", + "description": "Message shown when new credentials already found in database." + }, + "rememberEmptyUsername": { + "message": "(empty)", + "description": "Means an empty username." + }, + "rememberNoPassword": { + "message": "Please fill the password field before saving credentials.", + "description": "Message shown when no password is set when saving credentials." + }, "popupLoginText": { "message": "Select the login information you would like to get entered into the page.", "description": "A popup message shown when one or multiple credentials are present." @@ -400,7 +444,7 @@ "description": "Dismiss button text in popup when updating credentials." }, "popupButtonIgnore": { - "message": "Never ask for this page", + "message": "Don't ask again for this site", "description": "Ignore button text in popup or in notification when updating credentials." }, "popupButtonClose": { @@ -491,18 +535,6 @@ "message": "Reset", "description": "Reset button text for keyboard shortcuts." }, - "optionsLabelBlinkTime": { - "message": "Blink Time:", - "description": "Blink Time option text." - }, - "optionsLabelRedirectOffset": { - "message": "Redirect Offset:", - "description": "Redirect Offset option text." - }, - "optionsLabelRedirectAllowance": { - "message": "Maximum (Number of) Redirects:", - "description": "RMaximum (Number of) Redirects options text." - }, "optionsLabelDefaultGroup": { "message": "Default group for saving new passwords:", "description": "Default group options text." @@ -520,8 +552,8 @@ "description": "Default checkbox explanation help text." }, "optionsCheckboxUsePasswordGenerator": { - "message": "Activate password generator.", - "description": "Activate password generator checkbox text." + "message": "Activate password generator icons.", + "description": "Activate password generator icons checkbox text." }, "optionsCheckboxAutoRetrieveCredentials": { "message": "Automatically retrieve credentials.", @@ -560,7 +592,7 @@ "description": "Save domain only checkbox text." }, "optionsCheckboxShowLoginNotifications": { - "message": "Show a notification when new credentials can be saved to the database.", + "message": "Show a banner on the page when new credentials can be saved to the database.", "description": "Show login notifications checkbox text." }, "optionsCheckboxAutoFillAndSend": { @@ -591,24 +623,8 @@ "message": "Keyboard shortcuts", "description": "Keyboard shortcut header text." }, - "optionsBlinkTimeHelpText": { - "message": "Maximum time (ms) the icon should blink after detecting new credentials", - "description": "Blink Time option help text." - }, - "optionsRedirectOffsetHelpText": { - "message": "Minimum time (ms) the icon should blink before deactivating due to page redirects.", - "description": "Redirect Offset option help text." - }, - "optionsRedirectOffsetHelpTextSecond": { - "message": "-1 to only use blink time ignoring Maximum Redirects (old behavior)", - "description": "Redirect Offset option help text, second part." - }, - "optionsRedirectAllowanceHelpText": { - "message": "How many pages should the tab cycle through after the redirect offset before deactivating the icon", - "description": "Redirect Allowance option help text." - }, "optionsUsePasswordGeneratorHelpText": { - "message": "Adds a button to password fields for generating a new password.", + "message": "Adds an icon to password fields for generating a new password.", "description": "Password Generator option help text." }, "optionsUsePasswordGeneratorHelpTextSecond": { @@ -804,7 +820,7 @@ "description": "Site preferences info text, first part." }, "optionsSitePreferencesTabHelpTextSecond": { - "message": "To ignore new/modified credentials on a specific site, add them below or click the blinking KeePassXC-Browser icon and select", + "message": "To ignore new/modified credentials on a specific site, add them below or when using the credential banner on the page select", "description": "Site preferences info text, second part." }, "optionsSitePreferencesTabHelpTextThird": { diff --git a/keepassxc-browser/background/browserAction.js b/keepassxc-browser/background/browserAction.js index ca42b5d..42391a6 100755 --- a/keepassxc-browser/background/browserAction.js +++ b/keepassxc-browser/background/browserAction.js @@ -2,18 +2,10 @@ const browserAction = {}; -const BLINK_TIMEOUT_DEFAULT = 7500; -const BLINK_TIMEOUT_REDIRECT_THRESHOLD_TIME_DEFAULT = -1; -const BLINK_TIMEOUT_REDIRECT_COUNT_DEFAULT = 1; - -// Milliseconds for intervall (e.g. to update browserAction) -const _interval = 250; -let _loop = null; - -browserAction.show = function(callback, tab) { +browserAction.show = function(tab) { let data = {}; if (!page.tabs[tab.id] || page.tabs[tab.id].stack.length === 0) { - browserAction.showDefault(callback, tab); + browserAction.showDefault(tab); return; } else { data = page.tabs[tab.id].stack[page.tabs[tab.id].stack.length - 1]; @@ -32,106 +24,31 @@ browserAction.show = function(callback, tab) { } }; -browserAction.update = function(interval) { - if (!page.tabs[page.currentTabId] || page.tabs[page.currentTabId].stack.length === 0) { - return; - } - - const data = page.tabs[page.currentTabId].stack[page.tabs[page.currentTabId].stack.length - 1]; - - if (data.visibleForMilliSeconds !== undefined && data.visibleForMilliSeconds !== -1) { - if (data.visibleForMilliSeconds <= 0) { - browserAction.stackPop(page.currentTabId); - browserAction.disableLoop(); - browserAction.show(null, { 'id': page.currentTabId }); - page.clearCredentials(page.currentTabId); - return; - } - data.visibleForMilliSeconds -= interval; - } - - if (data.intervalIcon) { - data.intervalIcon.counter += 1; - if (data.intervalIcon.counter < data.intervalIcon.max) { - return; - } - - data.intervalIcon.counter = 0; - data.intervalIcon.index += 1; - - if (data.intervalIcon.index > data.intervalIcon.icons.length - 1) { - data.intervalIcon.index = 0; - } - - browser.browserAction.setIcon({ - tabId: page.currentTabId, - path: '/icons/toolbar/' + browserAction.generateIconName(null, data.intervalIcon.icons[data.intervalIcon.index]) - }); - } -}; - -browserAction.showDefault = function(callback, tab) { +browserAction.showDefault = async function(tab) { const stackData = { level: 1, iconType: 'normal', popup: 'popup.html' }; - keepass.isConfigured().then((response) => { - if (!response || keepass.isDatabaseClosed || !keepass.isKeePassXCAvailable || page.tabs[tab.id].errorMessage) { - stackData.iconType = 'cross'; - } - if (page.tabs[tab.id].loginList.length > 0) { - stackData.iconType = 'questionmark'; - stackData.popup = 'popup_login.html'; - } - - browserAction.stackUnshift(stackData, tab.id); - browserAction.disableLoop(); - browserAction.show(null, tab); + const response = await keepass.isConfigured().catch((err) => { + console.log('Error: Cannot show default popup: ' + err); }); + + if (!response || keepass.isDatabaseClosed || !keepass.isKeePassXCAvailable) { + stackData.iconType = 'cross'; + } + + if (page.tabs[tab.id] && page.tabs[tab.id].loginList.length > 0) { + stackData.iconType = 'questionmark'; + stackData.popup = 'popup_login.html'; + } + + browserAction.stackUnshift(stackData, tab.id); + browserAction.show(tab); }; -browserAction.stackAdd = function(callback, tab, icon, popup, level, push, visibleForMilliSeconds, visibleForPageUpdates, redirectOffset, dontShow) { - const id = tab.id || page.currentTabId; - - if (!level) { - level = 1; - } - - const stackData = { - level: level, - icon: icon - }; - - if (popup) { - stackData.popup = popup; - } - - if (visibleForMilliSeconds !== undefined) { - stackData.visibleForMilliSeconds = visibleForMilliSeconds; - } - - if (visibleForPageUpdates !== undefined) { - stackData.visibleForPageUpdates = visibleForPageUpdates; - } - - if (redirectOffset !== undefined) { - stackData.redirectOffset = redirectOffset; - } - - if (push) { - browserAction.stackPush(stackData, id); - } else { - browserAction.stackUnshift(stackData, id); - } - - if (!dontShow) { - browserAction.show(null, { 'id': id }); - } -}; - -browserAction.removeLevelFromStack = function(callback, tab, level, type, dontShow) { +browserAction.removeLevelFromStack = function(tab, level, type, dontShow) { if (!page.tabs[tab.id]) { return; } @@ -156,7 +73,7 @@ browserAction.removeLevelFromStack = function(callback, tab, level, type, dontSh page.tabs[tab.id].stack = newStack; if (!dontShow) { - browserAction.show(callback, tab); + browserAction.show(tab); } }; @@ -167,132 +84,16 @@ browserAction.stackPop = function(tabId) { browserAction.stackPush = function(data, tabId) { const id = tabId || page.currentTabId; - browserAction.removeLevelFromStack(null, { 'id': id }, data.level, '<=', true); + browserAction.removeLevelFromStack({ 'id': id }, data.level, '<=', true); page.tabs[id].stack.push(data); }; browserAction.stackUnshift = function(data, tabId) { const id = tabId || page.currentTabId; - browserAction.removeLevelFromStack(null, { 'id': id }, data.level, '<=', true); + browserAction.removeLevelFromStack({ 'id': id }, data.level, '<=', true); page.tabs[id].stack.unshift(data); }; - -browserAction.removeRememberPopup = function(callback, tab, removeImmediately) { - if (!page.tabs[tab.id]) { - return; - } - - if (page.tabs[tab.id].stack.length === 0) { - page.clearCredentials(tab.id); - return; - } - const data = page.tabs[tab.id].stack[page.tabs[tab.id].stack.length - 1]; - - if (removeImmediately || !isNaN(data.visibleForPageUpdates)) { - const currentMS = Date.now(); - if (removeImmediately || (data.visibleForPageUpdates <= 0 && data.redirectOffset > 0)) { - browserAction.stackPop(tab.id); - browserAction.show(null, { 'id': tab.id }); - page.clearCredentials(tab.id); - } else if (!isNaN(data.visibleForPageUpdates) && data.redirectOffset > 0 && currentMS >= data.redirectOffset) { - data.visibleForPageUpdates -= 1; - } - } -}; - -browserAction.setRememberPopup = function(tabId, username, password, url, usernameExists, credentialsList) { - browser.storage.local.get({ 'settings': {} }).then(function(item) { - const settings = item.settings; - - // Don't show anything if the site is in the ignore - if (settings.sitePreferences !== undefined) { - for (const site of settings.sitePreferences) { - if (site.ignore === IGNORE_NORMAL && (site.url === url || siteMatch(site.url, url))) { - return; - } - } - } - - const id = tabId || page.currentTabId; - let timeoutMinMillis = Number(getValueOrDefault(settings, 'blinkMinTimeout', BLINK_TIMEOUT_REDIRECT_THRESHOLD_TIME_DEFAULT, -1)); - - if (timeoutMinMillis > 0) { - timeoutMinMillis += Date.now(); - } - - const blinkTimeout = getValueOrDefault(settings, 'blinkTimeout', BLINK_TIMEOUT_DEFAULT, -1); - const pageUpdateAllowance = getValueOrDefault(settings, 'allowedRedirect', BLINK_TIMEOUT_REDIRECT_COUNT_DEFAULT, 0); - - const stackData = { - visibleForMilliSeconds: blinkTimeout, - visibleForPageUpdates: pageUpdateAllowance, - redirectOffset: timeoutMinMillis, - level: 10, - intervalIcon: { - index: 0, - counter: 0, - max: 2, - icons: [ 'icon_remember_red_background.png', 'icon_remember_red_lock.png' ] - }, - icon: 'icon_remember_red_background.png', - popup: 'popup_remember.html' - }; - - browserAction.activateLoop(); - browserAction.stackPush(stackData, id); - - page.tabs[id].credentials = { - username: username, - password: password, - url: url, - usernameExists: usernameExists, - list: credentialsList - }; - - browserAction.show(null, { 'id': id }); - - if (page.settings.showLoginNotifications) { - const message = tr('rememberCredentialsPopup'); - const buttons = [ - { - 'title': tr('popupButtonClose') - }, - { - 'title': tr('popupButtonIgnore') - } - ]; - - browser.notifications.create({ - 'type': 'basic', - 'iconUrl': browser.extension.getURL('icons/keepassxc_64x64.png'), - 'title': 'KeePassXC-Browser', - 'message': message, - 'buttons': buttons - }); - - browser.notifications.onButtonClicked.addListener((notificationId, index) => { - browser.notifications.clear(notificationId); - if (index === 1) { - browserAction.ignoreSite(url); - } - }); - } - }); -}; - -function getValueOrDefault(settings, key, defaultVal, min) { - try { - let val = settings[key]; - if (isNaN(val) || val < min) { - val = defaultVal; - } - return val; - } catch (e) { - return defaultVal; - } -} - browserAction.generateIconName = function(iconType, icon) { if (icon) { return icon; @@ -306,33 +107,17 @@ browserAction.generateIconName = function(iconType, icon) { return name; }; -browserAction.ignoreSite = function(url) { - browser.windows.getCurrent().then(() => { - // Get current active window - browser.tabs.query({ 'active': true, 'currentWindow': true }).then((tabs) => { - const tab = tabs[0]; +browserAction.ignoreSite = async function(url) { + await browser.windows.getCurrent(); - // Send the message to the current tab's content script - browser.runtime.getBackgroundPage().then(() => { - browser.tabs.sendMessage(tab.id, { - action: 'ignore_site', - args: [ url ] - }); - }); - }); + // Get current active window + const tabs = await browser.tabs.query({ 'active': true, 'currentWindow': true }); + const tab = tabs[0]; + + // Send the message to the current tab's content script + await browser.runtime.getBackgroundPage(); + browser.tabs.sendMessage(tab.id, { + action: 'ignore_site', + args: [ url ] }); }; - -// Interval which updates the browserAction (e.g. blinking icon) -browserAction.activateLoop = function() { - if (_loop === null) { - _loop = setInterval(function() { - browserAction.update(_interval); - }, _interval); - } -}; - -browserAction.disableLoop = function() { - clearInterval(_loop); - _loop = null; -}; diff --git a/keepassxc-browser/background/event.js b/keepassxc-browser/background/event.js index 1394ee6..708cb31 100755 --- a/keepassxc-browser/background/event.js +++ b/keepassxc-browser/background/event.js @@ -2,302 +2,263 @@ const kpxcEvent = {}; -kpxcEvent.onMessage = function(request, sender, callback) { +kpxcEvent.onMessage = async function(request, sender) { if (request.action in kpxcEvent.messageHandlers) { - //console.log('onMessage(' + request.action + ') for #' + sender.tab.id); if (!sender.hasOwnProperty('tab') || sender.tab.id < 1) { sender.tab = {}; sender.tab.id = page.currentTabId; } - kpxcEvent.invoke(kpxcEvent.messageHandlers[request.action], callback, sender.tab.id, request.args); - - // onMessage closes channel for callback automatically - // if this method does not return true - if (callback !== undefined) { - return true; - } + return await kpxcEvent.messageHandlers[request.action](sender.tab, request.args); } }; -/** - * Get interesting information about the given tab. - * Function adapted from AdBlock-Plus. - * - * @param {function} handler to call after invoke - * @param {function} callback to call after handler or null - * @param {integer} senderTabId - * @param {array} args - * @param {bool} secondTime - * @returns null (asynchronous) - */ -kpxcEvent.invoke = function(handler, callback, senderTabId, args, secondTime) { - if (senderTabId < 1) { - return; - } - - if (!page.tabs[senderTabId]) { - page.createTabEntry(senderTabId); - } - - // Remove information from no longer existing tabs - page.removePageInformationFromNotExistingTabs(); - - browser.tabs.get(senderTabId).then((tab) => { - if (!tab) { - return; - } - - if (!tab.url) { - // Issue 6877: tab URL is not set directly after you opened a window - // using window.open() - if (!secondTime) { - window.setTimeout(function() { - kpxcEvent.invoke(handler, callback, senderTabId, args, true); - }, 250); - } - return; - } - - if (!page.tabs[tab.id]) { - page.createTabEntry(tab.id); - } - - args = args || []; - - args.unshift(tab); - args.unshift(callback); - - if (handler) { - handler.apply(this, args); - } else { - console.log('undefined handler for tab ' + tab.id); - } - }).catch((e) => { - console.log(e); - }); -}; - -kpxcEvent.onShowNotification = function(callback, tab, message) { - if (page.settings.showNotifications) { - showNotification(message); - } -}; - -kpxcEvent.showStatus = function(configured, tab, callback) { +kpxcEvent.showStatus = async function(tab, configured) { let keyId = null; if (configured && keepass.databaseHash !== '') { keyId = keepass.keyRing[keepass.databaseHash].id; } - browserAction.showDefault(null, tab); + browserAction.showDefault(tab); const errorMessage = page.tabs[tab.id].errorMessage; - callback({ + return { identifier: keyId, configured: configured, databaseClosed: keepass.isDatabaseClosed, keePassXCAvailable: keepass.isKeePassXCAvailable, encryptionKeyUnrecognized: keepass.isEncryptionKeyUnrecognized, associated: keepass.isAssociated(), - error: errorMessage || null + error: errorMessage || null, + usernameFieldDetected: page.usernameFieldDetected + }; +}; + +kpxcEvent.onLoadSettings = async function() { + return await page.initSettings().catch((err) => { + console.log('onLoadSettings error: ' + err); + return Promise.reject(); }); }; -kpxcEvent.onLoadSettings = function(callback, tab) { - page.initSettings().then((settings) => { - callback(settings); - }, (err) => { - console.log('error loading settings: ' + err); +kpxcEvent.onLoadKeyRing = async function() { + const item = await browser.storage.local.get({ 'keyRing': {} }).catch((err) => { + console.log('kpxcEvent.onLoadKeyRing error: ' + err); + return Promise.reject(); }); + + keepass.keyRing = item.keyRing; + if (keepass.isAssociated() && !keepass.keyRing[keepass.associated.hash]) { + keepass.associated = { + 'value': false, + 'hash': null + }; + } + + return item.keyRing; }; -kpxcEvent.onLoadKeyRing = function(callback, tab) { - browser.storage.local.get({ 'keyRing': {} }).then(function(item) { - keepass.keyRing = item.keyRing; - if (keepass.isAssociated() && !keepass.keyRing[keepass.associated.hash]) { - keepass.associated = { - 'value': false, - 'hash': null - }; - } - callback(item.keyRing); - }, (err) => { - console.log('error loading keyRing: ' + err); - }); +kpxcEvent.onSaveSettings = async function(tab, args = []) { + const [ settings ] = args; + browser.storage.local.set({ 'settings': settings }); + kpxcEvent.onLoadSettings(tab); + return Promise.resolve(); }; -kpxcEvent.onSaveSettings = function(callback, tab, settings) { - browser.storage.local.set({ 'settings': settings }).then(function() { - kpxcEvent.onLoadSettings(callback, tab); - }); -}; - -kpxcEvent.onGetStatus = function(callback, tab, internalPoll = false, triggerUnlock = false) { +kpxcEvent.onGetStatus = async function(tab, args = []) { // When internalPoll is true the event is triggered from content script in intervals -> don't poll KeePassXC - if (!internalPoll) { - keepass.testAssociation((response) => { + try { + const [ internalPoll = false, triggerUnlock = false ] = args; + if (!internalPoll) { + const response = await keepass.testAssociation(tab, [ true, triggerUnlock ]); if (!response) { - kpxcEvent.showStatus(false, tab, callback); - return; + return kpxcEvent.showStatus(tab, false); } + } - keepass.isConfigured().then((configured) => { - kpxcEvent.showStatus(configured, tab, callback); - }); - }, tab, true, triggerUnlock); - } else { - keepass.isConfigured().then((configured) => { - kpxcEvent.showStatus(configured, tab, callback); - }); + const configured = await keepass.isConfigured(); + return kpxcEvent.showStatus(tab, configured); + } catch (err) { + console.log('Error: No status shown: ' + err); + return Promise.reject(); } }; -kpxcEvent.onReconnect = async function(callback, tab) { - const configured = await keepass.reconnect(callback, tab); +kpxcEvent.onReconnect = async function(tab) { + const configured = await keepass.reconnect(tab); if (configured) { browser.tabs.sendMessage(tab.id, { action: 'redetect_fields' }).catch((err) => { console.log(err); + return; }); } - kpxcEvent.showStatus(configured, tab, callback); + + return kpxcEvent.showStatus(tab, configured); }; -kpxcEvent.lockDatabase = function(callback, tab) { - keepass.lockDatabase(tab).then(() => { - kpxcEvent.showStatus(true, tab, callback); - }); +kpxcEvent.lockDatabase = async function(tab) { + try { + await keepass.lockDatabase(tab); + return kpxcEvent.showStatus(tab, false); + } catch (err) { + console.log('kpxcEvent.lockDatabase error: ' + err); + return false; + } }; -kpxcEvent.onPopStack = function(callback, tab) { +kpxcEvent.onPopStack = function(tab) { browserAction.stackPop(tab.id); - browserAction.show(null, tab); + browserAction.show(tab); + return Promise.resolve(); }; -kpxcEvent.onGetTabInformation = function(callback, tab) { +kpxcEvent.onGetTabInformation = function(tab) { const id = tab.id || page.currentTabId; - callback(page.tabs[id]); + return Promise.resolve(page.tabs[id]); }; -kpxcEvent.onGetConnectedDatabase = function(callback, tab) { - callback({ +kpxcEvent.onGetConnectedDatabase = function() { + return Promise.resolve({ count: Object.keys(keepass.keyRing).length, identifier: (keepass.keyRing[keepass.associated.hash]) ? keepass.keyRing[keepass.associated.hash].id : null }); }; -kpxcEvent.onGetKeePassXCVersions = function(callback, tab) { +kpxcEvent.onGetKeePassXCVersions = async function(tab) { if (keepass.currentKeePassXC === '') { - keepass.getDatabaseHash((res) => { - callback({ 'current': keepass.currentKeePassXC, 'latest': keepass.latestKeePassXC.version }); - }, tab); - } else { - callback({ 'current': keepass.currentKeePassXC, 'latest': keepass.latestKeePassXC.version }); + await keepass.getDatabaseHash(tab); + return { 'current': keepass.currentKeePassXC, 'latest': keepass.latestKeePassXC.version }; } + + return Promise.resolve({ 'current': keepass.currentKeePassXC, 'latest': keepass.latestKeePassXC.version }); }; -kpxcEvent.onCheckUpdateKeePassXC = function(callback, tab) { +kpxcEvent.onCheckUpdateKeePassXC = async function() { keepass.checkForNewKeePassXCVersion(); - callback({ current: keepass.currentKeePassXC.version, latest: keepass.latestKeePassXC.version }); + return { current: keepass.currentKeePassXC.version, latest: keepass.latestKeePassXC.version }; }; -kpxcEvent.onUpdateAvailableKeePassXC = function(callback, tab) { - callback(page.settings.checkUpdateKeePassXC > 0 ? keepass.keePassXCUpdateAvailable() : false); +kpxcEvent.onUpdateAvailableKeePassXC = async function() { + return (page.settings.checkUpdateKeePassXC > 0) ? keepass.keePassXCUpdateAvailable() : false; }; -kpxcEvent.onRemoveCredentialsFromTabInformation = function(callback, tab) { +kpxcEvent.onRemoveCredentialsFromTabInformation = function(tab) { const id = tab.id || page.currentTabId; page.clearCredentials(id); + page.clearSubmittedCredentials(); + return Promise.resolve(); }; -kpxcEvent.onSetRememberPopup = function(callback, tab, username, password, url, usernameExists, credentialsList) { - keepass.testAssociation((response) => { - if (response) { - keepass.isConfigured().then((configured) => { - if (configured) { - browserAction.setRememberPopup(tab.id, username, password, url, usernameExists, credentialsList); - } - }).catch((e) => { - console.log(e); - }); - } - }, tab); -}; - -kpxcEvent.onLoginPopup = function(callback, tab, logins) { +kpxcEvent.onLoginPopup = function(tab, logins) { const stackData = { level: 1, iconType: 'questionmark', popup: 'popup_login.html' }; + browserAction.stackUnshift(stackData, tab.id); - page.tabs[tab.id].loginList = logins; - browserAction.show(null, tab); + + if (logins.length > 0) { + page.tabs[tab.id].loginList = logins[0]; + } + + browserAction.show(tab); + return Promise.resolve(); }; -kpxcEvent.initHttpAuth = function(callback) { +kpxcEvent.initHttpAuth = function() { httpAuth.init(); - callback(); -}; + return Promise.resolve(); +} -kpxcEvent.onHTTPAuthPopup = function(callback, tab, data) { +kpxcEvent.onHTTPAuthPopup = function(tab, data) { const stackData = { level: 1, iconType: 'questionmark', popup: 'popup_httpauth.html' }; + browserAction.stackUnshift(stackData, tab.id); page.tabs[tab.id].loginList = data; - browserAction.show(null, tab); + browserAction.show(tab); + return Promise.resolve(); }; -kpxcEvent.onMultipleFieldsPopup = function(callback, tab) { +kpxcEvent.onMultipleFieldsPopup = function(tab) { const stackData = { level: 1, iconType: 'normal', popup: 'popup_multiple-fields.html' }; + browserAction.stackUnshift(stackData, tab.id); - browserAction.show(null, tab); + browserAction.show(tab); + return Promise.resolve(); }; -kpxcEvent.pageClearLogins = function(callback, tab, alreadyCalled) { +kpxcEvent.pageClearLogins = function(tab, alreadyCalled) { if (!alreadyCalled) { page.clearLogins(tab.id); } - callback(); + return Promise.resolve(); }; -kpxcEvent.pageGetLoginId = function(callback, tab) { - callback(page.loginId); +kpxcEvent.pageGetLoginId = function() { + return Promise.resolve(page.loginId); }; -kpxcEvent.pageSetLoginId = function(callback, tab, loginId) { +kpxcEvent.pageSetLoginId = function(tab, loginId) { page.loginId = loginId; + return Promise.resolve(); +}; + +kpxcEvent.pageClearSubmitted = function() { + page.clearSubmittedCredentials(); + return Promise.resolve(); +} + +kpxcEvent.pageGetSubmitted = function() { + return Promise.resolve(page.submittedCredentials); +}; + +kpxcEvent.pageSetSubmitted = function(tab, args = []) { + const [ submitted, username, password, url, oldCredentials ] = args; + page.setSubmittedCredentials(submitted, username, password, url, oldCredentials); + return Promise.resolve(); +}; + +kpxcEvent.onUsernameFieldDetected = function(tab, args = []) { + page.usernameFieldDetected = args[0]; }; // 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, 'create_new_group': keepass.createNewGroup, 'enable_automatic_reconnect': keepass.enableAutomaticReconnect, 'disable_automatic_reconnect': keepass.disableAutomaticReconnect, 'generate_password': keepass.generatePassword, 'get_connected_database': kpxcEvent.onGetConnectedDatabase, + 'get_database_hash': keepass.getDatabaseHash, 'get_database_groups': keepass.getDatabaseGroups, 'get_keepassxc_versions': kpxcEvent.onGetKeePassXCVersions, 'get_status': kpxcEvent.onGetStatus, 'get_tab_information': kpxcEvent.onGetTabInformation, 'init_http_auth': kpxcEvent.initHttpAuth, + 'is_connected': keepass.getIsKeePassXCAvailable, 'load_keyring': kpxcEvent.onLoadKeyRing, 'load_settings': kpxcEvent.onLoadSettings, 'lock-database': kpxcEvent.lockDatabase, 'page_clear_logins': kpxcEvent.pageClearLogins, + 'page_clear_submitted': kpxcEvent.pageClearSubmitted, 'page_get_login_id': kpxcEvent.pageGetLoginId, + 'page_get_submitted': kpxcEvent.pageGetSubmitted, 'page_set_login_id': kpxcEvent.pageSetLoginId, + 'page_set_submitted': kpxcEvent.pageSetSubmitted, 'pop_stack': kpxcEvent.onPopStack, 'popup_login': kpxcEvent.onLoginPopup, 'popup_multiple-fields': kpxcEvent.onMultipleFieldsPopup, @@ -306,9 +267,7 @@ kpxcEvent.messageHandlers = { 'retrieve_credentials': keepass.retrieveCredentials, 'show_default_browseraction': browserAction.showDefault, 'update_credentials': keepass.updateCredentials, + 'username_field_detected': kpxcEvent.onUsernameFieldDetected, 'save_settings': kpxcEvent.onSaveSettings, - 'set_remember_credentials': kpxcEvent.onSetRememberPopup, - 'show_notification': kpxcEvent.onShowNotification, - 'stack_add': browserAction.stackAdd, 'update_available_keepassxc': kpxcEvent.onUpdateAvailableKeePassXC }; diff --git a/keepassxc-browser/background/httpauth.js b/keepassxc-browser/background/httpauth.js index 367dc43..0389247 100755 --- a/keepassxc-browser/background/httpauth.js +++ b/keepassxc-browser/background/httpauth.js @@ -47,11 +47,10 @@ httpAuth.handleRequestCallback = function(details, callback) { httpAuth.processPendingCallbacks(details, callback, callback); }; -httpAuth.retrieveCredentials = function(tabId, url, submitUrl, forceCallback) { - return new Promise((resolve, reject) => { - keepass.retrieveCredentials((logins) => { - resolve(logins); - }, tabId, url, submitUrl, forceCallback, false, true); +httpAuth.retrieveCredentials = async function(tabId, url, submitUrl) { + return await keepass.retrieveCredentials(tabId, [ url, submitUrl, false, true ]).catch((err) => { + console.log('httpAuth.retrieveCredentials error: ' + err); + return Promise.reject(); }); }; @@ -69,7 +68,7 @@ httpAuth.processPendingCallbacks = async function(details, resolve, reject) { details.searchUrl = (details.isProxy && details.proxyUrl) ? details.proxyUrl : details.url; - const logins = await httpAuth.retrieveCredentials({ 'id': details.tabId }, details.searchUrl, details.searchUrl, true); + const logins = await httpAuth.retrieveCredentials({ 'id': details.tabId }, details.searchUrl, details.searchUrl); httpAuth.loginOrShowCredentials(logins, details, resolve, reject); }; @@ -87,7 +86,7 @@ httpAuth.loginOrShowCredentials = function(logins, details, resolve, reject) { if (page.settings.showNotifications) { showNotification(tr('multipleCredentialsDetected')); } - kpxcEvent.onHTTPAuthPopup(null, { 'id': details.tabId }, { 'logins': logins, 'url': details.searchUrl, 'resolve': resolve }); + kpxcEvent.onHTTPAuthPopup({ 'id': details.tabId }, { 'logins': logins, 'url': details.searchUrl, 'resolve': resolve }); } } else { reject({ cancel: false }); // No logins found diff --git a/keepassxc-browser/background/init.js b/keepassxc-browser/background/init.js index a74a732..0eee148 100644 --- a/keepassxc-browser/background/init.js +++ b/keepassxc-browser/background/init.js @@ -1,15 +1,17 @@ 'use strict'; -keepass.migrateKeyRing().then(() => { - page.initSettings().then(() => { - page.initOpenedTabs().then(() => { - httpAuth.init(); - keepass.reconnect().then(() => { - keepass.enableAutomaticReconnect(); - }); - }); - }); -}); +(async () => { + try { + await keepass.migrateKeyRing(); + await page.initSettings(); + await page.initOpenedTabs(); + await httpAuth.init(); + await keepass.reconnect(); + await keepass.enableAutomaticReconnect(); + } catch (e) { + console.log('init.js failed'); + } +})(); /** * Generate information structure for created tab and invoke all needed @@ -18,10 +20,12 @@ keepass.migrateKeyRing().then(() => { */ browser.tabs.onCreated.addListener((tab) => { if (tab.id > 0) { - //console.log('browser.tabs.onCreated(' + tab.id+ ')'); if (tab.selected) { page.currentTabId = tab.id; - kpxcEvent.invoke(page.switchTab, null, tab.id, []); + if (!page.tabs[tab.id]) { + page.createTabEntry(tab.id); + } + page.switchTab(tab); } } }); @@ -43,20 +47,24 @@ browser.tabs.onRemoved.addListener((tabId, removeInfo) => { * Invoke functions to retrieve credentials for focused tab * @param {object} activeInfo */ -browser.tabs.onActivated.addListener((activeInfo) => { +browser.tabs.onActivated.addListener(async function(activeInfo) { // Remove possible credentials from old tab information page.clearCredentials(page.currentTabId, true); - browserAction.removeRememberPopup(null, { 'id': page.currentTabId }, true); - browser.tabs.get(activeInfo.tabId).then((info) => { + try { + const info = await browser.tabs.get(activeInfo.tabId); if (info && info.id) { page.currentTabId = info.id; if (info.status === 'complete') { - //console.log('kpxcEvent.invoke(page.switchTab, null, '+info.id + ', []);'); - kpxcEvent.invoke(page.switchTab, null, info.id, []); + if (!page.tabs[info.id]) { + page.createTabEntry(info.id); + } + page.switchTab(info); } } - }); + } catch (err) { + console.log('Error: ' + err); + } }); /** @@ -71,8 +79,10 @@ browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { } if (changeInfo.status === 'complete') { - browserAction.showDefault(null, tab); - kpxcEvent.invoke(browserAction.removeRememberPopup, null, tabId, []); + browserAction.showDefault(tab); + if (!page.tabs[tab.id]) { + page.createTabEntry(tab.id); + } } }); @@ -82,7 +92,6 @@ const contextMenuItems = [ { title: tr('contextMenuFillUsernameAndPassword'), action: 'fill_username_password' }, { title: tr('contextMenuFillPassword'), action: 'fill_password' }, { title: tr('contextMenuFillTOTP'), action: 'fill_totp' }, - { title: tr('contextMenuShowPasswordGeneratorIcons'), action: 'activate_password_generator' }, { title: tr('contextMenuShowPasswordGenerator'), action: 'show_password_generator' }, { title: tr('contextMenuSaveCredentials'), action: 'remember_credentials' } ]; @@ -109,12 +118,11 @@ for (const item of contextMenuItems) { } // Listen for keyboard shortcuts specified by user -browser.commands.onCommand.addListener((command) => { +browser.commands.onCommand.addListener(async (command) => { if (contextMenuItems.some(e => e.action === command)) { - browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => { - if (tabs.length) { - browser.tabs.sendMessage(tabs[0].id, { action: command }); - } - }); + const tabs = await browser.tabs.query({ active: true, currentWindow: true }); + if (tabs.length) { + browser.tabs.sendMessage(tabs[0].id, { action: command }); + } } }); diff --git a/keepassxc-browser/background/keepass.js b/keepassxc-browser/background/keepass.js index 7f6a09c..030a71a 100755 --- a/keepassxc-browser/background/keepass.js +++ b/keepassxc-browser/background/keepass.js @@ -6,7 +6,7 @@ keepass.keyPair = { publicKey: null, secretKey: null }; keepass.serverPublicKey = ''; keepass.clientID = ''; keepass.isConnected = false; -keepass.isDatabaseClosed = false; +keepass.isDatabaseClosed = true; keepass.isKeePassXCAvailable = false; keepass.isEncryptionKeyUnrecognized = false; keepass.currentKeePassXC = ''; @@ -128,26 +128,23 @@ keepass.sendNativeMessage = function(request, enableTimeout = false) { }); }; -keepass.addCredentials = function(callback, tab, username, password, url, group, groupUuid) { - keepass.updateCredentials(callback, tab, null, username, password, url, group, groupUuid); +keepass.addCredentials = async function(tab, args = []) { + const [ username, password, url, group, groupUuid ] = args; + return keepass.updateCredentials(tab, [ null, username, password, url, group, groupUuid ]); }; -keepass.updateCredentials = function(callback, tab, entryId, username, password, url, group, groupUuid) { - if (tab && page.tabs[tab.id]) { - page.tabs[tab.id].errorMessage = null; - } - - keepass.testAssociation((taResponse) => { +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(null, tab); - callback([]); - return; + browserAction.showDefault(tab); + return []; } const kpAction = kpActions.SET_LOGIN; - const { dbid } = keepass.getCryptoKey(); - const nonce = keepass.getNonce(); - const incrementedNonce = keepass.incrementedNonce(nonce); + const [ dbid ] = keepass.getCryptoKey(); + const [ nonce, incrementedNonce ] = keepass.getNonces(); const messageData = { action: kpAction, @@ -167,43 +164,44 @@ keepass.updateCredentials = function(callback, tab, entryId, username, password, messageData.groupUuid = groupUuid; } - const request = { - action: kpAction, - message: keepass.encrypt(messageData, nonce), - nonce: nonce, - clientID: keepass.clientID - }; - - keepass.sendNativeMessage(request).then((response) => { - if (response.message && response.nonce) { - const res = keepass.decrypt(response.message, response.nonce); - if (!res) { - keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE); - callback('error'); - return; - } - - const message = nacl.util.encodeUTF8(res); - const parsed = JSON.parse(message); - callback(keepass.verifyResponse(parsed, incrementedNonce) ? 'success' : 'error'); - } else if (response.error && response.errorCode) { - keepass.handleError(tab, response.errorCode, response.error); - callback('error'); - } else { - browserAction.showDefault(null, tab); + const request = keepass.buildRequest(kpAction, keepass.encrypt(messageData, nonce), nonce, keepass.clientID); + const response = await keepass.sendNativeMessage(request); + if (response.message && response.nonce) { + const res = keepass.decrypt(response.message, response.nonce); + if (!res) { + keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE); + return 'error'; } - }); - }); + + const message = nacl.util.encodeUTF8(res); + const parsed = JSON.parse(message); + + // KeePassXC versions lower than 2.5.0 will have an empty parsed.error + let successMessage = parsed.error; + if (parsed.error === 'success' || parsed.error === '') { + successMessage = entryId ? 'updated' : 'created'; + } + return keepass.verifyResponse(parsed, incrementedNonce) ? successMessage : 'error'; + } else if (response.error && response.errorCode) { + keepass.handleError(tab, response.errorCode, response.error); + return 'error'; + } + + browserAction.showDefault(tab); + return []; + } catch (err) { + console.log('updateCredentials failed: ', err); + return []; + } }; -keepass.retrieveCredentials = function(callback, tab, url, submiturl, forceCallback, triggerUnlock = false, httpAuth = false) { - keepass.testAssociation((taResponse) => { +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(null, tab); - if (forceCallback) { - callback([]); - } - return; + browserAction.showDefault(tab); + return []; } if (tab && page.tabs[tab.id]) { @@ -211,16 +209,14 @@ keepass.retrieveCredentials = function(callback, tab, url, submiturl, forceCallb } if (!keepass.isConnected) { - callback([]); - return; + return []; } let entries = []; const keys = []; const kpAction = kpActions.GET_LOGINS; - const nonce = keepass.getNonce(); - const incrementedNonce = keepass.incrementedNonce(nonce); - const { dbid } = keepass.getCryptoKey(); + const [ nonce, incrementedNonce ] = keepass.getNonces(); + const [ dbid ] = keepass.getCryptoKey(); for (const keyHash in keepass.keyRing) { keys.push({ @@ -244,70 +240,63 @@ keepass.retrieveCredentials = function(callback, tab, url, submiturl, forceCallb messageData.httpAuth = 'true'; } - const request = { - action: kpAction, - message: keepass.encrypt(messageData, nonce), - nonce: nonce, - clientID: keepass.clientID - }; - - keepass.sendNativeMessage(request).then((response) => { - if (response.message && response.nonce) { - const res = keepass.decrypt(response.message, response.nonce); - if (!res) { - keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE); - callback([]); - return; - } - - const message = nacl.util.encodeUTF8(res); - const parsed = JSON.parse(message); - keepass.setcurrentKeePassXCVersion(parsed.version); - - if (keepass.verifyResponse(parsed, incrementedNonce)) { - entries = parsed.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(null, tab); - } - callback(entries); - } else { - console.log('RetrieveCredentials for ' + url + ' rejected'); - } - } else if (response.error && response.errorCode) { - keepass.handleError(tab, response.errorCode, response.error); - callback([]); - } else { - browserAction.showDefault(null, tab); - callback([]); + const request = keepass.buildRequest(kpAction, keepass.encrypt(messageData, nonce), nonce, keepass.clientID); + const response = await keepass.sendNativeMessage(request); + if (response.message && response.nonce) { + const res = keepass.decrypt(response.message, response.nonce); + if (!res) { + keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE); + return []; } - }); - }, tab, false, triggerUnlock); + + const message = nacl.util.encodeUTF8(res); + const parsed = JSON.parse(message); + keepass.setcurrentKeePassXCVersion(parsed.version); + + if (keepass.verifyResponse(parsed, incrementedNonce)) { + entries = parsed.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); + } + + return entries; + } else { + console.log('RetrieveCredentials for ' + url + ' rejected'); + } + } else if (response.error && response.errorCode) { + keepass.handleError(tab, response.errorCode, response.error); + return []; + } + + browserAction.showDefault(tab); + return []; + } catch (err) { + console.log('retrieveCredentials failed: ', err); + return []; + } }; -keepass.generatePassword = function(callback, tab) { +keepass.generatePassword = async function(tab) { if (!keepass.isConnected) { - callback([]); - return; + return []; } - keepass.testAssociation((taresponse) => { - if (!taresponse) { - browserAction.showDefault(null, tab); - callback([]); - return; + try { + const taResponse = await keepass.testAssociation(tab); + if (!taResponse) { + browserAction.showDefault(tab); + return []; } if (!keepass.compareVersion(keepass.requiredKeePassXC, keepass.currentKeePassXC)) { - callback([]); - return; + return []; } let passwords = []; const kpAction = kpActions.GENERATE_PASSWORD; - const nonce = keepass.getNonce(); - const incrementedNonce = keepass.incrementedNonce(nonce); + const [ nonce, incrementedNonce ] = keepass.getNonces(); const request = { action: kpAction, @@ -315,47 +304,50 @@ keepass.generatePassword = function(callback, tab) { clientID: keepass.clientID }; - keepass.sendNativeMessage(request).then((response) => { - if (response.message && response.nonce) { - const res = keepass.decrypt(response.message, response.nonce); - if (!res) { - keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE); - callback([]); - return; - } - - const message = nacl.util.encodeUTF8(res); - const parsed = JSON.parse(message); - keepass.setcurrentKeePassXCVersion(parsed.version); - - if (keepass.verifyResponse(parsed, incrementedNonce)) { - if (parsed.entries) { - passwords = parsed.entries; - keepass.updateLastUsed(keepass.databaseHash); - } else { - console.log('No entries returned. Is KeePassXC up-to-date?'); - } - } else { - console.log('GeneratePassword rejected'); - } - callback(passwords); - } else if (response.error && response.errorCode) { - keepass.handleError(tab, response.errorCode, response.error); + const response = await keepass.sendNativeMessage(request); + if (response.message && response.nonce) { + const res = keepass.decrypt(response.message, response.nonce); + if (!res) { + keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE); + return []; } - }); - }, tab); + + const message = nacl.util.encodeUTF8(res); + const parsed = JSON.parse(message); + keepass.setcurrentKeePassXCVersion(parsed.version); + + if (keepass.verifyResponse(parsed, incrementedNonce)) { + if (parsed.entries) { + passwords = parsed.entries; + keepass.updateLastUsed(keepass.databaseHash); + } else { + console.log('No entries returned. Is KeePassXC up-to-date?'); + } + } else { + console.log('GeneratePassword rejected'); + } + + return passwords; + } else if (response.error && response.errorCode) { + keepass.handleError(tab, response.errorCode, response.error); + } + + return passwords; + } catch (err) { + console.log('generatePassword failed: ', err); + return []; + } }; -keepass.associate = function(callback, tab) { +keepass.associate = async function(tab) { if (keepass.isAssociated()) { - callback([]); - return; + return AssociatedAction.ASSOCIATED; } - keepass.getDatabaseHash((hash) => { + try { + await keepass.getDatabaseHash(tab); if (keepass.isDatabaseClosed || !keepass.isKeePassXCAvailable) { - callback([]); - return; + return AssociatedAction.NOT_ASSOCIATED; } if (tab && page.tabs[tab.id]) { @@ -364,8 +356,7 @@ keepass.associate = function(callback, tab) { const kpAction = kpActions.ASSOCIATE; const key = nacl.util.encodeBase64(keepass.keyPair.publicKey); - const nonce = keepass.getNonce(); - const incrementedNonce = keepass.incrementedNonce(nonce); + const [ nonce, incrementedNonce ] = keepass.getNonces(); const idKeyPair = nacl.box.keyPair(); const idKey = nacl.util.encodeBase64(idKeyPair.publicKey); @@ -375,63 +366,61 @@ keepass.associate = function(callback, tab) { idKey: idKey }; - const request = { - action: kpAction, - message: keepass.encrypt(messageData, nonce), - nonce: nonce, - clientID: keepass.clientID, - triggerUnlock: 'true' - }; + const request = keepass.buildRequest(kpAction, keepass.encrypt(messageData, nonce), nonce, keepass.clientID, true); - keepass.sendNativeMessage(request).then((response) => { - if (response.message && response.nonce) { - const res = keepass.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); - keepass.setcurrentKeePassXCVersion(parsed.version); - const id = parsed.id; - - if (!keepass.verifyResponse(parsed, incrementedNonce)) { - keepass.handleError(tab, kpErrors.ASSOCIATION_FAILED); - } else { - // Use public key as identification key with older KeePassXC releases - const savedKey = keepass.compareVersion('2.3.4', keepass.currentKeePassXC) ? idKey : key; - keepass.setCryptoKey(id, savedKey); // Save the new identification public key as id key for the database - keepass.associated.value = true; - keepass.associated.hash = parsed.hash || 0; - } - - browserAction.show(callback, tab); - } else if (response.error && response.errorCode) { - keepass.handleError(tab, response.errorCode, response.error); + const response = await keepass.sendNativeMessage(request); + if (response.message && response.nonce) { + const res = keepass.decrypt(response.message, response.nonce); + if (!res) { + keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE); + return AssociatedAction.NOT_ASSOCIATED; } - }); - }, tab); + + const message = nacl.util.encodeUTF8(res); + const parsed = JSON.parse(message); + keepass.setcurrentKeePassXCVersion(parsed.version); + const id = parsed.id; + + if (!keepass.verifyResponse(parsed, incrementedNonce)) { + keepass.handleError(tab, kpErrors.ASSOCIATION_FAILED); + return AssociatedAction.NOT_ASSOCIATED; + } else { + // Use public key as identification key with older KeePassXC releases + const savedKey = keepass.compareVersion('2.3.4', keepass.currentKeePassXC) ? idKey : key; + keepass.setCryptoKey(id, savedKey); // Save the new identification public key as id key for the database + keepass.associated.value = true; + keepass.associated.hash = parsed.hash || 0; + } + + browserAction.show(tab); + return AssociatedAction.NEW_ASSOCIATION; + } else if (response.error && response.errorCode) { + keepass.handleError(tab, response.errorCode, response.error); + } + } catch (err) { + console.log('associate failed: ', err); + } + + return AssociatedAction.NOT_ASSOCIATED; }; -keepass.testAssociation = async function(callback, tab, enableTimeout = false, triggerUnlock = false) { +keepass.testAssociation = async function(tab, args = []) { if (tab && page.tabs[tab.id]) { page.tabs[tab.id].errorMessage = null; } - keepass.getDatabaseHash((dbHash) => { + try { + const [ enableTimeout = false, triggerUnlock = false ] = args; + const dbHash = await keepass.getDatabaseHash(tab, [ enableTimeout, triggerUnlock ]); if (!dbHash) { - callback(false); return false; } if (keepass.isDatabaseClosed || !keepass.isKeePassXCAvailable) { - callback(false); return false; } if (keepass.isAssociated()) { - callback(true); return true; } @@ -439,20 +428,17 @@ keepass.testAssociation = async function(callback, tab, enableTimeout = false, t if (tab && page.tabs[tab.id]) { keepass.handleError(tab, kpErrors.PUBLIC_KEY_NOT_FOUND); } - callback(false); return false; } const kpAction = kpActions.TEST_ASSOCIATE; - const nonce = keepass.getNonce(); - const incrementedNonce = keepass.incrementedNonce(nonce); - const { dbid, dbkey } = keepass.getCryptoKey(); + const [ nonce, incrementedNonce ] = keepass.getNonces(); + const [ dbid, dbkey ] = keepass.getCryptoKey(); if (dbkey === null || dbid === null) { if (tab && page.tabs[tab.id]) { keepass.handleError(tab, kpErrors.NO_SAVED_DATABASES_FOUND); } - callback(false); return false; } @@ -462,63 +448,60 @@ keepass.testAssociation = async function(callback, tab, enableTimeout = false, t key: dbkey }; - const request = { - action: kpAction, - message: keepass.encrypt(messageData, nonce), - nonce: nonce, - clientID: keepass.clientID - }; + const request = keepass.buildRequest(kpAction, keepass.encrypt(messageData, nonce), nonce, keepass.clientID); - keepass.sendNativeMessage(request, enableTimeout).then((response) => { - if (response.message && response.nonce) { - const res = keepass.decrypt(response.message, response.nonce); - if (!res) { - keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE); - callback(false); - return; - } - - const message = nacl.util.encodeUTF8(res); - const parsed = JSON.parse(message); - keepass.setcurrentKeePassXCVersion(parsed.version); - keepass.isEncryptionKeyUnrecognized = false; - - if (!keepass.verifyResponse(parsed, incrementedNonce)) { - 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 { - if (tab && page.tabs[tab.id]) { - delete page.tabs[tab.id].errorMessage; - } - } - } else if (response.error && response.errorCode) { - keepass.handleError(tab, response.errorCode, response.error); + const response = await keepass.sendNativeMessage(request, enableTimeout); + if (response.message && response.nonce) { + const res = keepass.decrypt(response.message, response.nonce); + if (!res) { + keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE); + return false; } - callback(keepass.isAssociated()); - }); - }, tab, enableTimeout, triggerUnlock); + + const message = nacl.util.encodeUTF8(res); + const parsed = JSON.parse(message); + keepass.setcurrentKeePassXCVersion(parsed.version); + keepass.isEncryptionKeyUnrecognized = false; + + if (!keepass.verifyResponse(parsed, incrementedNonce)) { + 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; + if (tab && page.tabs[tab.id]) { + delete page.tabs[tab.id].errorMessage; + } + } + } else if (response.error && response.errorCode) { + keepass.handleError(tab, response.errorCode, response.error); + } + + return keepass.isAssociated(); + } catch (err) { + console.log('testAssociation failed: ', err); + return false; + } }; -keepass.getDatabaseHash = function(callback, tab, enableTimeout = false, triggerUnlock = false) { +keepass.getDatabaseHash = async function(tab, args = []) { if (!keepass.isConnected) { keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED); - callback([]); - return; + return ''; } if (!keepass.serverPublicKey) { keepass.changePublicKeys(tab); } + const [ enableTimeout = false, triggerUnlock = false ] = args; const kpAction = kpActions.GET_DATABASE_HASH; - const nonce = keepass.getNonce(); - const incrementedNonce = keepass.incrementedNonce(nonce); + const [ nonce, incrementedNonce ] = keepass.getNonces(); const messageData = { action: kpAction, @@ -528,28 +511,18 @@ keepass.getDatabaseHash = function(callback, tab, enableTimeout = false, trigger const encrypted = keepass.encrypt(messageData, nonce); if (encrypted.length <= 0) { keepass.handleError(tab, kpErrors.PUBLIC_KEY_NOT_FOUND); - callback(keepass.databaseHash); - return; + return keepass.databaseHash; } - const request = { - action: kpAction, - message: encrypted, - nonce: nonce, - clientID: keepass.clientID - }; + const request = keepass.buildRequest(kpAction, keepass.encrypt(messageData, nonce), nonce, keepass.clientID, triggerUnlock); - if (triggerUnlock === true) { - request.triggerUnlock = 'true'; - } - - keepass.sendNativeMessage(request, enableTimeout).then((response) => { + try { + const response = await keepass.sendNativeMessage(request, enableTimeout); if (response.message && response.nonce) { const res = keepass.decrypt(response.message, response.nonce); if (!res) { keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE); - callback(''); - return; + return ''; } const message = nacl.util.encodeUTF8(res); @@ -559,7 +532,7 @@ keepass.getDatabaseHash = function(callback, tab, enableTimeout = false, trigger keepass.setcurrentKeePassXCVersion(parsed.version); keepass.databaseHash = parsed.hash || ''; - if (oldDatabaseHash && oldDatabaseHash !== keepass.databaseHash) { + if (oldDatabaseHash && oldDatabaseHash != keepass.databaseHash) { keepass.associated.value = false; keepass.associated.hash = null; } @@ -572,125 +545,125 @@ keepass.getDatabaseHash = function(callback, tab, enableTimeout = false, trigger keepass.updateDatabaseHash(parsed.oldHash, parsed.hash); } - callback(parsed.hash); - return; + return parsed.hash; } else if (parsed.errorCode) { keepass.databaseHash = ''; keepass.isDatabaseClosed = true; keepass.handleError(tab, kpErrors.DATABASE_NOT_OPENED); - callback(keepass.databaseHash); - return; + 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) { + console.log('getDatabaseHash failed: ', err); + return keepass.databaseHash; + } +}; + +keepass.changePublicKeys = async function(tab, enableTimeout = false) { + 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 ] = keepass.getNonces(); + keepass.clientID = nacl.util.encodeBase64(nacl.randomBytes(keepass.keySize)); + + const request = { + action: kpAction, + publicKey: key, + nonce: nonce, + clientID: keepass.clientID + }; + + try { + const response = await keepass.sendNativeMessage(request, enableTimeout); + keepass.setcurrentKeePassXCVersion(response.version); + + if (!keepass.verifyKeyResponse(response, key, incrementedNonce)) { + if (tab && page.tabs[tab.id]) { + keepass.handleError(tab, kpErrors.KEY_CHANGE_FAILED); + return false; } } else { - 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); - } - callback(keepass.databaseHash); - return; + keepass.isKeePassXCAvailable = true; + console.log('Server public key: ' + nacl.util.encodeBase64(keepass.serverPublicKey)); } - }); + + return true; + } catch (err) { + console.log('changePublicKeys failed: ', err); + return false; + } }; -keepass.changePublicKeys = function(tab, enableTimeout = false) { - return new Promise((resolve, reject) => { - if (!keepass.isConnected) { - keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED); - reject(false); - } +keepass.lockDatabase = async function(tab) { + 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 = keepass.getNonce(); - const incrementedNonce = keepass.incrementedNonce(nonce); - keepass.clientID = nacl.util.encodeBase64(nacl.randomBytes(keepass.keySize)); + const kpAction = kpActions.LOCK_DATABASE; + const [ nonce, incrementedNonce ] = keepass.getNonces(); - const request = { - action: kpAction, - publicKey: key, - nonce: nonce, - clientID: keepass.clientID - }; + const messageData = { + action: kpAction + }; - keepass.sendNativeMessage(request, enableTimeout).then((response) => { - keepass.setcurrentKeePassXCVersion(response.version); + const request = keepass.buildRequest(kpAction, keepass.encrypt(messageData, nonce), nonce, keepass.clientID); - if (!keepass.verifyKeyResponse(response, key, incrementedNonce)) { - if (tab && page.tabs[tab.id]) { - keepass.handleError(tab, kpErrors.KEY_CHANGE_FAILED); - } - reject(false); - } else { - keepass.isKeePassXCAvailable = true; - console.log('Server public key: ' + nacl.util.encodeBase64(keepass.serverPublicKey)); + try { + const response = await keepass.sendNativeMessage(request); + if (response.message && response.nonce) { + const res = keepass.decrypt(response.message, response.nonce); + if (!res) { + keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE); + return false; } - resolve(true); - }); - }); -}; -keepass.lockDatabase = function(tab) { - return new Promise((resolve, reject) => { - if (!keepass.isConnected) { - keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED); - reject(false); - } + const message = nacl.util.encodeUTF8(res); + const parsed = JSON.parse(message); + keepass.setcurrentKeePassXCVersion(parsed.version); - const kpAction = kpActions.LOCK_DATABASE; - const nonce = keepass.getNonce(); - const incrementedNonce = keepass.incrementedNonce(nonce); - - const messageData = { - action: kpAction - }; - - const request = { - action: kpAction, - message: keepass.encrypt(messageData, nonce), - nonce: nonce, - clientID: keepass.clientID - }; - - keepass.sendNativeMessage(request).then((response) => { - if (response.message && response.nonce) { - const res = keepass.decrypt(response.message, response.nonce); - if (!res) { - keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE); - resolve(false); - return; - } - - const message = nacl.util.encodeUTF8(res); - const parsed = JSON.parse(message); - keepass.setcurrentKeePassXCVersion(parsed.version); - - if (keepass.verifyResponse(parsed, incrementedNonce)) { - keepass.isDatabaseClosed = true; - keepass.updateDatabase(); - - // Display error message in the popup - keepass.handleError(tab, kpErrors.DATABASE_NOT_OPENED); - resolve(true); - } - } else if (response.error && response.errorCode) { + if (keepass.verifyResponse(parsed, incrementedNonce)) { keepass.isDatabaseClosed = true; - keepass.handleError(tab, response.errorCode, response.error); + keepass.updateDatabase(); + + // Display error message in the popup + keepass.handleError(tab, kpErrors.DATABASE_NOT_OPENED); + return true; } - resolve(false); - }); - }); + } else if (response.error && response.errorCode) { + keepass.isDatabaseClosed = true; + keepass.handleError(tab, response.errorCode, response.error); + } + + return false; + } catch (err) { + console.log('lockDatabase failed: ', err); + return false; + } }; -keepass.getDatabaseGroups = function(callback, tab) { - keepass.testAssociation((taResponse) => { +keepass.getDatabaseGroups = async function(tab) { + try { + const taResponse = await keepass.testAssociation(tab, [ false ]); if (!taResponse) { - browserAction.showDefault(null, tab); - callback([]); - return; + browserAction.showDefault(tab); + return []; } if (tab && page.tabs[tab.id]) { @@ -698,65 +671,59 @@ keepass.getDatabaseGroups = function(callback, tab) { } if (!keepass.isConnected) { - callback([]); - return; + return []; } let groups = []; const kpAction = kpActions.GET_DATABASE_GROUPS; - const nonce = keepass.getNonce(); - const incrementedNonce = keepass.incrementedNonce(nonce); + const [ nonce, incrementedNonce ] = keepass.getNonces(); const messageData = { action: kpAction }; - const request = { - action: kpAction, - message: keepass.encrypt(messageData, nonce), - nonce: nonce, - clientID: keepass.clientID - }; - - keepass.sendNativeMessage(request).then((response) => { - if (response.message && response.nonce) { - const res = keepass.decrypt(response.message, response.nonce); - if (!res) { - keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE); - callback([]); - return; - } - - const message = nacl.util.encodeUTF8(res); - const parsed = JSON.parse(message); - - if (keepass.verifyResponse(parsed, incrementedNonce)) { - groups = parsed.groups; - groups.defaultGroup = page.settings.defaultGroup; - groups.defaultGroupAlwaysAsk = page.settings.defaultGroupAlwaysAsk; - keepass.updateLastUsed(keepass.databaseHash); - callback(groups); - } else { - console.log('getDatabaseGroups rejected'); - callback([]); - } - } else if (response.error && response.errorCode) { - keepass.handleError(tab, response.errorCode, response.error); - callback([]); - } else { - browserAction.showDefault(null, tab); - callback([]); + const request = keepass.buildRequest(kpAction, keepass.encrypt(messageData, nonce), nonce, keepass.clientID); + const response = await keepass.sendNativeMessage(request); + if (response.message && response.nonce) { + const res = keepass.decrypt(response.message, response.nonce); + if (!res) { + keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE); + return []; } - }); - }, tab, false); + + const message = nacl.util.encodeUTF8(res); + const parsed = JSON.parse(message); + + if (keepass.verifyResponse(parsed, incrementedNonce)) { + groups = parsed.groups; + groups.defaultGroup = page.settings.defaultGroup; + groups.defaultGroupAlwaysAsk = page.settings.defaultGroupAlwaysAsk; + keepass.updateLastUsed(keepass.databaseHash); + return groups; + } else { + console.log('getDatabaseGroups rejected'); + return []; + } + } else if (response.error && response.errorCode) { + keepass.handleError(tab, response.errorCode, response.error); + return []; + } + + browserAction.showDefault(tab); + return []; + } catch (err) { + console.log('getDatabaseGroups failed: ', err); + return []; + } }; -keepass.createNewGroup = function(callback, tab, groupName) { - keepass.testAssociation((taResponse) => { +keepass.createNewGroup = async function(tab, args = []) { + try { + const [ groupName ] = args; + const taResponse = await keepass.testAssociation(tab, [ false ]); if (!taResponse) { - browserAction.showDefault(null, tab); - callback([]); - return; + browserAction.showDefault(tab); + return []; } if (tab && page.tabs[tab.id]) { @@ -764,54 +731,47 @@ keepass.createNewGroup = function(callback, tab, groupName) { } if (!keepass.isConnected) { - callback([]); - return; + return []; } const kpAction = kpActions.CREATE_NEW_GROUP; - const nonce = keepass.getNonce(); - const incrementedNonce = keepass.incrementedNonce(nonce); + const [ nonce, incrementedNonce ] = keepass.getNonces(); const messageData = { action: kpAction, groupName: groupName }; - const request = { - action: kpAction, - message: keepass.encrypt(messageData, nonce), - nonce: nonce, - clientID: keepass.clientID - }; - - keepass.sendNativeMessage(request).then((response) => { - if (response.message && response.nonce) { - const res = keepass.decrypt(response.message, response.nonce); - if (!res) { - keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE); - callback([]); - return; - } - - const message = nacl.util.encodeUTF8(res); - const parsed = JSON.parse(message); - - if (keepass.verifyResponse(parsed, incrementedNonce)) { - keepass.updateLastUsed(keepass.databaseHash); - callback(parsed); - } else { - console.log('getDatabaseGroups rejected'); - callback([]); - } - } else if (response.error && response.errorCode) { - keepass.handleError(tab, response.errorCode, response.error); - callback([]); - } else { - browserAction.showDefault(null, tab); - callback([]); + const request = keepass.buildRequest(kpAction, keepass.encrypt(messageData, nonce), nonce, keepass.clientID); + const response = await keepass.sendNativeMessage(request); + if (response.message && response.nonce) { + const res = keepass.decrypt(response.message, response.nonce); + if (!res) { + keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE); + return []; } - }); - }, tab, false); + + const message = nacl.util.encodeUTF8(res); + const parsed = JSON.parse(message); + + if (keepass.verifyResponse(parsed, incrementedNonce)) { + keepass.updateLastUsed(keepass.databaseHash); + return parsed; + } + + console.log('getDatabaseGroups rejected'); + return []; + } else if (response.error && response.errorCode) { + keepass.handleError(tab, response.errorCode, response.error); + return []; + } + + browserAction.showDefault(tab); + return []; + } catch (err) { + console.log('createNewGroup failed: ', err); + return []; + } }; keepass.generateNewKeyPair = function() { @@ -819,20 +779,17 @@ keepass.generateNewKeyPair = function() { //console.log(nacl.util.encodeBase64(keepass.keyPair.publicKey) + ' ' + nacl.util.encodeBase64(keepass.keyPair.secretKey)); }; -keepass.isConfigured = function() { - return new Promise((resolve, reject) => { - if (typeof(keepass.databaseHash) === 'undefined') { - keepass.getDatabaseHash((hash) => { - resolve(hash in keepass.keyRing); - }); - } else { - resolve(keepass.databaseHash in keepass.keyRing); - } - }); +keepass.isConfigured = async function() { + if (typeof(keepass.databaseHash) === 'undefined') { + const hash = keepass.getDatabaseHash(); + return hash in keepass.keyRing; + } + + return keepass.databaseHash in keepass.keyRing; }; -keepass.checkDatabaseHash = function(callback, tab) { - callback(keepass.databaseHash); +keepass.checkDatabaseHash = async function(tab) { + return keepass.databaseHash; }; keepass.isAssociated = function() { @@ -995,7 +952,7 @@ keepass.getNonce = function() { keepass.incrementedNonce = function(nonce) { const oldNonce = nacl.util.decodeBase64(nonce); - let newNonce = oldNonce.slice(0); + const newNonce = oldNonce.slice(0); // from libsodium/utils.c let i = 0; @@ -1009,6 +966,12 @@ keepass.incrementedNonce = function(nonce) { return nacl.util.encodeBase64(newNonce); }; +keepass.getNonces = function() { + const nonce = keepass.getNonce(); + const incrementedNonce = keepass.incrementedNonce(nonce); + return [ nonce, incrementedNonce ]; +}; + keepass.nativeConnect = function() { console.log('Connecting to native messaging host ' + keepass.nativeHostName); keepass.nativePort = browser.runtime.connectNative(keepass.nativeHostName); @@ -1105,7 +1068,7 @@ keepass.getCryptoKey = function() { let dbkey = null; let dbid = null; if (!(keepass.databaseHash in keepass.keyRing)) { - return { dbid, dbkey }; + return [ dbid, dbkey ]; } dbid = keepass.keyRing[keepass.databaseHash].id; @@ -1114,7 +1077,7 @@ keepass.getCryptoKey = function() { dbkey = keepass.keyRing[keepass.databaseHash].key; } - return { dbid, dbkey }; + return [ dbid, dbkey ]; }; keepass.setCryptoKey = function(id, key) { @@ -1131,6 +1094,7 @@ keepass.encrypt = function(input, nonce) { return nacl.util.encodeBase64(message); } } + return ''; }; @@ -1149,7 +1113,7 @@ keepass.enableAutomaticReconnect = function() { } if (keepass.reconnectLoop === null) { - keepass.reconnectLoop = setInterval(async() => { + keepass.reconnectLoop = setInterval(async () => { if (!keepass.isKeePassXCAvailable) { keepass.reconnect(); } @@ -1162,24 +1126,23 @@ keepass.disableAutomaticReconnect = function() { keepass.reconnectLoop = null; }; -keepass.reconnect = function(callback, tab) { - return new Promise((resolve) => { +keepass.reconnect = function(tab) { + return new Promise(async (resolve) => { keepass.connectToNative(); keepass.generateNewKeyPair(); - keepass.changePublicKeys(tab, true).then((r) => { - keepass.getDatabaseHash((gdRes) => { - if (gdRes !== '' && tab && page.tabs[tab.id]) { - delete page.tabs[tab.id].errorMessage; - } - keepass.testAssociation((associationResponse) => { - keepass.isConfigured().then((configured) => { - resolve(true); - }); - }); - }, tab); - }).catch((e) => { - resolve(false); + await keepass.changePublicKeys(tab, true).catch((e) => { + resolve(false); }); + + const hash = await keepass.getDatabaseHash(tab); + if (hash !== '' && tab && page.tabs[tab.id]) { + delete page.tabs[tab.id].errorMessage; + } + + await keepass.testAssociation(); + await keepass.isConfigured(); + keepass.updateDatabaseHashToContent(); + resolve(true); }); }; @@ -1187,33 +1150,34 @@ keepass.updatePopup = function(iconType) { if (page && page.tabs.length > 0) { const data = page.tabs[page.currentTabId].stack[page.tabs[page.currentTabId].stack.length - 1]; data.iconType = iconType; - browserAction.show(null, { 'id': page.currentTabId }); + browserAction.show({ 'id': page.currentTabId }); } }; // Updates the database hashes to content script -keepass.updateDatabase = function() { - keepass.testAssociation((associationResponse) => { - keepass.isConfigured().then((configured) => { - keepass.updatePopup(configured ? 'normal' : 'cross'); - keepass.updateDatabaseHashToContent(); - }); - }, null); +keepass.updateDatabase = async function() { + await keepass.testAssociation(null); + const configured = await keepass.isConfigured(); + keepass.updatePopup(configured ? 'normal' : 'cross'); + keepass.updateDatabaseHashToContent(); }; -keepass.updateDatabaseHashToContent = function() { - // Send message to content script - browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => { +keepass.updateDatabaseHashToContent = async function() { + try { + const tabs = await browser.tabs.query({ active: true, currentWindow: true }); if (tabs.length) { + // Send message to content script browser.tabs.sendMessage(tabs[0].id, { action: 'check_database_hash', hash: { old: keepass.previousDatabaseHash, new: keepass.databaseHash } }).catch((err) => { - console.log(err); + console.log('Error: No content script available for this tab.'); }); keepass.previousDatabaseHash = keepass.databaseHash; } - }); + } catch (err) { + console.log('updateDatabaseHashToContent failed: ', err); + } }; keepass.compareVersion = function(minimum, current, canBeEqual = true) { @@ -1225,3 +1189,22 @@ keepass.compareVersion = function(minimum, current, canBeEqual = true) { const cur = current.split('.', 3).map(s => s.padStart(4, '0')).join('.'); return (canBeEqual ? (min <= cur) : (min < cur)); }; + +keepass.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; +}; + +keepass.getIsKeePassXCAvailable = async function() { + return keepass.isKeePassXCAvailable; +}; diff --git a/keepassxc-browser/background/page.js b/keepassxc-browser/background/page.js index f161b60..08d6f6c 100755 --- a/keepassxc-browser/background/page.js +++ b/keepassxc-browser/background/page.js @@ -4,7 +4,7 @@ const defaultSettings = { checkUpdateKeePassXC: 3, autoCompleteUsernames: true, autoFillAndSend: false, - usePasswordGenerator: true, + usePasswordGeneratorIcons: false, autoFillSingleEntry: false, autoSubmit: false, autoRetrieveCredentials: true, @@ -17,79 +17,86 @@ const defaultSettings = { }; var page = {}; -page.tabs = []; -page.currentTabId = -1; page.blockedTabs = []; +page.currentTabId = -1; page.loginId = -1; +page.submitted = false; +page.submittedCredentials = {}; +page.tabs = []; +page.usernameFieldDetected = false; -page.initSettings = function() { - return new Promise((resolve, reject) => { - browser.storage.local.get({ 'settings': {} }).then((item) => { - page.settings = item.settings; - if (!('checkUpdateKeePassXC' in page.settings)) { - page.settings.checkUpdateKeePassXC = defaultSettings.checkUpdateKeePassXC; - } - if (!('autoCompleteUsernames' in page.settings)) { - page.settings.autoCompleteUsernames = defaultSettings.autoCompleteUsernames; - } - if (!('autoFillAndSend' in page.settings)) { - page.settings.autoFillAndSend = defaultSettings.autoFillAndSend; - } - if (!('usePasswordGenerator' in page.settings)) { - page.settings.usePasswordGenerator = defaultSettings.usePasswordGenerator; - } - if (!('autoFillSingleEntry' in page.settings)) { - page.settings.autoFillSingleEntry = defaultSettings.autoFillSingleEntry; - } - if (!('autoSubmit' in page.settings)) { - page.settings.autoSubmit = defaultSettings.autoSubmit; - } - if (!('autoRetrieveCredentials' in page.settings)) { - page.settings.autoRetrieveCredentials = defaultSettings.autoRetrieveCredentials; - } - if (!('showNotifications' in page.settings)) { - page.settings.showNotifications = defaultSettings.showNotifications; - } - if (!('showLoginNotifications' in page.settings)) { - page.settings.showLoginNotifications = defaultSettings.showLoginNotifications; - } - if (!('saveDomainOnly' in page.settings)) { - page.settings.saveDomainOnly = defaultSettings.saveDomainOnly; - } - if (!('autoReconnect' in page.settings)) { - page.settings.autoReconnect = defaultSettings.autoReconnect; - } - if (!('defaultGroup' in page.settings)) { - page.settings.defaultGroup = defaultSettings.defaultGroup; - } - if (!('defaultGroupAlwaysAsk' in page.settings)) { - page.settings.defaultGroupAlwaysAsk = defaultSettings.defaultGroupAlwaysAsk; - } - browser.storage.local.set({ 'settings': page.settings }); - resolve(page.settings); - }); - }); +page.initSettings = async function() { + try { + const item = await browser.storage.local.get({ 'settings': {} }); + page.settings = item.settings; + + if (!('checkUpdateKeePassXC' in page.settings)) { + page.settings.checkUpdateKeePassXC = defaultSettings.checkUpdateKeePassXC; + } + if (!('autoCompleteUsernames' in page.settings)) { + page.settings.autoCompleteUsernames = defaultSettings.autoCompleteUsernames; + } + if (!('autoFillAndSend' in page.settings)) { + page.settings.autoFillAndSend = defaultSettings.autoFillAndSend; + } + if (!('usePasswordGeneratorIcons' in page.settings)) { + page.settings.usePasswordGeneratorIcons = defaultSettings.usePasswordGeneratorIcons; + } + if (!('autoFillSingleEntry' in page.settings)) { + page.settings.autoFillSingleEntry = defaultSettings.autoFillSingleEntry; + } + if (!('autoSubmit' in page.settings)) { + page.settings.autoSubmit = defaultSettings.autoSubmit; + } + if (!('autoRetrieveCredentials' in page.settings)) { + page.settings.autoRetrieveCredentials = defaultSettings.autoRetrieveCredentials; + } + if (!('showNotifications' in page.settings)) { + page.settings.showNotifications = defaultSettings.showNotifications; + } + if (!('showLoginNotifications' in page.settings)) { + page.settings.showLoginNotifications = defaultSettings.showLoginNotifications; + } + if (!('saveDomainOnly' in page.settings)) { + page.settings.saveDomainOnly = defaultSettings.saveDomainOnly; + } + if (!('autoReconnect' in page.settings)) { + page.settings.autoReconnect = defaultSettings.autoReconnect; + } + if (!('defaultGroup' in page.settings)) { + page.settings.defaultGroup = defaultSettings.defaultGroup; + } + if (!('defaultGroupAlwaysAsk' in page.settings)) { + page.settings.defaultGroupAlwaysAsk = defaultSettings.defaultGroupAlwaysAsk; + } + + await browser.storage.local.set({ 'settings': page.settings }); + return page.settings; + } catch (err) { + console.log('page.initSettings error: ' + err); + return Promise.reject(); + } }; -page.initOpenedTabs = function() { - return new Promise((resolve, reject) => { - browser.tabs.query({}).then((tabs) => { - for (const i of tabs) { - page.createTabEntry(i.id); - } +page.initOpenedTabs = async function() { + try { + const tabs = await browser.tabs.query({}); + for (const i of tabs) { + page.createTabEntry(i.id); + } - // Set initial tab-ID - browser.tabs.query({ 'active': true, 'currentWindow': true }).then((currentTabs) => { - if (currentTabs.length === 0) { - resolve(); - return; // For example: only the background devtools or a popup are opened - } - page.currentTabId = currentTabs[0].id; - browserAction.show(null, currentTabs[0]); - resolve(); - }); - }); - }); + // Set initial tab-ID + const currentTabs = await browser.tabs.query({ active: true, currentWindow: true }); + if (currentTabs.length === 0) { + return Promise.resolve(); + } + page.currentTabId = currentTabs[0].id; + browserAction.show(currentTabs[0]); + return Promise.resolve(); + } catch (err) { + console.log('page.initOpenedTabs error: ' + err); + return Promise.reject(); + } }; page.isValidProtocol = function(url) { @@ -98,8 +105,8 @@ page.isValidProtocol = function(url) { return !(url.indexOf('.') === -1 || (protocol !== 'http' && protocol !== 'https' && protocol !== 'ftp' && protocol !== 'sftp')); }; -page.switchTab = function(callback, tab) { - browserAction.showDefault(null, tab); +page.switchTab = function(tab) { + browserAction.showDefault(tab); browser.tabs.sendMessage(tab.id, { action: 'activated_tab' }).catch((e) => {}); }; @@ -108,7 +115,8 @@ page.clearCredentials = function(tabId, complete) { return; } - page.tabs[tabId].credentials = {}; + page.usernameFieldDetected = false; + page.tabs[tabId].credentials = []; delete page.tabs[tabId].credentials; if (complete) { @@ -128,30 +136,43 @@ page.clearLogins = function(tabId) { page.tabs[tabId].loginList = []; }; +page.setSubmittedCredentials = function(submitted, username, password, url, oldCredentials) { + page.submittedCredentials.submitted = submitted; + page.submittedCredentials.username = username; + page.submittedCredentials.password = password; + page.submittedCredentials.url = url; + page.submittedCredentials.oldCredentials = oldCredentials; +}; + +page.clearSubmittedCredentials = function() { + page.submitted = false; + page.submittedCredentials = {}; +}; + page.createTabEntry = function(tabId) { page.tabs[tabId] = { 'stack': [], 'errorMessage': null, 'loginList': [] }; + page.clearSubmittedCredentials(); }; -page.removePageInformationFromNotExistingTabs = function() { +page.removePageInformationFromNotExistingTabs = async function() { const rand = Math.floor(Math.random() * 1001); if (rand === 28) { - browser.tabs.query({}).then(function(tabs) { - const tabIds = []; - const infoIds = Object.keys(page.tabs); + const tabs = await browser.tabs.query({}); + const tabIds = []; + const infoIds = Object.keys(page.tabs); - for (const t of tabs) { - tabIds[t.id] = true; - } + for (const t of tabs) { + tabIds[t.id] = true; + } - for (const i of infoIds) { - if (!(i in tabIds)) { - delete page.tabs[i]; - } + for (const i of infoIds) { + if (!(i in tabIds)) { + delete page.tabs[i]; } - }); + } } }; diff --git a/keepassxc-browser/content/autocomplete.js b/keepassxc-browser/content/autocomplete.js index 0483cf2..9ac794f 100644 --- a/keepassxc-browser/content/autocomplete.js +++ b/keepassxc-browser/content/autocomplete.js @@ -34,13 +34,9 @@ kpxcAutocomplete.create = function(input, showListInstantly = false, autoSubmit kpxcAutocomplete.showList = function(inputField) { kpxcAutocomplete.closeList(); kpxcAutocomplete.input = inputField; - const div = kpxcUI.createElement('div', 'kpxcAutocomplete-items', { 'id': 'kpxcAutocomplete-list' }); - // Element position - const rect = inputField.getBoundingClientRect(); - div.style.top = String((rect.top + document.body.scrollTop) + inputField.offsetHeight) + 'px'; - div.style.left = String((rect.left + document.body.scrollLeft)) + 'px'; - div.style.minWidth = String(inputField.offsetWidth) + 'px'; + const div = kpxcUI.createElement('div', 'kpxcAutocomplete-items', { 'id': 'kpxcAutocomplete-list' }); + kpxcAutocomplete.updatePosition(inputField, div); div.style.zIndex = '2147483646'; document.body.append(div); @@ -56,6 +52,10 @@ kpxcAutocomplete.showList = function(inputField) { // Save index for combination.loginId const index = Array.prototype.indexOf.call(e.currentTarget.parentElement.childNodes, e.currentTarget); + browser.runtime.sendMessage({ + action: 'page_set_login_id', args: [ index ] + }); + inputField.value = this.getElementsByTagName('input')[0].value; kpxcAutocomplete.fillPassword(inputField.value, index); kpxcAutocomplete.closeList(); @@ -188,6 +188,7 @@ kpxcAutocomplete.keyPress = function(e) { kpxcAutocomplete.fillPassword = function(value, index) { const fieldId = kpxcAutocomplete.input.getAttribute('data-kpxc-id'); kpxcFields.prepareId(fieldId); + const givenType = kpxcAutocomplete.input.type === 'password' ? 'password' : 'username'; const combination = kpxcFields.getCombination(givenType, fieldId); combination.loginId = index; @@ -196,6 +197,18 @@ kpxcAutocomplete.fillPassword = function(value, index) { kpxcAutocomplete.input.setAttribute('fetched', true); }; +kpxcAutocomplete.updatePosition = function(inputField, elem) { + const div = elem || $('.kpxcAutocomplete-items'); + if (!div) { + return; + } + + const rect = inputField.getBoundingClientRect(); + div.style.top = Pixels((rect.top + document.scrollingElement.scrollTop) + inputField.offsetHeight); + div.style.left = Pixels((rect.left + document.scrollingElement.scrollLeft)); + div.style.minWidth = Pixels(inputField.offsetWidth); +}; + // Detect click outside autocomplete document.addEventListener('click', function(e) { if (!e.isTrusted) { @@ -207,7 +220,31 @@ document.addEventListener('click', function(e) { return; } - if (e.target !== kpxcAutocomplete.input && (e.target.nodeName !== kpxcAutocomplete.input.nodeName)) { + if (e.target !== kpxcAutocomplete.input && + !e.target.classList.contains('kpxc-username-icon') && + e.target.nodeName !== kpxcAutocomplete.input.nodeName) { kpxcAutocomplete.closeList(e.target); } }); + +// Handle autocomplete position on window resize +window.addEventListener('resize', function() { + if (!kpxc.settings.autoCompleteUsernames) { + return; + } + + if (kpxcAutocomplete.input) { + kpxcAutocomplete.updatePosition(kpxcAutocomplete.input); + } +}); + +// Handle autocomplete position on scroll +window.addEventListener('scroll', function() { + if (!kpxc.settings.autoCompleteUsernames) { + return; + } + + if (kpxcAutocomplete.input) { + kpxcAutocomplete.updatePosition(kpxcAutocomplete.input); + } +}); diff --git a/keepassxc-browser/content/banner.js b/keepassxc-browser/content/banner.js new file mode 100644 index 0000000..49e6102 --- /dev/null +++ b/keepassxc-browser/content/banner.js @@ -0,0 +1,390 @@ +'use strict'; + +const DEFAULT_BROWSER_GROUP = 'KeePassXC-Browser Passwords'; + +var kpxcBanner = {}; +kpxcBanner.banner = undefined; +kpxcBanner.created = false; +kpxcBanner.credentials = {}; + +kpxcBanner.destroy = function() { + kpxcBanner.created = false; + kpxcBanner.credentials = {}; + + const dialog = $('.kpxc-banner-dialog'); + if (dialog) { + document.body.removeChild(dialog); + } + + browser.runtime.sendMessage({ + action: 'remove_credentials_from_tab_information' + }); + + const banners = document.querySelectorAll('.kpxc-banner'); + if (banners.length > 0) { + for (const b of banners) { + document.body.removeChild(b); + } + return; + } + + document.body.removeChild(kpxcBanner.banner); +}; + +kpxcBanner.create = async function(credentials = {}) { + if (!kpxc.settings.showLoginNotifications || kpxcBanner.created) { + return; + } + + // Check if database is closed + const state = await browser.runtime.sendMessage({ action: 'check_database_hash' }); + if (state === '') { + //kpxcUI.createNotification('error', tr('rememberErrorDatabaseClosed')); + return; + } + + // Don't show anything if the site is in the ignore + if (kpxc.settings.sitePreferences !== undefined) { + for (const site of kpxc.settings.sitePreferences) { + if (site.ignore !== IGNORE_NOTHING && (site.url === credentials.url || siteMatch(site.url, credentials.url))) { + return; + } + } + } + + kpxcBanner.created = true; + kpxcBanner.credentials = credentials; + + const banner = kpxcUI.createElement('div', 'kpxc-banner', { 'id': 'container' }); + banner.style.zIndex = '2147483646'; + + const bannerInfo = kpxcUI.createElement('div', 'banner-info'); + const bannerButtons = kpxcUI.createElement('div', 'banner-buttons'); + + const className = (isFirefox() ? 'kpxc-banner-icon-moz' : 'kpxc-banner-icon'); + const icon = kpxcUI.createElement('span', className, { 'alt': 'logo' }); + + const infoText = kpxcUI.createElement('span', '', {}, tr('rememberInfoText')); + const usernameText = kpxcUI.createElement('span', 'small', {}, tr('popupUsername') + ' '); + const usernameSpan = kpxcUI.createElement('span', 'small info information-username', {}, credentials.username); + + const newButton = kpxcUI.createElement('button', 'kpxc-button kpxc-green-button', { 'id': 'kpxc-banner-btn-new' }, tr('popupButtonNew')); + const updateButton = kpxcUI.createElement('button', 'kpxc-button kpxc-orange-button', { 'id': 'kpxc-banner-btn-update' }, tr('popupButtonUpdate')); + const dismissButton = kpxcUI.createElement('button', 'kpxc-button kpxc-red-button', { 'id': 'kpxc-banner-btn-dismiss' }, tr('popupButtonDismiss')); + + const separator = kpxcUI.createElement('div', 'separator'); + const ignoreCheckbox = kpxcUI.createElement('input', 'kpxc-checkbox', { type: 'checkbox', name: 'ignoreCheckbox' }); + const checkboxLabel = kpxcUI.createElement('label', 'kpxc-checkbox-label', { for: 'ignoreCheckbox' }, tr('popupButtonIgnore')); + + // No existing credentials to update --> disable Update button + if (credentials.list.length === 0) { + updateButton.classList.remove('kpxc-orange-button'); + updateButton.disabled = true; + } + + newButton.addEventListener('click', function(e) { + if (!e.isTrusted) { + return; + } + kpxcBanner.saveNewCredentials(credentials); + }); + + updateButton.addEventListener('click', function(e) { + if (!e.isTrusted) { + return; + } + kpxcBanner.updateCredentials(credentials); + }); + + dismissButton.addEventListener('click', function(e) { + if (!e.isTrusted) { + return; + } + + // If a banner dialog is shown, display the main banner + const dialog = $('.kpxc-banner-dialog'); + if (dialog) { + $('#kpxc-banner-btn-new').hidden = false; + $('#kpxc-banner-btn-update').hidden = false; + $('.kpxc-checkbox').disabled = false; + document.body.removeChild(dialog); + } else { + if (ignoreCheckbox.checked) { + kpxc.ignoreSite([ window.top.location.href ]); + } + kpxcBanner.destroy(); + } + }); + + kpxcBanner.banner = banner; + bannerInfo.appendMultiple(icon, infoText, usernameText, usernameSpan); + bannerButtons.appendMultiple(newButton, updateButton, separator, ignoreCheckbox, checkboxLabel, dismissButton); + banner.appendMultiple(bannerInfo, bannerButtons); + document.body.appendChild(banner); +}; + +kpxcBanner.saveNewCredentials = async function(credentials = {}) { + const result = await browser.runtime.sendMessage({ + action: 'get_database_groups' + }); + + // Only the Root group and no KeePassXC-Browser passwords -> save to default + // Or when default group is not set and defaultGroupAskAlways is disabled -> save to default + if ((result.groups === undefined || (result.groups.length > 0 && result.groups[0].children.length === 0)) || + (!result.defaultGroupAlwaysAsk && (result.defaultGroup === '' || result.defaultGroup === DEFAULT_BROWSER_GROUP))) { + const res = await browser.runtime.sendMessage({ + action: 'add_credentials', + args: [ credentials.username, credentials.password, credentials.url ] + }); + kpxcBanner.verifyResult(res); + return; + } else if (!result.defaultGroupAlwaysAsk && (result.defaultGroup !== '' || result.defaultGroup !== DEFAULT_BROWSER_GROUP)) { + // Another group name has been specified + const [ gname, guuid ] = kpxcBanner.getDefaultGroup(result.groups[0].children, result.defaultGroup); + if (gname === '' && guuid === '') { + // Root group is used -> use the root path + if (result.defaultGroup.toLowerCase() === 'root') { + result.defaultGroup = '/'; + } + + // Create a new group + const newGroup = await browser.runtime.sendMessage({ + action: 'create_new_group', + args: [ result.defaultGroup ] + }); + + if (newGroup.name && newGroup.uuid) { + const res = await browser.runtime.sendMessage({ + action: 'add_credentials', + args: [ credentials.username, credentials.password, credentials.url, newGroup.name, newGroup.uuid ] + }); + kpxcBanner.verifyResult(res); + } else { + kpxcUI.createNotification('error', tr('rememberErrorCreatingNewGroup')); + } + return; + } + + const res = await browser.runtime.sendMessage({ + action: 'add_credentials', + args: [ credentials.username, credentials.password, credentials.url, gname, guuid ] + }); + kpxcBanner.verifyResult(res); + return; + } + + const addChildren = function(group, parentElement, depth) { + ++depth; + const padding = depth * 20; + + for (const child of group.children) { + const a = createLink(child.name, child.uuid, child.children.length > 0); + a.setAttribute('id', 'child'); + a.style.paddingLeft = Pixels(padding); + + if (parentElement.getAttribute('id') === 'root') { + a.setAttribute('id', 'root-child'); + } + + $('ul#list').appendChild(a); + addChildren(child, a, depth); + } + }; + + const createLink = function(group, groupUuid, hasChildren) { + const a = kpxcUI.createElement('a', 'list-group-item', { 'href': '#' }, group); + a.addEventListener('click', async function(e) { + e.preventDefault(); + if (!e.isTrusted) { + return; + } + + const res = await browser.runtime.sendMessage({ + action: 'add_credentials', + args: [ credentials.username, credentials.password, credentials.url, group, groupUuid ] + }); + + kpxcBanner.verifyResult(res); + }); + + if (hasChildren) { + a.textContent = '\u25BE ' + group; + } + return a; + }; + + kpxcBanner.createGroupDialog(); + + // Create the link list for group selection + let depth = 0; + for (const g of result.groups) { + const a = createLink(g.name, g.uuid, g.children.length > 0); + a.setAttribute('id', 'root'); + + $('ul#list').appendChild(a); + addChildren(g, a, depth); + } + + $('.kpxc-banner-dialog').style.display = 'block'; +}; + +kpxcBanner.updateCredentials = async function(credentials = {}) { + // Only one entry which could be updated + if (credentials.list.length === 1) { + // Use the current username if it's empty + if (!credentials.username) { + credentials.username = credentials.list[0].login; + } + + const res = await browser.runtime.sendMessage({ + action: 'update_credentials', + args: [ credentials.list[0].uuid, credentials.username, credentials.password, credentials.url ] + }); + kpxcBanner.verifyResult(res); + } else { + await kpxcBanner.createCredentialDialog(); + $('.kpxc-banner-dialog .username-new .strong').textContent = credentials.username; + $('.kpxc-banner-dialog .username-exists .strong').textContent = credentials.username; + + if (credentials.usernameExists) { + $('.kpxc-banner-dialog .username-new').style.display = 'none'; + $('.kpxc-banner-dialog .username-exists').style.display = 'block'; + } else { + $('.kpxc-banner-dialog .username-new').style.display = 'block'; + $('.kpxc-banner-dialog .username-exists').style.display = 'none'; + } + + for (let i = 0; i < credentials.list.length; i++) { + const a = kpxcUI.createElement('a', 'list-group-item', { 'href': '#', 'entryId': i }, `${credentials.list[i].login} (${credentials.list[i].name})`); + a.addEventListener('click', function(e) { + e.preventDefault(); + if (!e.isTrusted) { + return; + } + + const entryId = e.target.getAttribute('entryId'); + + // Use the current username if it's empty + if (!credentials.username) { + credentials.username = credentials.list[entryId].login; + } + + let url = credentials.url; + url = (url.length > 50) ? url.substring(0, 50) + '...' : url; + + // Check if the password has changed for the updated credentials + browser.runtime.sendMessage({ + action: 'retrieve_credentials', + args: [ url, '', true ] // Sets triggerUnlock to true + }).then(async (creds) => { + if (!creds || creds.length !== credentials.list.length) { + kpxcBanner.verifyResult('error'); + return; + } + + const res = await browser.runtime.sendMessage({ + action: 'update_credentials', + args: [ credentials.list[entryId].uuid, credentials.username, credentials.password, credentials.url ] + }); + kpxcBanner.verifyResult(res); + }); + }); + + if (credentials.usernameExists && credentials.username === credentials.list[i].login) { + a.style.fontWeight = 'bold'; + } + + $('ul#list').appendChild(a); + } + + $('.kpxc-banner-dialog').style.display = 'block'; + } +}; + +kpxcBanner.verifyResult = function(code) { + if (code === 'error') { + kpxcUI.createNotification('error', tr('rememberErrorCannotSaveCredentials')); + } else if (code === 'created') { + kpxcUI.createNotification('success', tr('rememberCredentialsSaved', kpxcBanner.credentials.username || tr('rememberEmptyUsername'))); + } else if (code === 'updated') { + kpxcUI.createNotification('success', tr('rememberCredentialsUpdated', kpxcBanner.credentials.username || tr('rememberEmptyUsername'))); + } else if (code === 'canceled') { + kpxcUI.createNotification('warning', tr('rememberCredentialsNotSaved')); + } else { + kpxcUI.createNotification('error', tr('rememberErrorDatabaseClosed')); + } + kpxcBanner.destroy(); +}; + +// Traverse the groups and ensure all paths are found +kpxcBanner.getDefaultGroup = function(groups, defaultGroup) { + const getGroup = function(group, splitted, depth) { + ++depth; + for (const g of group) { + if (g.name === splitted[depth]) { + if (splitted.length === (depth + 1)) { + return [ g.name, g.uuid ]; + } + return getGroup(g.children, splitted, depth); + } + } + return [ '', '' ]; + }; + + let depth = -1; + const splitted = defaultGroup.split('/'); + return getGroup(groups, splitted, depth); +}; + +kpxcBanner.createCredentialDialog = async function() { + $('#kpxc-banner-btn-new').hidden = true; + $('#kpxc-banner-btn-update').hidden = true; + $('.kpxc-checkbox').disabled = true; + + const connectedDatabase = await browser.runtime.sendMessage({ + action: 'get_connected_database' + }); + const databaseName = connectedDatabase.count > 0 ? connectedDatabase.identifier : ''; + + const dialog = kpxcUI.createElement('div', 'kpxc-banner-dialog'); + const databaseText = kpxcUI.createElement('p'); + const spanDatabaseText = kpxcUI.createElement('span', '', {}, tr('rememberSaving')); + const usernameNew = kpxcUI.createElement('p', 'username-new'); + const usernameExists = kpxcUI.createElement('p', 'username-exists'); + const chooseCreds = kpxcUI.createElement('p', '', {}, tr('rememberChooseCredentials')); + const list = kpxcUI.createElement('ul', 'list-group', { 'id': 'list' }); + const spanNewUsername = kpxcUI.createElement('span', '', {}, tr('rememberNewUsername')); + const spanUsernameExists = kpxcUI.createElement('span', '', {}, tr('rememberUsernameExists')); + + // Set dialog position + dialog.style.top = Pixels(kpxcBanner.banner.offsetHeight); + dialog.style.right = '0'; + + databaseText.appendChild(spanDatabaseText); + const spanName = kpxcUI.createElement('span', 'strong', {}, databaseName); + databaseText.append(spanName); + + usernameNew.appendChild(spanNewUsername); + usernameExists.appendChild(spanUsernameExists); + usernameNew.append(kpxcUI.createElement('span', 'strong')); + usernameExists.append(kpxcUI.createElement('span', 'strong')); + dialog.appendMultiple(databaseText, usernameNew, usernameExists, chooseCreds, list); + document.body.appendChild(dialog); +}; + +kpxcBanner.createGroupDialog = function() { + $('#kpxc-banner-btn-new').hidden = true; + $('#kpxc-banner-btn-update').hidden = true; + $('.kpxc-checkbox').disabled = true; + + const dialog = kpxcUI.createElement('div', 'kpxc-banner-dialog'); + const chooseGroup = kpxcUI.createElement('p', '', {}, tr('rememberChooseGroup')); + const list = kpxcUI.createElement('ul', 'list-group', { 'id': 'list' }); + + // Set dialog position + dialog.style.top = Pixels(kpxcBanner.banner.offsetHeight); + dialog.style.right = Pixels(0); + + dialog.appendMultiple(chooseGroup, list); + document.body.appendChild(dialog); +}; diff --git a/keepassxc-browser/content/define.js b/keepassxc-browser/content/define.js index 33cd2b4..baac773 100644 --- a/keepassxc-browser/content/define.js +++ b/keepassxc-browser/content/define.js @@ -134,6 +134,10 @@ kpxcDefine.isFieldSelected = function(kpxcId) { kpxcDefine.markAllUsernameFields = function(chooser) { kpxcDefine.eventFieldClick = function(e, elem) { + if (!e.isTrusted) { + return; + } + const field = elem || e.currentTarget; kpxcDefine.selection.username = field.getAttribute('data-kpxc-id'); field.classList.add('kpxcDefine-fixed-username-field'); @@ -147,6 +151,10 @@ kpxcDefine.markAllUsernameFields = function(chooser) { kpxcDefine.markAllPasswordFields = function(chooser, more = false) { kpxcDefine.eventFieldClick = function(e, elem) { + if (!e.isTrusted) { + return; + } + const field = elem || e.currentTarget; kpxcDefine.selection.password = field.getAttribute('data-kpxc-id'); field.classList.add('kpxcDefine-fixed-password-field'); @@ -164,6 +172,10 @@ kpxcDefine.markAllPasswordFields = function(chooser, more = false) { kpxcDefine.markAllStringFields = function(chooser) { kpxcDefine.eventFieldClick = function(e, elem) { + if (!e.isTrusted) { + return; + } + const field = elem || e.currentTarget; const value = field.getAttribute('data-kpxc-id'); kpxcDefine.selection.fields[value] = true; @@ -189,10 +201,10 @@ kpxcDefine.markFields = function(chooser, pattern) { if (kpxcFields.isVisible(i)) { const field = kpxcUI.createElement('div', 'kpxcDefine-fixed-field', { 'data-kpxc-id': i.getAttribute('data-kpxc-id') }); const rect = i.getBoundingClientRect(); - field.style.top = rect.top + 'px'; - field.style.left = rect.left + 'px'; - field.style.width = rect.width + 'px'; - field.style.height = rect.height + 'px'; + field.style.top = Pixels(rect.top); + field.style.left = Pixels(rect.left); + field.style.width = Pixels(rect.width); + field.style.height = Pixels(rect.height); field.textContent = String(index); field.addEventListener('click', function(e) { kpxcDefine.eventFieldClick(e); @@ -262,7 +274,11 @@ kpxcDefine.prepareStep3 = function() { $('#kpxcDefine-btn-confirm').style.display = 'inline-block'; }; -kpxcDefine.skip = function() { +kpxcDefine.skip = function(e) { + if (!e.isTrusted) { + return; + } + if (kpxcDefine.dataStep === 1) { kpxcDefine.selection.username = null; kpxcDefine.prepareStep2(); @@ -274,20 +290,32 @@ kpxcDefine.skip = function() { } }; -kpxcDefine.again = function() { +kpxcDefine.again = function(e) { + if (!e.isTrusted) { + return; + } + kpxcDefine.resetSelection(); kpxcDefine.prepareStep1(); kpxcDefine.markAllUsernameFields('#kpxcDefine-fields'); }; -kpxcDefine.more = function() { +kpxcDefine.more = function(e) { + if (!e.isTrusted) { + return; + } + if (kpxcDefine.dataStep === 2) { kpxcDefine.prepareStep2(); kpxcDefine.markAllPasswordFields('#kpxcDefine-fields', true); } }; -kpxcDefine.confirm = function() { +kpxcDefine.confirm = async function(e) { + if (!e.isTrusted) { + return; + } + if (kpxcDefine.dataStep !== 3) { return; } @@ -317,7 +345,7 @@ kpxcDefine.confirm = function() { fields: fieldIds }; - browser.runtime.sendMessage({ + await browser.runtime.sendMessage({ action: 'save_settings', args: [ kpxc.settings ] }); @@ -325,7 +353,11 @@ kpxcDefine.confirm = function() { kpxcDefine.close(); }; -kpxcDefine.discard = function() { +kpxcDefine.discard = async function(e) { + if (!e.isTrusted) { + return; + } + if (!$('#kpxcDefine-btn-discard')) { return; } @@ -333,12 +365,12 @@ kpxcDefine.discard = function() { const location = kpxc.getDocumentLocation(); delete kpxc.settings['defined-custom-fields'][location]; - browser.runtime.sendMessage({ + await browser.runtime.sendMessage({ action: 'save_settings', args: [ kpxc.settings ] }); - browser.runtime.sendMessage({ + await browser.runtime.sendMessage({ action: 'load_settings' }); @@ -347,6 +379,10 @@ kpxcDefine.discard = function() { // Handle the keyboard events kpxcDefine.keyDown = function(e) { + if (!e.isTrusted) { + return; + } + if (e.key === 'Escape') { kpxcDefine.close(); } else if (e.key === 'Enter') { diff --git a/keepassxc-browser/content/keepassxc-browser.js b/keepassxc-browser/content/keepassxc-browser.js index fe60ba0..c661bca 100755 --- a/keepassxc-browser/content/keepassxc-browser.js +++ b/keepassxc-browser/content/keepassxc-browser.js @@ -1,10 +1,16 @@ 'use strict'; +var ManualFill = { + NONE: 0, + PASS: 1, + BOTH: 2 +}; + // contains already called method names var _called = {}; _called.retrieveCredentials = false; _called.clearLogins = false; -_called.manualFillRequested = 'none'; +_called.manualFillRequested = ManualFill.NONE; let _singleInputEnabledForPage = false; const _maximumInputs = 100; @@ -18,46 +24,21 @@ var _observerIds = []; let _documentURL = document.location.href; // These are executed in each frame -browser.runtime.onMessage.addListener(function(req, sender) { +browser.runtime.onMessage.addListener(async function(req, sender) { if ('action' in req) { if (req.action === 'fill_user_pass_with_specific_login') { - if (kpxc.credentials[req.id]) { - let combination = null; - if (kpxc.u) { - kpxc.setValueWithChange(kpxc.u, kpxc.credentials[req.id].login); - combination = kpxcFields.getCombination('username', kpxc.u); - browser.runtime.sendMessage({ - action: 'page_set_login_id', args: [ req.id ] - }); - kpxc.u.focus(); - } - if (kpxc.p) { - kpxc.setValueWithChange(kpxc.p, kpxc.credentials[req.id].password); - browser.runtime.sendMessage({ - action: 'page_set_login_id', args: [ req.id ] - }); - combination = kpxcFields.getCombination('password', kpxc.p); - } - - let list = []; - if (kpxc.fillInStringFields(combination.fields, kpxc.credentials[req.id].stringFields, list)) { - kpxcForm.destroy(false, { 'password': list.list[0], 'username': list.list[1] }); - } - } + kpxc.fillWithSpecificLogin(req.id); } else if (req.action === 'fill_username_password') { - _called.manualFillRequested = 'both'; - kpxc.receiveCredentialsIfNecessary().then((response) => { - kpxc.fillInFromActiveElement(false); - }); + _called.manualFillRequested = ManualFill.BOTH; + await kpxc.receiveCredentialsIfNecessary(); + kpxc.fillInFromActiveElement(false); } else if (req.action === 'fill_password') { - _called.manualFillRequested = 'pass'; - kpxc.receiveCredentialsIfNecessary().then((response) => { - kpxc.fillInFromActiveElement(false, true); // passOnly to true - }); + _called.manualFillRequested = ManualFill.PASS; + await kpxc.receiveCredentialsIfNecessary(); + kpxc.fillInFromActiveElement(false, true); // passOnly to true } else if (req.action === 'fill_totp') { - kpxc.receiveCredentialsIfNecessary().then((response) => { - kpxc.fillInFromActiveElementTOTPOnly(false); - }); + await kpxc.receiveCredentialsIfNecessary(); + kpxc.fillInFromActiveElementTOTPOnly(false); } else if (req.action === 'clear_credentials') { kpxcEvents.clearCredentials(); return Promise.resolve(); @@ -68,21 +49,20 @@ browser.runtime.onMessage.addListener(function(req, sender) { kpxc.ignoreSite(req.args); } else if (req.action === 'check_database_hash' && 'hash' in req) { kpxc.detectDatabaseChange(req.hash); - } else if (req.action === 'activate_password_generator') { - kpxc.initPasswordGenerator(kpxcFields.getAllFields()); } else if (req.action === 'remember_credentials') { kpxc.contextMenuRememberCredentials(); } else if (req.action === 'choose_credential_fields') { kpxcDefine.init(); } else if (req.action === 'redetect_fields') { - browser.runtime.sendMessage({ + const response = await browser.runtime.sendMessage({ action: 'load_settings' - }).then((response) => { - kpxc.settings = response; - kpxc.initCredentialFields(true); }); + kpxc.settings = response; + kpxc.initCredentialFields(true); } else if (req.action === 'show_password_generator') { kpxcPassword.trigger(); + } else if (req.action === 'add_username_only_option') { + kpxc.addToSitePreferences(); } } }); @@ -98,7 +78,8 @@ function _fs(fieldId) { } -var kpxcForm = {}; +const kpxcForm = {}; +kpxcForm.passwordInputs = []; kpxcForm.init = function(form, credentialFields) { if (!form.getAttribute('kpxcForm-initialized') && (credentialFields.password || credentialFields.username)) { @@ -106,7 +87,7 @@ kpxcForm.init = function(form, credentialFields) { kpxcForm.setInputFields(form, credentialFields); form.addEventListener('submit', kpxcForm.onSubmit); - const submitButton = kpxc.getSubmitButton(form); + const submitButton = kpxc.getFormSubmitButton(form); if (submitButton !== undefined) { submitButton.addEventListener('click', kpxcForm.onSubmit); } @@ -117,44 +98,95 @@ kpxcForm.destroy = function(form, credentialFields) { if (form === false && credentialFields) { const field = _f(credentialFields.password) || _f(credentialFields.username); if (field) { - form = field.closest('form'); + form = kpxc.getForm(field); + if (form && form.length > 0) { + form.removeEventListener('submit', kpxcForm.onSubmit); + } } } - if (form && form.length > 0) { - form.removeEventListener('submit', kpxcForm.onSubmit); - } + kpxcForm.passwordInputs = []; }; kpxcForm.setInputFields = function(form, credentialFields) { form.setAttribute('kpxcUsername', credentialFields.username); form.setAttribute('kpxcPassword', credentialFields.password); + + // Save all the password input fields from the form + kpxcForm.passwordInputs = Array.from(form.elements).filter(e => e.nodeName === 'INPUT' && e.type === 'password'); }; -kpxcForm.onSubmit = function() { +kpxcForm.onSubmit = async function(e) { + if (!e.isTrusted) { + return; + } + const form = this.nodeName === 'FORM' ? this : this.form; const usernameId = form.getAttribute('kpxcUsername'); const passwordId = form.getAttribute('kpxcPassword'); - + const usernameField = _f(usernameId); + const passwordField = _f(passwordId); let usernameValue = ''; let passwordValue = ''; - const usernameField = _f(usernameId); - const passwordField = _f(passwordId); - if (usernameField) { usernameValue = usernameField.value || usernameField.placeholder; + } else if (kpxc.credentials.length === 1) { + // Single entry found for the page, use the username of it instead of an empty one + usernameValue = kpxc.credentials[0].login; } - if (passwordField) { + + // Check if the form has three password fields -> a possible password change form + if (kpxcForm.passwordInputs.length >= 2) { + passwordValue = kpxcForm.getNewPassword(); + } else if (passwordField) { + // Use the combination password field instead passwordValue = passwordField.value; } + // Return if credentials are already found + if (kpxc.credentials.some(c => c.login === usernameValue && c.password === passwordValue)) { + return; + } + + browser.runtime.sendMessage({ + action: 'page_set_submitted', + args: [ true, usernameValue, passwordValue, trimURL(window.top.location.href), kpxc.credentials ] + }); + + // Show the banner if the page does not reload kpxc.rememberCredentials(usernameValue, passwordValue); }; +// Retrieve new password from a form with three elements: Current, New, Repeat New +kpxcForm.getNewPassword = function() { + if (kpxcForm.passwordInputs.length < 2) { + return ''; + } + + // Just two password fields, current and new + if (kpxcForm.passwordInputs.length === 2 && + kpxcForm.passwordInputs[0] !== kpxcForm.passwordInputs[1]) { + return kpxcForm.passwordInputs[1].value; + } + + // Choose the last three password fields. The first ones are almost always for something else + const current = kpxcForm.passwordInputs[kpxcForm.passwordInputs.length - 3].value; + const newPass = kpxcForm.passwordInputs[kpxcForm.passwordInputs.length - 2].value; + const repeatNew = kpxcForm.passwordInputs[kpxcForm.passwordInputs.length - 1].value; + + if (current !== newPass && newPass !== '' && newPass === repeatNew) { + return newPass; + } else if (current === newPass && repeatNew !== newPass) { + // Reverse form where current password is at the bottom + return repeatNew; + } + + return ''; +}; + var kpxcFields = {}; - kpxcFields.inputQueryPattern = 'input[type=\'text\'], input[type=\'email\'], input[type=\'password\'], input[type=\'tel\'], input[type=\'number\'], input[type=\'username\'], input:not([type])'; // copied from Sizzle.js @@ -201,26 +233,6 @@ kpxcFields.prepareId = function(id) { return (id + '').replace(kpxcFields.rcssescape, kpxcFields.fcssescape); }; -/** - * Returns the first parent element satifying the {@code predicate} mapped by {@code resultFn} or else {@code defaultVal}. - * @param {HTMLElement} element The start element (excluded, starting with the parents) - * @param {function} predicate Matcher for the element to find, type (HTMLElement) => boolean - * @param {function} resultFn Callback function of type (HTMLElement) => {*} called for the first matching element - * @param {fun} defaultValFn Fallback return value supplier, if no element matching the predicate can be found - */ -kpxcFields.traverseParents = function(element, predicate, resultFn = () => true, defaultValFn = () => false) { - for (let f = element.parentElement; f !== null; f = f.parentElement) { - if (predicate(f)) { - return resultFn(f); - } - } - return defaultValFn(); -}; - -kpxcFields.getOverflowHidden = function(field) { - return kpxcFields.traverseParents(field, f => f.style.overflow === 'hidden'); -}; - // Checks if input field is a search field. Attributes or form action containing 'search', or parent element holding // role="search" will be identified as a search field. kpxcFields.isSearchField = function(target) { @@ -234,7 +246,7 @@ kpxcFields.isSearchField = function(target) { } // Check closest form - const closestForm = target.closest('form'); + const closestForm = kpxc.getForm(target); if (closestForm) { // Check form action const formAction = closestForm.getAttribute('action'); @@ -252,10 +264,8 @@ kpxcFields.isSearchField = function(target) { } } - // Check parent elements for role="search" - const roleFunc = f => f.getAttribute('role'); - const roleValue = kpxcFields.traverseParents(target, roleFunc, roleFunc, () => null); - if (roleValue && roleValue === 'search') { + // Check parent elements for role='search' + if (target.closest('[role~=\'search\']')) { return true; } @@ -290,6 +300,16 @@ kpxcFields.getAllFields = function() { } _detectedFields = fields.length; + + // Show add username-only option for the site in popup + if (!_singleInputEnabledForPage && + fields.length === 1 && fields[0].getLowerCaseAttribute('type') !== 'password') { + browser.runtime.sendMessage({ + action: 'username_field_detected', + args: [ true ] + }); + } + return fields; }; @@ -308,7 +328,7 @@ kpxcFields.getAllCombinations = function(inputs) { for (const i of inputs) { if (i) { - if (i.getAttribute('type') && i.getAttribute('type').toLowerCase() === 'password') { + if (i.getLowerCaseAttribute('type') === 'password') { const uId = (!uField || uField.length < 1) ? null : uField.getAttribute('data-kpxc-id'); const combination = { @@ -411,7 +431,7 @@ kpxcFields.getUsernameField = function(passwordId, checkDisabled) { return null; } - const form = passwordField.closest('form'); + const form = kpxc.getForm(passwordField); let usernameField = null; // Search all inputs on this one form @@ -423,10 +443,11 @@ kpxcFields.getUsernameField = function(passwordId, checkDisabled) { return false; // Break } - if (i.getAttribute('type') && i.getAttribute('type').toLowerCase() === 'password') { + if (i.getLowerCaseAttribute('type') === 'password') { return true; // Continue } + kpxcUsernameField.initField(usernameField); usernameField = i; } } else { @@ -438,7 +459,7 @@ kpxcFields.getUsernameField = function(passwordId, checkDisabled) { break; } - if (i.getAttribute('type') && i.getAttribute('type').toLowerCase() === 'password') { + if (i.getLowerCaseAttribute('type') === 'password') { continue; } @@ -470,7 +491,7 @@ kpxcFields.getPasswordField = function(usernameId, checkDisabled) { return null; } - const form = usernameField.closest('form'); + const form = kpxc.getForm(usernameField); let passwordField = null; // Search all inputs on this one form @@ -483,10 +504,8 @@ kpxcFields.getPasswordField = function(usernameId, checkDisabled) { passwordField = null; } - if (kpxc.settings.usePasswordGenerator) { - kpxcPassword.init(); - kpxcPassword.initField(passwordField); - } + kpxcPassword.init(kpxc.settings.usePasswordGeneratorIcons); + kpxcPassword.initField(passwordField); } else { // Search all inputs on page const inputs = kpxcFields.getAllFields(); @@ -497,7 +516,7 @@ kpxcFields.getPasswordField = function(usernameId, checkDisabled) { if (i.getAttribute('data-kpxc-id') === usernameId) { active = true; } - if (active && i.getAttribute('type') && i.getAttribute('type').toLowerCase() === 'password') { + if (active && i.getLowerCaseAttribute('type') === 'password') { passwordField = i; break; } @@ -519,7 +538,7 @@ kpxcFields.getPasswordField = function(usernameId, checkDisabled) { return passwordField; }; -kpxcFields.prepareCombinations = function(combinations) { +kpxcFields.prepareCombinations = async function(combinations) { for (const c of combinations) { const pwField = _f(c.password); // Needed for auto-complete: don't overwrite manually filled-in password field @@ -530,11 +549,23 @@ kpxcFields.prepareCombinations = function(combinations) { }); } - // Initialize form-submit for remembering credentials const fieldId = c.password || c.username; const field = _f(fieldId); + + // If no username field is found, handle the single password field as such + const usernameField = c.username ? _f(c.username) : field; + + // Change icon for username field + const res = await browser.runtime.sendMessage({ + action: 'get_status', + args: [ true ] + }); + + kpxcUsernameField.initField(usernameField, res.databaseClosed); + + // Initialize form-submit for remembering credentials if (field) { - const form = field.closest('form'); + const form = kpxc.getForm(field); if (form && form.length > 0) { kpxcForm.init(form, c); } @@ -571,7 +602,8 @@ kpxcFields.useDefinedCredentialFields = function() { return false; }; -var kpxcObserverHelper = {}; +const kpxcObserverHelper = {}; + kpxcObserverHelper.inputTypes = [ 'text', 'email', @@ -610,11 +642,15 @@ kpxcObserverHelper.getInputs = function(target) { } }); - // Do not allow more visible inputs than _maximumInputs (default value: 100) - if (inputFields.length === 0 || inputFields.length > _maximumInputs) { + if (inputFields.length === 0) { return []; } + // Do not allow more visible inputs than _maximumInputs (default value: 100) -> return the first 100 + if (inputFields.length > _maximumInputs) { + return inputFields.slice(0, _maximumInputs); + } + // Only include input fields that match with kpxcObserverHelper.inputTypes const inputs = []; for (const i of inputFields) { @@ -721,6 +757,12 @@ const observer = new MutationObserver(function(mutations, obs) { // Handle attributes only if CSS display is modified if (mut.type === 'attributes') { + // Check if some class is changed that folds a form or input field(s) + if (mut.attributeName === 'class' && mut.target.querySelectorAll('form input').length > 0) { + kpxc.initCredentialFields(true); + continue; + } + const newValue = mut.target.getAttribute(mut.attributeName); if (newValue && (newValue.includes('display') || newValue.includes('z-index'))) { if (mut.target.style.display !== 'none') { @@ -743,7 +785,7 @@ observer.observe(document, { attributes: true, childList: true, characterData: true, - attributeFilter: [ 'style' ] + attributeFilter: [ 'style', 'class' ] }); var kpxc = {}; @@ -754,13 +796,35 @@ kpxc.url = null; kpxc.submitUrl = null; kpxc.credentials = []; -const initcb = function() { - browser.runtime.sendMessage({ - action: 'load_settings' - }).then((response) => { - kpxc.settings = response; - kpxc.initCredentialFields(); - }); +const initcb = async function() { + try { + const settings = await browser.runtime.sendMessage({ + action: 'load_settings' + }); + + kpxc.settings = settings; + await kpxc.initCredentialFields(); + + // Retrieve submitted credentials if available. + const creds = await browser.runtime.sendMessage({ + action: 'page_get_submitted' + }); + + if (creds && creds.submitted) { + // If username field is not set, wait for credentials in kpxc.retrieveCredentialsCallback. + if (!creds.username) { + return; + } + + await browser.runtime.sendMessage({ + action: 'page_clear_submitted' + }); + + kpxc.rememberCredentials(creds.username, creds.password, creds.url, creds.oldCredentials); + } + } catch (err) { + console.log('Error: Cannot load extension settings'); + } }; if (document.readyState === 'complete' || (document.readyState !== 'loading' && !document.documentElement.doScroll)) { @@ -776,7 +840,6 @@ kpxc.init = function() { // Clears all from the content and background scripts, including autocomplete kpxc.clearAllFromPage = function() { kpxcEvents.clearCredentials(); - browser.runtime.sendMessage({ action: 'page_clear_logins' }); @@ -789,128 +852,128 @@ kpxc.clearAllFromPage = function() { }; // Switch credentials if database is changed or closed -kpxc.detectDatabaseChange = function(response) { +kpxc.detectDatabaseChange = async function(response) { kpxc.clearAllFromPage(); + kpxcUsernameField.switchIcon(true); + if (document.visibilityState !== 'hidden') { if (response.new !== '' && response.new !== response.old) { _called.retrieveCredentials = false; - browser.runtime.sendMessage({ + const settings = await browser.runtime.sendMessage({ action: 'load_settings' - }).then((settings) => { - kpxc.settings = settings; - kpxc.initCredentialFields(true); - - // 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. - if (_called.manualFillRequested && _called.manualFillRequested !== 'none') { - kpxc.fillInFromActiveElement(false, _called.manualFillRequested === 'pass'); - _called.manualFillRequested = 'none'; - } }); + kpxc.settings = settings; + await kpxc.initCredentialFields(true); + kpxcUsernameField.switchIcon(false); // Unlocked + + // 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. + if (_called.manualFillRequested && _called.manualFillRequested !== ManualFill.NONE) { + await kpxc.fillInFromActiveElement(false, _called.manualFillRequested === ManualFill.PASS); + _called.manualFillRequested = ManualFill.NONE; + } } } }; -kpxc.initCredentialFields = function(forceCall) { +kpxc.initCredentialFields = async function(forceCall) { if (_called.initCredentialFields && !forceCall) { return; } _called.initCredentialFields = true; - browser.runtime.sendMessage({ 'action': 'page_clear_logins', args: [ _called.clearLogins ] }).then(() => { - _called.clearLogins = true; + await browser.runtime.sendMessage({ + action: 'page_clear_logins', + args: [ _called.clearLogins ] + }); - // Check site preferences - kpxc.initializeSitePreferences(); - if (kpxc.settings.sitePreferences) { - for (const site of kpxc.settings.sitePreferences) { - try { - if (siteMatch(site.url, window.top.location.href) || site.url === window.top.location.href) { - if (site.ignore === IGNORE_FULL) { - return; - } + _called.clearLogins = true; - _singleInputEnabledForPage = site.usernameOnly; + // Check site preferences + kpxc.initializeSitePreferences(); + if (kpxc.settings.sitePreferences) { + for (const site of kpxc.settings.sitePreferences) { + try { + if (siteMatch(site.url, window.top.location.href) || site.url === window.top.location.href) { + if (site.ignore === IGNORE_FULL) { + return; } - } catch (err) { - return; + + _singleInputEnabledForPage = site.usernameOnly; } + } catch (err) { + return; } } + } - const inputs = kpxcFields.getAllFields(); - if (inputs.length === 0) { - return; - } + const inputs = kpxcFields.getAllFields(); + if (inputs.length === 0) { + return; + } - kpxcFields.prepareVisibleFieldsWithID('select'); - kpxc.initPasswordGenerator(inputs); + kpxcFields.prepareVisibleFieldsWithID('select'); + kpxc.initPasswordGenerator(inputs); - if (!kpxcFields.useDefinedCredentialFields()) { - // Get all combinations of username + password fields - kpxcFields.combinations = kpxcFields.getAllCombinations(inputs); - } - kpxcFields.prepareCombinations(kpxcFields.combinations); + if (!kpxcFields.useDefinedCredentialFields()) { + // Get all combinations of username + password fields + kpxcFields.combinations = kpxcFields.getAllCombinations(inputs); + } + kpxcFields.prepareCombinations(kpxcFields.combinations); - if (kpxcFields.combinations.length === 0 && inputs.length === 0) { - browser.runtime.sendMessage({ - action: 'show_default_browseraction' - }); - return; - } + if (kpxcFields.combinations.length === 0 && inputs.length === 0) { + browser.runtime.sendMessage({ + action: 'show_default_browseraction' + }); + return; + } - kpxc.url = document.location.origin; - kpxc.submitUrl = kpxc.getFormActionUrl(kpxcFields.combinations[0]); + kpxc.url = document.location.origin; + kpxc.submitUrl = kpxc.getFormActionUrl(kpxcFields.combinations[0]); - // Get submitUrl for a single input - if (!kpxc.submitUrl && kpxcFields.combinations.length === 1 && inputs.length === 1) { - kpxc.submitUrl = kpxc.getFormActionUrlFromSingleInput(inputs[0]); - } + // Get submitUrl for a single input + if (!kpxc.submitUrl && kpxcFields.combinations.length === 1 && inputs.length === 1) { + kpxc.submitUrl = kpxc.getFormActionUrlFromSingleInput(inputs[0]); + } - if (kpxc.settings.autoRetrieveCredentials && _called.retrieveCredentials === false && (kpxc.url && kpxc.submitUrl)) { - browser.runtime.sendMessage({ - action: 'retrieve_credentials', - args: [ kpxc.url, kpxc.submitUrl ] - }).then(kpxc.retrieveCredentialsCallback).catch((e) => { - console.log(e); - }); - } else if (_singleInputEnabledForPage) { - kpxc.preparePageForMultipleCredentials(kpxc.credentials); - } - }); + if (kpxc.settings.autoRetrieveCredentials && _called.retrieveCredentials === false && (kpxc.url && kpxc.submitUrl)) { + _called.retrieveCredentials = true; + kpxc.retrieveCredentialsCallback(await browser.runtime.sendMessage({ + action: 'retrieve_credentials', + args: [ kpxc.url, kpxc.submitUrl ] + })); + } else if (_singleInputEnabledForPage) { + kpxc.preparePageForMultipleCredentials(kpxc.credentials); + } }; kpxc.initPasswordGenerator = function(inputs) { - if (kpxc.settings.usePasswordGenerator) { - kpxcPassword.init(); + kpxcPassword.init(kpxc.settings.usePasswordGeneratorIcons); - for (let i = 0; i < inputs.length; i++) { - if (inputs[i] && inputs[i].getAttribute('type') && inputs[i].getAttribute('type').toLowerCase() === 'password') { - kpxcPassword.initField(inputs[i], inputs, i); - } + for (let i = 0; i < inputs.length; i++) { + if (inputs[i] && inputs[i].getLowerCaseAttribute('type') === 'password') { + kpxcPassword.initField(inputs[i], inputs, i); } } }; -kpxc.receiveCredentialsIfNecessary = function() { - return new Promise((resolve, reject) => { - if (kpxc.credentials.length === 0 && _called.retrieveCredentials === false) { - browser.runtime.sendMessage({ - action: 'retrieve_credentials', - args: [ kpxc.url, kpxc.submitUrl, false, true ] // Sets triggerUnlock to true - }).then((credentials) => { - // If the database was locked, this is scope never met. In these cases the response is met at kpxc.detectDatabaseChange - _called.manualFillRequested = 'none'; - kpxc.retrieveCredentialsCallback(credentials, false); - resolve(credentials); - }); - } else { - resolve(kpxc.credentials); - } - }); +kpxc.receiveCredentialsIfNecessary = async function() { + if (kpxc.credentials.length === 0 && _called.retrieveCredentials === false) { + const credentials = await browser.runtime.sendMessage({ + action: 'retrieve_credentials', + args: [ kpxc.url, kpxc.submitUrl, true ] // Sets triggerUnlock to true + }); + + // If the database was locked, this is scope never met. In these cases the response is met at kpxc.detectDatabaseChange + _called.manualFillRequested = ManualFill.NONE; + kpxc.retrieveCredentialsCallback(credentials, false); + return credentials; + } + + return kpxc.credentials; }; -kpxc.retrieveCredentialsCallback = function(credentials, dontAutoFillIn) { +kpxc.retrieveCredentialsCallback = async function(credentials, dontAutoFillIn) { if (kpxcFields.combinations.length > 0) { kpxc.u = _f(kpxcFields.combinations[0].username); kpxc.p = _f(kpxcFields.combinations[0].password); @@ -918,8 +981,20 @@ kpxc.retrieveCredentialsCallback = function(credentials, dontAutoFillIn) { if (credentials && credentials.length > 0) { kpxc.credentials = credentials; - kpxc.prepareFieldsForCredentials(!Boolean(dontAutoFillIn)); - _called.retrieveCredentials = true; + kpxc.prepareFieldsForCredentials(!dontAutoFillIn); + } + + // Retrieve submitted credentials if available + const creds = await browser.runtime.sendMessage({ + action: 'page_get_submitted' + }); + + if (creds && creds.submitted) { + await browser.runtime.sendMessage({ + action: 'page_clear_submitted' + }); + + kpxc.rememberCredentials(creds.username, creds.password, creds.url, creds.oldCredentials); } }; @@ -959,6 +1034,10 @@ kpxc.prepareFieldsForCredentials = function(autoFillInForSingle) { }; kpxc.preparePageForMultipleCredentials = function(credentials) { + if (credentials.length === 0) { + return; + } + function getLoginText(credential) { const visibleLogin = (credential.login.length > 0) ? credential.login : tr('credentialsNoUsername'); if (credential.expired && credential.expired === 'true') { @@ -1008,6 +1087,17 @@ kpxc.preparePageForMultipleCredentials = function(credentials) { } }; +// Returns the form that includes the inputField +kpxc.getForm = function(inputField) { + for (const f of document.forms) { + for (const e of f.elements) { + if (e === inputField) { + return f; + } + } + } +}; + kpxc.getFormActionUrl = function(combination) { if (!combination) { return null; @@ -1018,11 +1108,11 @@ kpxc.getFormActionUrl = function(combination) { return null; } - const form = field.closest('form'); + const form = kpxc.getForm(field); let action = null; if (form && form.length > 0) { - action = form[0].action; + action = form.action; } if (typeof(action) !== 'string' || action === '') { @@ -1047,7 +1137,7 @@ kpxc.getFormActionUrlFromSingleInput = function(field) { }; // Get the form submit button instead if action URL is same as the page itself -kpxc.getSubmitButton = function(form) { +kpxc.getFormSubmitButton = function(form) { const action = kpxc.submitUrl || form.action; if (action.includes(document.location.origin + document.location.pathname)) { for (const i of form.elements) { @@ -1066,7 +1156,7 @@ kpxc.getSubmitButton = function(form) { return undefined; }; -kpxc.fillInCredentials = function(combination, onlyPassword, suppressWarnings) { +kpxc.fillInCredentials = async function(combination, onlyPassword, suppressWarnings) { const action = kpxc.getFormActionUrl(combination); const u = _f(combination.username); const p = _f(combination.password); @@ -1076,9 +1166,9 @@ kpxc.fillInCredentials = function(combination, onlyPassword, suppressWarnings) { const fieldId = combination.password || combination.username; const field = _f(fieldId); if (field) { - const form2 = field.closest('form'); - if (form2 && form2.length > 0) { - kpxcForm.init(form2, combination); + const form = kpxc.getForm(field); + if (form && form.length > 0) { + kpxcForm.init(form, combination); } } } @@ -1096,13 +1186,13 @@ kpxc.fillInCredentials = function(combination, onlyPassword, suppressWarnings) { kpxc.url = document.location.origin; kpxc.submitUrl = action; - browser.runtime.sendMessage({ + const credentials = await browser.runtime.sendMessage({ action: 'retrieve_credentials', - args: [ kpxc.url, kpxc.submitUrl, false, true ] - }).then((credentials) => { - kpxc.retrieveCredentialsCallback(credentials, true); - kpxc.fillIn(combination, onlyPassword, suppressWarnings); + args: [ kpxc.url, kpxc.submitUrl, true ] // Sets triggerUnlock to true }); + + kpxc.retrieveCredentialsCallback(credentials, true); + kpxc.fillIn(combination, onlyPassword, suppressWarnings); } }; @@ -1132,11 +1222,7 @@ kpxc.fillInFromActiveElement = function(suppressWarnings, passOnly = false) { if (passOnly) { if (!_f(combination.password)) { - const message = tr('fieldsNoPasswordField'); - browser.runtime.sendMessage({ - action: 'show_notification', - args: [ message ] - }); + kpxcUI.createNotification('warning', tr('fieldsNoPasswordField')); return; } } @@ -1146,30 +1232,30 @@ kpxc.fillInFromActiveElement = function(suppressWarnings, passOnly = false) { kpxc.fillInCredentials(combination, passOnly, suppressWarnings); }; -kpxc.fillInFromActiveElementTOTPOnly = function() { +kpxc.fillInFromActiveElementTOTPOnly = async function() { const el = document.activeElement; kpxcFields.setUniqueId(el); const fieldId = kpxcFields.prepareId(el.getAttribute('data-kpxc-id')); - browser.runtime.sendMessage({ + const index = await browser.runtime.sendMessage({ action: 'page_get_login_id' - }).then((pos) => { - if (pos >= 0 && kpxc.credentials[pos]) { - // Check the value from stringFields (to be removed) - const currentField = _fs(fieldId); - if (kpxc.credentials[pos].stringFields && kpxc.credentials[pos].stringFields.length > 0) { - const stringFields = kpxc.credentials[pos].stringFields; - for (const s of stringFields) { - const val = s['KPH: {TOTP}']; - if (val) { - kpxc.setValue(currentField, val); - } - } - } else if (kpxc.credentials[pos].totp && kpxc.credentials[pos].totp.length > 0) { - kpxc.setValue(currentField, kpxc.credentials[pos].totp); - } - } }); + + if (index >= 0 && kpxc.credentials[index]) { + // Check the value from stringFields (to be removed) + const currentField = _fs(fieldId); + if (kpxc.credentials[index].stringFields && kpxc.credentials[index].stringFields.length > 0) { + const stringFields = kpxc.credentials[index].stringFields; + for (const s of stringFields) { + const val = s['KPH: {TOTP}']; + if (val) { + kpxc.setValue(currentField, val); + } + } + } else if (kpxc.credentials[index].totp && kpxc.credentials[index].totp.length > 0) { + kpxc.setValue(currentField, kpxc.credentials[index].totp); + } + } }; kpxc.setValue = function(field, value) { @@ -1219,14 +1305,40 @@ kpxc.setValueWithChange = function(field, value) { field.dispatchEvent(new Event('change', { 'bubbles': true })); }; +kpxc.fillWithSpecificLogin = function(id) { + if (kpxc.credentials[id]) { + let combination = null; + if (kpxc.u) { + kpxc.setValueWithChange(kpxc.u, kpxc.credentials[id].login); + combination = kpxcFields.getCombination('username', kpxc.u); + browser.runtime.sendMessage({ + action: 'page_set_login_id', + args: [ id ] + }); + kpxc.u.focus(); + } + if (kpxc.p) { + kpxc.setValueWithChange(kpxc.p, kpxc.credentials[id].password); + browser.runtime.sendMessage({ + action: 'page_set_login_id', + args: [ id ] + }); + combination = kpxcFields.getCombination('password', kpxc.p); + } + + let list = []; + if (kpxc.fillInStringFields(combination.fields, kpxc.credentials[id].stringFields, list)) { + kpxcForm.destroy(false, { 'password': list.list[0], 'username': list.list[1] }); + } + + kpxcAutocomplete.closeList(); + } +}; + kpxc.fillIn = function(combination, onlyPassword, suppressWarnings) { // No credentials available if (kpxc.credentials.length === 0 && !suppressWarnings) { - const message = tr('credentialsNoLoginsFound'); - browser.runtime.sendMessage({ - action: 'show_notification', - args: [ message ] - }); + kpxcUI.createNotification('error', tr('credentialsNoLoginsFound')); return; } @@ -1261,11 +1373,7 @@ kpxc.fillIn = function(combination, onlyPassword, suppressWarnings) { if (!filledIn) { if (!suppressWarnings) { - const message = tr('fieldsFill'); - browser.runtime.sendMessage({ - action: 'show_notification', - args: [ message ] - }); + kpxcUI.createNotification('error', tr('fieldsFill')); } return; } @@ -1297,11 +1405,7 @@ kpxc.fillIn = function(combination, onlyPassword, suppressWarnings) { if (!filledIn) { if (!suppressWarnings) { - const message = tr('fieldsFill'); - browser.runtime.sendMessage({ - action: 'show_notification', - args: [ message ] - }); + kpxcUI.createNotification('error', tr('fieldsFill')); } return; } @@ -1351,6 +1455,10 @@ kpxc.fillIn = function(combination, onlyPassword, suppressWarnings) { if (countPasswords > 1) { if (!suppressWarnings) { const target = onlyPassword ? pField : uField; + if (!target) { + return; + } + if (kpxcAutocomplete.started) { kpxcAutocomplete.showList(target); } else { @@ -1361,17 +1469,17 @@ kpxc.fillIn = function(combination, onlyPassword, suppressWarnings) { return; } else if (countPasswords < 1) { if (!suppressWarnings) { - const message = tr('credentialsNoUsernameFound'); - browser.runtime.sendMessage({ - action: 'show_notification', - args: [ message ] - }); + kpxcUI.createNotification('error', tr('credentialsNoUsernameFound')); } return; } } else { if (!suppressWarnings) { const target = onlyPassword ? pField : uField; + if (!target) { + return; + } + if (kpxcAutocomplete.started) { kpxcAutocomplete.showList(target); } else { @@ -1383,10 +1491,13 @@ kpxc.fillIn = function(combination, onlyPassword, suppressWarnings) { } } + // Close autocomplete menu after fill + kpxcAutocomplete.closeList(); + // Auto-submit if (kpxc.settings.autoSubmit) { const form = kpxc.u.form || kpxc.p.form; - const submitButton = kpxc.getSubmitButton(form); + const submitButton = kpxc.getFormSubmitButton(form); if (submitButton !== undefined) { submitButton.click(); } else { @@ -1395,7 +1506,7 @@ kpxc.fillIn = function(combination, onlyPassword, suppressWarnings) { } }; -kpxc.contextMenuRememberCredentials = function() { +kpxc.contextMenuRememberCredentials = async function() { const el = document.activeElement; if (el.tagName.toLowerCase() !== 'input') { return; @@ -1419,30 +1530,42 @@ kpxc.contextMenuRememberCredentials = function() { if (usernameField) { usernameValue = usernameField.value; } + if (passwordField) { passwordValue = passwordField.value; } - if (!kpxc.rememberCredentials(usernameValue, passwordValue)) { - const message = tr('rememberNothingChanged'); - browser.runtime.sendMessage({ - action: 'show_notification', - args: [ message ] - }); + const result = await kpxc.rememberCredentials(usernameValue, passwordValue); + if (result === undefined) { + kpxcUI.createNotification('error', tr('rememberNoPassword')); + return; + } + + if (!result) { + kpxcUI.createNotification('warning', tr('rememberCredentialsExists')); } }; -kpxc.rememberCredentials = function(usernameValue, passwordValue) { +/** + * Gets the credential list and shows the update banner + * @param {string} usernameValue Submitted username + * @param {string} passwordValue Submitted password + * @param {string} urlValue URL of the page where password change was detected + * @param {Array} oldCredentials Credentials saved from the password change page, if available + */ +kpxc.rememberCredentials = async function(usernameValue, passwordValue, urlValue, oldCredentials) { + const credentials = kpxc.credentials || oldCredentials; + // No password given or field cleaned by a site-running script // --> no password to save if (passwordValue === '') { - return false; + return undefined; } let usernameExists = false; let nothingChanged = false; - for (const c of kpxc.credentials) { + for (const c of credentials) { if (c.login === usernameValue && c.password === passwordValue) { nothingChanged = true; break; @@ -1455,7 +1578,7 @@ kpxc.rememberCredentials = function(usernameValue, passwordValue) { if (!nothingChanged) { if (!usernameExists) { - for (const c of kpxc.credentials) { + for (const c of credentials) { if (c.login === usernameValue) { usernameExists = true; break; @@ -1463,7 +1586,7 @@ kpxc.rememberCredentials = function(usernameValue, passwordValue) { } } const credentialsList = []; - for (const c of kpxc.credentials) { + for (const c of credentials) { credentialsList.push({ login: c.login, name: c.name, @@ -1482,29 +1605,57 @@ kpxc.rememberCredentials = function(usernameValue, passwordValue) { } } - browser.runtime.sendMessage({ - action: 'set_remember_credentials', - args: [ usernameValue, passwordValue, url, usernameExists, credentialsList ] - }); + urlValue = urlValue || url; + // Set usernameValue to the first one in the list, or the selected entry + if (usernameValue === '') { + if (credentialsList.length === 1) { + usernameValue = credentialsList[0].login; + } else if (credentialsList.length > 1) { + const index = await browser.runtime.sendMessage({ + action: 'page_get_login_id' + }); + + if (index >= 0) { + usernameValue = credentialsList[index].login; + } + } + } + + // Show the banner + const newCredentials = { + username: usernameValue, + password: passwordValue, + url: urlValue, + usernameExists: usernameExists, + list: credentialsList + }; + + kpxcBanner.create(newCredentials); return true; } return false; }; -kpxc.ignoreSite = function(sites) { +kpxc.getSite = function(sites) { if (!sites || sites.length === 0) { - return; + return ''; } - let site = sites[0]; + let site = trimURL(sites[0]); kpxc.initializeSitePreferences(); if (slashNeededForUrl(site)) { site += '/'; } + return site; +}; + +kpxc.ignoreSite = async function(sites) { + const site = kpxc.getSite(sites); + // Check if the site already exists let siteExists = false; for (const existingSite of kpxc.settings['sitePreferences']) { @@ -1522,12 +1673,47 @@ kpxc.ignoreSite = function(sites) { }); } - browser.runtime.sendMessage({ + await browser.runtime.sendMessage({ action: 'save_settings', args: [ kpxc.settings ] }); }; +kpxc.addToSitePreferences = async function(sites) { + kpxc.initializeSitePreferences(); + + // Returns a predefined URL for certain sites + const site = kpxcSites.definedURL(trimURL(window.top.location.href)); + + // Check if the site already exists -> update the current settings + let siteExists = false; + for (const existingSite of kpxc.settings['sitePreferences']) { + if (existingSite.url === site) { + existingSite.ignore = IGNORE_NOTHING; + existingSite.usernameOnly = true; + siteExists = true; + } + } + + if (!siteExists) { + kpxc.settings['sitePreferences'].push({ + url: site, + ignore: IGNORE_NOTHING, + usernameOnly: true + }); + } + + await browser.runtime.sendMessage({ + action: 'save_settings', + args: [ kpxc.settings ] + }); + + browser.runtime.sendMessage({ + action: 'username_field_detected', + args: [ false ] + }); +}; + // Delete previously created Object if it exists. It will be replaced by an Array kpxc.initializeSitePreferences = function() { if (kpxc.settings['sitePreferences'] !== undefined && kpxc.settings['sitePreferences'].constructor === Object) { @@ -1544,7 +1730,7 @@ kpxc.getDocumentLocation = function() { }; -var kpxcEvents = {}; +const kpxcEvents = {}; kpxcEvents.clearCredentials = function() { kpxc.credentials = []; @@ -1563,18 +1749,21 @@ kpxcEvents.clearCredentials = function() { } }; -kpxcEvents.triggerActivatedTab = function() { +kpxcEvents.triggerActivatedTab = async function() { // Doesn't run a second time because of _called.initCredentialFields set to true kpxc.init(); + // Update username field lock state + const state = await browser.runtime.sendMessage({ action: 'check_database_hash' }); + kpxcUsernameField.switchIcon(state === ''); + // initCredentialFields calls also "retrieve_credentials", to prevent it // check of init() was already called if (_called.initCredentialFields && (kpxc.url && kpxc.submitUrl) && kpxc.settings.autoRetrieveCredentials) { - browser.runtime.sendMessage({ + _called.retrieveCredentials = true; + kpxc.retrieveCredentialsCallback(await browser.runtime.sendMessage({ action: 'retrieve_credentials', args: [ kpxc.url, kpxc.submitUrl ] - }).then(kpxc.retrieveCredentialsCallback).catch((e) => { - console.log(e); - }); + })); } }; diff --git a/keepassxc-browser/content/pwgen.js b/keepassxc-browser/content/pwgen.js index b84e41b..a9b5a8a 100644 --- a/keepassxc-browser/content/pwgen.js +++ b/keepassxc-browser/content/pwgen.js @@ -11,41 +11,18 @@ kpxcPassword.diffX = 0; kpxcPassword.diffY = 0; kpxcPassword.dialog = null; kpxcPassword.titleBar = null; +kpxcPassword.useIcons = false; -const DOMRectToArray = function(domRect) { - return [ domRect.bottom, domRect.height, domRect.left, domRect.right, domRect.top, domRect.width, domRect.x, domRect.y ]; -}; - -/** -* Detects if the input field appears or disappears -> show/hide the icon -* - boundingClientRect with slightly (< -10) negative values -> hidden -* - intersectionRatio === 0 -> hidden -* - isIntersecting === false -> hidden -* - intersectionRatio > 0 -> shown -* - isIntersecting === true -> shown -*/ try { kpxcPassword.observer = new IntersectionObserver((entries) => { - for (const entry of entries) { - const rect = DOMRectToArray(entry.boundingClientRect); - - if ((entry.intersectionRatio === 0 && !entry.isIntersecting) || (rect.some(x => x < -10))) { - kpxcPassword.icon.style.display = 'none'; - } else if (entry.intersectionRatio > 0 && entry.isIntersecting) { - kpxcPassword.icon.style.display = 'block'; - - // Wait for possible DOM animations - setTimeout(() => { - kpxcPassword.setIconPosition(kpxcPassword.icon, entry.target); - }, 500); - } - } + kpxcUI.updateFromIntersectionObserver(kpxcPassword, entries); }); } catch (err) { console.log(err); } -kpxcPassword.init = function() { +kpxcPassword.init = function(useIcons) { + kpxcPassword.useIcons = useIcons; if ('initPasswordGenerator' in _called) { return; } @@ -58,24 +35,26 @@ kpxcPassword.initField = function(field, inputs, pos) { return; } - // Observer the visibility - if (kpxcPassword.observer) { - kpxcPassword.observer.observe(field); - } - if (field.getAttribute('kpxc-password-generator')) { return; } field.setAttribute('kpxc-password-generator', true); - kpxcPassword.createIcon(field); + if (kpxcPassword.useIcons) { + // Observer the visibility + if (kpxcPassword.observer) { + kpxcPassword.observer.observe(field); + } + kpxcPassword.createIcon(field); + } + kpxcPassword.inputField = field; let found = false; if (inputs) { for (let i = pos + 1; i < inputs.length; i++) { - if (inputs[i] && inputs[i].getAttribute('type') && inputs[i].getAttribute('type').toLowerCase() === 'password') { + if (inputs[i] && inputs[i].getLowerCaseAttribute('type') === 'password') { field.setAttribute('kpxc-pwgen-next-field-id', inputs[i].getAttribute('data-kpxc-id')); field.setAttribute('kpxc-pwgen-next-is-password-field', (i === 0)); found = true; @@ -101,27 +80,33 @@ kpxcPassword.createIcon = function(field) { 'offset': offset, 'kpxc-pwgen-field-id': field.getAttribute('data-kpxc-id') }); - icon.style.zIndex = '9999'; - icon.style.width = String(size) + 'px'; - icon.style.height = String(size) + 'px'; + icon.style.zIndex = '10000000'; + icon.style.width = Pixels(size); + icon.style.height = Pixels(size); icon.addEventListener('click', function(e) { + if (!e.isTrusted) { + return; + } + e.preventDefault(); kpxcPassword.showDialog(field, icon); }); - kpxcPassword.setIconPosition(icon, field); + kpxcUI.setIconPosition(icon, field); kpxcPassword.icon = icon; document.body.appendChild(icon); }; -kpxcPassword.setIconPosition = function(icon, field) { - const rect = field.getBoundingClientRect(); - const offset = Number(icon.getAttribute('offset')); - const size = Number(icon.getAttribute('size')); - - icon.style.top = String((rect.top + document.scrollingElement.scrollTop) + offset + 1) + 'px'; - icon.style.left = String((rect.left + document.scrollingElement.scrollLeft) + field.offsetWidth - size - offset) + 'px'; +kpxcPassword.removeIcon = function(field) { + if (field.getAttribute('kpxc-password-generator')) { + const pwgenIcons = document.querySelectorAll('.kpxc-pwgen-icon'); + for (const i of pwgenIcons) { + if (i.getAttribute('kpxc-pwgen-field-id') === field.getAttribute('data-kpxc-id')) { + document.body.removeChild(i); + } + } + } }; kpxcPassword.createDialog = function() { @@ -140,45 +125,50 @@ kpxcPassword.createDialog = function() { const titleBar = kpxcUI.createElement('div', 'kpxc-pwgen-titlebar', {}, tr('passwordGeneratorTitle')); const closeButton = kpxcUI.createElement('div', 'kpxc-pwgen-close', {}, '×'); closeButton.addEventListener('click', function(e) { + if (!e.isTrusted) { + return; + } + kpxcPassword.openDialog(); }); titleBar.append(closeButton); const passwordRow = kpxcUI.createElement('div', 'kpxc-pwgen-password-row'); const input = kpxcUI.createElement('input', 'kpxc-pwgen-input', { 'placeholder': tr('passwordGeneratorPlaceholder'), 'type': 'text', 'tabindex': '-1' }); - const inputLabel = kpxcUI.createElement('label', 'kpxc-pwgen-bits', {}, tr('passwordGeneratorBits', '???')); - passwordRow.appendMultiple(input, inputLabel); - - const nextFillRow = kpxcUI.createElement('div', 'kpxc-pwgen-nextfill-row'); - const checkbox = kpxcUI.createElement('input', 'kpxc-pwgen-checkbox', { 'id': 'kpxc-pwgen-checkbox', 'type': 'checkbox' }); - const checkboxLabel = kpxcUI.createElement('label', 'kpxc-pwgen-checkbox-label', { 'for': 'kpxc-pwgen-checkbox' }, tr('passwordGeneratorLabel')); - nextFillRow.appendMultiple(checkbox, checkboxLabel); + passwordRow.appendMultiple(input); // Buttons const buttonsRow = kpxcUI.createElement('div', 'kpxc-pwgen-buttons'); - const generateButton = kpxcUI.createElement('button', 'kpxc-button kpxc-white-button', { 'id': 'kpxc-pwgen-btn-generate' }, tr('passwordGeneratorGenerate')); - const copyButton = kpxcUI.createElement('button', 'kpxc-button', { 'id': 'kpxc-pwgen-btn-copy' }, tr('passwordGeneratorCopy')); - const fillButton = kpxcUI.createElement('button', 'kpxc-button', { 'id': 'kpxc-pwgen-btn-fill' }, tr('passwordGeneratorFillAndCopy')); + const generateButton = kpxcUI.createElement('button', 'kpxc-button kpxc-orange-button', { 'id': 'kpxc-pwgen-btn-generate' }, tr('passwordGeneratorGenerate')); + const copyButton = kpxcUI.createElement('button', 'kpxc-button kpxc-orange-button', { 'id': 'kpxc-pwgen-btn-copy' }, tr('passwordGeneratorCopy')); + const fillButton = kpxcUI.createElement('button', 'kpxc-button kpxc-green-button', { 'id': 'kpxc-pwgen-btn-fill' }, tr('passwordGeneratorFill')); generateButton.addEventListener('click', function(e) { kpxcPassword.generate(e); }); + fillButton.addEventListener('click', function(e) { + kpxcPassword.fill(e); + kpxcPassword.openDialog(); + }); + copyButton.addEventListener('click', function(e) { kpxcPassword.copy(e); }); - fillButton.addEventListener('click', function(e) { - kpxcPassword.fill(e); - }); - buttonsRow.appendMultiple(generateButton, copyButton, fillButton); - dialog.appendMultiple(titleBar, passwordRow, nextFillRow, buttonsRow); + dialog.appendMultiple(titleBar, passwordRow, buttonsRow); wrapper.append(dialog); const icon = $('.kpxc-pwgen-icon'); - dialog.style.top = String(icon.offsetTop + icon.offsetHeight) + 'px'; - dialog.style.left = icon.style.left; + if (icon) { + dialog.style.top = Pixels(icon.offsetTop + icon.offsetHeight); + dialog.style.left = icon.style.left; + } else { + const rect = document.activeElement.getBoundingClientRect(); + dialog.style.top = Pixels(rect.top + rect.height); + dialog.style.left = Pixels(rect.left); + } document.body.append(wrapper); @@ -209,7 +199,7 @@ kpxcPassword.openDialog = function() { }; kpxcPassword.trigger = function() { - kpxcPassword.showDialog(kpxcPassword.inputField, kpxcPassword.icon); + kpxcPassword.showDialog(kpxcPassword.inputField || document.activeElement, kpxcPassword.icon); }; kpxcPassword.showDialog = function(field, icon) { @@ -224,8 +214,14 @@ kpxcPassword.showDialog = function(field, icon) { // Adjust the dialog location if (kpxcPassword.dialog) { - kpxcPassword.dialog.style.top = String(icon.offsetTop + icon.offsetHeight) + 'px'; - kpxcPassword.dialog.style.left = icon.style.left; + if (icon) { + kpxcPassword.dialog.style.top = Pixels(icon.offsetTop + icon.offsetHeight); + kpxcPassword.dialog.style.left = icon.style.left; + } else { + const rect = document.activeElement.getBoundingClientRect(); + kpxcPassword.dialog.style.top = Pixels(rect.top + rect.height); + kpxcPassword.dialog.style.left = Pixels(rect.left); + } kpxcPassword.dialog.setAttribute('kpxc-pwgen-field-id', field.getAttribute('data-kpxc-id')); kpxcPassword.dialog.setAttribute('kpxc-pwgen-next-field-id', field.getAttribute('kpxc-pwgen-next-field-id')); @@ -244,27 +240,34 @@ kpxcPassword.showDialog = function(field, icon) { } }; -kpxcPassword.generate = function(e) { +kpxcPassword.generate = async function(e) { + // This function can be also called from non-events if (e) { + if (!e.isTrusted) { + return; + } e.preventDefault(); } - browser.runtime.sendMessage({ + callbackGeneratedPassword(await browser.runtime.sendMessage({ action: 'generate_password' - }).then(kpxcPassword.callbackGeneratedPassword).catch((err) => { - console.log(err); - }); + })); }; kpxcPassword.copy = function(e) { - e.preventDefault(); - if (kpxcPassword.copyPasswordToClipboard()) { - kpxcPassword.greenButton('#kpxc-pwgen-btn-copy'); - kpxcPassword.whiteButton('#kpxc-pwgen-btn-fill'); + if (!e.isTrusted) { + return; } + + e.preventDefault(); + kpxcPassword.copyPasswordToClipboard(); }; kpxcPassword.fill = function(e) { + if (!e.isTrusted) { + return; + } + e.preventDefault(); // Use the active input field @@ -285,19 +288,10 @@ kpxcPassword.fill = function(e) { } field.value = password.value; - if ($('.kpxc-pwgen-checkbox').checked) { - if (field.getAttribute('kpxc-pwgen-next-field-id')) { - const nextFieldId = field.getAttribute('kpxc-pwgen-next-field-id'); - const nextField = $('input[data-kpxc-id=\'' + nextFieldId + '\']'); - if (nextField) { - nextField.value = password.value; - } - } - } - - if (kpxcPassword.copyPasswordToClipboard()) { - kpxcPassword.greenButton('#kpxc-pwgen-btn-fill'); - kpxcPassword.whiteButton('#kpxc-pwgen-btn-copy'); + const nextFieldId = field.getAttribute('kpxc-pwgen-next-field-id'); + const nextField = $('input[data-kpxc-id=\'' + nextFieldId + '\']'); + if (nextField) { + nextField.value = password.value; } } }; @@ -312,33 +306,20 @@ kpxcPassword.copyPasswordToClipboard = function() { return false; }; -kpxcPassword.callbackGeneratedPassword = function(entries) { +const callbackGeneratedPassword = function(entries) { if (entries && entries.length >= 1) { const errorMessage = $('#kpxc-pwgen-error'); if (errorMessage) { - kpxcPassword.enableButtons(); - - $('.kpxc-pwgen-checkbox').parentElement.style.display = 'block'; - $('.kpxc-pwgen-bits').style.display = 'block'; + enableButtons(); const input = $('.kpxc-pwgen-input'); input.style.display = 'block'; errorMessage.remove(); } - kpxcPassword.whiteButton('#kpxc-pwgen-btn-fill'); - kpxcPassword.whiteButton('#kpxc-pwgen-btn-copy'); $('.kpxc-pwgen-input').value = entries[0].password; - if (entries[0].entropy) { - $('.kpxc-pwgen-bits').textContent = tr('passwordGeneratorBits', (Number.isNaN(entries[0].entropy) ? '???' : String(entries[0].entropy.toFixed(2)))); - } else { - $('.kpxc-pwgen-bits').textContent = tr('passwordGeneratorBits', (isNaN(entries[0].login) ? '???' : entries[0].login)); - } } else { if (document.querySelectorAll('div#kpxc-pwgen-error').length === 0) { - $('.kpxc-pwgen-checkbox').parentElement.style.display = 'none'; - $('.kpxc-pwgen-bits').style.display = 'none'; - const input = $('.kpxc-pwgen-input'); input.style.display = 'none'; @@ -347,34 +328,18 @@ kpxcPassword.callbackGeneratedPassword = function(entries) { errorMessage.style.whiteSpace = 'pre'; input.parentElement.append(errorMessage); - kpxcPassword.disableButtons(); + disableButtons(); } } }; -kpxcPassword.onRequestPassword = function() { - browser.runtime.sendMessage({ - action: 'generate_password' - }).then(kpxcPassword.callbackGeneratedPassword); -}; - -kpxcPassword.greenButton = function(button) { - $(button).classList.remove('kpxc-white-button'); - $(button).classList.add('kpxc-green-button'); -}; - -kpxcPassword.whiteButton = function(button) { - $(button).classList.remove('kpxc-green-button'); - $(button).classList.add('kpxc-white-button'); -}; - -kpxcPassword.enableButtons = function() { +const enableButtons = function() { $('#kpxc-pwgen-btn-generate').textContent = tr('passwordGeneratorGenerate'); $('#kpxc-pwgen-btn-copy').style.display = 'inline-block'; $('#kpxc-pwgen-btn-fill').style.display = 'inline-block'; }; -kpxcPassword.disableButtons = function() { +const disableButtons = function() { $('#kpxc-pwgen-btn-generate').textContent = tr('passwordGeneratorTryAgain'); $('#kpxc-pwgen-btn-copy').style.display = 'none'; $('#kpxc-pwgen-btn-fill').style.display = 'none'; @@ -382,28 +347,10 @@ kpxcPassword.disableButtons = function() { // Handle icon position on window resize window.addEventListener('resize', function(e) { - if (kpxcPassword.inputField && kpxcPassword.icon) { - kpxcPassword.setIconPosition(kpxcPassword.icon, kpxcPassword.inputField); - } + kpxcUI.updateIconPosition(kpxcPassword); }); // Handle icon position on scroll window.addEventListener('scroll', function(e) { - if (kpxcPassword.inputField && kpxcPassword.icon) { - kpxcPassword.setIconPosition(kpxcPassword.icon, kpxcPassword.inputField); - } -}); - -// Closes the dialog when clicked outside of it) -document.addEventListener('click', function(e) { - if (kpxcPassword.dialog && kpxcPassword.dialog.style.display === 'block') { - const dialogEndX = kpxcPassword.dialog.offsetLeft + kpxcPassword.dialog.offsetWidth; - const dialogEndY = kpxcPassword.dialog.offsetTop + kpxcPassword.dialog.offsetHeight; - - if ((e.clientX < kpxcPassword.dialog.offsetLeft || e.clientX > dialogEndX) || - (e.clientY < kpxcPassword.dialog.offsetTop || e.clientY > dialogEndY) && - !e.target.classList.contains('kpxc-pwgen-icon')) { - kpxcPassword.openDialog(); - } - } + kpxcUI.updateIconPosition(kpxcPassword); }); diff --git a/keepassxc-browser/content/sites.js b/keepassxc-browser/content/sites.js new file mode 100644 index 0000000..0575b16 --- /dev/null +++ b/keepassxc-browser/content/sites.js @@ -0,0 +1,35 @@ +'use strict'; + +// List of sites that need special handling. 'rep' is the replacement URL. +const siteList = [ + { + url: 'accounts.google.com', + rep: 'https://accounts.google.com/*' + } +]; + +var kpxcSites = {}; + +// Returns a predefined URL for certain sites to ensure compatibility with Site Preferences +kpxcSites.definedURL = function(url) { + for (const site of siteList) { + if (url.includes(site.url)) { + return site.rep; + } + } + + return url; +}; + +// UNUSED: Adds all common sites with multi-page login to Site Preferences +kpxcSites.addAllCommonSites = function() { + kpxc.initializeSitePreferences(); + + for (const site of siteList) { + kpxc.settings['sitePreferences'].push({ + url: site.rep, + ignore: IGNORE_NOTHING, + usernameOnly: true + }); + } +}; diff --git a/keepassxc-browser/content/ui.js b/keepassxc-browser/content/ui.js index 5774eb5..96c5ac3 100644 --- a/keepassxc-browser/content/ui.js +++ b/keepassxc-browser/content/ui.js @@ -5,6 +5,11 @@ var $ = function(elem) { return document.querySelector(elem); }; +// Returns a string with 'px' for CSS styles +var Pixels = function(value) { + return String(value) + 'px'; +}; + var kpxcUI = {}; // Wrapper for creating elements @@ -31,6 +36,83 @@ kpxcUI.createElement = function(type, classes, attributes, textContent) { return element; }; +kpxcUI.updateIconPosition = function(iconClass) { + if (iconClass.inputField && iconClass.icon) { + kpxcUI.setIconPosition(iconClass.icon, iconClass.inputField); + } +}; + +kpxcUI.setIconPosition = function(icon, field) { + const rect = field.getBoundingClientRect(); + const offset = Number(icon.getAttribute('offset')); + const size = Number(icon.getAttribute('size')); + + icon.style.top = Pixels((rect.top + document.scrollingElement.scrollTop) + offset + 1); + icon.style.left = Pixels((rect.left + document.scrollingElement.scrollLeft) + field.offsetWidth - size - offset); +}; + +/** +* Detects if the input field appears or disappears -> show/hide the icon +* - boundingClientRect with slightly (< -10) negative values -> hidden +* - intersectionRatio === 0 -> hidden +* - isIntersecting === false -> hidden +* - intersectionRatio > 0 -> shown +* - isIntersecting === true -> shown +*/ +kpxcUI.updateFromIntersectionObserver = function(iconClass, entries) { + for (const entry of entries) { + const rect = DOMRectToArray(entry.boundingClientRect); + + if ((entry.intersectionRatio === 0 && !entry.isIntersecting) || (rect.some(x => x < -10))) { + iconClass.icon.style.display = 'none'; + } else if (entry.intersectionRatio > 0 && entry.isIntersecting) { + iconClass.icon.style.display = 'block'; + + // Wait for possible DOM animations + setTimeout(() => { + kpxcUI.setIconPosition(iconClass.icon, entry.target); + }, 500); + } + } +}; + +/** + * Creates a self-disappearing notification banner to DOM + * @param {string} type Notification type: (success, info, warning, error) + * @param {string} message The message shown + */ +kpxcUI.createNotification = function(type, message) { + if (!kpxc.settings.showNotifications || !type || !message) { + return; + } + + const banner = kpxcUI.createElement('div', 'kpxc-notification kpxc-notification-' + type, {}); + type = type.charAt(0).toUpperCase() + type.slice(1) + '!'; + + const className = (isFirefox() ? 'kpxc-banner-icon-moz' : 'kpxc-banner-icon'); + const icon = kpxcUI.createElement('span', className, { 'alt': 'logo' }); + const label = kpxcUI.createElement('span', 'kpxc-label', {}, type); + const msg = kpxcUI.createElement('span', '', {}, message); + + banner.addEventListener('click', function() { + document.body.removeChild(banner); + }); + + banner.appendMultiple(icon, label, msg); + document.body.appendChild(banner); + + // Destroy the banner after five seconds + setTimeout(() => { + if ($('.kpxc-notification')) { + document.body.removeChild(banner); + } + }, 5000); +}; + +const DOMRectToArray = function(domRect) { + return [ domRect.bottom, domRect.height, domRect.left, domRect.right, domRect.top, domRect.width, domRect.x, domRect.y ]; +}; + // Enables dragging document.addEventListener('mousemove', function(e) { if (kpxcPassword.selected === kpxcPassword.titleBar) { @@ -38,8 +120,8 @@ document.addEventListener('mousemove', function(e) { const yPos = e.clientY - kpxcPassword.diffY; if (kpxcPassword.selected !== null) { - kpxcPassword.dialog.style.left = xPos + 'px'; - kpxcPassword.dialog.style.top = yPos + 'px'; + kpxcPassword.dialog.style.left = Pixels(xPos); + kpxcPassword.dialog.style.top = Pixels(yPos); } } @@ -48,8 +130,8 @@ document.addEventListener('mousemove', function(e) { const yPos = e.clientY - kpxcDefine.diffY; if (kpxcDefine.selected !== null) { - kpxcDefine.dialog.style.left = xPos + 'px'; - kpxcDefine.dialog.style.top = yPos + 'px'; + kpxcDefine.dialog.style.left = Pixels(xPos); + kpxcDefine.dialog.style.top = Pixels(yPos); } } }); @@ -64,3 +146,7 @@ HTMLDivElement.prototype.appendMultiple = function(...args) { this.append(a); } }; + +Element.prototype.getLowerCaseAttribute = function(attr) { + return this.getAttribute(attr) ? this.getAttribute(attr).toLowerCase() : undefined; +}; diff --git a/keepassxc-browser/content/username-field.js b/keepassxc-browser/content/username-field.js new file mode 100644 index 0000000..601dee7 --- /dev/null +++ b/keepassxc-browser/content/username-field.js @@ -0,0 +1,137 @@ +'use strict'; + +var kpxcUsernameField = {}; +kpxcUsernameField.icon = null; +kpxcUsernameField.inputField = null; + +try { + kpxcUsernameField.observer = new IntersectionObserver((entries) => { + kpxcUI.updateFromIntersectionObserver(kpxcUsernameField, entries); + }); +} catch (err) { + console.log(err); +} + +kpxcUsernameField.initField = function(field, databaseClosed = true) { + if (!field || field.getAttribute('kpxc-username-field') === 'true') { + return; + } + + field.setAttribute('kpxc-username-field', 'true'); + + // Observer the visibility + if (kpxcUsernameField.observer) { + kpxcUsernameField.observer.observe(field); + } + + createIcon(field, databaseClosed); + kpxcUsernameField.inputField = field; +}; + +kpxcUsernameField.switchIcon = function(locked) { + const icons = $('.kpxc-username-icon'); + if (!icons || icons.length === 0) { + return; + } + const icon = icons; + + if (locked) { + icon.classList.remove(getIconClassName()); + icon.classList.add(getIconClassName(true)); + icon.title = tr('usernameLockedFieldText'); + } else { + icon.classList.remove(getIconClassName(true)); + icon.classList.add(getIconClassName()); + icon.title = tr('usernameFieldText'); + } +}; + +const createIcon = function(target, databaseClosed) { + // Remove any existing password generator icons from the input field + if (target.getAttribute('kpxc-password-generator')) { + kpxcPassword.removeIcon(target); + } + + const field = target; + const className = getIconClassName(databaseClosed); + const size = (field.offsetHeight > 28) ? 24 : 16; + let offset = Math.floor((field.offsetHeight - size) / 3); + offset = (offset < 0) ? 0 : offset; + + const icon = kpxcUI.createElement('div', 'kpxc kpxc-username-icon ' + className, + { + 'title': databaseClosed ? tr('usernameLockedFieldText') : tr('usernameFieldText'), + 'alt': tr('usernameFieldIcon'), + 'size': size, + 'offset': offset, + 'kpxc-pwgen-field-id': field.getAttribute('data-kpxc-id') + }); + icon.style.zIndex = '10000000'; + icon.style.width = Pixels(size); + icon.style.height = Pixels(size); + + icon.addEventListener('click', async function(e) { + if (!e.isTrusted) { + return; + } + + e.preventDefault(); + + if (!kpxcFields.isVisible(field)) { + document.body.removeChild(icon); + field.removeAttribute('kpxc-username-field'); + return; + } + + const connected = await browser.runtime.sendMessage({ action: 'is_connected' }); + if (!connected) { + kpxcUI.createNotification('error', tr('errorNotConnected')); + return; + } + + const databaseHash = await browser.runtime.sendMessage({ action: 'check_database_hash' }); + if (databaseHash === '') { + // Triggers database unlock + _called.manualFillRequested = ManualFill.BOTH; + await browser.runtime.sendMessage({ + action: 'get_database_hash', + args: [ false, true ] // Set triggerUnlock to true + }); + } + + if (icon.className.includes('unlock')) { + fillCredentials(); + } + }); + + kpxcUI.setIconPosition(icon, field); + kpxcUsernameField.icon = icon; + document.body.appendChild(icon); +}; + +const getIconClassName = function(locked = false) { + if (locked) { + return (isFirefox() ? 'lock-moz' : 'lock'); + } + return (isFirefox() ? 'unlock-moz' : 'unlock'); +}; + +const fillCredentials = function() { + const fieldId = kpxcUsernameField.inputField.getAttribute('data-kpxc-id'); + kpxcFields.prepareId(fieldId); + + const givenType = kpxcUsernameField.inputField.type === 'password' ? 'password' : 'username'; + const combination = kpxcFields.getCombination(givenType, fieldId); + + kpxc.fillInCredentials(combination, givenType === 'password', false); +}; + +// Handle icon position on window resize +window.addEventListener('resize', function(e) { + kpxcUI.updateIconPosition(kpxcUsernameField); +}); + +// Handle icon position on scroll +window.addEventListener('scroll', function(e) { + kpxcUI.updateIconPosition(kpxcUsernameField); +}); diff --git a/keepassxc-browser/css/banner.css b/keepassxc-browser/css/banner.css new file mode 100644 index 0000000..3b05509 --- /dev/null +++ b/keepassxc-browser/css/banner.css @@ -0,0 +1,138 @@ +div.kpxc-banner { + animation-duration: 0.2s; + animation-name: slidein; + background-color: #ffffff; + border-bottom: 1px solid #38383d !important; + box-shadow: 0 4px 6px 0 hsla(0, 0%, 0%, 0.2) !important; + box-sizing: border-box !important; + color: #000000 !important; + font-family: sans-serif !important; + font-size: 1em !important; + height: auto !important; + left: 0px !important; + line-height: 1 !important; + margin: 0 auto !important; + min-height: 2em !important; + max-height: 4em !important; + overflow-x: hidden !important; + padding: 4px !important; + position: fixed !important; + top: 0px !important; + width: 100% !important; + display: flex !important; + justify-content: space-between !important; +} + +@keyframes slidein { + from { + max-height: 0em; + } + + to { + max-height: 4em; + } +} + +div.kpxc-banner .banner-info > span { + margin: 4px; +} + +div.kpxc-banner div.banner-info, div.banner-buttons { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8px; +} + +div.kpxc-banner .small { + font-size: 80%; + color: #787878; +} + +div.kpxc-banner .kpxc-banner-icon { + width: 24px; + height: 24px; + overflow: hidden; + background: url('chrome-extension://__MSG_@@extension_id__/icons/keepassxc.svg') right no-repeat; + background-size: contain; +} + +div.kpxc-banner .kpxc-banner-icon-moz { + width: 24px; + height: 24px; + overflow: hidden; + background: url('moz-extension://__MSG_@@extension_id__/icons/keepassxc.svg') right no-repeat; + background-size: contain; +} + +.separator { + border-left: 1px solid #ccc; + height: 100% !important; + margin: 10px !important; +} + +div.kpxc-banner .kpxc-checkbox { + margin: 2px !important; +} + +div.kpxc-banner label { + font-family: 'Lato', sans-serif !important; + font-size: 12px !important; + font-weight: normal !important; + margin: 4px !important; +} + +div.kpxc-banner-dialog { + display: block !important; + position: fixed !important; + margin: 0 auto; + width: 680px; + max-height: 600px; + max-width: 460px; + background-color: white; + border: 1px solid #ccc; + border-radius: 0 0 4px 4px; + box-shadow: 0 4px 6px 0 hsla(0, 0%, 0%, 0.2); + padding: 8px; + z-index: 10000000; +} + +div.kpxc-banner-dialog > ul { + list-style-type: disc !important; + margin-bottom: 0 !important; + margin-top: 0 !important; + padding-left: 0 !important; +} + +div.kpxc-banner-dialog > p > span.strong { + font-weight: bold !important; +} + +div.kpxc-banner-dialog .list-group { + font-size: .9em !important; +} + +div.kpxc-banner-dialog .list-group-item { + border: 1px solid #ddd !important; + color: #000 !important; + display: block !important; + padding: 5px 10px; + position: relative !important; + text-decoration: none !important; +} + +div.kpxc-banner-dialog .list-group-item:first-child { + border-top-left-radius: 4px !important; + border-top-right-radius: 4px !important; +} + +div.kpxc-banner-dialog .list-group-item:last-child { + margin-bottom: 0 !important; + border-bottom-right-radius: 4px !important; + border-bottom-left-radius: 4px !important; +} + +div.kpxc-banner-dialog .list-group-item:hover { + cursor: pointer !important;; + background-color: #eee !important; +} diff --git a/keepassxc-browser/css/button.css b/keepassxc-browser/css/button.css index 16945ad..79df313 100644 --- a/keepassxc-browser/css/button.css +++ b/keepassxc-browser/css/button.css @@ -1,48 +1,55 @@ .kpxc-button { + background-color: #fff; + background-image: none !important; + border: 1px solid #ccc !important; + border-radius: .25em !important; font-size: 12px !important; font-family: 'Lato', sans-serif !important; height: 28px !important; margin: .2em !important; - background-color: #fff; - border: 1px solid #888 !important; - border-radius: 4px !important; + padding: 1px 7px 2px !important; } .kpxc-white-button { - color: #333 !important; - background-color: #fff !important; - border-color: #ccc !important; + background-color: #f8f9fa !important; + border-color: #f8f9fa !important; + color: #212529 !important; } .kpxc-red-button { + background-color: #dc3545 !important; + border-color: #dc3545 !important; color: #fff !important; - background-color: #d9534f !important; - border-color: #d43f3a !important; } .kpxc-orange-button { - color: #fff !important; - background-color: #f0ad4e !important; - border-color: #eea236 !important; + background-color: #ffc107 !important; + border-color: #ffc107 !important; + color: #212529 !important; } .kpxc-blue-button { + background-color: #007bff !important; + border-color: #007bff !important; color: #fff !important; - background-color: #337ab7 !important; - border-color: #2e6da4 !important; } .kpxc-green-button { + background-color: #28a745 !important; + border-color: #28a745 !important; color: #fff !important; - background-color: #5cb85c !important; - border-color: #4cae4c !important; } .kpxc-button:hover { - background-color: #eee; - cursor: pointer; + cursor: pointer !important; + filter: brightness(92%) !important; + transition: all .15s; } -.kpxc-button:active { - background-color: #ddd; -} \ No newline at end of file +.kpxc-button:disabled { + border-color: #ccc !important; +} + +.kpxc-button:disabled:hover { + background-color: #fff !important; +} diff --git a/keepassxc-browser/css/notification.css b/keepassxc-browser/css/notification.css new file mode 100644 index 0000000..55c74a3 --- /dev/null +++ b/keepassxc-browser/css/notification.css @@ -0,0 +1,69 @@ +.kpxc-notification { + border: 1px solid transparent; + border-radius: 4px; + box-shadow: 0 4px 6px 0 hsla(0, 0%, 0%, 0.2); + cursor: pointer; + font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; + font-size: 14px; + margin: 0px auto; + margin-bottom: 20px; + padding: 15px; + padding-right: 35px; + position: fixed; + right: 0px; + top: 0px; + width: 520px; + z-index: 9999; +} + +.kpxc-notification span { + padding: 2px; +} + +.kpxc-notification .kpxc-banner-icon { + width: 24px; + height: 24px; + padding: 10px; + margin-right: 4px; + overflow: hidden; + background: url('chrome-extension://__MSG_@@extension_id__/icons/keepassxc.svg') right no-repeat; + background-size: contain; +} + +.kpxc-notification .kpxc-banner-icon-moz { + width: 24px; + height: 24px; + padding: 10px; + margin-right: 4px; + overflow: hidden; + background: url('moz-extension://__MSG_@@extension_id__/icons/keepassxc.svg') right no-repeat; + background-size: contain; +} + +.kpxc-notification .kpxc-label { + font-weight: bold; +} + +.kpxc-notification-success { + color: #155724; + background-color: #d4edda; + border-color: #c3e6cb; +} + +.kpxc-notification-warning { + color: #856404; + background-color: #fff3cd; + border-color: #ffeeba; +} + +.kpxc-notification-info { + color: #004085; + background-color: #cce5ff; + border-color: #b8daff; +} + +.kpxc-notification-error { + color: #721c24; + background-color: #f8d7da; + border-color: #f5c6cb; +} diff --git a/keepassxc-browser/css/pwgen.css b/keepassxc-browser/css/pwgen.css index 97ae4dd..55fa1ee 100644 --- a/keepassxc-browser/css/pwgen.css +++ b/keepassxc-browser/css/pwgen.css @@ -1,67 +1,41 @@ .kpxc-pwgen-dialog { - font-family: 'Lato', sans-serif !important; - width: 350px; - min-height: 80px; background-color: white; border: 1px solid #ccc; border-radius: 4px; box-shadow: 0 4px 6px 0 hsla(0, 0%, 0%, 0.2); - z-index: 2147483645; + font-family: 'Lato', sans-serif !important; + min-height: 80px; padding: .1em .2em .1em .2em; position: absolute !important; + width: 350px; + z-index: 10000000 !important; } .kpxc-pwgen-titlebar { - font-size: 14px !important; background-color: #6cac4d; color: #fff; - padding: .4em .2em .4em .8em; - margin: .2em 0 .2em 0; - position: relative; cursor: move; + font-size: 12px !important; + margin: .2em 0 .2em 0; + padding: .4em .2em .4em .8em; + position: relative; } .kpxc-pwgen-password-row { color: #fff; - padding: .4em 1em; - margin: .2em 0 .2em 0; display: flex; -} - -.kpxc-pwgen-nextfill-row { - font-size: 12px !important; - color: #000; - padding: .4em 1em; margin: .2em 0 .2em 0; + padding: .4em 1em; } .kpxc-pwgen-buttons { + align-items: center; + border-top: 1px solid #ccc; color: #fff; + display: flex; + justify-content: left; padding: .4em 1em; margin: .2em 0 .2em 0; - border-top: 1px solid #ccc; - display: flex; - align-items: center; - justify-content: center; -} - -.kpxc-pwgen-bits { - font-size: 12px !important; - font-style: normal !important; - font-weight: normal !important; - text-align: center; - color: #000; - background-color: #eee; - border: 1px solid #ccc; - padding: .4em; - border-radius: 4px; - width: 30%; - min-width: 60px; - border-top-left-radius: 0; - border-bottom-left-radius: 0; - border-collapse: separate; - vertical-align: middle; - white-space: nowrap; } .kpxc-pwgen-close { @@ -80,9 +54,14 @@ } .kpxc-pwgen-input { - width: 100%; + background: none !important; + border-radius: 4px !important; + color: #000 !important; font-size: 12px !important; font-weight: normal !important; + height: auto !important; + padding: 4px !important; + width: 100% !important; } .kpxc-pwgen-checkbox-label { @@ -100,8 +79,8 @@ } .kpxc-pwgen-icon { - position: absolute; cursor: pointer; + position: absolute; } .kpxc-pwgen-icon.key { diff --git a/keepassxc-browser/css/username.css b/keepassxc-browser/css/username.css new file mode 100644 index 0000000..9321a03 --- /dev/null +++ b/keepassxc-browser/css/username.css @@ -0,0 +1,24 @@ +.kpxc-username-icon { + position: absolute; + cursor: pointer; +} + +.kpxc-username-icon.lock { + background: url('chrome-extension://__MSG_@@extension_id__/icons/locked.svg') right no-repeat; + background-size: contain; +} + +.kpxc-username-icon.lock-moz { + background: url('moz-extension://__MSG_@@extension_id__/icons/locked.svg') right no-repeat; + background-size: contain; +} + +.kpxc-username-icon.unlock { + background: url('chrome-extension://__MSG_@@extension_id__/icons/keepassxc.svg') right no-repeat; + background-size: contain; +} + +.kpxc-username-icon.unlock-moz { + background: url('moz-extension://__MSG_@@extension_id__/icons/keepassxc.svg') right no-repeat; + background-size: contain; +} \ No newline at end of file diff --git a/keepassxc-browser/global.js b/keepassxc-browser/global.js index 58c29f5..e2b635c 100755 --- a/keepassxc-browser/global.js +++ b/keepassxc-browser/global.js @@ -21,6 +21,13 @@ var showNotification = function(message) { }); }; +var AssociatedAction = { + NOT_ASSOCIATED: 0, + ASSOCIATED: 1, + NEW_ASSOCIATION: 2, + CANCELED: 3 +}; + /** * Transforms a valid match pattern into a regular expression * which matches all URLs included by that pattern. @@ -71,6 +78,8 @@ var matchPatternToRegExp = function(pattern) { } if (path) { + path = trimURL(path); + if (path === '*') { regex += '(/.*)?'; } else if (path.charAt(0) !== '/') { @@ -97,3 +106,8 @@ var slashNeededForUrl = function(pattern) { function tr(key, params) { return browser.i18n.getMessage(key, params); } + +// Removes everything after '?' from URL +var trimURL = function(url) { + return url.indexOf('?') !== -1 ? url.split('?')[0] : url; +} diff --git a/keepassxc-browser/icons/key.png b/keepassxc-browser/icons/key.png deleted file mode 100644 index f21e46a..0000000 Binary files a/keepassxc-browser/icons/key.png and /dev/null differ diff --git a/keepassxc-browser/icons/locked.svg b/keepassxc-browser/icons/locked.svg new file mode 100644 index 0000000..a578bd1 --- /dev/null +++ b/keepassxc-browser/icons/locked.svg @@ -0,0 +1,208 @@ + + diff --git a/keepassxc-browser/manifest.json b/keepassxc-browser/manifest.json index 15399b8..81f782f 100755 --- a/keepassxc-browser/manifest.json +++ b/keepassxc-browser/manifest.json @@ -54,18 +54,24 @@ ], "js": [ "browser-polyfill.min.js", + "content/banner.js", "content/autocomplete.js", "content/define.js", "content/keepassxc-browser.js", "content/pwgen.js", + "content/sites.js", "content/ui.js", + "content/username-field.js", "global.js" ], "css": [ "css/autocomplete.css", + "css/banner.css", "css/button.css", "css/define.css", - "css/pwgen.css" + "css/notification.css", + "css/pwgen.css", + "css/username.css" ], "run_at": "document_idle", "all_frames": true @@ -102,7 +108,9 @@ } }, "web_accessible_resources": [ - "icons/key.svg" + "icons/keepassxc.svg", + "icons/key.svg", + "icons/locked.svg" ], "permissions": [ "activeTab", diff --git a/keepassxc-browser/options/options.html b/keepassxc-browser/options/options.html index f956411..1f69278 100644 --- a/keepassxc-browser/options/options.html +++ b/keepassxc-browser/options/options.html @@ -38,56 +38,6 @@ : error
--
-
-