Replace tldjs with @browserpass/url, check port (#179)
This commit is contained in:
@@ -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:
|
||||
|
@@ -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
|
||||
}
|
||||
})
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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",
|
||||
|
@@ -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");
|
||||
}
|
||||
|
||||
|
@@ -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(
|
||||
|
@@ -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"
|
||||
|
Reference in New Issue
Block a user