Replace tldjs with @browserpass/url, check port (#179)

This commit is contained in:
Erayd
2019-10-13 11:30:46 +13:00
committed by Maxim Baz
parent 02efc62c73
commit f19a44d3ce
7 changed files with 82 additions and 45 deletions

View File

@@ -103,7 +103,7 @@ If not, repeat the installation instructions for the extension.
Browserpass was designed with an assumption that certain conventions are being followed when organizing your password store.
1. In order to benefit of phishing attack protection, a password entry file, or any of its parent folders, must contain a full domain name (including TLD like `.com`) in their name in order to automatically match a website. However, entries which do not contain such a domain in their name may still be manually selected.
1. In order to benefit of phishing attack protection, a password entry file, or any of its parent folders, must contain a full domain name (including TLD like `.com`) and optionally port in their name in order to automatically match a website. However, entries which do not contain such a domain in their name may still be manually selected.
File names are not allowed to contain `\` or `/` characters, because both of them are considered to be path separators.
@@ -171,6 +171,8 @@ In order for Browserpass to correctly determine matching entries, it is expected
Browserpass will display entries for the current domain, as well as all parent entries, but not entries from different subdomains. Suppose you are currently on `https://v3.app.example.com`, Browserpass will present all the following entries in popup (if they exist): `v3.app.example.com`, `app.example.com`, `example.com`; but it will not present entries like `v2.app.example.com` or `wiki.example.com`.
Browserpass can also distinguish credentials meant for different ports, so for example an entry `example.com.gpg` will show up in Browserpass popup when you browse `example.com` on any port, however an entry `example.com:8080.gpg` will only show up on `8080` port.
Finally Browserpass will also present entries that you have recently used on this domain, even if they don't actually meet the usual matching requirements. Suppose you have a password for `amazon.com`, but you open `https://amazon.co.uk`, at first Browserpass will present no entries (because nothing matches `amazon.co.uk`), but if you hit <kbd>Backspace</kbd>, find `amazon.com` and use it to login, next time you visit `https://amazon.co.uk` and open Browserpass, `amazon.com` entry will already be present.
The sorting algorithm implemented in Browserpass will use several intuitions to try to order results in the most expected way for a user:

View File

@@ -4,6 +4,7 @@
require("chrome-extension-async");
const sha1 = require("sha1");
const idb = require("idb");
const BrowserpassURL = require("@browserpass/url");
const helpers = require("./helpers");
// native application id
@@ -137,7 +138,7 @@ async function updateMatchingPasswordsCount(tabId, forceRefresh = false) {
try {
const tab = await chrome.tabs.get(tabId);
badgeCache.settings.host = new URL(tab.url).hostname;
badgeCache.settings.origin = new URL(tab.url).origin;
} catch (e) {
throw new Error(`Unable to determine domain of the tab with id ${tabId}`);
}
@@ -146,7 +147,7 @@ async function updateMatchingPasswordsCount(tabId, forceRefresh = false) {
const files = helpers.ignoreFiles(badgeCache.files, badgeCache.settings);
const logins = helpers.prepareLogins(files, badgeCache.settings);
const matchedPasswordsCount = logins.reduce(
(acc, login) => acc + (login.recent.count || login.inCurrentDomain ? 1 : 0),
(acc, login) => acc + (login.recent.count || login.inCurrentHost ? 1 : 0),
0
);
@@ -228,13 +229,14 @@ async function saveRecent(settings, login, remove = false) {
login.recent.count++;
}
login.recent.when = Date.now();
settings.recent[sha1(settings.host + sha1(login.store.id + sha1(login.login)))] = login.recent;
settings.recent[sha1(settings.origin + sha1(login.store.id + sha1(login.login)))] =
login.recent;
// save to local storage
localStorage.setItem("recent", JSON.stringify(settings.recent));
// a new entry was added to the popup matching list, need to refresh the count
if (!login.inCurrentDomain && login.recent.count === 1) {
if (!login.inCurrentHost && login.recent.count === 1) {
updateMatchingPasswordsCount(settings.tab.id, true);
}
@@ -246,7 +248,7 @@ async function saveRecent(settings, login, remove = false) {
db.createObjectStore("log", { keyPath: "time" });
}
});
await db.add("log", { time: Date.now(), host: settings.host, login: login.login });
await db.add("log", { time: Date.now(), host: settings.origin, login: login.login });
} catch {
// ignore any errors and proceed without saving a log entry to Indexed DB
}
@@ -266,7 +268,7 @@ async function dispatchFill(settings, request, allFrames, allowForeign, allowNoS
request = Object.assign(deepCopy(request), {
allowForeign: allowForeign,
allowNoSecret: allowNoSecret,
foreignFills: settings.foreignFills[settings.host] || {}
foreignFills: settings.foreignFills[settings.origin] || {}
});
let perFrameResults = await chrome.tabs.executeScript(settings.tab.id, {
@@ -284,10 +286,10 @@ async function dispatchFill(settings, request, allFrames, allowForeign, allowNoS
let foreignFillsChanged = false;
for (let frame of perFrameResults) {
if (typeof frame.foreignFill !== "undefined") {
if (typeof settings.foreignFills[settings.host] === "undefined") {
settings.foreignFills[settings.host] = {};
if (typeof settings.foreignFills[settings.origin] === "undefined") {
settings.foreignFills[settings.origin] = {};
}
settings.foreignFills[settings.host][frame.foreignOrigin] = frame.foreignFill;
settings.foreignFills[settings.origin][frame.foreignOrigin] = frame.foreignFill;
foreignFillsChanged = true;
}
}
@@ -310,7 +312,7 @@ async function dispatchFill(settings, request, allFrames, allowForeign, allowNoS
async function dispatchFocusOrSubmit(settings, request, allFrames, allowForeign) {
request = Object.assign(deepCopy(request), {
allowForeign: allowForeign,
foreignFills: settings.foreignFills[settings.host] || {}
foreignFills: settings.foreignFills[settings.origin] || {}
});
let perFrameResults = await chrome.tabs.executeScript(settings.tab.id, {
@@ -417,7 +419,7 @@ async function fillFields(settings, login, fields) {
// try again using all available frames if we couldn't fill an "important" field
if (
!filledFields.includes(importantFieldToFill) &&
settings.foreignFills[settings.host] !== false
settings.foreignFills[settings.origin] !== false
) {
allowForeign = true;
filledFields = filledFields.concat(
@@ -455,7 +457,7 @@ async function fillFields(settings, login, fields) {
}
// try again using all available frames
if (!filledFields.length && settings.foreignFills[settings.host] !== false) {
if (!filledFields.length && settings.foreignFills[settings.origin] !== false) {
allowForeign = true;
filledFields = filledFields.concat(
await dispatchFill(
@@ -581,7 +583,9 @@ async function getFullSettings() {
// Fill current tab info
try {
settings.tab = (await chrome.tabs.query({ active: true, currentWindow: true }))[0];
settings.host = new URL(settings.tab.url).hostname;
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) {}
return settings;
@@ -772,7 +776,7 @@ async function handleMessage(settings, message, sendResponse) {
case "launch":
case "launchInNewTab":
try {
var url = message.login.fields.url || message.login.domain;
var url = message.login.fields.url || message.login.host;
if (!url) {
throw new Error("No URL is defined for this entry");
}
@@ -1084,6 +1088,7 @@ function triggerOTPExtension(settings, action, otp) {
otp: otp,
settings: {
host: settings.host,
origin: settings.origin,
tab: settings.tab
}
})

View File

@@ -2,12 +2,11 @@
"use strict";
const FuzzySort = require("fuzzysort");
const TldJS = require("tldjs");
const sha1 = require("sha1");
const ignore = require("ignore");
const BrowserpassURL = require("@browserpass/url");
module.exports = {
pathToDomain,
prepareLogins,
filterSortLogins,
ignoreFiles
@@ -18,30 +17,30 @@ module.exports = {
/**
* Get the deepest available domain component of a path
*
* @since 3.0.0
* @since 3.2.3
*
* @param string path Path to parse
* @param string currentHost Current hostname for the active tab
* @return string|null Extracted domain
* @param object currentHost Current host info for the active tab
* @return object|null Extracted domain info
*/
function pathToDomain(path, currentHost) {
function pathToInfo(path, currentHost) {
var parts = path.split(/\//).reverse();
for (var key in parts) {
if (parts[key].indexOf("@") >= 0) {
continue;
}
var t = TldJS.parse(parts[key]);
var info = BrowserpassURL.parseHost(parts[key]);
// Part is considered to be a domain component in one of the following cases:
// - it is a valid domain with well-known TLD (github.com, login.github.com)
// - it is a valid domain with unknown TLD but the current host is it's subdomain (login.pi.hole)
// - it is or isnt a valid domain but the current host matches it EXACTLY (localhost, pi.hole)
// - it is or isn't a valid domain with any TLD but the current host matches it EXACTLY (localhost, pi.hole)
// - it is or isn't a valid domain with any TLD but the current host is its subdomain (login.pi.hole)
if (
t.isValid &&
((t.domain !== null && (t.tldExists || currentHost.endsWith(`.${t.hostname}`))) ||
currentHost === t.hostname)
info.validDomain ||
currentHost.hostname === info.hostname ||
currentHost.hostname.endsWith(`.${info.hostname}`)
) {
return t.hostname;
return info;
}
}
@@ -60,6 +59,7 @@ function pathToDomain(path, currentHost) {
function prepareLogins(files, settings) {
const logins = [];
let index = 0;
let origin = new BrowserpassURL(settings.origin);
for (let storeId in files) {
for (let key in files[storeId]) {
@@ -70,11 +70,40 @@ function prepareLogins(files, settings) {
login: files[storeId][key].replace(/\.gpg$/i, ""),
allowFill: true
};
login.domain = pathToDomain(storeId + "/" + login.login, settings.host);
login.inCurrentDomain =
settings.host == login.domain || settings.host.endsWith("." + login.domain);
// extract url info from path
let pathInfo = pathToInfo(storeId + "/" + login.login, origin);
if (pathInfo) {
// set assumed host
login.host = pathInfo.port
? `${pathInfo.hostname}:${pathInfo.port}`
: pathInfo.hostname;
// check whether extracted path info matches the current origin
login.inCurrentHost = origin.hostname === pathInfo.hostname;
// check whether the current origin is subordinate to extracted path info, meaning:
// - that the path info is not a single level domain (e.g. com, net, local)
// - and that the current origin is a subdomain of that path info
if (
pathInfo.hostname.includes(".") &&
origin.hostname.endsWith(`.${pathInfo.hostname}`)
) {
login.inCurrentHost = true;
}
// filter out entries with a non-matching port
if (pathInfo.port && pathInfo.port !== origin.port) {
login.inCurrentHost = false;
}
} else {
login.host = null;
login.inCurrentHost = false;
}
// update recent counter
login.recent =
settings.recent[sha1(settings.host + sha1(login.store.id + sha1(login.login)))];
settings.recent[sha1(settings.origin + sha1(login.store.id + sha1(login.login)))];
if (!login.recent) {
login.recent = {
when: 0,
@@ -124,7 +153,7 @@ function filterSortLogins(logins, searchQuery, currentDomainOnly) {
return false;
});
var remainingInCurrentDomain = candidates.filter(
login => login.inCurrentDomain && !login.recent.count
login => login.inCurrentHost && !login.recent.count
);
candidates = recent.concat(remainingInCurrentDomain);
}

View File

@@ -15,14 +15,14 @@
}
],
"dependencies": {
"@browserpass/url": "^1.1.3",
"chrome-extension-async": "^3.3.2",
"fuzzysort": "^1.1.4",
"idb": "^4.0.3",
"ignore": "^5.1.4",
"mithril": "^1.1.0",
"moment": "^2.24.0",
"sha1": "^1.1.1",
"tldjs": "^2.3.0"
"sha1": "^1.1.1"
},
"devDependencies": {
"browserify": "^16.2.3",

View File

@@ -47,7 +47,7 @@ async function run() {
throw new Error(settings.hostError.params.message);
}
if (typeof settings.host === "undefined") {
if (typeof settings.origin === "undefined") {
throw new Error("Unable to retrieve current tab information");
}

View File

@@ -29,6 +29,7 @@ function SearchInterface(popup) {
*/
function view(ctl, params) {
var self = this;
var host = new URL(this.popup.settings.origin).host;
return m(
"form.part.search",
{
@@ -59,7 +60,7 @@ function view(ctl, params) {
[
this.popup.currentDomainOnly
? m("div.hint.badge", [
this.popup.settings.host,
host,
m("div.remove-hint", {
onclick: function(e) {
var target = document.querySelector(

View File

@@ -2,6 +2,13 @@
# yarn lockfile v1
"@browserpass/url@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@browserpass/url/-/url-1.1.3.tgz#8b94896198e7032b80c7d82e836d59e42b769669"
integrity sha512-XWhYNZo0BGcyFxkum8g1DeUkFw9QyqcaRTOEinyPSpIubh+aL2VQxbdItitVtS5qkkowqaA5Xt1H2pAqu8ic8w==
dependencies:
punycode "^2.1.1"
JSONStream@^1.0.3:
version "1.3.5"
resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"
@@ -1089,7 +1096,7 @@ punycode@^1.3.2, punycode@^1.4.1:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
punycode@^2.1.0:
punycode@^2.1.0, punycode@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
@@ -1347,13 +1354,6 @@ timers-browserify@^1.0.1:
dependencies:
process "~0.11.0"
tldjs@^2.3.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/tldjs/-/tldjs-2.3.1.tgz#cf09c3eb5d7403a9e214b7d65f3cf9651c0ab039"
integrity sha512-W/YVH/QczLUxVjnQhFC61Iq232NWu3TqDdO0S/MtXVz4xybejBov4ud+CIwN9aYqjOecEqIy0PscGkwpG9ZyTw==
dependencies:
punycode "^1.4.1"
to-arraybuffer@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"