'use strict'; const ManualFill = { NONE: 0, PASS: 1, BOTH: 2 }; const acceptedOTPFields = [ '2fa', 'auth', 'challenge', 'code', 'mfa', 'otp', 'token' ]; // Contains already called method names const _called = {}; _called.retrieveCredentials = false; _called.clearLogins = false; _called.manualFillRequested = ManualFill.NONE; let _singleInputEnabledForPage = false; let _databaseClosed = true; const _maximumInputs = 100; const _maximumMutations = 200; // Count of detected form fields on the page var _detectedFields = 0; // Element id's containing input fields detected by MutationObserver const _observerIds = []; // Document URL let _documentURL = document.location.href; // These are executed in each frame browser.runtime.onMessage.addListener(async function(req, sender) { if ('action' in req) { if (req.action === 'fill_user_pass_with_specific_login') { kpxc.fillWithSpecificLogin(req.id); } else if (req.action === 'fill_username_password') { _called.manualFillRequested = ManualFill.BOTH; await kpxc.receiveCredentialsIfNecessary(); kpxc.fillInFromActiveElement(false); } else if (req.action === 'fill_password') { _called.manualFillRequested = ManualFill.PASS; await kpxc.receiveCredentialsIfNecessary(); kpxc.fillInFromActiveElement(false, true); // passOnly to true } else if (req.action === 'fill_totp') { await kpxc.receiveCredentialsIfNecessary(); kpxc.fillInFromActiveElementTOTPOnly(); } else if (req.action === 'clear_credentials') { kpxcEvents.clearCredentials(); return Promise.resolve(); } else if (req.action === 'activated_tab') { kpxcEvents.triggerActivatedTab(); return Promise.resolve(); } else if (req.action === 'ignore_site') { kpxc.ignoreSite(req.args); } else if (req.action === 'check_database_hash' && 'hash' in req) { kpxc.detectDatabaseChange(req.hash); } else if (req.action === 'remember_credentials') { kpxc.contextMenuRememberCredentials(); } else if (req.action === 'choose_credential_fields') { kpxcDefine.init(); } else if (req.action === 'redetect_fields') { const response = await browser.runtime.sendMessage({ action: 'load_settings' }); kpxc.settings = response; kpxc.initCredentialFields(true); } else if (req.action === 'show_password_generator') { kpxcPasswordDialog.trigger(); } else if (req.action === 'add_username_only_option') { kpxc.addToSitePreferences(); } } }); function _f(fieldId) { const inputs = document.querySelectorAll(`input[data-kpxc-id='${fieldId}']`); return inputs.length > 0 ? inputs[0] : null; } function _fs(fieldId) { const inputs = document.querySelectorAll(`input[data-kpxc-id='${fieldId}'], select[data-kpxc-id='${fieldId}']`); return inputs.length > 0 ? inputs[0] : null; } const kpxcForm = {}; kpxcForm.passwordInputs = []; kpxcForm.init = function(form, credentialFields) { if (!form.getAttribute('kpxcForm-initialized') && (credentialFields.password || credentialFields.username)) { form.setAttribute('kpxcForm-initialized', true); kpxcForm.setInputFields(form, credentialFields); form.addEventListener('submit', kpxcForm.onSubmit); const submitButton = kpxc.getFormSubmitButton(form); if (submitButton !== undefined) { submitButton.addEventListener('click', kpxcForm.onSubmit); } } }; kpxcForm.destroy = function(form, credentialFields) { if (form === false && credentialFields) { const field = _f(credentialFields.password) || _f(credentialFields.username); if (field) { form = kpxc.getForm(field); 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 = 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 = ''; 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; } // 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 ''; }; const 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 kpxcFields.rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g; kpxcFields.fcssescape = function(ch, asCodePoint) { if (asCodePoint) { // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER if (ch === '\0') { return '\uFFFD'; } // Control characters and (dependent upon position) numbers get escaped as code points return ch.slice(0, -1) + '\\' + ch.charCodeAt(ch.length - 1).toString(16) + ' '; } // Other potentially-special ASCII characters get backslash-escaped return '\\' + ch; }; // Unique number as new IDs for input fields kpxcFields.uniqueNumber = 342845638; // Objects with combination of username + password fields kpxcFields.combinations = []; kpxcFields.setUniqueId = function(field) { if (field && !field.getAttribute('data-kpxc-id')) { // Use ID of field if it is unique const fieldId = field.getAttribute('id'); if (fieldId) { const foundIds = document.querySelectorAll('input#' + kpxcFields.prepareId(fieldId)); if (foundIds.length === 1) { field.setAttribute('data-kpxc-id', fieldId); return; } } // Create own ID if no ID is set for this field kpxcFields.uniqueNumber += 1; field.setAttribute('data-kpxc-id', 'kpxcpw' + String(kpxcFields.uniqueNumber)); } }; kpxcFields.prepareId = function(id) { return (id + '').replace(kpxcFields.rcssescape, kpxcFields.fcssescape); }; // 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) { const attributes = target.attributes; // Check element attributes for (const attr of attributes) { if ((attr.value && (attr.value.toLowerCase().includes('search')) || attr.value === 'q')) { return true; } } // Check closest form const closestForm = kpxc.getForm(target); if (closestForm) { // Check form action const formAction = closestForm.getAttribute('action'); if (formAction && (formAction.toLowerCase().includes('search') && !formAction.toLowerCase().includes('research'))) { return true; } // Check form class and id const closestFormId = closestForm.getAttribute('id'); const closestFormClass = closestForm.className; if (closestFormClass && (closestForm.className.toLowerCase().includes('search') || (closestFormId && closestFormId.toLowerCase().includes('search') && !closestFormId.toLowerCase().includes('research')))) { return true; } } // Check parent elements for role='search' if (target.closest('[role~=\'search\']')) { return true; } return false; }; kpxcFields.isVisible = function(field) { const rect = field.getBoundingClientRect(); // Check CSS visibility const fieldStyle = getComputedStyle(field); if (fieldStyle.visibility && (fieldStyle.visibility === 'hidden' || fieldStyle.visibility === 'collapse')) { return false; } // Check element position and size if (rect.x < 0 || rect.y < 0 || rect.width < 8 || rect.height < 8) { return false; } return true; }; kpxcFields.isAutocompleteAppropriate = function(field) { const autocomplete = field.getLowerCaseAttribute('autocomplete'); return !(autocomplete === 'off' || autocomplete === 'new-password'); }; kpxcFields.getAllFields = function() { const fields = []; const inputs = kpxcObserverHelper.getInputs(document); for (const i of inputs) { if (kpxcFields.isVisible(i) && !kpxcFields.isSearchField(i) && kpxcFields.isAutocompleteAppropriate(i)) { kpxcFields.setUniqueId(i); fields.push(i); } } _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; }; kpxcFields.prepareVisibleFieldsWithID = function(pattern) { const patterns = document.querySelectorAll(pattern); for (const i of patterns) { if (kpxcFields.isVisible(i) && i.style.visibility !== 'hidden' && i.style.visibility !== 'collapsed') { kpxcFields.setUniqueId(i); } } }; kpxcFields.getAllCombinations = function(inputs) { const fields = []; let uField = null; for (const i of inputs) { if (i) { if (i.getLowerCaseAttribute('type') === 'password') { const uId = (!uField || uField.length < 1) ? null : uField.getAttribute('data-kpxc-id'); const combination = { username: uId, password: i.getAttribute('data-kpxc-id') }; fields.push(combination); // Reset selected username field uField = null; } else { // Username field uField = i; } } } if (_singleInputEnabledForPage && fields.length === 0 && uField) { const combination = { username: uField.getAttribute('data-kpxc-id'), password: null }; fields.push(combination); } return fields; }; kpxcFields.getCombination = function(givenType, fieldId) { if (kpxcFields.combinations.length === 0) { if (kpxcFields.useDefinedCredentialFields()) { return kpxcFields.combinations[0]; } } // Use defined credential fields (already loaded into combinations) const location = kpxc.getDocumentLocation(); if (kpxc.settings['defined-custom-fields'] && kpxc.settings['defined-custom-fields'][location]) { return kpxcFields.combinations[0]; } for (const c of kpxcFields.combinations) { if (c[givenType] === fieldId) { return c; } } // Find new combination let combination = { username: null, password: null }; let newCombi = false; if (givenType === 'username') { const passwordField = kpxcFields.getPasswordField(fieldId, true); let passwordId = null; if (passwordField) { passwordId = kpxcFields.prepareId(passwordField.getAttribute('data-kpxc-id')); } combination = { username: fieldId, password: passwordId }; newCombi = true; } else if (givenType === 'password') { const usernameField = kpxcFields.getUsernameField(fieldId, true); let usernameId = null; if (usernameField) { usernameId = kpxcFields.prepareId(usernameField.getAttribute('data-kpxc-id')); } combination = { username: usernameId, password: fieldId }; newCombi = true; } if (combination.username || combination.password) { kpxcFields.combinations.push(combination); } if (combination.username) { if (kpxc.credentials.length > 0) { kpxc.preparePageForMultipleCredentials(kpxc.credentials); } } if (newCombi) { combination.isNew = true; } return combination; }; /** * Return the username field or null if it not exists */ kpxcFields.getUsernameField = async function(passwordId, checkDisabled) { const passwordField = _f(passwordId); if (!passwordField) { return null; } const form = kpxc.getForm(passwordField); let usernameField = null; // Search all inputs on this one form if (form) { const inputs = form.querySelectorAll(kpxcFields.inputQueryPattern); for (const i of inputs) { kpxcFields.setUniqueId(i); if (i.getAttribute('data-kpxc-id') === passwordId) { return false; // Break } if (i.getLowerCaseAttribute('type') === 'password') { return true; // Continue } if (kpxc.settings.showLoginFormIcon && await kpxc.passwordFilled() === false) { kpxcUsernameIcons.newIcon(usernameField); } usernameField = i; } } else { // Search all inputs on page const inputs = kpxcFields.getAllFields(); kpxc.initPasswordGenerator(inputs); for (const i of inputs) { if (i.getAttribute('data-kpxc-id') === passwordId) { break; } if (i.getLowerCaseAttribute('type') === 'password') { continue; } usernameField = i; } } if (usernameField && !checkDisabled) { const usernameId = usernameField.getAttribute('data-kpxc-id'); // Check if usernameField is already used by another combination for (const c of kpxcFields.combinations) { if (c.username === usernameId) { usernameField = null; break; } } } kpxcFields.setUniqueId(usernameField); return usernameField; }; /** * Return the password field or null if it not exists */ kpxcFields.getPasswordField = function(usernameId, checkDisabled) { const usernameField = _f(usernameId); if (!usernameField) { return null; } const form = kpxc.getForm(usernameField); let passwordField = null; // Search all inputs on this one form if (form) { const inputs = form.querySelectorAll('input[type=\'password\']'); if (inputs.length > 0) { passwordField = inputs[0]; } if (passwordField && passwordField.length < 1) { passwordField = null; } kpxcPasswordIcons.newIcon(kpxc.settings.usePasswordGeneratorIcons, passwordField, [], undefined, _databaseClosed); } else { // Search all inputs on page const inputs = kpxcFields.getAllFields(); kpxc.initPasswordGenerator(inputs); let active = false; for (const i of inputs) { if (i.getAttribute('data-kpxc-id') === usernameId) { active = true; } if (active && i.getLowerCaseAttribute('type') === 'password') { passwordField = i; break; } } } if (passwordField && !checkDisabled) { const passwordId = passwordField.getAttribute('data-kpxc-id'); // Check if passwordField is already used by another combination for (const c of kpxcFields.combinations) { if (c.password === passwordId) { passwordField = null; break; } } } kpxcFields.setUniqueId(passwordField); return passwordField; }; 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 if (pwField && !pwField.getAttribute('kpxcFields-onChange')) { pwField.setAttribute('kpxcFields-onChange', true); pwField.addEventListener('change', function() { this.setAttribute('unchanged', false); }); } 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; if (kpxc.settings.showLoginFormIcon && await kpxc.passwordFilled() === false) { kpxcUsernameIcons.newIcon(usernameField, _databaseClosed); } // Initialize form-submit for remembering credentials if (field) { const form = kpxc.getForm(field); if (form && form.length > 0) { kpxcForm.init(form, c); } } } }; kpxcFields.useDefinedCredentialFields = function() { const location = kpxc.getDocumentLocation(); if (kpxc.settings['defined-custom-fields'] && kpxc.settings['defined-custom-fields'][location]) { const creds = kpxc.settings['defined-custom-fields'][location]; let found = _f(creds.username) || _f(creds.password); for (const i of creds.fields) { if (_fs(i)) { found = true; break; } } if (found) { const fields = { username: creds.username, password: creds.password, fields: creds.fields }; kpxcFields.combinations = []; kpxcFields.combinations.push(fields); return true; } } return false; }; const kpxcObserverHelper = {}; kpxcObserverHelper.inputTypes = [ 'text', 'email', 'password', 'tel', 'number', 'username', // Note: Not a standard undefined, // Input field can be without any type. Include this and null to the list. null ]; // Ignores all nodes that doesn't contain elements kpxcObserverHelper.ignoredNode = function(target) { if (target.nodeType === Node.ATTRIBUTE_NODE || target.nodeType === Node.TEXT_NODE || target.nodeType === Node.CDATA_SECTION_NODE || target.nodeType === Node.PROCESSING_INSTRUCTION_NODE || target.nodeType === Node.COMMENT_NODE || target.nodeType === Node.DOCUMENT_TYPE_NODE || target.nodeType === Node.NOTATION_NODE) { return true; } return false; }; kpxcObserverHelper.getInputs = function(target) { // Ignores target element if it's not an element node if (kpxcObserverHelper.ignoredNode(target)) { return []; } // Filter out any input fields with type 'hidden' right away const inputFields = []; Array.from(target.getElementsByTagName('input')).forEach((e) => { if (e.type !== 'hidden' && !e.readOnly) { inputFields.push(e); } }); 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) { let type = i.getLowerCaseAttribute('type'); if (kpxcObserverHelper.inputTypes.includes(type)) { inputs.push(i); } } return inputs; }; kpxcObserverHelper.getId = function(target) { return target.classList.length === 0 ? target.id : target.classList; }; kpxcObserverHelper.ignoredElement = function(target) { // Ignore elements that do not have a className (including SVG) if (typeof target.className !== 'string') { return true; } // Ignore KeePassXC-Browser classes if (target.className && target.className !== undefined && target.className.includes('kpxc')) { return true; } return false; }; kpxcObserverHelper.handleObserverAdd = function(target) { if (kpxcObserverHelper.ignoredElement(target)) { return; } const inputs = kpxcObserverHelper.getInputs(target); if (inputs.length === 0) { return; } const neededLength = _detectedFields === 1 ? 0 : 1; const id = kpxcObserverHelper.getId(target); if (inputs.length > neededLength && !_observerIds.includes(id)) { // Save target element id for preventing multiple calls to initCredentialsFields() _observerIds.push(id); // Sometimes the settings haven't been loaded before new input fields are detected if (Object.keys(kpxc.settings).length === 0) { kpxc.init(); } else { kpxc.initCredentialFields(true, inputs); } } }; kpxcObserverHelper.handleObserverRemove = function(target) { if (kpxcObserverHelper.ignoredElement(target)) { return; } const inputs = kpxcObserverHelper.getInputs(target); if (inputs.length === 0) { return; } // Remove target element id from the list const id = kpxcObserverHelper.getId(target); if (_observerIds.includes(id)) { const index = _observerIds.indexOf(id); if (index >= 0) { _observerIds.splice(index, 1); } } }; kpxcObserverHelper.detectURLChange = function() { if (_documentURL !== document.location.href) { _documentURL = document.location.href; kpxcEvents.clearCredentials(); kpxc.initCredentialFields(true); } }; MutationObserver = window.MutationObserver || window.WebKitMutationObserver; const kpxc = {}; kpxc.settings = {}; kpxc.u = null; kpxc.p = null; kpxc.url = null; kpxc.submitUrl = null; kpxc.credentials = []; const initcb = async function() { try { const settings = await browser.runtime.sendMessage({ action: 'load_settings' }); kpxc.settings = settings; // Don't initialize MutationObserver if the site is ignored if (kpxc.siteIgnored()) { return; } if (kpxc.settings.useObserver) { kpxc.initObserver(); } await kpxc.initCredentialFields(); // Retrieve submitted credentials if available. const creds = await browser.runtime.sendMessage({ action: 'page_get_submitted' }); const redirectCount = await browser.runtime.sendMessage({ action: 'page_get_redirect_count' }); if (creds && creds.submitted) { // If username field is not set, wait for credentials in kpxc.retrieveCredentialsCallback. if (!creds.username) { return; } if (redirectCount >= kpxc.settings.redirectAllowance) { 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)) { initcb(); } else { document.addEventListener('DOMContentLoaded', initcb); } kpxc.init = function() { initcb(); }; // Detects DOM changes in the document kpxc.initObserver = function() { const observer = new MutationObserver(function(mutations, obs) { if (document.visibilityState === 'hidden' || kpxcUI.mouseDown) { return; } // Limit the mutation handling if (mutations.length > _maximumMutations) { mutations.slice(0, _maximumMutations); } for (const mut of mutations) { // Skip text nodes if (mut.target.nodeType === Node.TEXT_NODE) { continue; } // Check document URL change and detect new fields kpxcObserverHelper.detectURLChange(); // 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') { kpxcObserverHelper.handleObserverAdd(mut.target); } else { kpxcObserverHelper.handleObserverRemove(mut.target); } } } else if (mut.type === 'childList') { kpxcObserverHelper.handleObserverAdd((mut.addedNodes.length > 0) ? mut.addedNodes[0] : mut.target); kpxcObserverHelper.handleObserverRemove((mut.removedNodes.length > 0) ? mut.removedNodes[0] : mut.target); } } }); // define what element should be observed by the observer // and what types of mutations trigger the callback observer.observe(document, { subtree: true, attributes: true, childList: true, characterData: true, attributeFilter: [ 'style', 'class' ] }); }; // Clears all from the content and background scripts, including autocomplete kpxc.clearAllFromPage = function() { kpxcEvents.clearCredentials(); browser.runtime.sendMessage({ action: 'page_clear_logins' }); // Switch back to default popup browser.runtime.sendMessage({ action: 'get_status', args: [ true ] // Set polling to true, this is an internal function call }); }; // Switch credentials if database is changed or closed kpxc.detectDatabaseChange = async function(response) { _databaseClosed = true; kpxc.clearAllFromPage(); kpxc.switchIcons(true); if (document.visibilityState !== 'hidden') { if (response.new !== '' && response.new !== response.old) { _called.retrieveCredentials = false; const settings = await browser.runtime.sendMessage({ action: 'load_settings' }); kpxc.settings = settings; await kpxc.initCredentialFields(true); kpxc.switchIcons(false); // Unlocked _databaseClosed = false; // 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; } } } }; // Checks if the site has been ignored using Site Preferences kpxc.siteIgnored = function(condition) { kpxc.initializeSitePreferences(); if (kpxc.settings.sitePreferences) { let currentLocation; try { currentLocation = window.top.location.href; } catch (err) { // Cross-domain security error inspecting window.top.location.href. // This catches an error when an iframe is being accessed from another (sub)domain -> use the iframe URL instead. currentLocation = window.self.location.href; } const currentSetting = condition || IGNORE_FULL; for (const site of kpxc.settings.sitePreferences) { if (siteMatch(site.url, currentLocation) || site.url === currentLocation) { if (site.ignore === currentSetting) { return true; } _singleInputEnabledForPage = site.usernameOnly; } } } return false; }; kpxc.initCredentialFields = async function(forceCall, inputs) { if (_called.initCredentialFields && !forceCall) { return; } _called.initCredentialFields = true; await browser.runtime.sendMessage({ action: 'page_clear_logins', args: _called.clearLogins }); _called.clearLogins = true; if (kpxc.siteIgnored()) { return; } // If target input fields are not defined, get inputs from the whole document if (inputs === undefined) { inputs = kpxcFields.getAllFields(); } if (inputs.length === 0) { return; } // Update database closed status const res = await browser.runtime.sendMessage({ action: 'get_status', args: [ true ] }); _databaseClosed = res.databaseClosed; kpxcFields.prepareVisibleFieldsWithID('select'); kpxc.initPasswordGenerator(inputs); if (kpxc.settings.showOTPIcon) { kpxc.initOTPFields(inputs); } 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; } 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]); } 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.usePasswordGeneratorIcons) { return; } for (let i = 0; i < inputs.length; i++) { if (inputs[i] && inputs[i].getLowerCaseAttribute('type') === 'password') { kpxcPasswordIcons.newIcon(true, inputs[i], inputs, i, _databaseClosed); } } }; kpxc.initOTPFields = function(inputs, databaseClosed) { for (const i of inputs) { const id = i.getLowerCaseAttribute('id'); const name = i.getLowerCaseAttribute('name'); const autocomplete = i.getLowerCaseAttribute('autocomplete'); if (autocomplete === 'one-time-code' || acceptedOTPFields.some(f => (id && id.includes(f)) || (name && name.includes(f)))) { kpxcTOTPIcons.newIcon(i, _databaseClosed); } } }; 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; await kpxc.retrieveCredentialsCallback(credentials, false); return credentials; } return kpxc.credentials; }; 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); } if (credentials && credentials.length > 0) { kpxc.credentials = credentials; 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); } }; kpxc.prepareFieldsForCredentials = function(autoFillInForSingle) { // Only one login for this site if (autoFillInForSingle && kpxc.settings.autoFillSingleEntry && kpxc.credentials.length === 1) { let combination = null; if (!kpxc.p && !kpxc.u && kpxcFields.combinations.length > 0) { kpxc.u = _f(kpxcFields.combinations[0].username); kpxc.p = _f(kpxcFields.combinations[0].password); combination = kpxcFields.combinations[0]; } if (kpxc.u) { kpxc.setValueWithChange(kpxc.u, kpxc.credentials[0].login); combination = kpxcFields.getCombination('username', kpxc.u); } if (kpxc.p) { kpxc.setValueWithChange(kpxc.p, kpxc.credentials[0].password); combination = kpxcFields.getCombination('password', kpxc.p); } if (combination) { let list = []; if (kpxc.fillInStringFields(combination.fields, kpxc.credentials[0].stringFields, list)) { kpxcForm.destroy(false, { 'password': list.list[0], 'username': list.list[1] }); } } // Generate popup-list of usernames + descriptions browser.runtime.sendMessage({ action: 'popup_login', args: [ `${kpxc.credentials[0].login} (${kpxc.credentials[0].name})` ] }); } else if (kpxc.credentials.length > 1 || (kpxc.credentials.length > 0 && (!kpxc.settings.autoFillSingleEntry || !autoFillInForSingle))) { kpxc.preparePageForMultipleCredentials(kpxc.credentials); } }; 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') { return `${visibleLogin} (${credential.name}) [${tr('credentialExpired')}]`; } return `${visibleLogin} (${credential.name})`; } // Add usernames + descriptions to autocomplete-list and popup-list const usernames = []; kpxcAutocomplete.elements = []; for (let i = 0; i < credentials.length; i++) { const loginText = getLoginText(credentials[i]); usernames.push(loginText); const item = { label: loginText, value: credentials[i].login, loginId: i }; kpxcAutocomplete.elements.push(item); } // Generate popup-list of usernames + descriptions browser.runtime.sendMessage({ action: 'popup_login', args: usernames }); // Initialize autocomplete for username fields if (kpxc.settings.autoCompleteUsernames) { for (const i of kpxcFields.combinations) { // Both username and password fields are visible if (_detectedFields >= 2) { if (_f(i.username)) { kpxcAutocomplete.create(_f(i.username), false, kpxc.settings.autoSubmit); } } else if (_detectedFields === 1) { if (_f(i.username)) { kpxcAutocomplete.create(_f(i.username), false, kpxc.settings.autoSubmit); } if (_f(i.password)) { kpxcAutocomplete.create(_f(i.password), false, kpxc.settings.autoSubmit); } } } } }; // 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; } const field = _f(combination.password) || _f(combination.username); if (field === null) { return null; } const form = kpxc.getForm(field); let action = null; if (form && form.length > 0) { action = form.action; } if (typeof(action) !== 'string' || action === '') { action = document.location.origin + document.location.pathname; } return action; }; kpxc.getFormActionUrlFromSingleInput = function(field) { if (!field) { return null; } let action = field.formAction; if (typeof(action) !== 'string' || action === '') { action = document.location.origin + document.location.pathname; } return action; }; // Get the form submit button instead if action URL is same as the page itself 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) { if (i.type === 'submit') { return i; } } } // Try to find another button. Select the first one. const buttons = Array.from(form.querySelectorAll('button[type=\'button\'], button[type=\'submit\'], input[type=\'button\'], button:not([type])')); if (buttons.length > 0) { return buttons[0]; } return undefined; }; kpxc.fillInCredentials = async function(combination, onlyPassword, suppressWarnings) { const action = kpxc.getFormActionUrl(combination); const u = _f(combination.username); const p = _f(combination.password); if (combination.isNew) { // Initialize form-submit for remembering credentials const fieldId = combination.password || combination.username; const field = _f(fieldId); if (field) { const form = kpxc.getForm(field); if (form && form.length > 0) { kpxcForm.init(form, combination); } } } if (u) { kpxc.u = u; } if (p) { kpxc.p = p; } if (kpxc.url === document.location.origin && kpxc.submitUrl === action && kpxc.credentials.length > 0) { kpxc.fillIn(combination, onlyPassword, suppressWarnings); } else { kpxc.url = document.location.origin; kpxc.submitUrl = action; const credentials = await browser.runtime.sendMessage({ action: 'retrieve_credentials', args: [ kpxc.url, kpxc.submitUrl, true ] // Sets triggerUnlock to true }); await kpxc.retrieveCredentialsCallback(credentials, true); kpxc.fillIn(combination, onlyPassword, suppressWarnings); } }; kpxc.fillInFromActiveElement = function(suppressWarnings, passOnly = false) { const el = document.activeElement; if (el.tagName.toLowerCase() !== 'input') { if (kpxcFields.combinations.length > 0) { kpxc.fillInCredentials(kpxcFields.combinations[0], passOnly, suppressWarnings); // Focus to the input field const field = _f(passOnly ? kpxcFields.combinations[0].password : kpxcFields.combinations[0].username); if (field) { field.focus(); } } return; } kpxcFields.setUniqueId(el); const fieldId = kpxcFields.prepareId(el.getAttribute('data-kpxc-id')); let combination = null; if (el.getAttribute('type') === 'password') { combination = kpxcFields.getCombination('password', fieldId); } else { combination = kpxcFields.getCombination('username', fieldId); } if (passOnly) { if (!_f(combination.password)) { kpxcUI.createNotification('warning', tr('fieldsNoPasswordField')); return; } } delete combination.loginId; kpxc.fillInCredentials(combination, passOnly, suppressWarnings); }; kpxc.fillInFromActiveElementTOTPOnly = async function(target) { const el = target || document.activeElement; kpxcFields.setUniqueId(el); const fieldId = kpxcFields.prepareId(el.getAttribute('data-kpxc-id')); const index = await browser.runtime.sendMessage({ action: 'page_get_login_id' }); 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) { if (field.matches('select')) { value = value.toLowerCase().trim(); const options = field.querySelectorAll('option'); for (const o of options) { if (o.textContent.toLowerCase().trim() === value) { kpxc.setValueWithChange(field, o.value); return false; } } } else { kpxc.setValueWithChange(field, value); } }; kpxc.fillInStringFields = function(fields, stringFields, filledInFields) { let filledIn = false; filledInFields.list = []; if (fields && stringFields && fields.length > 0 && stringFields.length > 0) { for (let i = 0; i < fields.length; i++) { const currentField = _fs(fields[i]); const stringFieldValue = Object.values(stringFields[i]); if (currentField && stringFieldValue[0]) { kpxc.setValue(currentField, stringFieldValue[0]); filledInFields.list.push(fields[i]); filledIn = true; } } } return filledIn; }; kpxc.setValueWithChange = function(field, value) { if (kpxc.settings.respectMaxLength === true) { const attributeMaxlength = field.getAttribute('maxlength'); if (attributeMaxlength && !isNaN(attributeMaxlength) && attributeMaxlength > 0) { value = value.substr(0, attributeMaxlength); } } field.value = value; field.dispatchEvent(new Event('input', { 'bubbles': true })); 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) { kpxcUI.createNotification('error', tr('credentialsNoLoginsFound')); return; } let skipAutoSubmit = false; const uField = _f(combination.username); const pField = _f(combination.password); // Exactly one pair of credentials available if (kpxc.credentials.length === 1) { let filledIn = false; if (kpxc.credentials[0].skipAutoSubmit !== undefined) { skipAutoSubmit = kpxc.credentials[0].skipAutoSubmit === 'true'; } if (uField && (!onlyPassword || _singleInputEnabledForPage)) { kpxc.setValueWithChange(uField, kpxc.credentials[0].login); browser.runtime.sendMessage({ action: 'page_set_login_id', args: 0 }); filledIn = true; } if (pField) { pField.setAttribute('type', 'password'); kpxc.setValueWithChange(pField, kpxc.credentials[0].password); pField.setAttribute('unchanged', true); browser.runtime.sendMessage({ action: 'page_set_login_id', args: 0 }); filledIn = true; kpxc.setPasswordFilled(true); } let list = []; if (kpxc.fillInStringFields(combination.fields, kpxc.credentials[0].stringFields, list)) { kpxcForm.destroy(false, { 'password': list.list[0], 'username': list.list[1] }); filledIn = true; } if (!filledIn) { if (!suppressWarnings) { kpxcUI.createNotification('error', tr('fieldsFill')); } return; } } else if (combination.loginId !== undefined && kpxc.credentials[combination.loginId]) { // Specific login ID given let filledIn = false; if (kpxc.credentials[0].skipAutoSubmit !== undefined) { skipAutoSubmit = kpxc.credentials[combination.loginId].skipAutoSubmit === 'true'; } if (uField && (!onlyPassword || _singleInputEnabledForPage)) { kpxc.setValueWithChange(uField, kpxc.credentials[combination.loginId].login); browser.runtime.sendMessage({ action: 'page_set_login_id', args: combination.loginId }); filledIn = true; } if (pField) { kpxc.setValueWithChange(pField, kpxc.credentials[combination.loginId].password); pField.setAttribute('unchanged', true); browser.runtime.sendMessage({ action: 'page_set_login_id', args: combination.loginId }); filledIn = true; kpxc.setPasswordFilled(true); } let list = []; if (kpxc.fillInStringFields(combination.fields, kpxc.credentials[combination.loginId].stringFields, list)) { kpxcForm.destroy(false, { 'password': list.list[0], 'username': list.list[1] }); filledIn = true; } if (!filledIn) { if (!suppressWarnings) { kpxcUI.createNotification('error', tr('fieldsFill')); } return; } } else { // Multiple credentials available // Check if only one password for given username exists let countPasswords = 0; if (uField) { let valPassword = ''; let valUsername = ''; let valStringFields = []; const valQueryUsername = uField.value.toLowerCase(); // Find passwords to given username (even those with empty username) for (const c of kpxc.credentials) { if (c.login.toLowerCase() === valQueryUsername) { countPasswords += 1; valPassword = c.password; valUsername = c.login; valStringFields = c.stringFields; if (c.skipAutoSubmit !== undefined) { skipAutoSubmit = c.skipAutoSubmit === 'true'; } } } // For the correct notification message: 0 = no logins, X > 1 = too many logins if (countPasswords === 0) { countPasswords = kpxc.credentials.length; } // Only one mapping username found if (countPasswords === 1) { if (!onlyPassword) { kpxc.setValueWithChange(uField, valUsername); } if (pField) { kpxc.setValueWithChange(pField, valPassword); pField.setAttribute('unchanged', true); kpxc.setPasswordFilled(true); } let list = []; if (kpxc.fillInStringFields(combination.fields, valStringFields, list)) { kpxcForm.destroy(false, { 'password': list.list[0], 'username': list.list[1] }); } } // User has to select correct credentials by himself if (countPasswords > 1) { if (!suppressWarnings) { const target = onlyPassword ? pField : uField; if (!target) { return; } if (kpxcAutocomplete.started) { kpxcAutocomplete.showList(target); } else { kpxcAutocomplete.create(target, true, kpxc.settings.autoSubmit); } target.focus(); } return; } else if (countPasswords < 1) { if (!suppressWarnings) { kpxcUI.createNotification('error', tr('credentialsNoUsernameFound')); } return; } } else { if (!suppressWarnings) { const target = onlyPassword ? pField : uField; if (!target) { return; } if (kpxcAutocomplete.started) { kpxcAutocomplete.showList(target); } else { kpxcAutocomplete.create(target, true, kpxc.settings.autoSubmit); } target.focus(); return; } } } // Close autocomplete menu after fill kpxcAutocomplete.closeList(); // Auto-submit if (kpxc.settings.autoSubmit && !skipAutoSubmit) { const form = kpxc.u.form || kpxc.p.form; const submitButton = kpxc.getFormSubmitButton(form); if (submitButton !== undefined) { submitButton.click(); } else { form.submit(); } } }; kpxc.contextMenuRememberCredentials = async function() { const el = document.activeElement; if (el.tagName.toLowerCase() !== 'input') { return; } kpxcFields.setUniqueId(el); const fieldId = kpxcFields.prepareId(el.getAttribute('data-kpxc-id')); let combination = null; if (el.getAttribute('type') === 'password') { combination = kpxcFields.getCombination('password', fieldId); } else { combination = kpxcFields.getCombination('username', fieldId); } let usernameValue = ''; let passwordValue = ''; const usernameField = _f(combination.username); const passwordField = _f(combination.password); if (usernameField) { usernameValue = usernameField.value; } if (passwordField) { passwordValue = passwordField.value; } const result = await kpxc.rememberCredentials(usernameValue, passwordValue); if (result === undefined) { kpxcUI.createNotification('error', tr('rememberNoPassword')); return; } if (!result) { kpxcUI.createNotification('warning', tr('rememberCredentialsExists')); } }; /** * 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 = (oldCredentials !== undefined && oldCredentials.length > 0) ? oldCredentials : kpxc.credentials; // No password given or field cleaned by a site-running script // --> no password to save if (passwordValue === '') { return undefined; } let usernameExists = false; let nothingChanged = false; for (const c of credentials) { if (c.login === usernameValue && c.password === passwordValue) { nothingChanged = true; break; } if (c.login === usernameValue) { usernameExists = true; } } if (!nothingChanged) { if (!usernameExists) { for (const c of credentials) { if (c.login === usernameValue) { usernameExists = true; break; } } } const credentialsList = []; for (const c of credentials) { credentialsList.push({ login: c.login, name: c.name, uuid: c.uuid }); } let url = this.action; if (!url) { url = kpxc.settings.saveDomainOnlyNewCreds ? document.location.origin : document.location.href; if (url.indexOf('?') > 0) { url = url.substring(0, url.indexOf('?')); if (url.length < document.location.origin.length) { url = document.location.origin; } } } 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.getSite = function(sites) { if (!sites || sites.length === 0) { return ''; } 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']) { if (existingSite.url === site) { existingSite.ignore = IGNORE_NORMAL; siteExists = true; } } if (!siteExists) { kpxc.settings['sitePreferences'].push({ url: site, ignore: IGNORE_NORMAL, usernameOnly: false }); } 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) { delete kpxc.settings['sitePreferences']; } if (!kpxc.settings['sitePreferences']) { kpxc.settings['sitePreferences'] = []; } }; kpxc.getDocumentLocation = function() { return kpxc.settings.saveDomainOnly ? document.location.origin : document.location.href; }; // Sets the icons to corresponding database lock status kpxc.switchIcons = function(locked) { kpxcUsernameIcons.switchIcon(locked); kpxcPasswordIcons.switchIcon(locked); kpxcTOTPIcons.switchIcon(locked); }; kpxc.setPasswordFilled = function(state) { browser.runtime.sendMessage({ action: 'password_set_filled', args: state }); }; kpxc.passwordFilled = async function() { return await browser.runtime.sendMessage({ action: 'password_get_filled' }); }; const kpxcEvents = {}; kpxcEvents.clearCredentials = function() { kpxc.credentials = []; kpxcAutocomplete.elements = []; _called.retrieveCredentials = false; if (kpxc.settings.autoCompleteUsernames) { for (const c of kpxcFields.combinations) { const uField = _f(c.username); if (uField) { if (uField.classList.contains('ui-autocomplete-input')) { uField.autocomplete('destroy'); } } } } }; 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' }); kpxc.switchIcons(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) { _called.retrieveCredentials = true; kpxc.retrieveCredentialsCallback(await browser.runtime.sendMessage({ action: 'retrieve_credentials', args: [ kpxc.url, kpxc.submitUrl ] })); } };