Add context screen & integrated TOTP support (#229)

This commit is contained in:
Erayd
2020-09-10 07:13:15 +12:00
committed by GitHub
parent 0f8608ec7e
commit 29a7043264
18 changed files with 616 additions and 98 deletions

View File

@@ -25,6 +25,7 @@ In order to use Browserpass you must also install a [companion native messaging
- [Password store locations](#password-store-locations)
- [Options](#options)
- - [A note about autosubmit](#a-note-about-autosubmit)
- - [A note about OTP](#a-note-about-otp)
- [Usage data](#usage-data)
- [Security](#security)
- [Privacy](#privacy)
@@ -33,7 +34,6 @@ In order to use Browserpass you must also install a [companion native messaging
- [Error: Unable to fetch and parse login fields](#error-unable-to-fetch-and-parse-login-fields)
- [How to use the same username and password pair on multiple domains](#how-to-use-the-same-username-and-password-pair-on-multiple-domains)
- [Why Browserpass on Firefox does not work on Mozilla domains?](#why-browserpass-on-firefox-does-not-work-on-mozilla-domains)
- [Why is OTP not supported?](#why-is-otp-not-supported)
- [Building the extension](#building-the-extension)
- [Build locally](#build-locally)
- [Load an unpacked extension](#load-an-unpacked-extension)
@@ -217,15 +217,16 @@ Using the `Custom store locations` setting in the browser extension options, you
The list of available options:
| Name | Description |
| --------------------------------------------------------------- | ------------------------------------------------------------ |
| Automatically submit forms after filling (aka `autoSubmit`) | Make Browserpass automatically submit the login form for you |
| Default username (aka `username`) | Username to use when it's not defined in the password file |
| Custom gpg binary (aka `gpgPath`) | Path to a custom `gpg` binary to use |
| Custom store locations | List of password stores to use |
| Custom store locations - badge background color (aka `bgColor`) | Badge background color for a given password store in popup |
| Custom store locations - badge text color (aka `color`) | Badge text color for a given password store in popup |
| Ignore items (aka `ignore`) | Ignore all matching logins |
| Name | Description |
| --------------------------------------------------------------- | ------------------------------------------------------------- |
| Automatically submit forms after filling (aka `autoSubmit`) | Make Browserpass automatically submit the login form for you |
| Enable support for OTP tokens (aka `enableOTP`) | Generate TOTP codes if a TOTP seed is found in the pass entry |
| Default username (aka `username`) | Username to use when it's not defined in the password file |
| Custom gpg binary (aka `gpgPath`) | Path to a custom `gpg` binary to use |
| Custom store locations | List of password stores to use |
| Custom store locations - badge background color (aka `bgColor`) | Badge background color for a given password store in popup |
| Custom store locations - badge text color (aka `color`) | Badge text color for a given password store in popup |
| Ignore items (aka `ignore`) | Ignore all matching logins |
Browserpass allows configuring certain settings in different places places using the following priority, highest first:
@@ -233,6 +234,7 @@ Browserpass allows configuring certain settings in different places places using
- `autoSubmit`
1. Options defined in `.browserpass.json` file located in the root of a password store:
- `autoSubmit`
- `enableOTP`
- `gpgPath`
- `username`
- `bgColor`
@@ -240,6 +242,7 @@ Browserpass allows configuring certain settings in different places places using
- `ignore`
1. Options defined in browser extension options:
- Automatically submit forms after filling (aka `autoSubmit`)
- Enable support for OTP tokens (aka `enableOTP`)
- Default username (aka `username`)
- Custom gpg binary (aka `gpgPath`)
- Custom store locations
@@ -252,6 +255,16 @@ While we provide autosubmit as an option for users, we do not recommend it. This
As the demand for autosubmit is extremely high, we have decided to provide it anyway - however it is disabled by default, and we recommend that users do not enable it.
### A note about OTP
Tools like `pass-otp` make it possible to use `pass` for generating OTP codes, however keeping both passwords and OTP URI in the same location diminishes the major benefit that OTP is supposed to provide: two factor authentication. The purpose of multi-factor authentication is to protect your account even when attackers gain access to your password store, but if your OTP seed is stored in the same place, all auth factors will be compromised at once. In particular, Browserpass has access to the entire contents of your password entries, so if it is ever compromised, all your accounts will be at risk, even though you signed up for 2FA.
Browserpass is opinionated, it does not promote `pass-otp` and by default does not generate OTP codes from OTP seeds in password entries, even though there are other password managers that provide such functionality out of the box.
There are valid scenarios for using `pass-otp` (e.g. it gives protection against intercepting your password during transmission), but users are strongly advised to very carefully consider whether `pass-otp` is really an appropriate solution - and if so, come up with their own ways of accessing OTP codes that conforms to their security requirements. For the majority of people `pass-otp` is not recommended; using any phone app like Authy will be a much better and more secure alternative, because this way attackers would have to not only break into your password store, but they would _also_ have to break into your phone.
If you still want the OTP support regardless, you may enable it in the Browserpass settings.
## Usage data
Browserpass keeps metadata of recently used credentials in local storage and Indexed DB of the background page. This is first and foremost internal data to make Browserpass function properly, used for example to implement the [Password matching and sorting](#password-matching-and-sorting) algorithm, but nevertheless you might find it useful to explore using your browser's devtools. For example, if you are considering to rotate all passwords that you used in the past month (e.g. if you just found out that you had a malicious app installed for several weeks), you can retrieve such list from Indexed DB quite easily (open an issue if you need help).
@@ -360,16 +373,6 @@ The full list of blocked domains at the time of writing is:
- sync.services.mozilla.com
- testpilot.firefox.com
### Why is OTP not supported?
Tools like `pass-otp` make it possible to use `pass` for generating OTP codes, however keeping both passwords and OTP URI in the same location diminishes the major benefit that OTP is supposed to provide: two factor authentication. The purpose of multi-factor authentication is to protect your account even when attackers gain access to your password store, but if your OTP seed is stored in the same place, all auth factors will be compromised at once. In particular, Browserpass has access to the entire contents of your password entries, so if it is ever compromised, all your accounts will be at risk, even though you signed up for 2FA.
Browserpass is opinionated, it does not promote `pass-otp` and intentionally does not support generating OTP codes from OTP URIs in password entiries, even though there are other password managers that provide such functionality.
There are valid scenarios for using `pass-otp` (e.g. it gives protection against intercepting your password during transmission), but users are strongly advised to very carefully consider whether `pass-otp` is really an appropriate solution - and if so, come up with their own ways of accessing OTP codes that conforms to their security requirements (for example by using dmenu/rofi scripts). For the majority of people `pass-otp` is not recommended; using any phone app like Authy will be a much better and more secure alternative, because this way attackers would have to not only break into your password store, but they would _also_ have to break into your phone.
If you still want the OTP support, it is provided via a separate extension [browserpass-otp](https://github.com/browserpass/browserpass-otp). That extension integrates with Browserpass to ensure a streamlined workflow, for example if the OTP extension is installed, it will be automatically triggered when Browserpass fills an entry and an OTP token is present.
## Building the extension
### Build locally

View File

@@ -10,14 +10,6 @@ const helpers = require("./helpers");
// native application id
var appID = "com.github.browserpass.native";
// OTP extension id
var otpID = [
"afjjoildnccgmjbblnklbohcbjehjaph", // webstore releases
"jbnpmhhgnchcoljeobafpinmchnpdpin", // github releases
"fcmmcnalhjjejhpnlfnddimcdlmpkbdf", // local unpacked
"browserpass-otp@maximbaz.com", // firefox
];
// default settings
var defaultSettings = {
autoSubmit: false,
@@ -26,6 +18,7 @@ var defaultSettings = {
foreignFills: {},
username: null,
theme: "dark",
enableOTP: false,
};
var authListeners = {};
@@ -562,7 +555,6 @@ async function getFullSettings() {
try {
settings.tab = (await chrome.tabs.query({ active: true, currentWindow: true }))[0];
let originInfo = new BrowserpassURL(settings.tab.url);
settings.host = originInfo.host; // TODO remove this after OTP extension is migrated
settings.origin = originInfo.origin;
} catch (e) {}
@@ -750,6 +742,28 @@ async function handleMessage(settings, message, sendResponse) {
});
}
break;
case "copyOTP":
if (settings.enableOTP) {
try {
if (!message.login.fields.otp) {
throw new Exception("No OTP seed available");
}
copyToClipboard(helpers.makeTOTP(message.login.fields.otp.params));
sendResponse({ status: "ok" });
} catch (e) {
sendResponse({
status: "error",
message: "Unable to copy OTP token",
});
}
} else {
sendResponse({ status: "error", message: "OTP support is disabled" });
}
break;
case "getDetails":
sendResponse({ status: "ok", login: message.login });
break;
case "launch":
case "launchInNewTab":
@@ -831,9 +845,13 @@ async function handleMessage(settings, message, sendResponse) {
break;
}
// trigger browserpass-otp
if (typeof message.login !== "undefined" && message.login.fields.hasOwnProperty("otp")) {
triggerOTPExtension(settings, message.action, message.login.fields.otp);
// copy OTP token after fill
if (
settings.enableOTP &&
typeof message.login !== "undefined" &&
message.login.fields.hasOwnProperty("otp")
) {
copyToClipboard(helpers.makeTOTP(message.login.fields.otp.params));
}
}
@@ -885,7 +903,7 @@ async function parseFields(settings, login) {
secret: ["secret", "password", "pass"],
login: ["login", "username", "user"],
openid: ["openid"],
otp: ["otp", "totp", "hotp"],
otp: ["otp", "totp"],
url: ["url", "uri", "website", "site", "link", "launch"],
};
login.settings = {
@@ -893,10 +911,9 @@ async function parseFields(settings, login) {
};
var lines = login.raw.split(/[\r\n]+/).filter((line) => line.trim().length > 0);
lines.forEach(function (line) {
// check for uri-encoded otp
if (line.match(/^otpauth:\/\/.+/)) {
login.fields.otp = { key: null, data: line };
return;
// check for uri-encoded otp without line prefix
if (line.match(/^otpauth:\/\/.+/i)) {
line = `otp: ${line}`;
}
// split key / value & ignore non-k/v lines
@@ -918,11 +935,7 @@ async function parseFields(settings, login) {
Array.isArray(login.fields[key]) &&
login.fields[key].includes(parts[0].toLowerCase())
) {
if (key === "otp") {
login.fields[key] = { key: parts[0].toLowerCase(), data: parts[1] };
} else {
login.fields[key] = parts[1];
}
login.fields[key] = parts[1];
break;
}
}
@@ -962,6 +975,41 @@ async function parseFields(settings, login) {
delete login.settings[key];
}
}
// preprocess otp
if (settings.enableOTP && login.fields.hasOwnProperty("otp")) {
if (login.fields.otp.match(/^otpauth:\/\/.+/i)) {
// attempt to parse otp data as URI
try {
let url = new URL(login.fields.otp.toLowerCase());
let otpParts = url.pathname.split("/").filter((s) => s.trim());
login.fields.otp = {
raw: login.fields.otp,
params: {
type: otpParts[0] === "otp" ? "totp" : otpParts[0],
secret: url.searchParams.get("secret").toUpperCase(),
algorithm: url.searchParams.get("algorithm") || "sha1",
digits: parseInt(url.searchParams.get("digits") || "6"),
period: parseInt(url.searchParams.get("period") || "30"),
},
};
} catch (e) {
throw new Exception(`Unable to parse URI: ${otp.data}`, e);
}
} else {
// use default params for secret-only otp data
login.fields.otp = {
raw: login.fields.otp,
params: {
type: "totp",
secret: login.fields.otp.toUpperCase(),
algorithm: "sha1",
digits: 6,
period: 30,
},
};
}
}
}
/**
@@ -1046,41 +1094,6 @@ async function saveSettings(settings) {
}
}
/**
* Trigger OTP extension (browserpass-otp)
*
* @since 3.0.13
*
* @param object settings Settings object
* @param string action Browserpass action
* @param object otp OTP field data
* @return void
*/
function triggerOTPExtension(settings, action, otp) {
// trigger otp extension
for (let targetID of otpID) {
chrome.runtime
.sendMessage(targetID, {
version: chrome.runtime.getManifest().version,
action: action,
otp: otp,
settings: {
host: settings.host,
origin: settings.origin,
tab: settings.tab,
},
})
// Both response & error are noop functions, because we don't care about
// the response, and if there's an error it just means the otp extension
// is probably not installed. We can't detect that without requesting the
// management permission, so this is an acceptable workaround.
.then(
(noop) => null,
(noop) => null
);
}
}
/**
* Handle browser extension installation and updates
*

View File

@@ -0,0 +1,93 @@
Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

View File

@@ -4,12 +4,15 @@
const FuzzySort = require("fuzzysort");
const sha1 = require("sha1");
const ignore = require("ignore");
const hash = require("hash.js");
const Authenticator = require("otplib").authenticator.Authenticator;
const BrowserpassURL = require("@browserpass/url");
module.exports = {
prepareLogins,
filterSortLogins,
ignoreFiles,
makeTOTP,
};
//----------------------------------- Function definitions ----------------------------------//
@@ -220,6 +223,37 @@ function filterSortLogins(logins, searchQuery, currentDomainOnly) {
return candidates;
}
/**
* Generate TOTP token
*
* @since 3.6.0
*
* @param object params OTP generation params
* @return string Generated OTP code
*/
function makeTOTP(params) {
switch (params.algorithm) {
case "sha1":
case "sha256":
case "sha512":
break;
default:
throw new Error(`Unsupported TOTP algorithm: ${params.algorithm}`);
}
var generator = new Authenticator();
generator.options = {
crypto: {
createHmac: (a, k) => hash.hmac(hash[a], k),
},
algorithm: params.algorithm,
digits: params.digits,
step: params.period,
};
return generator.generate(params.secret);
}
//----------------------------------- Private functions ----------------------------------//
/**

View File

@@ -55,6 +55,9 @@ function view(ctl, params) {
"Automatically submit forms after filling (not recommended)"
)
);
nodes.push(
createCheckbox.call(this, "enableOTP", "Enable support for OTP tokens (not recommended)")
);
nodes.push(createInput.call(this, "username", "Default username", "john.smith"));
nodes.push(createInput.call(this, "gpgPath", "Custom gpg binary", "/path/to/gpg"));

View File

@@ -18,10 +18,12 @@
"@browserpass/url": "^1.1.6",
"chrome-extension-async": "^3.4.1",
"fuzzysort": "^1.1.4",
"hash.js": "^1.1.7",
"idb": "^4.0.5",
"ignore": "^5.1.8",
"mithril": "^1.1.7",
"moment": "^2.27.0",
"otplib": "^11.0.0",
"sha1": "^1.1.1"
},
"devDependencies": {

View File

@@ -15,6 +15,11 @@
@hint-bg-color: #d79921,
@hint-color: #363636,
@match-text-bg-color: transparent,
@match-text-color: #d79921
@match-text-color: #d79921,
@invert-text-color: #414141,
@snack-color: #525252,
@snack-label-color: #afafaf,
@progress-color: #bd861a,
@edit-bg-color: #4a4a4a
);
}

View File

@@ -15,12 +15,19 @@
@hint-bg-color: #1c7ed6,
@hint-color: #e7f5ff,
@match-text-bg-color: #cfecff,
@match-text-color: #1873ea
@match-text-color: #1873ea,
@invert-text-color: #f1f3f5,
@snack-color: #7a7a7a,
@snack-label-color: #7a7a7a,
@progress-color: #c7d5ff,
@edit-bg-color: #ffffff
);
.part.login .name .line1 .recent,
.part.login .action.copy-password,
.part.login .action.copy-user {
.part.login .action.copy-user,
.part.login .action.details,
.part.details .action.copy {
filter: invert(85%);
}
@@ -29,8 +36,22 @@
.part.login .action.copy-password:focus,
.part.login .action.copy-password:hover,
.part.login .action.copy-user:focus,
.part.login .action.copy-user:hover {
.part.login .action.copy-user:hover,
.part.login .action.details:focus,
.part.login .action.details:hover {
// colour such that invert(85%) ~= @hover-bg-color
background-color: #0c0804;
}
.part.details .part.snack {
&.line-otp {
background: transparent;
}
.progress-container {
background-color: #ffffff;
z-index: -1;
margin-top: -4px;
height: 34px;
}
}
}

View File

@@ -12,7 +12,12 @@
@hint-bg-color,
@hint-color,
@match-text-bg-color,
@match-text-color) {
@match-text-color,
@invert-text-color,
@snack-color,
@snack-label-color,
@progress-color,
@edit-bg-color) {
html,
body {
background-color: @bg-color;
@@ -27,6 +32,34 @@
color: @error-text-color;
}
.part.details {
.part {
&.snack {
background-color: @edit-bg-color;
border-color: @snack-color;
.label {
background-color: @snack-label-color;
color: @invert-text-color;
}
.char.num,
.char.punct {
color: @match-text-color;
}
.progress-container {
background: transparent;
.progress {
background-color: @progress-color;
}
}
}
&.raw textarea {
background-color: @edit-bg-color;
border-color: @snack-color;
color: @text-color;
}
}
}
.part.search {
background-color: @input-bg-color;
}
@@ -53,11 +86,11 @@
background-color: @default-bg-color;
}
.part.login > .name:hover,
.part.login > .name:focus,
.part.login > .action:hover,
.part.login > .action:focus,
.part.login:focus > .name {
.part.login:not(.details-header) > .name:hover,
.part.login:not(.details-header) > .name:focus,
.part.login:not(.details-header) > .action:hover,
.part.login:not(.details-header) > .action:focus,
.part.login:not(.details-header):focus > .name {
background-color: @hover-bg-color;
}

View File

@@ -0,0 +1,144 @@
module.exports = DetailsInterface;
const m = require("mithril");
const Moment = require("moment");
const helpers = require("../helpers");
/**
* Login details interface
*
* @since 3.6.0
*
* @param object settings Settings object
* @param array login Target login object
* @return void
*/
function DetailsInterface(settings, login) {
// public methods
this.attach = attach;
this.view = view;
//fields
this.settings = settings;
this.login = login;
// get basename & dirname of entry
this.login.basename = this.login.login.substr(this.login.login.lastIndexOf("/") + 1);
this.login.dirname = this.login.login.substr(0, this.login.login.lastIndexOf("/")) + "/";
}
/**
* Attach the interface on the given element
*
* @since 3.6.0
*
* @param DOMElement element Target element
* @return void
*/
function attach(element) {
m.mount(element, this);
}
/**
* Generates vnodes for render
*
* @since 3.6.0
*
* @param function ctl Controller
* @param object params Runtime params
* @return []Vnode
*/
function view(ctl, params) {
const login = this.login;
const storeBgColor = login.store.bgColor || login.store.settings.bgColor;
const storeColor = login.store.color || login.store.settings.color;
const passChars = login.fields.secret.split("").map((c) => {
if (c.match(/[0-9]/)) {
return m("span.char.num", c);
} else if (c.match(/[^\w\s]/)) {
return m("span.char.punct", c);
}
return m("span.char", c);
});
var nodes = [];
nodes.push(
m("div.part.login.details-header", [
m("div.name", [
m("div.line1", [
m(
"div.store.badge",
{
style: `background-color: ${storeBgColor};
color: ${storeColor}`,
},
login.store.name
),
m("div.path", [m.trust(login.dirname)]),
login.recent.when > 0
? m("div.recent", {
title:
"Used here " +
login.recent.count +
" time" +
(login.recent.count > 1 ? "s" : "") +
", last " +
Moment(new Date(login.recent.when)).fromNow(),
})
: null,
]),
m("div.line2", [m.trust(login.basename)]),
]),
]),
m("div.part.details", [
m("div.part.snack.line-secret", [
m("div.label", "Secret"),
m("div.chars", passChars),
m("div.action.copy", { onclick: () => login.doAction("copyPassword") }),
]),
m("div.part.snack.line-login", [
m("div.label", "Login"),
m("div", login.fields.login),
m("div.action.copy", { onclick: () => login.doAction("copyUsername") }),
]),
(() => {
if (
this.settings.enableOTP &&
login.fields.otp &&
login.fields.otp.params.type === "totp"
) {
// update progress
let progress = this.progress;
let updateProgress = (vnode) => {
let period = login.fields.otp.params.period;
let remaining = period - ((Date.now() / 1000) % period);
vnode.dom.style.transition = "none";
vnode.dom.style.width = `${(remaining / period) * 100}%`;
setTimeout(function () {
vnode.dom.style.transition = `width linear ${remaining}s`;
vnode.dom.style.width = "0%";
}, 100);
setTimeout(function () {
m.redraw();
}, remaining);
};
let progressNode = m("div.progress", {
oncreate: updateProgress,
onupdate: updateProgress,
});
// display otp snack
return m("div.part.snack.line-otp", [
m("div.label", "Token"),
m("div.progress-container", progressNode),
m("div", helpers.makeTOTP(login.fields.otp.params)),
m("div.action.copy", { onclick: () => login.doAction("copyOTP") }),
]);
}
})(),
m("div.part.raw", m("textarea", login.raw.trim())),
])
);
return nodes;
}

14
src/popup/icon-copy.svg Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 210.107 210.107" xml:space="preserve" stroke="#c4c4c4" fill="#c4c4c4">
<g>
<path d="M168.506,0H80.235C67.413,0,56.981,10.432,56.981,23.254v2.854h-15.38
c-12.822,0-23.254,10.432-23.254,23.254v137.492c0,12.822,10.432,23.254,23.254,23.254h88.271
c12.822,0,23.253-10.432,23.253-23.254V184h15.38c12.822,0,23.254-10.432,23.254-23.254V23.254C191.76,10.432,181.328,0,168.506,0z
M138.126,186.854c0,4.551-3.703,8.254-8.253,8.254H41.601c-4.551,0-8.254-3.703-8.254-8.254V49.361
c0-4.551,3.703-8.254,8.254-8.254h88.271c4.551,0,8.253,3.703,8.253,8.254V186.854z M176.76,160.746
c0,4.551-3.703,8.254-8.254,8.254h-15.38V49.361c0-12.822-10.432-23.254-23.253-23.254H71.981v-2.854
c0-4.551,3.703-8.254,8.254-8.254h88.271c4.551,0,8.254,3.703,8.254,8.254V160.746z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 451.846 451.847" stroke="#c4c4c4" fill="#c4c4c4">
<g>
<path d="M345.441,248.292L151.154,442.573c-12.359,12.365-32.397,12.365-44.75,0c-12.354-12.354-12.354-32.391,0-44.744
L278.318,225.92L106.409,54.017c-12.354-12.359-12.354-32.394,0-44.748c12.354-12.359,32.391-12.359,44.75,0l194.287,194.284
c6.177,6.18,9.262,14.271,9.262,22.366C354.708,234.018,351.617,242.115,345.441,248.292z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 504 B

View File

@@ -106,15 +106,20 @@ function view(ctl, params) {
]),
m("div.line2", [m.trust(result.display)]),
]),
m("div.action.copy-user", {
tabindex: 0,
title: "Copy username | <Ctrl+Shift+C>",
action: "copyUsername",
}),
m("div.action.copy-password", {
tabindex: 0,
title: "Copy password | <Ctrl+C>",
action: "copyPassword",
}),
m("div.action.copy-user", {
m("div.action.details", {
tabindex: 0,
title: "Copy username | <Ctrl+Shift+C>",
action: "copyUsername",
title: "Open Details | <Ctrl+O>",
action: "getDetails",
}),
]
);
@@ -188,6 +193,8 @@ function keyHandler(e) {
e.target.querySelector(".action").focus();
} else if (e.target.nextElementSibling) {
e.target.nextElementSibling.focus();
} else {
this.doAction("getDetails");
}
break;
case "ArrowLeft":
@@ -218,6 +225,11 @@ function keyHandler(e) {
this.doAction(e.shiftKey ? "launchInNewTab" : "launch");
}
break;
case "KeyO":
if (e.ctrlKey) {
this.doAction("getDetails");
}
break;
case "Home": {
document.querySelector(".part.search input[type=text]").focus();
document.querySelector(".logins").scrollTo(0, 0);

View File

@@ -3,7 +3,9 @@
require("chrome-extension-async");
const Interface = require("./interface");
const DetailsInterface = require("./detailsInterface");
const helpers = require("../helpers");
const m = require("mithril");
run();
@@ -21,11 +23,8 @@ function handleError(error, type = "error") {
if (type == "error") {
console.log(error);
}
var errorNode = document.createElement("div");
errorNode.setAttribute("class", "part " + type);
errorNode.textContent = error.toString();
document.body.innerHTML = "";
document.body.appendChild(errorNode);
var node = { view: () => m(`div.part.${type}`, error.toString()) };
m.mount(document.body, node);
}
/**
@@ -100,6 +99,12 @@ async function withLogin(action) {
case "copyUsername":
handleError("Copying username to clipboard...", "notice");
break;
case "copyOTP":
handleError("Copying OTP token to clipboard...", "notice");
break;
case "getDetails":
handleError("Loading entry details...", "notice");
break;
default:
handleError("Please wait...", "notice");
break;
@@ -117,7 +122,18 @@ async function withLogin(action) {
if (response.status != "ok") {
throw new Error(response.message);
} else {
window.close();
if (response.login && typeof response.login === "object") {
response.login.doAction = withLogin.bind({
settings: this.settings,
login: response.login,
});
}
if (action === "getDetails") {
var details = new DetailsInterface(this.settings, response.login);
details.attach(document.body);
} else {
window.close();
}
}
} catch (e) {
handleError(e);

View File

@@ -20,6 +20,13 @@
src: local("Open Sans Light"), url("/fonts/OpenSans-Light.ttf") format("truetype");
}
@font-face {
font-family: "Source Code Pro";
font-style: normal;
font-weight: 400;
src: local("Source Code Pro"), url("/fonts/SourceCodePro-Regular.ttf") format("truetype");
}
html,
body {
font-family: "Open Sans";
@@ -66,6 +73,11 @@ body {
padding: 1px 4px;
}
.details .header {
display: flex;
margin-bottom: 4px;
}
.part {
box-sizing: border-box;
display: flex;
@@ -91,6 +103,80 @@ body {
padding: 7px;
}
.part.details {
flex-direction: column;
padding: 5px 10px 10px;
& > .part {
display: flex;
margin-bottom: 11px;
&:last-child {
margin-bottom: 0;
}
&.snack {
border: 1px solid;
border-radius: 2px;
height: 36px;
padding: 4px;
.char {
white-space: pre;
}
& > .label {
border-radius: 2px 0 0 2px;
cursor: default;
display: flex;
flex-grow: 0;
font-weight: bold;
justify-content: flex-end;
margin: -5px 8px -5px -5px;
padding: 4px 8px 4px 4px;
width: 3.25em;
}
& > :not(.label) {
display: flex;
align-items: center;
font-family: Source Code Pro, monospace;
}
& > .copy {
cursor: pointer;
flex-grow: 0;
padding: 0 24px 0 0;
background-image: url("/popup/icon-copy.svg");
background-position: top 4px right 4px;
background-repeat: no-repeat;
background-size: 16px;
margin: 2px;
}
& > .progress-container {
z-index: 2;
position: absolute;
margin: 30px 0 -4px calc(3.25em + 7px);
height: 1px;
width: calc(100% - 6.5em + 12px);
& > .progress {
height: 100%;
margin: 0;
}
}
}
&.raw textarea {
border: 1px solid;
border-radius: 2px;
flex-grow: 1;
font-family: Source Code Pro, monospace;
min-height: 110px;
min-width: 340px;
outline: none;
padding: 10px;
white-space: pre;
}
& > * {
flex-grow: 1;
align-items: center;
}
}
}
.part.search {
padding: 6px 28px 6px 6px;
background-image: url("/popup/icon-search.svg");
@@ -134,10 +220,19 @@ body {
.part.login {
display: flex;
cursor: pointer;
align-items: center;
height: @login-height;
&.details-header {
height: calc(@login-height + 6px);
padding: 0 4px;
outline: none;
}
&:not(.details-header) {
cursor: pointer;
}
&:hover,
&:focus {
outline: none;
@@ -186,6 +281,10 @@ body {
&.copy-user {
background-image: url("/popup/icon-user.svg");
}
&.details {
background-image: url("/popup/icon-details.svg");
}
}
}

View File

@@ -116,6 +116,12 @@ function view(ctl, params) {
);
}
break;
case "KeyO":
if (e.ctrlKey && e.target.selectionStart == e.target.selectionEnd) {
e.preventDefault();
self.popup.results[0].doAction("getDetails");
}
break;
case "End": {
if (e.target.selectionStart === e.target.value.length) {
let logins = document.querySelectorAll(".login");

View File

@@ -514,7 +514,7 @@ hash-base@^3.0.0:
readable-stream "^3.6.0"
safe-buffer "^5.2.0"
hash.js@^1.0.0, hash.js@^1.0.3:
hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7:
version "1.1.7"
resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==
@@ -772,6 +772,13 @@ os-browserify@~0.3.0:
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=
otplib@^11.0.0:
version "11.0.1"
resolved "https://registry.yarnpkg.com/otplib/-/otplib-11.0.1.tgz#7d64aa87029f07c99c7f96819fb10cdb67dea886"
integrity sha512-oi57teljNyWTC/JqJztHOtSGeFNDiDh5C1myd+faocUtFAX27Sm1mbx69kpEJ8/JqrblI3kAm4Pqd6tZJoOIBQ==
dependencies:
thirty-two "1.0.2"
pako@~1.0.5:
version "1.0.11"
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
@@ -1080,6 +1087,11 @@ syntax-error@^1.1.1:
dependencies:
acorn-node "^1.2.0"
thirty-two@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/thirty-two/-/thirty-two-1.0.2.tgz#4ca2fffc02a51290d2744b9e3f557693ca6b627a"
integrity sha1-TKL//AKlEpDSdEueP1V2k8prYno=
through2@^2.0.0:
version "2.0.5"
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"