Merge pull request #625 from keepassxreboot/feature/totp_field_fill

Add TOTP icon filling and improve icon handling
This commit is contained in:
Sami Vänttinen 2019-11-24 11:08:23 +02:00 committed by GitHub
commit 3688b080d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 473 additions and 45 deletions

View file

@ -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."

View file

@ -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,

View file

@ -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;

View file

@ -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

View file

@ -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;

View file

@ -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);
};

View file

@ -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 = {};

View file

@ -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,

View file

@ -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;
}

View file

@ -0,0 +1,216 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="100"
height="100"
viewBox="0 0 99.999997 100"
id="svg2"
version="1.1"
sodipodi:docname="otp.svg"
inkscape:version="0.92.2 5c3e80d, 2017-08-06">
<title
id="title836">OTP icon</title>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1402"
inkscape:window-height="1053"
id="namedview29"
showgrid="false"
inkscape:zoom="4.72"
inkscape:cx="19.20533"
inkscape:cy="22.698472"
inkscape:window-x="867"
inkscape:window-y="158"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" />
<defs
id="defs4">
<linearGradient
id="linearGradient4257">
<stop
style="stop-color:#808080;stop-opacity:1"
offset="0"
id="stop4259" />
<stop
style="stop-color:#4d4d4d;stop-opacity:1"
offset="1"
id="stop4261" />
</linearGradient>
<linearGradient
id="linearGradient4316-3-6">
<stop
style="stop-color:#226e23;stop-opacity:1"
offset="0"
id="stop4173" />
<stop
style="stop-color:#63ab3a;stop-opacity:1"
offset="1"
id="stop4175" />
</linearGradient>
<linearGradient
id="linearGradient4316-3">
<stop
id="stop4167"
offset="0"
style="stop-color:#226e23;stop-opacity:1" />
<stop
id="stop4169"
offset="1"
style="stop-color:#63ab3a;stop-opacity:1" />
</linearGradient>
<linearGradient
id="linearGradient4316">
<stop
style="stop-color:#226e23;stop-opacity:1"
offset="0"
id="stop4318" />
<stop
style="stop-color:#63ab3a;stop-opacity:1"
offset="1"
id="stop4320" />
</linearGradient>
<linearGradient
id="linearGradient4153"
osb:paint="solid">
<stop
style="stop-color:#b3b3b3;stop-opacity:1;"
offset="0"
id="stop4155" />
</linearGradient>
<linearGradient
xlink:href="#linearGradient4316"
id="linearGradient4324"
x1="50.757614"
y1="964.83679"
x2="50.757614"
y2="1042.2632"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0561225,0,0,1.0561225,-2.8061215,-1008.6172)" />
<radialGradient
gradientUnits="userSpaceOnUse"
r="43.571938"
fy="41.189114"
fx="-82.91127"
cy="41.189114"
cx="-82.91127"
id="radialGradient5106"
xlink:href="#linearGradient4316" />
<linearGradient
xlink:href="#linearGradient4316"
id="linearGradient4324-3"
x1="50.757614"
y1="964.83679"
x2="50.757614"
y2="1042.2632"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0561225,0,0,1.0561225,118.96071,-1109.1994)" />
<linearGradient
xlink:href="#linearGradient4316"
id="linearGradient4324-3-6"
x1="50.757614"
y1="964.83679"
x2="50.757614"
y2="1042.2632"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0561225,0,0,1.0561225,-2.8061235,-1008.6171)" />
<linearGradient
gradientUnits="userSpaceOnUse"
y2="86.356995"
x2="53.238865"
y1="12.753036"
x1="53.238865"
id="linearGradient5199"
xlink:href="#linearGradient4316-3-6" />
<linearGradient
xlink:href="#linearGradient4257"
id="linearGradient4263"
x1="50.09866"
y1="86.831215"
x2="49.526104"
y2="8.6772995"
gradientUnits="userSpaceOnUse" />
</defs>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>OTP icon</dc:title>
<cc:license
rdf:resource="http://scripts.sil.org/OFL" />
<dc:creator>
<cc:Agent>
<dc:title>KeePassXC Team</dc:title>
</cc:Agent>
</dc:creator>
<dc:rights>
<cc:Agent>
<dc:title>KeePassXC Team</dc:title>
</cc:Agent>
</dc:rights>
<dc:subject>
<rdf:Bag>
<rdf:li>otp</rdf:li>
<rdf:li>totp</rdf:li>
</rdf:Bag>
</dc:subject>
<dc:date>2019</dc:date>
</cc:Work>
<cc:License
rdf:about="http://scripts.sil.org/OFL">
<cc:permits
rdf:resource="http://scripts.sil.org/pub/OFL/Reproduction" />
<cc:permits
rdf:resource="http://scripts.sil.org/pub/OFL/Distribution" />
<cc:permits
rdf:resource="http://scripts.sil.org/pub/OFL/Embedding" />
<cc:permits
rdf:resource="http://scripts.sil.org/pub/OFL/DerivativeWorks" />
<cc:requires
rdf:resource="http://scripts.sil.org/pub/OFL/Notice" />
<cc:requires
rdf:resource="http://scripts.sil.org/pub/OFL/Attribution" />
<cc:requires
rdf:resource="http://scripts.sil.org/pub/OFL/ShareAlike" />
<cc:requires
rdf:resource="http://scripts.sil.org/pub/OFL/DerivativeRenaming" />
<cc:requires
rdf:resource="http://scripts.sil.org/pub/OFL/BundlingWhenSelling" />
</cc:License>
</rdf:RDF>
</metadata>
<path
style="fill:#6cac4d;fill-opacity:1;stroke:#467720;stroke-width:3.28421712;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
d="m 17.573148,24.970678 h 64.853704 c 4.426642,0 7.990329,4.214625 7.990329,9.449831 v 27.398502 c 0,5.235207 -3.563687,9.449832 -7.990329,9.449832 H 78.373496 74.320139 L 74.28734,83.531602 64.520559,71.268843 H 62.160069 58.106713 54.053356 50 45.946644 41.893287 37.839931 33.786574 29.733218 25.679861 21.626504 17.573148 c -4.426642,0 -7.9903288,-4.214625 -7.9903288,-9.449832 V 34.420509 c 0,-5.235206 3.5636868,-9.449831 7.9903288,-9.449831 z"
id="rect872"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ssssscccccccccccccccssss" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0.60000002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="20"
y="69.295219"
id="text868"><tspan
sodipodi:role="line"
id="tspan866"
x="26"
y="69.295219">***</tspan></text>
</svg>

