Skip to content

Commit

Permalink
Add context screen & integrated TOTP support (#229)
Browse files Browse the repository at this point in the history
  • Loading branch information
erayd authored Sep 9, 2020
1 parent 0f8608e commit 29a7043
Show file tree
Hide file tree
Showing 18 changed files with 616 additions and 98 deletions.
43 changes: 23 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -217,29 +217,32 @@ 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:

1. Options defined in specific `*.gpg` files, only apply to these password entries:
- `autoSubmit`
1. Options defined in `.browserpass.json` file located in the root of a password store:
- `autoSubmit`
- `enableOTP`
- `gpgPath`
- `username`
- `bgColor`
- `color`
- `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
Expand All @@ -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).
Expand Down Expand Up @@ -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
Expand Down
127 changes: 70 additions & 57 deletions src/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
"[email protected]", // firefox
];

// default settings
var defaultSettings = {
autoSubmit: false,
Expand All @@ -26,6 +18,7 @@ var defaultSettings = {
foreignFills: {},
username: null,
theme: "dark",
enableOTP: false,
};

var authListeners = {};
Expand Down Expand Up @@ -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) {}

Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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));
}
}

Expand Down Expand Up @@ -885,18 +903,17 @@ 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 = {
autoSubmit: { name: "autosubmit", type: "bool" },
};
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
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -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,
},
};
}
}
}

/**
Expand Down Expand Up @@ -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
*
Expand Down
Loading

0 comments on commit 29a7043

Please sign in to comment.