From 0456ddc17a8b7c23650bb2394adbb994885e6229 Mon Sep 17 00:00:00 2001 From: varjolintu Date: Fri, 15 Jan 2021 15:42:57 +0200 Subject: [PATCH 1/5] Fill multiple single TOTP fields --- .../content/keepassxc-browser.js | 63 +++++++++++++++++-- .../content/totp-autocomplete.js | 3 +- keepassxc-browser/content/totp-field.js | 1 + 3 files changed, 60 insertions(+), 7 deletions(-) diff --git a/keepassxc-browser/content/keepassxc-browser.js b/keepassxc-browser/content/keepassxc-browser.js index f40c9b6..69c12dc 100755 --- a/keepassxc-browser/content/keepassxc-browser.js +++ b/keepassxc-browser/content/keepassxc-browser.js @@ -317,6 +317,7 @@ kpxcForm.saveForm = function(form, combination) { username: combination.username, password: combination.password, totp: combination.totp, + totpInputs: Array.from(form.elements).filter(e => e.nodeName === 'INPUT' && kpxcTOTPIcons.isValid(e)), passwordInputs: Array.from(form.elements).filter(e => e.nodeName === 'INPUT' && e.type === 'password') }); }; @@ -376,6 +377,29 @@ kpxcFields.getAllCombinations = async function(inputs) { combinations.push(combination); } + // Check for multiple segmented TOTP fields + if (combinations.length === 0) { + kpxcFields.getSegmentedTOTPFields(inputs, combinations); + } + + return combinations; +}; + +// Adds segmented TOTP fields to the combination if found +kpxcFields.getSegmentedTOTPFields = function(inputs, combinations) { + const form = inputs.length > 0 ? inputs[0].form : undefined; + if (acceptedOTPFields.some(f => form.className.includes(f) || form.id.includes(f) || form.name.includes(f))) { + const totpInputs = Array.from(form.elements).filter(e => e.nodeName === 'INPUT' && e.type !== 'password'); + if (totpInputs.length === 6) { + const combination = { + form: form, + totpInputs: totpInputs + }; + + combinations.push(combination); + } + } + return combinations; }; @@ -420,8 +444,8 @@ kpxcFields.getCombination = async function(field, givenType) { for (const combination of kpxc.combinations) { if (!givenType && Object.values(combination).find(c => c === field)) { return combination; - } else if (givenType) { - if (combination[givenType] === field) { + } else if (givenType && combination[givenType]) { + if (combination[givenType] === field || combination[givenType].includes(field)) { return combination; } } @@ -833,12 +857,12 @@ kpxc.fillFromTOTP = async function(target) { const el = target || document.activeElement; const credentialList = await kpxc.updateTOTPList(); - if (credentialList.length === 0) { + if (credentialList && credentialList.length === 0) { kpxcUI.createNotification('warning', tr('credentialsNoTOTPFound')); return; } - if (credentialList.length === 1) { + if (credentialList && credentialList.length === 1) { kpxc.fillTOTPFromUuid(el, credentialList[0].uuid); return; } @@ -865,18 +889,45 @@ kpxc.fillTOTPFromUuid = async function(el, uuid) { return; } - kpxc.setValue(el, totp); + kpxc.setTOTPValue(el, totp); } else if (user.stringFields && user.stringFields.length > 0) { const stringFields = user.stringFields; for (const s of stringFields) { const val = s['KPH: {TOTP}']; if (val) { - kpxc.setValue(el, val); + kpxc.setTOTPValue(el, val); } } } }; +// Set normal or segmented TOTP value +kpxc.setTOTPValue = function(elem, val) { + if (kpxc.combinations.length === 0) { + return; + } + + for (const comb of kpxc.combinations) { + if (comb.totpInputs && comb.totpInputs.length === 6) { + kpxc.fillSegmentedTotp(elem, val, comb.totpInputs); + return; + } + } + + kpxc.setValue(elem, val); +}; + +// Fill TOTP in parts +kpxc.fillSegmentedTotp = function(elem, val, totpInputs) { + if (!totpInputs.includes(elem)) { + return; + } + + for (let i = 0; i < 6; ++i) { + kpxc.setValue(totpInputs[i], val[i]); + } +}; + // Fill requested from username icon kpxc.fillFromUsernameIcon = async function(combination) { await kpxc.receiveCredentialsIfNecessary(); diff --git a/keepassxc-browser/content/totp-autocomplete.js b/keepassxc-browser/content/totp-autocomplete.js index 463cb52..0ed6de7 100644 --- a/keepassxc-browser/content/totp-autocomplete.js +++ b/keepassxc-browser/content/totp-autocomplete.js @@ -27,7 +27,8 @@ TOTPAutocomplete.prototype.itemEnter = async function(index, elements) { }; TOTPAutocomplete.prototype.fillTotp = async function(index, uuid) { - const combination = await kpxcFields.getCombination(this.input); + const combination = await kpxcFields.getCombination(this.input, 'totp') + || await kpxcFields.getCombination(this.input, 'totpInputs'); combination.loginId = index; kpxc.fillTOTPFromUuid(this.input, uuid); }; diff --git a/keepassxc-browser/content/totp-field.js b/keepassxc-browser/content/totp-field.js index b2968c6..0059d0c 100644 --- a/keepassxc-browser/content/totp-field.js +++ b/keepassxc-browser/content/totp-field.js @@ -13,6 +13,7 @@ const acceptedOTPFields = [ 'otp', 'token', 'twofa', + 'two-factor', 'twofactor' ]; From ab485379a6b243a1059b992163b06f6d2919f6f1 Mon Sep 17 00:00:00 2001 From: varjolintu Date: Sun, 7 Mar 2021 11:54:57 +0200 Subject: [PATCH 2/5] Add icon to the right side of the segmented fields --- keepassxc-browser/content/keepassxc-browser.js | 10 +++++++++- keepassxc-browser/content/totp-field.js | 18 +++++++++--------- keepassxc-browser/content/ui.js | 16 +++++++++++----- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/keepassxc-browser/content/keepassxc-browser.js b/keepassxc-browser/content/keepassxc-browser.js index 69c12dc..a6808c2 100755 --- a/keepassxc-browser/content/keepassxc-browser.js +++ b/keepassxc-browser/content/keepassxc-browser.js @@ -397,6 +397,14 @@ kpxcFields.getSegmentedTOTPFields = function(inputs, combinations) { }; combinations.push(combination); + + // Create an icon to the right side of the segmented fields + kpxcTOTPIcons.newIcon(totpInputs[totpInputs.length - 1], kpxc.databaseState, true); + kpxcIcons.icons.push({ + field: totpInputs[totpInputs.length - 1], + iconType: kpxcIcons.iconTypes.TOTP, + segmented: true + }); } } @@ -617,7 +625,7 @@ kpxcFields.useCustomLoginFields = async function() { // Handle custom TOTP field if (totp) { totp.setAttribute('kpxc-defined', 'totp'); - kpxcTOTPIcons.newIcon(totp, kpxc.databaseState, true); + kpxcTOTPIcons.newIcon(totp, kpxc.databaseState); } const combinations = []; diff --git a/keepassxc-browser/content/totp-field.js b/keepassxc-browser/content/totp-field.js index 0059d0c..e4af242 100644 --- a/keepassxc-browser/content/totp-field.js +++ b/keepassxc-browser/content/totp-field.js @@ -20,8 +20,8 @@ const acceptedOTPFields = [ var kpxcTOTPIcons = {}; kpxcTOTPIcons.icons = []; -kpxcTOTPIcons.newIcon = function(field, databaseState = DatabaseState.DISCONNECTED, forced = false) { - kpxcTOTPIcons.icons.push(new TOTPFieldIcon(field, databaseState, forced)); +kpxcTOTPIcons.newIcon = function(field, databaseState = DatabaseState.DISCONNECTED, segmented = false) { + kpxcTOTPIcons.icons.push(new TOTPFieldIcon(field, databaseState, segmented)); }; kpxcTOTPIcons.switchIcon = function(state) { @@ -76,25 +76,25 @@ kpxcTOTPIcons.isValid = function(field, forced) { }; class TOTPFieldIcon extends Icon { - constructor(field, databaseState = DatabaseState.DISCONNECTED, forced = false) { - super(field, databaseState); + constructor(field, databaseState = DatabaseState.DISCONNECTED, segmented = false) { + super(field, databaseState, segmented); - this.initField(field, forced); + this.initField(field, segmented); kpxcUI.monitorIconPosition(this); } } -TOTPFieldIcon.prototype.initField = function(field, forced) { +TOTPFieldIcon.prototype.initField = function(field, segmented) { // Observer the visibility if (this.observer) { this.observer.observe(field); } - this.createIcon(field); + this.createIcon(field, segmented); this.inputField = field; }; -TOTPFieldIcon.prototype.createIcon = function(field) { +TOTPFieldIcon.prototype.createIcon = function(field, segmented = false) { const className = (isFirefox() ? 'moz' : 'default'); // Size the icon dynamically, but not greater than 24 or smaller than 14 @@ -126,7 +126,7 @@ TOTPFieldIcon.prototype.createIcon = function(field) { kpxc.fillFromTOTP(field); }); - kpxcUI.setIconPosition(icon, field, this.rtl); + kpxcUI.setIconPosition(icon, field, this.rtl, segmented); this.icon = icon; const styleSheet = document.createElement('link'); diff --git a/keepassxc-browser/content/ui.js b/keepassxc-browser/content/ui.js index b6a6226..b8018d2 100644 --- a/keepassxc-browser/content/ui.js +++ b/keepassxc-browser/content/ui.js @@ -23,11 +23,12 @@ const Pixels = function(value) { // Basic icon class class Icon { - constructor(field, databaseState = DatabaseState.DISCONNECTED) { + constructor(field, databaseState = DatabaseState.DISCONNECTED, segmented = false) { this.databaseState = databaseState; this.icon = null; this.inputField = null; this.rtl = kpxcUI.isRTL(field); + this.segmented = segmented; try { this.observer = new IntersectionObserver((entries) => { @@ -103,7 +104,7 @@ kpxcUI.monitorIconPosition = function(iconClass) { kpxcUI.updateIconPosition = function(iconClass) { if (iconClass.inputField && iconClass.icon) { - kpxcUI.setIconPosition(iconClass.icon, iconClass.inputField, iconClass.rtl); + kpxcUI.setIconPosition(iconClass.icon, iconClass.inputField, iconClass.rtl, iconClass.segmented); } }; @@ -112,13 +113,18 @@ kpxcUI.calculateIconOffset = function(field, size) { return (offset < 0) ? 0 : offset; }; -kpxcUI.setIconPosition = function(icon, field, rtl = false) { +kpxcUI.setIconPosition = function(icon, field, rtl = false, segmented = false) { const rect = field.getBoundingClientRect(); const size = Number(icon.getAttribute('size')); const offset = kpxcUI.calculateIconOffset(field, size); - const left = kpxcUI.bodyStyle.position.toLowerCase() === 'relative' ? rect.left - kpxcUI.bodyRect.left : rect.left; + let left = kpxcUI.bodyStyle.position.toLowerCase() === 'relative' ? rect.left - kpxcUI.bodyRect.left : rect.left; const top = kpxcUI.bodyStyle.position.toLowerCase() === 'relative' ? rect.top - kpxcUI.bodyRect.top : rect.top; + // Add more space for the icon to show it at the right side of the field if TOTP fields are segmented + if (segmented) { + left += size + 10; + } + icon.style.top = Pixels(top + document.scrollingElement.scrollTop + offset + 1); icon.style.left = rtl ? Pixels((left + document.scrollingElement.scrollLeft) + offset) @@ -181,7 +187,7 @@ kpxcUI.updateFromIntersectionObserver = function(iconClass, entries) { // Wait for possible DOM animations setTimeout(() => { - kpxcUI.setIconPosition(iconClass.icon, entry.target, iconClass.rtl); + kpxcUI.setIconPosition(iconClass.icon, entry.target, iconClass.rtl, iconClass.segmented); }, 400); } } From 7fcfc82a048bc167ad560c9325b5112cae85c9ca Mon Sep 17 00:00:00 2001 From: varjolintu Date: Sat, 13 Mar 2021 10:11:02 +0200 Subject: [PATCH 3/5] Add form length check if no TOTP ids are found --- keepassxc-browser/content/keepassxc-browser.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/keepassxc-browser/content/keepassxc-browser.js b/keepassxc-browser/content/keepassxc-browser.js index a6808c2..19249fb 100755 --- a/keepassxc-browser/content/keepassxc-browser.js +++ b/keepassxc-browser/content/keepassxc-browser.js @@ -388,7 +388,8 @@ kpxcFields.getAllCombinations = async function(inputs) { // Adds segmented TOTP fields to the combination if found kpxcFields.getSegmentedTOTPFields = function(inputs, combinations) { const form = inputs.length > 0 ? inputs[0].form : undefined; - if (acceptedOTPFields.some(f => form.className.includes(f) || form.id.includes(f) || form.name.includes(f))) { + if (acceptedOTPFields.some(f => form.className.includes(f) || form.id.includes(f) || form.name.includes(f)) + || form.length === 6) { const totpInputs = Array.from(form.elements).filter(e => e.nodeName === 'INPUT' && e.type !== 'password'); if (totpInputs.length === 6) { const combination = { From a27154ad47a5b779bf723150bc3ea06ca41d0f49 Mon Sep 17 00:00:00 2001 From: varjolintu Date: Sat, 13 Mar 2021 14:16:04 +0200 Subject: [PATCH 4/5] Add check for form --- keepassxc-browser/content/keepassxc-browser.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/keepassxc-browser/content/keepassxc-browser.js b/keepassxc-browser/content/keepassxc-browser.js index 19249fb..240aa67 100755 --- a/keepassxc-browser/content/keepassxc-browser.js +++ b/keepassxc-browser/content/keepassxc-browser.js @@ -388,6 +388,10 @@ kpxcFields.getAllCombinations = async function(inputs) { // Adds segmented TOTP fields to the combination if found kpxcFields.getSegmentedTOTPFields = function(inputs, combinations) { const form = inputs.length > 0 ? inputs[0].form : undefined; + if (!form) { + return combinations; + } + if (acceptedOTPFields.some(f => form.className.includes(f) || form.id.includes(f) || form.name.includes(f)) || form.length === 6) { const totpInputs = Array.from(form.elements).filter(e => e.nodeName === 'INPUT' && e.type !== 'password'); From 19ec4a60b89ab41da68a83a3f6a4d043c693297c Mon Sep 17 00:00:00 2001 From: varjolintu Date: Sun, 14 Mar 2021 11:20:21 +0200 Subject: [PATCH 5/5] Add support for segmented TOTP fields without a form --- .../content/keepassxc-browser.js | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/keepassxc-browser/content/keepassxc-browser.js b/keepassxc-browser/content/keepassxc-browser.js index 240aa67..960c0d3 100755 --- a/keepassxc-browser/content/keepassxc-browser.js +++ b/keepassxc-browser/content/keepassxc-browser.js @@ -387,14 +387,8 @@ kpxcFields.getAllCombinations = async function(inputs) { // Adds segmented TOTP fields to the combination if found kpxcFields.getSegmentedTOTPFields = function(inputs, combinations) { - const form = inputs.length > 0 ? inputs[0].form : undefined; - if (!form) { - return combinations; - } - - if (acceptedOTPFields.some(f => form.className.includes(f) || form.id.includes(f) || form.name.includes(f)) - || form.length === 6) { - const totpInputs = Array.from(form.elements).filter(e => e.nodeName === 'INPUT' && e.type !== 'password'); + const addTotpFieldsToCombination = function(inputFields) { + const totpInputs = Array.from(inputFields).filter(e => e.nodeName === 'INPUT' && e.type !== 'password'); if (totpInputs.length === 6) { const combination = { form: form, @@ -411,6 +405,16 @@ kpxcFields.getSegmentedTOTPFields = function(inputs, combinations) { segmented: true }); } + }; + + const form = inputs.length > 0 ? inputs[0].form : undefined; + if (form && (acceptedOTPFields.some(f => form.className.includes(f) || form.id.includes(f) || form.name.includes(f)) + || form.length === 6)) { + // Use the form's elements + addTotpFieldsToCombination(form.elements); + } else if (inputs.length === 6 && inputs.every(i => i.inputMode === 'numeric' && i.pattern.includes('0-9'))) { + // No form is found, but input fields are possibly segmented TOTP fields + addTotpFieldsToCombination(inputs); } return combinations;