After

Width:  |  Height:  |  Size: 7.2 KiB

View file

@ -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",

View file

@ -70,15 +70,15 @@
</p>
<hr />
<p>
<div class="checkbox">
<label class="checkbox">
<input type="checkbox" name="showLoginFormIcon" value="true"/><span data-i18n="optionsCheckboxUsernameIcons"></span>
</label>
<span class="help-block">
<span data-i18n="optionsShowLoginFormIconHelpText"></span>
</span>
</div>
</p>
<div class="checkbox">
<label class="checkbox">
<input type="checkbox" name="showLoginFormIcon" value="true"/><span data-i18n="optionsCheckboxUsernameIcons"></span>
</label>
<span class="help-block">
<span data-i18n="optionsShowLoginFormIconHelpText"></span>
</span>
</div>
</p>
<p>
<div class="checkbox">
<label class="checkbox">
@ -87,7 +87,18 @@
<span class="help-block">
<span data-i18n="optionsUsePasswordGeneratorHelpText"></span>
<br />
<span data-i18n="optionsUsePasswordGeneratorHelpTextSecond"></span></span>
<span data-i18n="optionsUsePasswordGeneratorHelpTextSecond"></span>
</span>
</div>
</p>
<p>
<div class="checkbox">
<label class="checkbox">
<input type="checkbox" name="showOTPIcon" value="true"/><span data-i18n="optionsCheckboxOTPIcons"></span>
</label>
<span class="help-block">
<span data-i18n="optionsShowOTPIconHelpText"></span>
</span>
</div>
</p>
<hr />