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

View File

@@ -2,12 +2,11 @@
"use strict"; "use strict";
const FuzzySort = require("fuzzysort"); const FuzzySort = require("fuzzysort");
const TldJS = require("tldjs");
const sha1 = require("sha1"); const sha1 = require("sha1");
const ignore = require("ignore"); const ignore = require("ignore");
const BrowserpassURL = require("@browserpass/url");
module.exports = { module.exports = {
pathToDomain,
prepareLogins, prepareLogins,
filterSortLogins, filterSortLogins,
ignoreFiles ignoreFiles
@@ -18,30 +17,30 @@ module.exports = {
/** /**
* Get the deepest available domain component of a path * 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 path Path to parse
* @param string currentHost Current hostname for the active tab * @param object currentHost Current host info for the active tab
* @return string|null Extracted domain * @return object|null Extracted domain info
*/ */
function pathToDomain(path, currentHost) { function pathToInfo(path, currentHost) {
var parts = path.split(/\//).reverse(); var parts = path.split(/\//).reverse();
for (var key in parts) { for (var key in parts) {
if (parts[key].indexOf("@") >= 0) { if (parts[key].indexOf("@") >= 0) {
continue; 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: // 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 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 isn't a valid domain with any TLD but the current host matches it EXACTLY (localhost, 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 is its subdomain (login.pi.hole)
if ( if (
t.isValid && info.validDomain ||
((t.domain !== null && (t.tldExists || currentHost.endsWith(`.${t.hostname}`))) || currentHost.hostname === info.hostname ||
currentHost === t.hostname) currentHost.hostname.endsWith(`.${info.hostname}`)
) { ) {
return t.hostname; return info;
} }
} }
@@ -60,6 +59,7 @@ function pathToDomain(path, currentHost) {
function prepareLogins(files, settings) { function prepareLogins(files, settings) {
const logins = []; const logins = [];
let index = 0; let index = 0;
let origin = new BrowserpassURL(settings.origin);
for (let storeId in files) { for (let storeId in files) {
for (let key in files[storeId]) { for (let key in files[storeId]) {
@@ -70,11 +70,40 @@ function prepareLogins(files, settings) {
login: files[storeId][key].replace(/\.gpg$/i, ""), login: files[storeId][key].replace(/\.gpg$/i, ""),
allowFill: true allowFill: true
}; };
login.domain = pathToDomain(storeId + "/" + login.login, settings.host);
login.inCurrentDomain = // extract url info from path
settings.host == login.domain || settings.host.endsWith("." + login.domain); 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 = 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) { if (!login.recent) {
login.recent = { login.recent = {
when: 0, when: 0,
@@ -124,7 +153,7 @@ function filterSortLogins(logins, searchQuery, currentDomainOnly) {
return false; return false;
}); });
var remainingInCurrentDomain = candidates.filter( var remainingInCurrentDomain = candidates.filter(
login => login.inCurrentDomain && !login.recent.count login => login.inCurrentHost && !login.recent.count
); );
candidates = recent.concat(remainingInCurrentDomain); candidates = recent.concat(remainingInCurrentDomain);
} }

View File

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

View File

@@ -47,7 +47,7 @@ async function run() {
throw new Error(settings.hostError.params.message); 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"); throw new Error("Unable to retrieve current tab information");
} }

View File

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

View File

@@ -2,6 +2,13 @@
# yarn lockfile v1 # 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: JSONStream@^1.0.3:
version "1.3.5" version "1.3.5"
resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" 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" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
punycode@^2.1.0: punycode@^2.1.0, punycode@^2.1.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
@@ -1347,13 +1354,6 @@ timers-browserify@^1.0.1:
dependencies: dependencies:
process "~0.11.0" 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: to-arraybuffer@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"