From 210b137243c6726215e960000f74ce9da617ef29 Mon Sep 17 00:00:00 2001 From: varjolintu Date: Sun, 29 Sep 2019 21:09:25 +0300 Subject: [PATCH] Add OTP icons with fill --- keepassxc-browser/_locales/en/messages.json | 16 ++ keepassxc-browser/background/event.js | 11 + keepassxc-browser/background/page.js | 6 + .../content/keepassxc-browser.js | 88 +++++-- keepassxc-browser/content/pwgen.js | 15 +- keepassxc-browser/content/totp-field.js | 79 +++++++ keepassxc-browser/content/ui.js | 12 + keepassxc-browser/content/username-field.js | 25 +- keepassxc-browser/css/totp.css | 14 ++ keepassxc-browser/icons/otp.svg | 216 ++++++++++++++++++ keepassxc-browser/manifest.json | 5 +- keepassxc-browser/options/options.html | 31 ++- 12 files changed, 473 insertions(+), 45 deletions(-) create mode 100644 keepassxc-browser/content/totp-field.js create mode 100644 keepassxc-browser/css/totp.css create mode 100644 keepassxc-browser/icons/otp.svg diff --git a/keepassxc-browser/_locales/en/messages.json b/keepassxc-browser/_locales/en/messages.json index 99935c5..df44b7c 100644 --- a/keepassxc-browser/_locales/en/messages.json +++ b/keepassxc-browser/_locales/en/messages.json @@ -167,6 +167,14 @@ "message": "Username field icon", "description": "Username field icon text." }, + "totpFieldText": { + "message": "Fill TOTP from KeePassXC", + "description": "OTP field icon hover text." + }, + "totpFieldIcon": { + "message": "TOTP field icon", + "description": "OTP field icon text." + }, "defineDismiss": { "message": "Dismiss", "description": "Dismiss button text when choosing custom login fields." @@ -567,6 +575,10 @@ "message": "Activate username field icons.", "description": "Activate username field icons textbox text." }, + "optionsCheckboxOTPIcons": { + "message": "Activate 2FA/OTP field icons.", + "description": "Activate OTP field icons textbox text." + }, "optionsCheckboxAutoRetrieveCredentials": { "message": "Automatically retrieve credentials.", "description": "Automatically retrieve credentials checkbox text." @@ -647,6 +659,10 @@ "message": "Adds an icon to username fields for filling credentials with a single mouse click.", "description": "Username field icon option help text." }, + "optionsShowOTPIconHelpText": { + "message": "Adds an icon to input fields that are recognized as OTP/2FA.", + "description": "OTP field icon option help text." + }, "optionsAutoRetrieveCredentialsHelpText": { "message": "KeePassXC-Browser will immediately retrieve credentials when a tab is activated.", "description": "Auto-Retrive Credentials option help text." diff --git a/keepassxc-browser/background/event.js b/keepassxc-browser/background/event.js index 42b2727..0994835 100755 --- a/keepassxc-browser/background/event.js +++ b/keepassxc-browser/background/event.js @@ -228,6 +228,15 @@ kpxcEvent.onUsernameFieldDetected = function(tab, detected) { page.usernameFieldDetected = detected; }; +kpxcEvent.passwordGetFilled = async function() { + return page.passwordFilled; +} + +kpxcEvent.passwordSetFilled = function(tab, state) { + page.passwordFilled = state; + return Promise.resolve(); +} + // All methods named in this object have to be declared BEFORE this! kpxcEvent.messageHandlers = { 'add_credentials': keepass.addCredentials, @@ -255,6 +264,8 @@ kpxcEvent.messageHandlers = { 'page_get_submitted': kpxcEvent.pageGetSubmitted, 'page_set_login_id': kpxcEvent.pageSetLoginId, 'page_set_submitted': kpxcEvent.pageSetSubmitted, + 'password_get_filled': kpxcEvent.passwordGetFilled, + 'password_set_filled': kpxcEvent.passwordSetFilled, 'pop_stack': kpxcEvent.onPopStack, 'popup_login': kpxcEvent.onLoginPopup, 'popup_multiple-fields': kpxcEvent.onMultipleFieldsPopup, diff --git a/keepassxc-browser/background/page.js b/keepassxc-browser/background/page.js index a703faf..50c9a30 100755 --- a/keepassxc-browser/background/page.js +++ b/keepassxc-browser/background/page.js @@ -11,6 +11,7 @@ const defaultSettings = { showNotifications: true, showLoginNotifications: true, showLoginFormIcon: true, + showOTPIcon: true, saveDomainOnly: true, autoReconnect: false, defaultGroup: '', @@ -21,6 +22,7 @@ var page = {}; page.blockedTabs = []; page.currentTabId = -1; page.loginId = -1; +page.passwordFilled = false; page.submitted = false; page.submittedCredentials = {}; page.tabs = []; @@ -61,6 +63,9 @@ page.initSettings = async function() { if (!('showLoginFormIcon' in page.settings)) { page.settings.showLoginFormIcon = defaultSettings.showLoginFormIcon; } + if (!('showOTPIcon' in page.settings)) { + page.settings.showOTPIcon = defaultSettings.showOTPIcon; + } if (!('saveDomainOnly' in page.settings)) { page.settings.saveDomainOnly = defaultSettings.saveDomainOnly; } @@ -114,6 +119,7 @@ page.clearCredentials = function(tabId, complete) { } page.usernameFieldDetected = false; + page.passwordFilled = false; page.tabs[tabId].credentials = []; delete page.tabs[tabId].credentials; diff --git a/keepassxc-browser/content/keepassxc-browser.js b/keepassxc-browser/content/keepassxc-browser.js index f2f71cd..2ad8389 100755 --- a/keepassxc-browser/content/keepassxc-browser.js +++ b/keepassxc-browser/content/keepassxc-browser.js @@ -6,12 +6,23 @@ const ManualFill = { BOTH: 2 }; -// contains already called method names +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; // Count of detected form fields on the page @@ -425,7 +436,7 @@ kpxcFields.getCombination = function(givenType, fieldId) { /** * Return the username field or null if it not exists */ -kpxcFields.getUsernameField = function(passwordId, checkDisabled) { +kpxcFields.getUsernameField = async function(passwordId, checkDisabled) { const passwordField = _f(passwordId); if (!passwordField) { return null; @@ -447,8 +458,8 @@ kpxcFields.getUsernameField = function(passwordId, checkDisabled) { return true; // Continue } - if (kpxc.settings.showLoginFormIcon) { - kpxcUsernameFields.newIcon(usernameField); + if (kpxc.settings.showLoginFormIcon && await kpxc.passwordFilled() === false) { + kpxcUsernameIcons.newIcon(usernameField); } usernameField = i; } @@ -506,7 +517,7 @@ kpxcFields.getPasswordField = function(usernameId, checkDisabled) { passwordField = null; } - kpxcPasswordIcons.newIcon(kpxc.settings.usePasswordGeneratorIcons, passwordField); + kpxcPasswordIcons.newIcon(kpxc.settings.usePasswordGeneratorIcons, passwordField, [], undefined, _databaseClosed); } else { // Search all inputs on page const inputs = kpxcFields.getAllFields(); @@ -556,14 +567,8 @@ kpxcFields.prepareCombinations = async function(combinations) { // 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 ] - }); - - if (kpxc.settings.showLoginFormIcon) { - kpxcUsernameFields.newIcon(usernameField, res.databaseClosed); + if (kpxc.settings.showLoginFormIcon && await kpxc.passwordFilled() === false) { + kpxcUsernameIcons.newIcon(usernameField, _databaseClosed); } // Initialize form-submit for remembering credentials @@ -854,8 +859,9 @@ kpxc.clearAllFromPage = function() { // Switch credentials if database is changed or closed kpxc.detectDatabaseChange = async function(response) { + _databaseClosed = true; kpxc.clearAllFromPage(); - kpxcUsernameFields.switchIcon(true); + kpxc.switchIcons(true); if (document.visibilityState !== 'hidden') { if (response.new !== '' && response.new !== response.old) { @@ -865,7 +871,8 @@ kpxc.detectDatabaseChange = async function(response) { }); kpxc.settings = settings; await kpxc.initCredentialFields(true); - kpxcUsernameFields.switchIcon(false); // Unlocked + 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. @@ -926,9 +933,20 @@ kpxc.initCredentialFields = async function(forceCall) { 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); @@ -964,7 +982,18 @@ kpxc.initCredentialFields = async function(forceCall) { kpxc.initPasswordGenerator = function(inputs) { for (let i = 0; i < inputs.length; i++) { if (inputs[i] && inputs[i].getLowerCaseAttribute('type') === 'password') { - kpxcPasswordIcons.newIcon(kpxc.settings.usePasswordGeneratorIcons, inputs[i], inputs, i); + kpxcPasswordIcons.newIcon(kpxc.settings.usePasswordGeneratorIcons, 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'); + + if (acceptedOTPFields.some(f => (id && id.includes(f)) || (name && name.includes(f)))) { + kpxcTOTPIcons.newIcon(i, _databaseClosed); } } }; @@ -1244,8 +1273,8 @@ kpxc.fillInFromActiveElement = function(suppressWarnings, passOnly = false) { kpxc.fillInCredentials(combination, passOnly, suppressWarnings); }; -kpxc.fillInFromActiveElementTOTPOnly = async function() { - const el = document.activeElement; +kpxc.fillInFromActiveElementTOTPOnly = async function(target) { + const el = target || document.activeElement; kpxcFields.setUniqueId(el); const fieldId = kpxcFields.prepareId(el.getAttribute('data-kpxc-id')); @@ -1379,6 +1408,7 @@ kpxc.fillIn = function(combination, onlyPassword, suppressWarnings) { action: 'page_set_login_id', args: 0 }); filledIn = true; + kpxc.setPasswordFilled(true); } let list = []; @@ -1416,6 +1446,7 @@ kpxc.fillIn = function(combination, onlyPassword, suppressWarnings) { action: 'page_set_login_id', args: combination.loginId }); filledIn = true; + kpxc.setPasswordFilled(true); } let list = []; @@ -1468,6 +1499,7 @@ kpxc.fillIn = function(combination, onlyPassword, suppressWarnings) { if (pField) { kpxc.setValueWithChange(pField, valPassword); pField.setAttribute('unchanged', true); + kpxc.setPasswordFilled(true); } let list = []; @@ -1754,6 +1786,24 @@ 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 = {}; @@ -1780,7 +1830,7 @@ kpxcEvents.triggerActivatedTab = async function() { // Update username field lock state const state = await browser.runtime.sendMessage({ action: 'check_database_hash' }); - kpxcUsernameFields.switchIcon(state === ''); + kpxc.switchIcons(state === ''); // initCredentialFields calls also "retrieve_credentials", to prevent it // check of init() was already called diff --git a/keepassxc-browser/content/pwgen.js b/keepassxc-browser/content/pwgen.js index c95b131..052bd2c 100644 --- a/keepassxc-browser/content/pwgen.js +++ b/keepassxc-browser/content/pwgen.js @@ -3,15 +3,20 @@ const kpxcPasswordIcons = {}; kpxcPasswordIcons.icons = []; -kpxcPasswordIcons.newIcon = function(field, databaseClosed = true) { - kpxcPasswordIcons.icons.push(new PasswordIcon(field, databaseClosed)); +kpxcPasswordIcons.newIcon = function(useIcons, field, inputs, pos, databaseClosed = true) { + kpxcPasswordIcons.icons.push(new PasswordIcon(useIcons, field, inputs, pos, databaseClosed)); +}; + +kpxcPasswordIcons.switchIcon = function(locked) { + kpxcPasswordIcons.icons.forEach(u => u.switchIcon(locked)); }; class PasswordIcon extends Icon { - constructor(useIcons, field, inputs, pos) { + constructor(useIcons, field, inputs, pos, databaseClosed) { super(); this.useIcons = useIcons; + this.databaseClosed = databaseClosed; this.initField(field, inputs, pos); kpxcUI.monitorIconPosition(this); @@ -72,6 +77,10 @@ PasswordIcon.prototype.createIcon = function(field) { icon.style.width = Pixels(size); icon.style.height = Pixels(size); + if (this.databaseClosed) { + icon.style.filter = 'saturate(0%)'; + } + icon.addEventListener('click', function(e) { if (!e.isTrusted) { return; diff --git a/keepassxc-browser/content/totp-field.js b/keepassxc-browser/content/totp-field.js new file mode 100644 index 0000000..9b30714 --- /dev/null +++ b/keepassxc-browser/content/totp-field.js @@ -0,0 +1,79 @@ +'use strict'; + +var kpxcTOTPIcons = {}; +kpxcTOTPIcons.icons = []; + +kpxcTOTPIcons.newIcon = function(field, databaseClosed = true) { + kpxcTOTPIcons.icons.push(new TOTPFieldIcon(field, databaseClosed)); +}; + +kpxcTOTPIcons.switchIcon = function(locked) { + kpxcTOTPIcons.icons.forEach(u => u.switchIcon(locked)); +}; + + +class TOTPFieldIcon extends Icon { + constructor(field, databaseClosed = true) { + super(); + this.icon = null; + this.inputField = null; + this.databaseClosed = databaseClosed; + + this.initField(field); + kpxcUI.monitorIconPosition(this); + } +}; + +TOTPFieldIcon.prototype.initField = function(field) { + if (!field || field.getAttribute('kpxc-totp-field') === 'true') { + return; + } + + field.setAttribute('kpxc-totp-field', 'true'); + + // Observer the visibility + if (this.observer) { + this.observer.observe(field); + } + + this.createIcon(field); + this.inputField = field; +}; + +TOTPFieldIcon.prototype.createIcon = function(field) { + const className = (isFirefox() ? 'moz' : 'default'); + + // Size the icon dynamically, but not greater than 24 or smaller than 14 + const size = Math.max(Math.min(24, field.offsetHeight - 4), 14); + let offset = Math.floor((field.offsetHeight - size) / 3); + offset = (offset < 0) ? 0 : offset; + + const icon = kpxcUI.createElement('div', 'kpxc kpxc-totp-icon ' + className, + { + 'title': tr('totpFieldText'), + 'alt': tr('totpFieldIcon'), + 'size': size, + 'offset': offset + }); + icon.style.zIndex = '10000000'; + icon.style.width = Pixels(size); + icon.style.height = Pixels(size); + + if (this.databaseClosed) { + icon.style.filter = 'saturate(0%)'; + } + + icon.addEventListener('click', async function(e) { + if (!e.isTrusted) { + return; + } + + e.preventDefault(); + await kpxc.receiveCredentialsIfNecessary(); + kpxc.fillInFromActiveElementTOTPOnly(field); + }); + + kpxcUI.setIconPosition(icon, field); + this.icon = icon; + document.body.appendChild(icon); +}; diff --git a/keepassxc-browser/content/ui.js b/keepassxc-browser/content/ui.js index 8f603c4..8da039d 100644 --- a/keepassxc-browser/content/ui.js +++ b/keepassxc-browser/content/ui.js @@ -21,6 +21,18 @@ class Icon { console.log(err); } } + + switchIcon(locked) { + if (!this.icon) { + return; + } + + if (locked) { + this.icon.style.filter = 'saturate(0%)'; + } else { + this.icon.style.filter = 'saturate(100%)'; + } + } }; const kpxcUI = {}; diff --git a/keepassxc-browser/content/username-field.js b/keepassxc-browser/content/username-field.js index 4db5f98..bfaf0f1 100644 --- a/keepassxc-browser/content/username-field.js +++ b/keepassxc-browser/content/username-field.js @@ -1,24 +1,25 @@ 'use strict'; -const kpxcUsernameFields = {}; -kpxcUsernameFields.icons = []; +const kpxcUsernameIcons = {}; +kpxcUsernameIcons.icons = []; -kpxcUsernameFields.newIcon = function(field, databaseClosed = true) { - kpxcUsernameFields.icons.push(new UsernameFieldIcon(field, databaseClosed)); +kpxcUsernameIcons.newIcon = function(field, databaseClosed = true) { + kpxcUsernameIcons.icons.push(new UsernameFieldIcon(field, databaseClosed)); }; -kpxcUsernameFields.switchIcon = function(locked) { - kpxcUsernameFields.icons.forEach(u => u.switchIcon(locked)); +kpxcUsernameIcons.switchIcon = function(locked) { + kpxcUsernameIcons.icons.forEach(u => u.switchIcon(locked)); }; class UsernameFieldIcon extends Icon { constructor(field, databaseClosed = true) { super(); + this.databaseClosed = databaseClosed; this.icon = null; this.inputField = null; - this.initField(field, databaseClosed); + this.initField(field,); kpxcUI.monitorIconPosition(this); } @@ -39,7 +40,7 @@ class UsernameFieldIcon extends Icon { } }; -UsernameFieldIcon.prototype.initField = function(field, databaseClosed = true) { +UsernameFieldIcon.prototype.initField = function(field) { if (!field || field.getAttribute('kpxc-username-field') === 'true') { return; } @@ -51,18 +52,18 @@ UsernameFieldIcon.prototype.initField = function(field, databaseClosed = true) { this.observer.observe(field); } - this.createIcon(field, databaseClosed); + this.createIcon(field); this.inputField = field; }; -UsernameFieldIcon.prototype.createIcon = function(target, databaseClosed) { +UsernameFieldIcon.prototype.createIcon = function(target) { // Remove any existing password generator icons from the input field if (target.getAttribute('kpxc-password-generator')) { kpxcPasswordDialog.removeIcon(target); } const field = target; - const className = getIconClassName(databaseClosed); + const className = getIconClassName(this.databaseClosed); // Size the icon dynamically, but not greater than 24 or smaller than 14 const size = Math.max(Math.min(24, field.offsetHeight - 4), 14); @@ -78,7 +79,7 @@ UsernameFieldIcon.prototype.createIcon = function(target, databaseClosed) { const icon = kpxcUI.createElement('div', 'kpxc kpxc-username-icon ' + className, { - 'title': databaseClosed ? tr('usernameLockedFieldText') : tr('usernameFieldText'), + 'title': this.databaseClosed ? tr('usernameLockedFieldText') : tr('usernameFieldText'), 'alt': tr('usernameFieldIcon'), 'size': size, 'offset': offset, diff --git a/keepassxc-browser/css/totp.css b/keepassxc-browser/css/totp.css new file mode 100644 index 0000000..8281132 --- /dev/null +++ b/keepassxc-browser/css/totp.css @@ -0,0 +1,14 @@ +.kpxc-totp-icon { + position: absolute; + cursor: pointer; +} + +.kpxc-totp-icon.default { + background: url('chrome-extension://__MSG_@@extension_id__/icons/otp.svg') right no-repeat; + background-size: contain; +} + +.kpxc-totp-icon.moz { + background: url('moz-extension://__MSG_@@extension_id__/icons/otp.svg') right no-repeat; + background-size: contain; +} \ No newline at end of file diff --git a/keepassxc-browser/icons/otp.svg b/keepassxc-browser/icons/otp.svg new file mode 100644 index 0000000..f0fe378 --- /dev/null +++ b/keepassxc-browser/icons/otp.svg @@ -0,0 +1,216 @@ + + + OTP icon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + OTP icon + + + + KeePassXC Team + + + + + KeePassXC Team + + + + + otp + totp + + + 2019 + + + + + + + + + + + + + + + + *** + diff --git a/keepassxc-browser/manifest.json b/keepassxc-browser/manifest.json index 0225a14..49ecbf5 100755 --- a/keepassxc-browser/manifest.json +++ b/keepassxc-browser/manifest.json @@ -58,6 +58,7 @@ "content/keepassxc-browser.js", "content/pwgen.js", "content/sites.js", + "content/totp-field.js", "content/username-field.js" ], "css": [ @@ -67,6 +68,7 @@ "css/define.css", "css/notification.css", "css/pwgen.css", + "css/totp.css", "css/username.css" ], "run_at": "document_idle", @@ -106,7 +108,8 @@ "web_accessible_resources": [ "icons/keepassxc.svg", "icons/key.svg", - "icons/locked.svg" + "icons/locked.svg", + "icons/otp.svg" ], "permissions": [ "activeTab", diff --git a/keepassxc-browser/options/options.html b/keepassxc-browser/options/options.html index 0f51046..7f54ee9 100644 --- a/keepassxc-browser/options/options.html +++ b/keepassxc-browser/options/options.html @@ -70,15 +70,15 @@


-

- - - - -
-

+
+ + + + +
+

+

+

+

+ + + +