From b634412c8a66cef31fbe37a3d025aedd86199ba5 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Sat, 23 Nov 2024 14:40:42 -0700 Subject: [PATCH 01/35] first draft changes required for manifest v3 migration --- src/manifest-chromium.json | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/manifest-chromium.json b/src/manifest-chromium.json index 2bc21aed..8c2dd5cc 100644 --- a/src/manifest-chromium.json +++ b/src/manifest-chromium.json @@ -1,20 +1,19 @@ { "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvlVUvevBvdeIFpvK5Xjcbd/cV8AsMNLg0Y7BmUetSTagjts949Tp12mNmWmIEEaE9Zwmfjl1ownWiclGhsoPSf6x7nP/i0j8yROv6TYibXLhZet9y4vnUMgtCIkb3O5RnuOl0Y+V3XUADwxotmgT1laPUThymJoYnWPv+lwDkYiEopX2Aq2amzRj8aMogNBUbAIkCMxfa9WK3Vm0QTAUdV4ii9WqzbgjHVruQpiFVq99W2U9ddsWNZjOG/36sFREuHw+reulQgblp9FZdaN1Q9X5cGcT5bncQIRB6K3wZYa805gFENc93Wslmzu6aUSEKqqPymlI5ikedaPlXPmlqwIDAQAB", - "manifest_version": 2, + "manifest_version": 3, "name": "Browserpass", "description": "Browser extension for zx2c4's pass (password manager)", "version": "3.9.0", "author": "Maxim Baz , Steve Gilberd ", "homepage_url": "https://github.com/browserpass/browserpass-extension", "background": { - "persistent": true, - "scripts": ["js/background.dist.js"] + "service_worker": "js/background.dist.js" }, "icons": { "16": "icon16.png", "128": "icon.png" }, - "browser_action": { + "action": { "default_icon": { "16": "icon16.png", "128": "icon.png" @@ -23,7 +22,6 @@ }, "options_ui": { "page": "options/options.html", - "chrome_style": true, "open_in_tab": false }, "permissions": [ @@ -34,12 +32,13 @@ "clipboardWrite", "nativeMessaging", "notifications", - "webRequest", - "webRequestBlocking", - "http://*/*", - "https://*/*" + "storage", + "webRequest" ], - "content_security_policy": "default-src 'none'; font-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self'", + "host_permissions": ["http://*/*", "https://*/*"], + "content_security_policy": { + "extension_pages": "default-src 'none'; font-src 'self'; img-src 'self' data:; script-src 'self'" + }, "commands": { "_execute_browser_action": { "suggested_key": { From 607da086a92253834e3747b7d49cfe94c91a5eb9 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Sat, 23 Nov 2024 14:44:06 -0700 Subject: [PATCH 02/35] minimum required changes to initially get background.js to run as service worker remove mythril from helpers, no access to window update for storage api changes chrome.browserAction => chrome.action. --- src/background.js | 77 ++++++++++++++++++++++++++--------- src/helpers.js | 35 ++++------------ src/helpers.ui.js | 32 +++++++++++++++ src/popup/addEditInterface.js | 3 +- src/popup/detailsInterface.js | 3 +- 5 files changed, 102 insertions(+), 48 deletions(-) create mode 100644 src/helpers.ui.js diff --git a/src/background.js b/src/background.js index b97a1b96..26eb68e2 100644 --- a/src/background.js +++ b/src/background.js @@ -39,7 +39,7 @@ var badgeCache = { // the last text copied to the clipboard is stored here in order to be cleared after 60 seconds let lastCopiedText = null; -chrome.browserAction.setBadgeBackgroundColor({ +chrome.action.setBadgeBackgroundColor({ color: "#666", }); @@ -156,7 +156,7 @@ async function updateMatchingPasswordsCount(tabId, forceRefresh = false) { ); // Set badge for the current tab - chrome.browserAction.setBadgeText({ + chrome.action.setBadgeText({ text: "" + (matchedPasswordsCount || ""), tabId: tabId, }); @@ -227,7 +227,8 @@ async function saveRecent(settings, login, remove = false) { var ignoreInterval = 60000; // 60 seconds - don't increment counter twice within this window // save store timestamp - localStorage.setItem("recent:" + login.store.id, JSON.stringify(Date.now())); + const ts = `recent${login.store.id}`; + chrome.storage.local.set({ ts: JSON.stringify(Date.now()) }); // update login usage count & timestamp if (Date.now() > login.recent.when + ignoreInterval) { @@ -238,7 +239,7 @@ async function saveRecent(settings, login, remove = false) { login.recent; // save to local storage - localStorage.setItem("recent", JSON.stringify(settings.recent)); + chrome.storage.local.set({ recent: JSON.stringify(settings.recent) }); // a new entry was added to the popup matching list, need to refresh the count if (!login.inCurrentHost && login.recent.count === 1) { @@ -478,12 +479,30 @@ async function fillFields(settings, login, fields) { * * @return object Local settings from the extension */ -function getLocalSettings() { +async function getLocalSettings() { var settings = helpers.deepCopy(defaultSettings); - for (var key in settings) { - var value = localStorage.getItem(key); - if (value !== null) { - settings[key] = JSON.parse(value); + + try { + // use for debugging only, since dev tools does not show extension storage + await chrome.storage.local.get(console.dir); + } catch (err) { + console.warn("could not display extension local storage"); + } + + var items = await chrome.storage.local.get(Object.keys(defaultSettings)); + for (var key in defaultSettings) { + var value = null; + if (Object.prototype.hasOwnProperty.call(items, key)) { + value = items[key]; + } + console.info(`getLocalSettings(), response for ${key}=`, value); + + if (value !== null && Boolean(value)) { + try { + settings[key] = value; + } catch (err) { + console.error(`getLocalSettings(), error JSON.parse(value):`, err, { key, value }); + } } } @@ -498,7 +517,7 @@ function getLocalSettings() { * @return object Full settings object */ async function getFullSettings() { - var settings = getLocalSettings(); + var settings = await getLocalSettings(); var configureSettings = Object.assign(helpers.deepCopy(settings), { defaultStore: {}, }); @@ -554,16 +573,30 @@ async function getFullSettings() { // Fill recent data for (var storeId in settings.stores) { - var when = localStorage.getItem("recent:" + storeId); - if (when) { - settings.stores[storeId].when = JSON.parse(when); + const whenKey = `recent:${storeId}`; + var when = await chrome.storage.local.get([whenKey]); + if (when && Object.prototype.hasOwnProperty.call(when, whenKey)) { + try { + settings.stores[storeId].when = JSON.parse(when[whenKey]); + } catch (err) { + console.error( + `getFullSettings() error fill stores recent data (${whenKey})`, + err, + when + ); + } } else { settings.stores[storeId].when = 0; } } - settings.recent = localStorage.getItem("recent"); - if (settings.recent) { - settings.recent = JSON.parse(settings.recent); + const recentKey = "recent"; + const recent = await chrome.storage.local.get(recentKey); + if (recent && Object.prototype.hasOwnProperty.call(recent, recentKey)) { + try { + settings.recent = JSON.parse(recent[recentKey]); + } catch (err) { + console.error(`getFullSettings() error recent`, err, recent); + } } else { settings.recent = {}; } @@ -1154,7 +1187,13 @@ async function saveSettings(settings) { for (var key in defaultSettings) { if (settingsToSave.hasOwnProperty(key)) { - localStorage.setItem(key, JSON.stringify(settingsToSave[key])); + const save = {}; + save[key] = settingsToSave[key]; + // chrome.storage.local.set(key, JSON.stringify(settingsToSave[key])); + console.info(`saving ${key}`, save); + await chrome.storage.local.set(save).then(() => { + console.log("saved setting"); + }); } } @@ -1186,8 +1225,8 @@ function onExtensionInstalled(details) { }; if (details.reason === "install") { - if (localStorage.getItem("installed") === null) { - localStorage.setItem("installed", Date.now()); + if (chrome.storage.local.get("installed") === null) { + chrome.storage.local.set({ installed: Date.now() }); show( "installed", "browserpass: Install native host app", diff --git a/src/helpers.js b/src/helpers.js index 9efddaed..e470ccd1 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -5,8 +5,7 @@ const FuzzySort = require("fuzzysort"); const sha1 = require("sha1"); const ignore = require("ignore"); const hash = require("hash.js"); -const m = require("mithril"); -const notify = require("./popup/notifications"); +// const notify = require("./popup/notifications"); const Authenticator = require("otplib").authenticator.Authenticator; const BrowserpassURL = require("@browserpass/url"); @@ -18,7 +17,6 @@ const fieldsPrefix = { url: ["url", "uri", "website", "site", "link", "launch"], }; -const containsNumbersRegEx = RegExp(/[0-9]/); const containsSymbolsRegEx = RegExp(/[\p{P}\p{S}]/, "u"); const LATEST_NATIVE_APP_VERSION = 3001000; @@ -29,7 +27,6 @@ module.exports = { deepCopy, filterSortLogins, handleError, - highlight, getSetting, ignoreFiles, makeTOTP, @@ -68,20 +65,23 @@ function handleError(error, type = "error") { case "error": console.log(error); // disable error timeout, to allow necessary user action - notify.errorMsg(error.toString(), 0); + // notify.errorMsg(error.toString(), 0); break; case "warning": - notify.warningMsg(error.toString()); + // notify.warningMsg(error.toString()); + console.warn(error.toString()); break; case "success": - notify.successMsg(error.toString()); + // notify.successMsg(error.toString()); + console.info(error.toString()); break; case "info": default: - notify.infoMsg(error.toString()); + // notify.infoMsg(error.toString()); + console.info(error.toString()); break; } } @@ -284,25 +284,6 @@ function prepareLogin(settings, storeId, file, index = 0, origin = undefined) { return login; } -/** - * Highlight password characters - * - * @since 3.8.0 - * - * @param {string} secret a string to be split by character - * @return {array} mithril vnodes to be rendered - */ -function highlight(secret = "") { - return secret.split("").map((c) => { - if (c.match(containsNumbersRegEx)) { - return m("span.char.num", c); - } else if (c.match(containsSymbolsRegEx)) { - return m("span.char.punct", c); - } - return m("span.char", c); - }); -} - /** * Filter and sort logins * diff --git a/src/helpers.ui.js b/src/helpers.ui.js new file mode 100644 index 00000000..cc92c30f --- /dev/null +++ b/src/helpers.ui.js @@ -0,0 +1,32 @@ +//------------------------------------- Initialisation --------------------------------------// +"use strict"; + +const m = require("mithril"); + +const containsNumbersRegEx = RegExp(/[0-9]/); +const containsSymbolsRegEx = RegExp(/[\p{P}\p{S}]/, "u"); + +module.exports = { + highlight, +}; + +//----------------------------------- Function definitions ----------------------------------// + +/** + * Highlight password characters + * + * @since 3.8.0 + * + * @param {string} secret a string to be split by character + * @return {array} mithril vnodes to be rendered + */ +function highlight(secret = "") { + return secret.split("").map((c) => { + if (c.match(containsNumbersRegEx)) { + return m("span.char.num", c); + } else if (c.match(containsSymbolsRegEx)) { + return m("span.char.punct", c); + } + return m("span.char", c); + }); +} diff --git a/src/popup/addEditInterface.js b/src/popup/addEditInterface.js index 0595f807..d13e6047 100644 --- a/src/popup/addEditInterface.js +++ b/src/popup/addEditInterface.js @@ -4,6 +4,7 @@ const Settings = require("./models/Settings"); const Tree = require("./models/Tree"); const notify = require("./notifications"); const helpers = require("../helpers"); +const helpersUI = require("../helpers.ui"); const layout = require("./layoutInterface"); const dialog = require("./modalDialog"); @@ -486,7 +487,7 @@ function AddEditInterface(settingsModel) { m( "div.chars", loginObj.hasOwnProperty("fields") - ? helpers.highlight(loginObj.fields.secret) + ? helpersUI.highlight(loginObj.fields.secret) : "" ), m("div.btn.generate", { diff --git a/src/popup/detailsInterface.js b/src/popup/detailsInterface.js index 23ec93cd..5ca2ad27 100644 --- a/src/popup/detailsInterface.js +++ b/src/popup/detailsInterface.js @@ -3,6 +3,7 @@ module.exports = DetailsInterface; const m = require("mithril"); const Moment = require("moment"); const helpers = require("../helpers"); +const helpersUI = require("../helpers.ui"); const layout = require("./layoutInterface"); const Login = require("./models/Login"); const Settings = require("./models/Settings"); @@ -97,7 +98,7 @@ function DetailsInterface(settingsModel) { const storeColor = Login.prototype.getStore(loginObj, "color"); const secret = (loginObj.hasOwnProperty("fields") ? loginObj.fields.secret : null) || ""; - const passChars = helpers.highlight(secret); + const passChars = helpersUI.highlight(secret); var nodes = []; nodes.push( From f2d7b85cdc8a9e958da2bbae58f6af2ee63e0624 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Mon, 30 Dec 2024 13:08:19 -0700 Subject: [PATCH 03/35] browserpass extension first working draft of manifest v3 in chromium: fill and copy browserpass/browserpass-extension#320 --- Makefile | 2 + src/Makefile | 8 ++- src/background.js | 133 ++++++++++++++++++++++++----------- src/helpers.js | 93 ------------------------ src/helpers.ui.js | 94 +++++++++++++++++++++++++ src/manifest-chromium.json | 2 + src/offscreen/offscreen.html | 3 + src/offscreen/offscreen.js | 79 +++++++++++++++++++++ src/popup/models/Login.js | 3 +- src/popup/popup.js | 2 +- 10 files changed, 282 insertions(+), 137 deletions(-) create mode 100644 src/offscreen/offscreen.html create mode 100644 src/offscreen/offscreen.js diff --git a/Makefile b/Makefile index b73fae3a..e1ff0223 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,7 @@ EXTENSION_FILES := \ src/popup/*.html \ src/popup/*.gif \ src/popup/*.svg \ + src/offscreen/*.html \ src/options/*.html EXTENSION_FILES := \ $(wildcard $(EXTENSION_FILES)) \ @@ -27,6 +28,7 @@ EXTENSION_FILES := \ src/css/options.dist.css \ src/js/background.dist.js \ src/js/popup.dist.js \ + src/js/offscreen.dist.js \ src/js/options.dist.js \ src/js/inject.dist.js CHROMIUM_FILES := $(patsubst src/%,chromium/%, $(EXTENSION_FILES)) diff --git a/src/Makefile b/src/Makefile index 6752479d..bbdfeb67 100644 --- a/src/Makefile +++ b/src/Makefile @@ -3,10 +3,10 @@ PRETTIER := node_modules/.bin/prettier LESSC := node_modules/.bin/lessc CLEAN_FILES := css js -PRETTIER_FILES := $(wildcard *.json *.js popup/*.js options/*.js *.less popup/*.less options/*.less *.html popup/*.html options/*.html) +PRETTIER_FILES := $(wildcard *.json *.js popup/*.js offscreen/*.js options/*.js *.less popup/*.less options/*.less *.html popup/*.html offscreen/*.html options/*.html) .PHONY: all -all: deps prettier css/popup.dist.css css/options.dist.css js/background.dist.js js/popup.dist.js js/options.dist.js js/inject.dist.js +all: deps prettier css/popup.dist.css css/options.dist.css js/background.dist.js js/popup.dist.js js/offscreen.dist.js js/options.dist.js js/inject.dist.js .PHONY: deps deps: @@ -32,6 +32,10 @@ js/popup.dist.js: $(BROWSERIFY) popup/*.js helpers.js [ -d js ] || mkdir -p js $(BROWSERIFY) -o js/popup.dist.js popup/popup.js +js/offscreen.dist.js: $(BROWSERIFY) offscreen/*.js + [ -d js ] || mkdir -p js + $(BROWSERIFY) -o js/offscreen.dist.js offscreen/offscreen.js + js/options.dist.js: $(BROWSERIFY) options/*.js [ -d js ] || mkdir -p js $(BROWSERIFY) -o js/options.dist.js options/options.js diff --git a/src/background.js b/src/background.js index 26eb68e2..126590c2 100644 --- a/src/background.js +++ b/src/background.js @@ -90,9 +90,9 @@ chrome.commands.onCommand.addListener(async (command) => { }); // handle fired alarms -chrome.alarms.onAlarm.addListener((alarm) => { +chrome.alarms.onAlarm.addListener(async (alarm) => { if (alarm.name === "clearClipboard") { - if (readFromClipboard() === lastCopiedText) { + if ((await readFromClipboard()) === lastCopiedText) { copyToClipboard("", false); } lastCopiedText = null; @@ -175,16 +175,13 @@ async function updateMatchingPasswordsCount(tabId, forceRefresh = false) { * @param boolean clear Whether to clear the clipboard after one minute * @return void */ -function copyToClipboard(text, clear = true) { - document.addEventListener( - "copy", - function (e) { - e.clipboardData.setData("text/plain", text); - e.preventDefault(); - }, - { once: true } - ); - document.execCommand("copy"); +async function copyToClipboard(text, clear = true) { + await setupOffscreenDocument("offscreen/offscreen.html"); + chrome.runtime.sendMessage({ + type: "copy-data-to-clipboard", + target: "offscreen-doc", + data: text, + }); if (clear) { lastCopiedText = text; @@ -199,17 +196,57 @@ function copyToClipboard(text, clear = true) { * * @return string The current plaintext content of the clipboard */ -function readFromClipboard() { - const ta = document.createElement("textarea"); - // these lines are carefully crafted to make paste work in both Chrome and Firefox - ta.contentEditable = true; - ta.textContent = ""; - document.body.appendChild(ta); - ta.select(); - document.execCommand("paste"); - const content = ta.value; - document.body.removeChild(ta); - return content; +async function readFromClipboard() { + await setupOffscreenDocument("offscreen/offscreen.html"); + + const response = await chrome.runtime.sendMessage({ + type: "read-from-clipboard", + target: "offscreen-doc", + }); + + if (response.status != "ok") { + console.error( + "failure reading from clipboard in offscreen document", + response.message || undefined + ); + return; + } + + return response.message; +} + +/** + * Setup offscreen document + * @since 3.10.0 + * @param string path - location of html document to be created + */ +let creatingOffscreen; // A global promise to avoid concurrency issues +async function setupOffscreenDocument(path) { + // Check all windows controlled by the service worker to see if one + // of them is the offscreen document with the given path + const offscreenUrl = chrome.runtime.getURL(path); + const existingContexts = await chrome.runtime.getContexts({ + contextTypes: ["OFFSCREEN_DOCUMENT"], + documentUrls: [offscreenUrl], + }); + + if (existingContexts.length > 0) { + return; + } + + // create offscreen document + if (!creatingOffscreen) { + creatingOffscreen = chrome.offscreen.createDocument({ + url: path, + reasons: [chrome.offscreen.Reason.CLIPBOARD], + justification: "Read / write text to the clipboard", + }); + } + + if (creatingOffscreen) { + await creatingOffscreen; + creatingOffscreen = null; + } } /** @@ -277,9 +314,18 @@ async function dispatchFill(settings, request, allFrames, allowForeign, allowNoS foreignFills: settings.foreignFills[settings.origin] || {}, }); - let perFrameResults = await chrome.tabs.executeScript(settings.tab.id, { - allFrames: allFrames, - code: `window.browserpass.fillLogin(${JSON.stringify(request)});`, + try { + await injectScript(settings, allFrames); + } catch { + throw new Error("Unable to inject script in the top frame"); + } + + let perFrameResults = await chrome.scripting.executeScript({ + target: { tabId: settings.tab.id, allFrames: allFrames }, + func: function (request) { + window.browserpass.fillLogin(request); + }, + args: [request], }); // merge filled fields into a single array @@ -321,9 +367,12 @@ async function dispatchFocusOrSubmit(settings, request, allFrames, allowForeign) foreignFills: settings.foreignFills[settings.origin] || {}, }); - await chrome.tabs.executeScript(settings.tab.id, { - allFrames: allFrames, - code: `window.browserpass.focusOrSubmit(${JSON.stringify(request)});`, + await chrome.scripting.executeScript({ + target: { tabId: settings.tab.id, allFrames: allFrames }, + func: function (request) { + window.browserpass.focusOrSubmit(request); + }, + args: [request], }); } @@ -339,9 +388,9 @@ async function injectScript(settings, allFrames) { return new Promise(async (resolve, reject) => { const waitTimeout = setTimeout(reject, MAX_WAIT); - await chrome.tabs.executeScript(settings.tab.id, { - allFrames: allFrames, - file: "js/inject.dist.js", + await chrome.scripting.executeScript({ + target: { tabId: settings.tab.id, allFrames: allFrames }, + files: ["js/inject.dist.js"], }); clearTimeout(waitTimeout); resolve(true); @@ -814,7 +863,7 @@ async function handleMessage(settings, message, sendResponse) { break; case "copyPassword": try { - copyToClipboard(message.login.fields.secret); + await copyToClipboard(message.login.fields.secret); await saveRecent(settings, message.login); sendResponse({ status: "ok" }); } catch (e) { @@ -826,7 +875,7 @@ async function handleMessage(settings, message, sendResponse) { break; case "copyUsername": try { - copyToClipboard(message.login.fields.login); + await copyToClipboard(message.login.fields.login); await saveRecent(settings, message.login); sendResponse({ status: "ok" }); } catch (e) { @@ -842,7 +891,7 @@ async function handleMessage(settings, message, sendResponse) { if (!message.login.fields.otp) { throw new Exception("No OTP seed available"); } - copyToClipboard(helpers.makeTOTP(message.login.fields.otp.params)); + await copyToClipboard(helpers.makeTOTP(message.login.fields.otp.params)); sendResponse({ status: "ok" }); } catch (e) { sendResponse({ @@ -913,7 +962,7 @@ async function handleMessage(settings, message, sendResponse) { helpers.getSetting("enableOTP", message.login, settings) && message.login.fields.hasOwnProperty("otp") ) { - copyToClipboard(helpers.makeTOTP(message.login.fields.otp.params)); + await copyToClipboard(helpers.makeTOTP(message.login.fields.otp.params)); } } catch (e) { try { @@ -1128,6 +1177,13 @@ async function receiveMessage(message, sender, sendResponse) { } try { + const msgLen = Object.keys(message).length; + const sendLen = Object.keys(sender).length; + const sendRes = typeof sendResponse; + console.debug( + `receiveMessage(message..${msgLen}, sender..${sendLen}, sendResponse..${sendRes})`, + { message, sender, sendResponse } + ); const settings = await getFullSettings(); handleMessage(settings, message, sendResponse); } catch (e) { @@ -1189,11 +1245,8 @@ async function saveSettings(settings) { if (settingsToSave.hasOwnProperty(key)) { const save = {}; save[key] = settingsToSave[key]; - // chrome.storage.local.set(key, JSON.stringify(settingsToSave[key])); console.info(`saving ${key}`, save); - await chrome.storage.local.set(save).then(() => { - console.log("saved setting"); - }); + await chrome.storage.local.set(save); } } diff --git a/src/helpers.js b/src/helpers.js index e470ccd1..edf09b8a 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -5,7 +5,6 @@ const FuzzySort = require("fuzzysort"); const sha1 = require("sha1"); const ignore = require("ignore"); const hash = require("hash.js"); -// const notify = require("./popup/notifications"); const Authenticator = require("otplib").authenticator.Authenticator; const BrowserpassURL = require("@browserpass/url"); @@ -26,13 +25,11 @@ module.exports = { LATEST_NATIVE_APP_VERSION, deepCopy, filterSortLogins, - handleError, getSetting, ignoreFiles, makeTOTP, prepareLogin, prepareLogins, - withLogin, }; //----------------------------------- Function definitions ----------------------------------// @@ -52,96 +49,6 @@ function deepCopy(obj) { return JSON.parse(JSON.stringify(obj)); } -/** - * Handle an error - * - * @since 3.0.0 - * - * @param Error error Error object - * @param string type Error type - */ -function handleError(error, type = "error") { - switch (type) { - case "error": - console.log(error); - // disable error timeout, to allow necessary user action - // notify.errorMsg(error.toString(), 0); - break; - - case "warning": - // notify.warningMsg(error.toString()); - console.warn(error.toString()); - break; - - case "success": - // notify.successMsg(error.toString()); - console.info(error.toString()); - break; - - case "info": - default: - // notify.infoMsg(error.toString()); - console.info(error.toString()); - break; - } -} - -/** - * Do a login action - * - * @since 3.0.0 - * - * @param string action Action to take - * @param object params Action parameters - * @return void - */ -async function withLogin(action, params = {}) { - try { - switch (action) { - case "fill": - handleError("Filling login details...", "info"); - break; - case "launch": - handleError("Launching URL...", "info"); - break; - case "launchInNewTab": - handleError("Launching URL in a new tab...", "info"); - break; - case "copyPassword": - handleError("Copying password to clipboard...", "info"); - break; - case "copyUsername": - handleError("Copying username to clipboard...", "info"); - break; - case "copyOTP": - handleError("Copying OTP token to clipboard...", "info"); - break; - default: - handleError("Please wait...", "info"); - break; - } - - const login = deepCopy(this.login); - - // hand off action to background script - var response = await chrome.runtime.sendMessage({ action, login, params }); - if (response.status != "ok") { - throw new Error(response.message); - } else { - if (response.login && typeof response.login === "object") { - response.login.doAction = withLogin.bind({ - settings: this.settings, - login: response.login, - }); - } else { - window.close(); - } - } - } catch (e) { - handleError(e); - } -} - /* * Get most relevant setting value * diff --git a/src/helpers.ui.js b/src/helpers.ui.js index cc92c30f..80fb7642 100644 --- a/src/helpers.ui.js +++ b/src/helpers.ui.js @@ -2,12 +2,16 @@ "use strict"; const m = require("mithril"); +const helpers = require("./helpers"); +const notify = require("./popup/notifications"); const containsNumbersRegEx = RegExp(/[0-9]/); const containsSymbolsRegEx = RegExp(/[\p{P}\p{S}]/, "u"); module.exports = { + handleError, highlight, + withLogin, }; //----------------------------------- Function definitions ----------------------------------// @@ -30,3 +34,93 @@ function highlight(secret = "") { return m("span.char", c); }); } + +/** + * Handle an error + * + * @since 3.0.0 + * + * @param Error error Error object + * @param string type Error type + */ +function handleError(error, type = "error") { + switch (type) { + case "error": + console.log(error); + // disable error timeout, to allow necessary user action + notify.errorMsg(error.toString(), 0); + break; + + case "warning": + notify.warningMsg(error.toString()); + console.warn(error.toString()); + break; + + case "success": + notify.successMsg(error.toString()); + console.info(error.toString()); + break; + + case "info": + default: + notify.infoMsg(error.toString()); + console.info(error.toString()); + break; + } +} + +/** + * Do a login action + * + * @since 3.0.0 + * + * @param string action Action to take + * @param object params Action parameters + * @return void + */ +async function withLogin(action, params = {}) { + try { + switch (action) { + case "fill": + handleError("Filling login details...", "info"); + break; + case "launch": + handleError("Launching URL...", "info"); + break; + case "launchInNewTab": + handleError("Launching URL in a new tab...", "info"); + break; + case "copyPassword": + handleError("Copying password to clipboard...", "info"); + break; + case "copyUsername": + handleError("Copying username to clipboard...", "info"); + break; + case "copyOTP": + handleError("Copying OTP token to clipboard...", "info"); + break; + default: + handleError("Please wait...", "info"); + break; + } + + const login = helpers.deepCopy(this.login); + + // hand off action to background script + var response = await chrome.runtime.sendMessage({ action, login, params }); + if (response.status != "ok") { + throw new Error(response.message); + } else { + if (response.login && typeof response.login === "object") { + response.login.doAction = withLogin.bind({ + settings: this.settings, + login: response.login, + }); + } else { + window.close(); + } + } + } catch (e) { + handleError(e); + } +} diff --git a/src/manifest-chromium.json b/src/manifest-chromium.json index 8c2dd5cc..ad0b8fc5 100644 --- a/src/manifest-chromium.json +++ b/src/manifest-chromium.json @@ -32,6 +32,8 @@ "clipboardWrite", "nativeMessaging", "notifications", + "offscreen", + "scripting", "storage", "webRequest" ], diff --git a/src/offscreen/offscreen.html b/src/offscreen/offscreen.html new file mode 100644 index 00000000..5e47e2a6 --- /dev/null +++ b/src/offscreen/offscreen.html @@ -0,0 +1,3 @@ + + + diff --git a/src/offscreen/offscreen.js b/src/offscreen/offscreen.js new file mode 100644 index 00000000..349b980c --- /dev/null +++ b/src/offscreen/offscreen.js @@ -0,0 +1,79 @@ +//------------------------------------- Initialization --------------------------------------// +"use strict"; + +//----------------------------------- Function definitions ----------------------------------// +chrome.runtime.onMessage.addListener(handleMessage); + +async function handleMessage(message, sender, sendResponse) { + if (sender.id !== chrome.runtime.id) { + // silently exit without responding when the source is foreign + return; + } + + // Return early if this message isn't meant for the offscreen document. + if (message.target !== "offscreen-doc") { + return; + } + + // Dispatch the message to an appropriate handler. + let reply; + try { + switch (message.type) { + case "copy-data-to-clipboard": + writeToClipboard(message.data); + break; + case "read-from-clipboard": + reply = readFromClipboard(); + break; + default: + console.warn(`Unexpected message type received: '${message.type}'.`); + } + sendResponse({ status: "ok", message: reply || undefined }); + } catch (e) { + sendResponse({ status: "error", message: e.toString() }); + } +} + +/** + * Read plain text from clipboard + * + * @since 3.2.0 + * + * @return string The current plaintext content of the clipboard + */ +function readFromClipboard() { + const ta = document.querySelector("#text"); + // these lines are carefully crafted to make paste work in both Chrome and Firefox + ta.contentEditable = true; + ta.textContent = ""; + ta.select(); + document.execCommand("paste"); + const content = ta.value; + return content; +} + +/** + * Copy text to clipboard and optionally clear it from the clipboard after one minute + * + * @since 3.2.0 + * + * @param string text Text to copy + * @param boolean clear Whether to clear the clipboard after one minute + * @return void + */ +async function writeToClipboard(text) { + // Error if we received the wrong kind of data. + if (typeof text !== "string") { + throw new TypeError(`Value provided must be a 'string', got '${typeof text}'.`); + } + + document.addEventListener( + "copy", + function (e) { + e.clipboardData.setData("text/plain", text); + e.preventDefault(); + }, + { once: true } + ); + document.execCommand("copy"); +} diff --git a/src/popup/models/Login.js b/src/popup/models/Login.js index 8d2e53bf..7acd2ca9 100644 --- a/src/popup/models/Login.js +++ b/src/popup/models/Login.js @@ -3,6 +3,7 @@ require("chrome-extension-async"); const sha1 = require("sha1"); const helpers = require("../../helpers"); +const helpersUI = require("../../helpers.ui"); const Settings = require("./Settings"); // Search for one of the secret prefixes @@ -54,7 +55,7 @@ function Login(settings, login = {}) { this.settings = settings; // This ensures doAction works in detailInterface, // and any other view in which it is necessary. - this.doAction = helpers.withLogin.bind({ + this.doAction = helpersUI.withLogin.bind({ settings: settings, login: login }); } diff --git a/src/popup/popup.js b/src/popup/popup.js index ff654ce0..8ee9c62e 100644 --- a/src/popup/popup.js +++ b/src/popup/popup.js @@ -7,7 +7,7 @@ require("chrome-extension-async"); const Login = require("./models/Login"); const Settings = require("./models/Settings"); // utils, libs -const helpers = require("../helpers"); +const helpers = require("../helpers.ui"); const m = require("mithril"); // components const AddEditInterface = require("./addEditInterface"); From 41c4a732d3ea5be180cce9ed43c9132ae052ce7e Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Mon, 30 Dec 2024 17:51:21 -0700 Subject: [PATCH 04/35] browserpass copy and fill actions work for FF and Chrome MV3, need to troubleshoot alarm to clear clipboard. browserpass/browserpass-extension#320 --- src/background.js | 85 +++++++++++++++++++++++++++++--------- src/manifest-chromium.json | 2 +- src/manifest-firefox.json | 21 +++++----- 3 files changed, 78 insertions(+), 30 deletions(-) diff --git a/src/background.js b/src/background.js index 126590c2..e0ec19bb 100644 --- a/src/background.js +++ b/src/background.js @@ -176,19 +176,47 @@ async function updateMatchingPasswordsCount(tabId, forceRefresh = false) { * @return void */ async function copyToClipboard(text, clear = true) { - await setupOffscreenDocument("offscreen/offscreen.html"); - chrome.runtime.sendMessage({ - type: "copy-data-to-clipboard", - target: "offscreen-doc", - data: text, - }); + console.debug(`copyToClipboard(${text}, ${clear})`); + if (isChrome()) { + await setupOffscreenDocument("offscreen/offscreen.html"); + chrome.runtime.sendMessage({ + type: "copy-data-to-clipboard", + target: "offscreen-doc", + data: text, + }); + } else { + document.addEventListener( + "copy", + function (e) { + e.clipboardData.setData("text/plain", text); + e.preventDefault(); + }, + { once: true } + ); + document.execCommand("copy"); + } + // @TODO: not sure alarm is firing when background is idle/inactive if (clear) { lastCopiedText = text; chrome.alarms.create("clearClipboard", { delayInMinutes: 1 }); } } +/** + * + */ +function isChrome() { + const ua = navigator.userAgent; + const matches = ua.match(/(chrom)/i) || []; + if (Object.keys(matches).length > 2 && /chrom/i.test(matches[1])) { + console.debug(`'isChrome == true`); + return true; + } + console.debug(`isChrome == false`); + return false; +} + /** * Read plain text from clipboard * @@ -197,22 +225,35 @@ async function copyToClipboard(text, clear = true) { * @return string The current plaintext content of the clipboard */ async function readFromClipboard() { - await setupOffscreenDocument("offscreen/offscreen.html"); + if (isChrome()) { + await setupOffscreenDocument("offscreen/offscreen.html"); - const response = await chrome.runtime.sendMessage({ - type: "read-from-clipboard", - target: "offscreen-doc", - }); + const response = await chrome.runtime.sendMessage({ + type: "read-from-clipboard", + target: "offscreen-doc", + }); - if (response.status != "ok") { - console.error( - "failure reading from clipboard in offscreen document", - response.message || undefined - ); - return; - } + if (response.status != "ok") { + console.error( + "failure reading from clipboard in offscreen document", + response.message || undefined + ); + return; + } - return response.message; + return response.message; + } else { + const ta = document.createElement("textarea"); + // these lines are carefully crafted to make paste work in both Chrome and Firefox + ta.contentEditable = true; + ta.textContent = ""; + document.body.appendChild(ta); + ta.select(); + document.execCommand("paste"); + const content = ta.value; + document.body.removeChild(ta); + return content; + } } /** @@ -571,6 +612,12 @@ async function getFullSettings() { defaultStore: {}, }); var response = await hostAction(configureSettings, "configure"); + console.debug( + `getFullSettings => hostAction(configureSettings..${ + Object.keys(configureSettings).length + }, configure)`, + { configureSettings } + ); if (response.status != "ok") { settings.hostError = response; } diff --git a/src/manifest-chromium.json b/src/manifest-chromium.json index ad0b8fc5..e77cc28b 100644 --- a/src/manifest-chromium.json +++ b/src/manifest-chromium.json @@ -42,7 +42,7 @@ "extension_pages": "default-src 'none'; font-src 'self'; img-src 'self' data:; script-src 'self'" }, "commands": { - "_execute_browser_action": { + "_execute_action": { "suggested_key": { "default": "Ctrl+Shift+L" } diff --git a/src/manifest-firefox.json b/src/manifest-firefox.json index 6b2c198f..601454e3 100644 --- a/src/manifest-firefox.json +++ b/src/manifest-firefox.json @@ -1,19 +1,18 @@ { - "manifest_version": 2, + "manifest_version": 3, "name": "Browserpass", "description": "Browser extension for zx2c4's pass (password manager)", "version": "3.9.0", "author": "Maxim Baz , Steve Gilberd ", "homepage_url": "https://github.com/browserpass/browserpass-extension", "background": { - "persistent": true, "scripts": ["js/background.dist.js"] }, "icons": { "16": "icon16.png", "128": "icon.png" }, - "browser_action": { + "action": { "default_icon": { "16": "icon16.png", "128": "icon.svg" @@ -32,20 +31,22 @@ "clipboardWrite", "nativeMessaging", "notifications", - "webRequest", - "webRequestBlocking", - "http://*/*", - "https://*/*" + "scripting", + "storage", + "webRequest" ], - "content_security_policy": "default-src 'none'; font-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self'", - "applications": { + "host_permissions": ["http://*/*", "https://*/*"], + "content_security_policy": { + "extension_pages": "default-src 'none'; font-src 'self'; img-src 'self' data:; script-src 'self'" + }, + "browser_specific_settings": { "gecko": { "id": "browserpass@maximbaz.com", "strict_min_version": "58.0" } }, "commands": { - "_execute_browser_action": { + "_execute_action": { "suggested_key": { "default": "Ctrl+Shift+L" } From e4fbbd21e782da7a7c9f8ffd34b7ced34a89063a Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Tue, 31 Dec 2024 12:26:07 -0700 Subject: [PATCH 05/35] fix clipboard clear and clean up some log messages. browserpass/browserpass-extension#320 --- src/background.js | 21 ++++++++++++++++++--- src/helpers.ui.js | 2 +- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/background.js b/src/background.js index e0ec19bb..df93589d 100644 --- a/src/background.js +++ b/src/background.js @@ -89,6 +89,16 @@ chrome.commands.onCommand.addListener(async (command) => { } }); +/** + * ensure service worker remains awake till clipboard is cleared + * + * @since 3.10.0 + */ +async function keepAlive() { + chrome.alarms.create("keepAlive", { when: Date.now() + 25e3 }); + await getFullSettings(); +} + // handle fired alarms chrome.alarms.onAlarm.addListener(async (alarm) => { if (alarm.name === "clearClipboard") { @@ -96,6 +106,13 @@ chrome.alarms.onAlarm.addListener(async (alarm) => { copyToClipboard("", false); } lastCopiedText = null; + } else if (alarm.name === "keepAlive") { + const current = await readFromClipboard(); + console.debug("keepAlive fired", { current, lastCopiedText }); + // stop if either value changes + if (current === lastCopiedText) { + await keepAlive() + } } }); @@ -176,7 +193,6 @@ async function updateMatchingPasswordsCount(tabId, forceRefresh = false) { * @return void */ async function copyToClipboard(text, clear = true) { - console.debug(`copyToClipboard(${text}, ${clear})`); if (isChrome()) { await setupOffscreenDocument("offscreen/offscreen.html"); chrome.runtime.sendMessage({ @@ -200,6 +216,7 @@ async function copyToClipboard(text, clear = true) { if (clear) { lastCopiedText = text; chrome.alarms.create("clearClipboard", { delayInMinutes: 1 }); + await keepAlive() } } @@ -210,10 +227,8 @@ function isChrome() { const ua = navigator.userAgent; const matches = ua.match(/(chrom)/i) || []; if (Object.keys(matches).length > 2 && /chrom/i.test(matches[1])) { - console.debug(`'isChrome == true`); return true; } - console.debug(`isChrome == false`); return false; } diff --git a/src/helpers.ui.js b/src/helpers.ui.js index 80fb7642..77edc32d 100644 --- a/src/helpers.ui.js +++ b/src/helpers.ui.js @@ -46,7 +46,7 @@ function highlight(secret = "") { function handleError(error, type = "error") { switch (type) { case "error": - console.log(error); + console.error(error); // disable error timeout, to allow necessary user action notify.errorMsg(error.toString(), 0); break; From 70a231fcae3197121568389bb56b12038cfded0b Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Tue, 31 Dec 2024 12:28:50 -0700 Subject: [PATCH 06/35] add doc string to isChrome method. browserpass/browserpass-extension#320 --- src/background.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/background.js b/src/background.js index df93589d..ac591dda 100644 --- a/src/background.js +++ b/src/background.js @@ -221,7 +221,9 @@ async function copyToClipboard(text, clear = true) { } /** + * returns true if agent string is Chrome / Chromium * + * @since 3.10.0 */ function isChrome() { const ua = navigator.userAgent; From bade9126097736ef6a3f8949952609fecd704ef6 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Wed, 12 Mar 2025 04:53:46 -0600 Subject: [PATCH 07/35] added async handler in background to webRequest for onAuthRequired, credential provision not yet implemented. browserpass/browserpass-extension#320 --- src/background.js | 111 +++++++++++++++++++++-------------- src/helpers.js | 28 +++++++++ src/helpers.ui.js | 19 ++++++ src/manifest-chromium.json | 3 +- src/manifest-firefox.json | 3 +- src/popup/searchinterface.js | 14 ++++- 6 files changed, 131 insertions(+), 47 deletions(-) diff --git a/src/background.js b/src/background.js index ac591dda..1641f9b2 100644 --- a/src/background.js +++ b/src/background.js @@ -89,6 +89,61 @@ chrome.commands.onCommand.addListener(async (command) => { } }); +let currentAuthRequest = null; + +function resolveAuthRequest(message, url) { + if (currentAuthRequest) { + if (new URL(currentAuthRequest.url).href === new URL(url).href) { + console.info("resolve current auth request", url); + currentAuthRequest.resolve(message); + chrome.windows.onRemoved.removeListener(currentAuthRequest.handleCloseAuthModal); + currentAuthRequest = null; + } + } else { + console.warn("resolve auth request received without existing details", url); + } +} + +async function createAuthRequestModal(url, callback) { + const popup = await chrome.windows.create({ + url: url, + width: 450, + left: 450, + height: 280, + top: 280, + type: "popup", + }); + + const handleCloseAuthModal = function (windowId) { + if (popup.id == windowId) { + console.info("clean up after auth request", { popup, url, windowId }); + resolveAuthRequest({ cancel: false }, url); + } + }; + + currentAuthRequest = { resolve: callback, url, handleCloseAuthModal }; + chrome.windows.onRemoved.addListener(handleCloseAuthModal); +} + +chrome.webRequest.onAuthRequired.addListener( + function (details, chromeOnlyAsyncCallback) { + console.debug("auth requested", { details, callback: chromeOnlyAsyncCallback }); + const url = `/popup/popup.html?authUrl=${encodeURIComponent(details.url)}`; + + return new Promise((resolvePromise, _) => { + const resolve = chromeOnlyAsyncCallback || resolvePromise; + if (currentAuthRequest) { + console.warn("another auth request is already in progress"); + resolve({}); + } else { + createAuthRequestModal(url, resolve); + } + }); + }, + { urls: [""] }, + helpers.isChrome() ? ["asyncBlocking"] : ["blocking"] +); + /** * ensure service worker remains awake till clipboard is cleared * @@ -111,7 +166,7 @@ chrome.alarms.onAlarm.addListener(async (alarm) => { console.debug("keepAlive fired", { current, lastCopiedText }); // stop if either value changes if (current === lastCopiedText) { - await keepAlive() + await keepAlive(); } } }); @@ -193,7 +248,7 @@ async function updateMatchingPasswordsCount(tabId, forceRefresh = false) { * @return void */ async function copyToClipboard(text, clear = true) { - if (isChrome()) { + if (helpers.isChrome()) { await setupOffscreenDocument("offscreen/offscreen.html"); chrome.runtime.sendMessage({ type: "copy-data-to-clipboard", @@ -212,28 +267,13 @@ async function copyToClipboard(text, clear = true) { document.execCommand("copy"); } - // @TODO: not sure alarm is firing when background is idle/inactive if (clear) { lastCopiedText = text; chrome.alarms.create("clearClipboard", { delayInMinutes: 1 }); - await keepAlive() + await keepAlive(); } } -/** - * returns true if agent string is Chrome / Chromium - * - * @since 3.10.0 - */ -function isChrome() { - const ua = navigator.userAgent; - const matches = ua.match(/(chrom)/i) || []; - if (Object.keys(matches).length > 2 && /chrom/i.test(matches[1])) { - return true; - } - return false; -} - /** * Read plain text from clipboard * @@ -242,7 +282,7 @@ function isChrome() { * @return string The current plaintext content of the clipboard */ async function readFromClipboard() { - if (isChrome()) { + if (helpers.isChrome()) { await setupOffscreenDocument("offscreen/offscreen.html"); const response = await chrome.runtime.sendMessage({ @@ -373,7 +413,7 @@ async function dispatchFill(settings, request, allFrames, allowForeign, allowNoS }); try { - await injectScript(settings, allFrames); + await injectScript(settings.tab, allFrames); } catch { throw new Error("Unable to inject script in the top frame"); } @@ -437,17 +477,17 @@ async function dispatchFocusOrSubmit(settings, request, allFrames, allowForeign) /** * Inject script * - * @param object settings Settings object + * @param object tab Tab object * @param boolean allFrames Inject in all frames * @return object Cancellable promise */ -async function injectScript(settings, allFrames) { +async function injectScript(tab, allFrames) { const MAX_WAIT = 1000; return new Promise(async (resolve, reject) => { const waitTimeout = setTimeout(reject, MAX_WAIT); await chrome.scripting.executeScript({ - target: { tabId: settings.tab.id, allFrames: allFrames }, + target: { tabId: tab.id, allFrames: allFrames }, files: ["js/inject.dist.js"], }); clearTimeout(waitTimeout); @@ -466,14 +506,14 @@ async function injectScript(settings, allFrames) { async function fillFields(settings, login, fields) { // inject script try { - await injectScript(settings, false); + await injectScript(settings.tab, false); } catch { throw new Error("Unable to inject script in the top frame"); } let injectedAllFrames = false; try { - await injectScript(settings, true); + await injectScript(settings.tab, true); injectedAllFrames = true; } catch { // we'll proceed with trying to fill only the top frame @@ -724,28 +764,11 @@ async function getFullSettings() { return settings; } -/** - * Get most relevant setting value - * - * @param string key Setting key - * @param object login Login object - * @param object settings Settings object - * @return object Setting value - */ -function getSetting(key, login, settings) { - if (typeof login.settings[key] !== "undefined") { - return login.settings[key]; - } - if (typeof settings.stores[login.store.id].settings[key] !== "undefined") { - return settings.stores[login.store.id].settings[key]; - } - - return settings[key]; -} - /** * Handle modal authentication requests (e.g. HTTP basic) * + * @deprecated 3.10.0 no longer supported by Chrome, due to removal + * of blocking webRequest * @since 3.0.0 * * @param object requestDetails Auth request details diff --git a/src/helpers.js b/src/helpers.js index edf09b8a..af6afc72 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -25,8 +25,10 @@ module.exports = { LATEST_NATIVE_APP_VERSION, deepCopy, filterSortLogins, + getPopupUrl, getSetting, ignoreFiles, + isChrome, makeTOTP, prepareLogin, prepareLogins, @@ -49,6 +51,17 @@ function deepCopy(obj) { return JSON.parse(JSON.stringify(obj)); } +/** + * Returns url string of the html popup page + * @since 3.10.0 + * + * @returns string + */ +function getPopupUrl() { + const base = chrome || browser; + return base.runtime.getURL("popup/popup.html"); +} + /* * Get most relevant setting value * @@ -68,6 +81,21 @@ function getSetting(key, login, settings) { return settings[key]; } +/** + * returns true if agent string is Chrome / Chromium + * + * @since 3.10.0 + */ +function isChrome() { + // return chrome.browser.runtime.getURL('/').startsWith('chrome') + const ua = navigator.userAgent; + const matches = ua.match(/(chrom)/i) || []; + if (Object.keys(matches).length > 2 && /chrom/i.test(matches[1])) { + return true; + } + return false; +} + /** * Get the deepest available domain component of a path * diff --git a/src/helpers.ui.js b/src/helpers.ui.js index 77edc32d..ed44c58d 100644 --- a/src/helpers.ui.js +++ b/src/helpers.ui.js @@ -11,6 +11,7 @@ const containsSymbolsRegEx = RegExp(/[\p{P}\p{S}]/, "u"); module.exports = { handleError, highlight, + parseAuthUrl, withLogin, }; @@ -69,6 +70,24 @@ function handleError(error, type = "error") { } } +/** + * Returns decoded url param for "authUrl" if present + * @since 3.10.0 + * @returns string | null + */ +function parseAuthUrl() { + const currentUrl = (window && `${window.location.origin}${window.location.pathname}`) || null; + console.debug("parseAuthUrl", { currentUrl }); + if (currentUrl === helpers.getPopupUrl()) { + const encodedUrl = new URLSearchParams(window.location.search).get("authUrl"); + console.debug("parseAuthUrl", { encodedUrl }); + if (encodedUrl) { + return decodeURIComponent(encodedUrl); + } + } + return null; +} + /** * Do a login action * diff --git a/src/manifest-chromium.json b/src/manifest-chromium.json index e77cc28b..20584b16 100644 --- a/src/manifest-chromium.json +++ b/src/manifest-chromium.json @@ -35,7 +35,8 @@ "offscreen", "scripting", "storage", - "webRequest" + "webRequest", + "webRequestAuthProvider" ], "host_permissions": ["http://*/*", "https://*/*"], "content_security_policy": { diff --git a/src/manifest-firefox.json b/src/manifest-firefox.json index 601454e3..cbe4a328 100644 --- a/src/manifest-firefox.json +++ b/src/manifest-firefox.json @@ -33,7 +33,8 @@ "notifications", "scripting", "storage", - "webRequest" + "webRequest", + "webRequestAuthProvider" ], "host_permissions": ["http://*/*", "https://*/*"], "content_security_policy": { diff --git a/src/popup/searchinterface.js b/src/popup/searchinterface.js index 9374fd79..1efee801 100644 --- a/src/popup/searchinterface.js +++ b/src/popup/searchinterface.js @@ -1,6 +1,8 @@ module.exports = SearchInterface; const BrowserpassURL = require("@browserpass/url"); +const helpers = require("../helpers"); +const helpersUI = require("../helpers.ui"); const m = require("mithril"); /** @@ -30,7 +32,17 @@ function SearchInterface(popup) { */ function view(ctl, params) { var self = this; - var host = new BrowserpassURL(this.popup.settings.origin).host; + const authUrl = helpersUI.parseAuthUrl(); + + let url = ""; + if (authUrl) { + url = new BrowserpassURL(authUrl); + } else { + url = new BrowserpassURL(this.popup.settings.origin); + } + console.debug("SearchInterface", { authUrl, url }); + var host = url.host; + return m( "form.part.search", { From 0322cf01ed834c8527beb305577acbefd031c8e2 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Wed, 12 Mar 2025 04:57:13 -0600 Subject: [PATCH 08/35] for security auth request url should include full extension url, not just file path. browserpass/browserpass-extension#320 --- src/background.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/background.js b/src/background.js index 1641f9b2..3ed7d6b0 100644 --- a/src/background.js +++ b/src/background.js @@ -128,7 +128,7 @@ async function createAuthRequestModal(url, callback) { chrome.webRequest.onAuthRequired.addListener( function (details, chromeOnlyAsyncCallback) { console.debug("auth requested", { details, callback: chromeOnlyAsyncCallback }); - const url = `/popup/popup.html?authUrl=${encodeURIComponent(details.url)}`; + const url = `${helpers.getPopupUrl()}?authUrl=${encodeURIComponent(details.url)}`; return new Promise((resolvePromise, _) => { const resolve = chromeOnlyAsyncCallback || resolvePromise; From c3447f2147ba4a46d6e327f23ac8a8426ff342e8 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Wed, 12 Mar 2025 05:03:30 -0600 Subject: [PATCH 09/35] add constant for auth url query param. browserpass/browserpass-extension#320 --- src/background.js | 4 +++- src/helpers.js | 2 ++ src/helpers.ui.js | 4 +++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/background.js b/src/background.js index 3ed7d6b0..b8624170 100644 --- a/src/background.js +++ b/src/background.js @@ -128,7 +128,9 @@ async function createAuthRequestModal(url, callback) { chrome.webRequest.onAuthRequired.addListener( function (details, chromeOnlyAsyncCallback) { console.debug("auth requested", { details, callback: chromeOnlyAsyncCallback }); - const url = `${helpers.getPopupUrl()}?authUrl=${encodeURIComponent(details.url)}`; + const url = + `${helpers.getPopupUrl()}` + + `?${helpers.AUTH_URL_QUERY_PARAM}=${encodeURIComponent(details.url)}`; return new Promise((resolvePromise, _) => { const resolve = chromeOnlyAsyncCallback || resolvePromise; diff --git a/src/helpers.js b/src/helpers.js index af6afc72..e1a5eab9 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -18,10 +18,12 @@ const fieldsPrefix = { const containsSymbolsRegEx = RegExp(/[\p{P}\p{S}]/, "u"); const LATEST_NATIVE_APP_VERSION = 3001000; +const AUTH_URL_QUERY_PARAM = "authUrl"; module.exports = { containsSymbolsRegEx, fieldsPrefix, + AUTH_URL_QUERY_PARAM, LATEST_NATIVE_APP_VERSION, deepCopy, filterSortLogins, diff --git a/src/helpers.ui.js b/src/helpers.ui.js index ed44c58d..efa1574b 100644 --- a/src/helpers.ui.js +++ b/src/helpers.ui.js @@ -79,7 +79,9 @@ function parseAuthUrl() { const currentUrl = (window && `${window.location.origin}${window.location.pathname}`) || null; console.debug("parseAuthUrl", { currentUrl }); if (currentUrl === helpers.getPopupUrl()) { - const encodedUrl = new URLSearchParams(window.location.search).get("authUrl"); + const encodedUrl = new URLSearchParams(window.location.search).get( + helpers.AUTH_URL_QUERY_PARAM + ); console.debug("parseAuthUrl", { encodedUrl }); if (encodedUrl) { return decodeURIComponent(encodedUrl); From 7eb69160e6fce72953f0862bc003e8bde51aa954 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Thu, 20 Mar 2025 20:49:19 -0600 Subject: [PATCH 10/35] async auth modal window now handles filtered search on auth host browserpass/browserpass-extension#320 --- src/background.js | 18 +++++++++++++++--- src/helpers.js | 24 +++++++++++++++++++++++- src/helpers.ui.js | 21 --------------------- src/popup/interface.js | 12 ++++++++++-- src/popup/searchinterface.js | 6 +++--- 5 files changed, 51 insertions(+), 30 deletions(-) diff --git a/src/background.js b/src/background.js index b8624170..4b943ea0 100644 --- a/src/background.js +++ b/src/background.js @@ -93,6 +93,7 @@ let currentAuthRequest = null; function resolveAuthRequest(message, url) { if (currentAuthRequest) { + console.info("attempting to resolve auth request", { currentAuthRequest, message, url }); if (new URL(currentAuthRequest.url).href === new URL(url).href) { console.info("resolve current auth request", url); currentAuthRequest.resolve(message); @@ -105,13 +106,15 @@ function resolveAuthRequest(message, url) { } async function createAuthRequestModal(url, callback) { + // https://developer.chrome.com/docs/extensions/reference/api/windows const popup = await chrome.windows.create({ url: url, width: 450, left: 450, - height: 280, - top: 280, + height: 300, + top: 300, type: "popup", + focused: true, }); const handleCloseAuthModal = function (windowId) { @@ -761,7 +764,16 @@ async function getFullSettings() { settings.tab = (await chrome.tabs.query({ active: true, currentWindow: true }))[0]; let originInfo = new BrowserpassURL(settings.tab.url); settings.origin = originInfo.origin; - } catch (e) {} + + const authUrl = helpers.parseAuthUrl(settings.tab.url); + if (authUrl && currentAuthRequest && currentAuthRequest.url) { + settings.authRequested = authUrl.startsWith( + helpers.parseAuthUrl(currentAuthRequest.url) + ); + } + } catch (e) { + console.error(`getFullsettings() failure getting tab: ${e}`, { e }); + } return settings; } diff --git a/src/helpers.js b/src/helpers.js index e1a5eab9..68dbd94a 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -32,6 +32,7 @@ module.exports = { ignoreFiles, isChrome, makeTOTP, + parseAuthUrl, prepareLogin, prepareLogins, }; @@ -131,6 +132,25 @@ function pathToInfo(path, currentHost) { return null; } +/** + * Returns decoded url param for "authUrl" if present + * @since 3.10.0 + * @param string url string to parse and compare against extension popup url + * @returns string | null + */ +function parseAuthUrl(url) { + const currentUrl = url || null; + + // query url not exact match when includes fragments, so must start with extension url + if (currentUrl && `${currentUrl}`.startsWith(getPopupUrl())) { + const encodedUrl = new URL(currentUrl).searchParams.get(AUTH_URL_QUERY_PARAM); + if (encodedUrl) { + return decodeURIComponent(encodedUrl); + } + } + return null; +} + /** * Prepare list of logins based on provided files * @@ -143,7 +163,9 @@ function pathToInfo(path, currentHost) { function prepareLogins(files, settings) { const logins = []; let index = 0; - let origin = new BrowserpassURL(settings.origin); + let origin = new BrowserpassURL( + settings.authRequested ? parseAuthUrl(settings.tab.url) : settings.origin + ); for (let storeId in files) { for (let key in files[storeId]) { diff --git a/src/helpers.ui.js b/src/helpers.ui.js index efa1574b..77edc32d 100644 --- a/src/helpers.ui.js +++ b/src/helpers.ui.js @@ -11,7 +11,6 @@ const containsSymbolsRegEx = RegExp(/[\p{P}\p{S}]/, "u"); module.exports = { handleError, highlight, - parseAuthUrl, withLogin, }; @@ -70,26 +69,6 @@ function handleError(error, type = "error") { } } -/** - * Returns decoded url param for "authUrl" if present - * @since 3.10.0 - * @returns string | null - */ -function parseAuthUrl() { - const currentUrl = (window && `${window.location.origin}${window.location.pathname}`) || null; - console.debug("parseAuthUrl", { currentUrl }); - if (currentUrl === helpers.getPopupUrl()) { - const encodedUrl = new URLSearchParams(window.location.search).get( - helpers.AUTH_URL_QUERY_PARAM - ); - console.debug("parseAuthUrl", { encodedUrl }); - if (encodedUrl) { - return decodeURIComponent(encodedUrl); - } - } - return null; -} - /** * Do a login action * diff --git a/src/popup/interface.js b/src/popup/interface.js index 4054e45a..0c24ff98 100644 --- a/src/popup/interface.js +++ b/src/popup/interface.js @@ -5,7 +5,7 @@ const Moment = require("moment"); const SearchInterface = require("./searchinterface"); const layout = require("./layoutInterface"); const helpers = require("../helpers"); -const Settings = require("./models/Settings"); +let overrideDefaultSearchOnce = true; /** * Popup main interface @@ -228,7 +228,15 @@ function renderMainView(ctl, params) { * @return void */ function search(searchQuery) { - this.results = helpers.filterSortLogins(this.logins, searchQuery, this.currentDomainOnly); + const authUrl = overrideDefaultSearchOnce && helpers.parseAuthUrl(this.settings.tab.url); + + if (overrideDefaultSearchOnce && this.settings.authRequested && authUrl) { + const authUrlInfo = new URL(authUrl); + this.results = helpers.filterSortLogins(this.logins, authUrlInfo.host, true); + } else { + this.results = helpers.filterSortLogins(this.logins, searchQuery, this.currentDomainOnly); + } + overrideDefaultSearchOnce = false; } /** diff --git a/src/popup/searchinterface.js b/src/popup/searchinterface.js index 1efee801..bc14ac5d 100644 --- a/src/popup/searchinterface.js +++ b/src/popup/searchinterface.js @@ -32,15 +32,15 @@ function SearchInterface(popup) { */ function view(ctl, params) { var self = this; - const authUrl = helpersUI.parseAuthUrl(); let url = ""; - if (authUrl) { + const authUrl = helpers.parseAuthUrl((window && `${window.location.href}`) || null); + if (this.popup.settings.authRequested && authUrl) { url = new BrowserpassURL(authUrl); } else { url = new BrowserpassURL(this.popup.settings.origin); } - console.debug("SearchInterface", { authUrl, url }); + var host = url.host; return m( From bc037a748b7ff4fd7987b3b75a02d095730b6558 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Fri, 21 Mar 2025 19:12:31 -0600 Subject: [PATCH 11/35] fix display quirk for detached popup window browserpass/browserpass-extension#320 --- src/popup/popup.js | 9 +++++++++ src/popup/popup.less | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/src/popup/popup.js b/src/popup/popup.js index 8ee9c62e..41f128e3 100644 --- a/src/popup/popup.js +++ b/src/popup/popup.js @@ -43,6 +43,15 @@ async function run() { root.classList.remove("colors-dark"); root.classList.add(`colors-${settings.theme}`); + /** + * Only set width: min-content for the attached popup, + * and allow content to fill detached window + */ + if (!Object.prototype.hasOwnProperty.call(settings, "authRequested")) { + root.classList.add("attached"); + document.getElementsByTagName("body")[0].classList.add("attached"); + } + // set theme const theme = settings.theme === "auto" diff --git a/src/popup/popup.less b/src/popup/popup.less index 0db03f65..60968771 100644 --- a/src/popup/popup.less +++ b/src/popup/popup.less @@ -36,6 +36,10 @@ body { min-width: 260px; overflow-x: hidden; white-space: nowrap; +} + +html.attached, +body.attached { width: min-content; } From 26e5b2d7ab60ec602102d9cf74fba3582e2792b8 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Fri, 21 Mar 2025 20:52:37 -0600 Subject: [PATCH 12/35] first working draft of successful auth request browserpass/browserpass-extension#320 --- src/background.js | 37 ++++++++++++++++++++++++++++++------- src/manifest-chromium.json | 1 + 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/background.js b/src/background.js index 4b943ea0..10517e1e 100644 --- a/src/background.js +++ b/src/background.js @@ -94,7 +94,7 @@ let currentAuthRequest = null; function resolveAuthRequest(message, url) { if (currentAuthRequest) { console.info("attempting to resolve auth request", { currentAuthRequest, message, url }); - if (new URL(currentAuthRequest.url).href === new URL(url).href) { + if (new URL(url).href.startsWith(new URL(currentAuthRequest.url).href)) { console.info("resolve current auth request", url); currentAuthRequest.resolve(message); chrome.windows.onRemoved.removeListener(currentAuthRequest.handleCloseAuthModal); @@ -105,7 +105,7 @@ function resolveAuthRequest(message, url) { } } -async function createAuthRequestModal(url, callback) { +async function createAuthRequestModal(url, callback, details) { // https://developer.chrome.com/docs/extensions/reference/api/windows const popup = await chrome.windows.create({ url: url, @@ -118,13 +118,16 @@ async function createAuthRequestModal(url, callback) { }); const handleCloseAuthModal = function (windowId) { + console.info("pop up clean up called", { windowId }); if (popup.id == windowId) { console.info("clean up after auth request", { popup, url, windowId }); resolveAuthRequest({ cancel: false }, url); + } else { + console.warn("pop window clean up did not match", { popup, windowId }); } }; - currentAuthRequest = { resolve: callback, url, handleCloseAuthModal }; + currentAuthRequest = { resolve: callback, url, details, handleCloseAuthModal }; chrome.windows.onRemoved.addListener(handleCloseAuthModal); } @@ -141,7 +144,7 @@ chrome.webRequest.onAuthRequired.addListener( console.warn("another auth request is already in progress"); resolve({}); } else { - createAuthRequestModal(url, resolve); + createAuthRequestModal(url, resolve, details); } }); }, @@ -1050,9 +1053,29 @@ async function handleMessage(settings, message, sendResponse) { try { let fields = message.login.fields.openid ? ["openid"] : ["login", "secret"]; - // dispatch initial fill request - var filledFields = await fillFields(settings, message.login, fields); - await saveRecent(settings, message.login); + if (settings.authRequested) { + console.info("handleMessage() attempt to resolve current auth request: ", { + currentAuthRequest, + login: message.login, + fields, + }); + resolveAuthRequest( + { + authCredentials: { + username: message.login.fields.login, + password: message.login.fields.secret, + }, + }, + settings.tab.url + ); + await saveRecent(settings, message.login); + // don't send response window is closed + break; + } else { + // dispatch initial fill request + var filledFields = await fillFields(settings, message.login, fields); + await saveRecent(settings, message.login); + } // no need to check filledFields, because fillFields() already throws an error if empty sendResponse({ status: "ok", filledFields: filledFields }); diff --git a/src/manifest-chromium.json b/src/manifest-chromium.json index 20584b16..a7c0758e 100644 --- a/src/manifest-chromium.json +++ b/src/manifest-chromium.json @@ -35,6 +35,7 @@ "offscreen", "scripting", "storage", + "webNavigation", "webRequest", "webRequestAuthProvider" ], From 6cbb5b3eb3062770bbd01e0e3ed6d9ab29a83402 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Sat, 22 Mar 2025 08:42:50 -0600 Subject: [PATCH 13/35] async auth request is now working, beta stable browserpass/browserpass-extension#320 --- src/background.js | 55 +++++++++--------------------------- src/manifest-chromium.json | 1 - src/popup/searchinterface.js | 1 - 3 files changed, 14 insertions(+), 43 deletions(-) diff --git a/src/background.js b/src/background.js index 10517e1e..78e16276 100644 --- a/src/background.js +++ b/src/background.js @@ -91,17 +91,15 @@ chrome.commands.onCommand.addListener(async (command) => { let currentAuthRequest = null; -function resolveAuthRequest(message, url) { +function resolveAuthRequest(message, senderUrl) { if (currentAuthRequest) { - console.info("attempting to resolve auth request", { currentAuthRequest, message, url }); - if (new URL(url).href.startsWith(new URL(currentAuthRequest.url).href)) { - console.info("resolve current auth request", url); + if (new URL(senderUrl).href.startsWith(new URL(currentAuthRequest.url).href)) { + console.info("Resolve current auth request", senderUrl); currentAuthRequest.resolve(message); - chrome.windows.onRemoved.removeListener(currentAuthRequest.handleCloseAuthModal); currentAuthRequest = null; } } else { - console.warn("resolve auth request received without existing details", url); + console.warn("Resolve auth request received without existing details", senderUrl); } } @@ -117,23 +115,11 @@ async function createAuthRequestModal(url, callback, details) { focused: true, }); - const handleCloseAuthModal = function (windowId) { - console.info("pop up clean up called", { windowId }); - if (popup.id == windowId) { - console.info("clean up after auth request", { popup, url, windowId }); - resolveAuthRequest({ cancel: false }, url); - } else { - console.warn("pop window clean up did not match", { popup, windowId }); - } - }; - - currentAuthRequest = { resolve: callback, url, details, handleCloseAuthModal }; - chrome.windows.onRemoved.addListener(handleCloseAuthModal); + currentAuthRequest = { resolve: callback, url, details, popup }; } chrome.webRequest.onAuthRequired.addListener( function (details, chromeOnlyAsyncCallback) { - console.debug("auth requested", { details, callback: chromeOnlyAsyncCallback }); const url = `${helpers.getPopupUrl()}` + `?${helpers.AUTH_URL_QUERY_PARAM}=${encodeURIComponent(details.url)}`; @@ -141,7 +127,7 @@ chrome.webRequest.onAuthRequired.addListener( return new Promise((resolvePromise, _) => { const resolve = chromeOnlyAsyncCallback || resolvePromise; if (currentAuthRequest) { - console.warn("another auth request is already in progress"); + console.warn("Another auth request is already in progress"); resolve({}); } else { createAuthRequestModal(url, resolve, details); @@ -677,12 +663,7 @@ async function getFullSettings() { defaultStore: {}, }); var response = await hostAction(configureSettings, "configure"); - console.debug( - `getFullSettings => hostAction(configureSettings..${ - Object.keys(configureSettings).length - }, configure)`, - { configureSettings } - ); + if (response.status != "ok") { settings.hostError = response; } @@ -767,7 +748,12 @@ async function getFullSettings() { settings.tab = (await chrome.tabs.query({ active: true, currentWindow: true }))[0]; let originInfo = new BrowserpassURL(settings.tab.url); settings.origin = originInfo.origin; + } catch (e) { + console.error(`getFullsettings() failure getting tab: ${e}`, { e }); + } + // check for auth url + try { const authUrl = helpers.parseAuthUrl(settings.tab.url); if (authUrl && currentAuthRequest && currentAuthRequest.url) { settings.authRequested = authUrl.startsWith( @@ -775,7 +761,7 @@ async function getFullSettings() { ); } } catch (e) { - console.error(`getFullsettings() failure getting tab: ${e}`, { e }); + console.error(`getFullsettings() failure parsing auth url: ${e}`, { e }); } return settings; @@ -1054,11 +1040,6 @@ async function handleMessage(settings, message, sendResponse) { let fields = message.login.fields.openid ? ["openid"] : ["login", "secret"]; if (settings.authRequested) { - console.info("handleMessage() attempt to resolve current auth request: ", { - currentAuthRequest, - login: message.login, - fields, - }); resolveAuthRequest( { authCredentials: { @@ -1069,7 +1050,7 @@ async function handleMessage(settings, message, sendResponse) { settings.tab.url ); await saveRecent(settings, message.login); - // don't send response window is closed + sendResponse({ status: "ok" }); break; } else { // dispatch initial fill request @@ -1301,13 +1282,6 @@ async function receiveMessage(message, sender, sendResponse) { } try { - const msgLen = Object.keys(message).length; - const sendLen = Object.keys(sender).length; - const sendRes = typeof sendResponse; - console.debug( - `receiveMessage(message..${msgLen}, sender..${sendLen}, sendResponse..${sendRes})`, - { message, sender, sendResponse } - ); const settings = await getFullSettings(); handleMessage(settings, message, sendResponse); } catch (e) { @@ -1369,7 +1343,6 @@ async function saveSettings(settings) { if (settingsToSave.hasOwnProperty(key)) { const save = {}; save[key] = settingsToSave[key]; - console.info(`saving ${key}`, save); await chrome.storage.local.set(save); } } diff --git a/src/manifest-chromium.json b/src/manifest-chromium.json index a7c0758e..20584b16 100644 --- a/src/manifest-chromium.json +++ b/src/manifest-chromium.json @@ -35,7 +35,6 @@ "offscreen", "scripting", "storage", - "webNavigation", "webRequest", "webRequestAuthProvider" ], diff --git a/src/popup/searchinterface.js b/src/popup/searchinterface.js index bc14ac5d..b87f6c9e 100644 --- a/src/popup/searchinterface.js +++ b/src/popup/searchinterface.js @@ -2,7 +2,6 @@ module.exports = SearchInterface; const BrowserpassURL = require("@browserpass/url"); const helpers = require("../helpers"); -const helpersUI = require("../helpers.ui"); const m = require("mithril"); /** From e9c97d0efe6d72848400529ba92ba29d13549b0f Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Sat, 22 Mar 2025 16:29:05 -0600 Subject: [PATCH 14/35] attempt to fix some issues for fillFields due to difference in api for inject / scripting, browserpass/browserpass-extension#320 --- src/background.js | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/background.js b/src/background.js index 78e16276..b3f25cae 100644 --- a/src/background.js +++ b/src/background.js @@ -407,7 +407,7 @@ async function dispatchFill(settings, request, allFrames, allowForeign, allowNoS }); try { - await injectScript(settings.tab, allFrames); + await injectScript(settings, allFrames); } catch { throw new Error("Unable to inject script in the top frame"); } @@ -415,7 +415,7 @@ async function dispatchFill(settings, request, allFrames, allowForeign, allowNoS let perFrameResults = await chrome.scripting.executeScript({ target: { tabId: settings.tab.id, allFrames: allFrames }, func: function (request) { - window.browserpass.fillLogin(request); + return window.browserpass.fillLogin(request); }, args: [request], }); @@ -423,7 +423,7 @@ async function dispatchFill(settings, request, allFrames, allowForeign, allowNoS // merge filled fields into a single array let filledFields = perFrameResults .reduce((merged, frameResult) => merged.concat(frameResult.filledFields), []) - .filter((val, i, merged) => merged.indexOf(val) === i); + .filter((val, i, merged) => val && merged.indexOf(val) === i); // if user answered a foreign-origin confirmation, // store the answers in the settings @@ -441,6 +441,11 @@ async function dispatchFill(settings, request, allFrames, allowForeign, allowNoS await saveSettings(settings); } + console.debug("dispatchFill finished => ", { + foreignFillsChanged, + filledFields, + perFrameResults, + }); return filledFields; } @@ -471,17 +476,17 @@ async function dispatchFocusOrSubmit(settings, request, allFrames, allowForeign) /** * Inject script * - * @param object tab Tab object + * @param object settings Settings object * @param boolean allFrames Inject in all frames * @return object Cancellable promise */ -async function injectScript(tab, allFrames) { +async function injectScript(settings, allFrames) { const MAX_WAIT = 1000; return new Promise(async (resolve, reject) => { const waitTimeout = setTimeout(reject, MAX_WAIT); await chrome.scripting.executeScript({ - target: { tabId: tab.id, allFrames: allFrames }, + target: { tabId: settings.tab.id, allFrames: allFrames }, files: ["js/inject.dist.js"], }); clearTimeout(waitTimeout); @@ -500,14 +505,14 @@ async function injectScript(tab, allFrames) { async function fillFields(settings, login, fields) { // inject script try { - await injectScript(settings.tab, false); + await injectScript(settings, false); } catch { throw new Error("Unable to inject script in the top frame"); } let injectedAllFrames = false; try { - await injectScript(settings.tab, true); + await injectScript(settings, true); injectedAllFrames = true; } catch { // we'll proceed with trying to fill only the top frame @@ -746,19 +751,23 @@ async function getFullSettings() { // Fill current tab info try { settings.tab = (await chrome.tabs.query({ active: true, currentWindow: true }))[0]; - let originInfo = new BrowserpassURL(settings.tab.url); - settings.origin = originInfo.origin; + if (settings.tab) { + let originInfo = new BrowserpassURL(settings.tab.url); + settings.origin = originInfo.origin; + } } catch (e) { console.error(`getFullsettings() failure getting tab: ${e}`, { e }); } // check for auth url try { - const authUrl = helpers.parseAuthUrl(settings.tab.url); - if (authUrl && currentAuthRequest && currentAuthRequest.url) { - settings.authRequested = authUrl.startsWith( - helpers.parseAuthUrl(currentAuthRequest.url) - ); + if (settings.tab) { + const authUrl = helpers.parseAuthUrl(settings.tab.url); + if (authUrl && currentAuthRequest && currentAuthRequest.url) { + settings.authRequested = authUrl.startsWith( + helpers.parseAuthUrl(currentAuthRequest.url) + ); + } } } catch (e) { console.error(`getFullsettings() failure parsing auth url: ${e}`, { e }); From aa3ff8f8a4febfa3a4eca4b0201b1f978b271459 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Sat, 22 Mar 2025 16:39:56 -0600 Subject: [PATCH 15/35] remove extra inject script leftover from initial troubleshooting. browserpass/browserpass-extension#320 --- src/background.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/background.js b/src/background.js index b3f25cae..222dc5a2 100644 --- a/src/background.js +++ b/src/background.js @@ -406,12 +406,6 @@ async function dispatchFill(settings, request, allFrames, allowForeign, allowNoS foreignFills: settings.foreignFills[settings.origin] || {}, }); - try { - await injectScript(settings, allFrames); - } catch { - throw new Error("Unable to inject script in the top frame"); - } - let perFrameResults = await chrome.scripting.executeScript({ target: { tabId: settings.tab.id, allFrames: allFrames }, func: function (request) { From 0da8990053a490c5575bedec53411cfa3a6de716 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Sat, 22 Mar 2025 20:37:06 -0600 Subject: [PATCH 16/35] fixed dispatchFill parsing of results.filledFields, browserpass/browserpass-extension#320 --- src/background.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/background.js b/src/background.js index 222dc5a2..4b24fc85 100644 --- a/src/background.js +++ b/src/background.js @@ -416,7 +416,7 @@ async function dispatchFill(settings, request, allFrames, allowForeign, allowNoS // merge filled fields into a single array let filledFields = perFrameResults - .reduce((merged, frameResult) => merged.concat(frameResult.filledFields), []) + .reduce((merged, frameResult) => merged.concat(frameResult.result.filledFields), []) .filter((val, i, merged) => val && merged.indexOf(val) === i); // if user answered a foreign-origin confirmation, @@ -435,11 +435,6 @@ async function dispatchFill(settings, request, allFrames, allowForeign, allowNoS await saveSettings(settings); } - console.debug("dispatchFill finished => ", { - foreignFillsChanged, - filledFields, - perFrameResults, - }); return filledFields; } From b3deb12211b899f007c3570caf052fc2a1a6b8b5 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Tue, 25 Mar 2025 17:14:00 -0600 Subject: [PATCH 17/35] add fall back clean up for auth request browserpass/browserpass-extension#320 --- src/background.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/background.js b/src/background.js index 4b24fc85..3594d18a 100644 --- a/src/background.js +++ b/src/background.js @@ -115,7 +115,17 @@ async function createAuthRequestModal(url, callback, details) { focused: true, }); + function onPopupClose(windowId) { + console.debug("onPopupClose", { windowId, currentAuthRequest }); + const waitingRequestId = + (currentAuthRequest && currentAuthRequest.popup && currentAuthRequest.popup.id) || + false; + if (waitingRequestId === windowId) { + chrome.alarms.create("clearAuthRequest", { when: Date.now() + 1e3 }); + } + } currentAuthRequest = { resolve: callback, url, details, popup }; + chrome.windows.onRemoved.addListener(onPopupClose); } chrome.webRequest.onAuthRequired.addListener( @@ -151,6 +161,7 @@ async function keepAlive() { // handle fired alarms chrome.alarms.onAlarm.addListener(async (alarm) => { if (alarm.name === "clearClipboard") { + console.debug("clearClipboard fired", { current, lastCopiedText }); if ((await readFromClipboard()) === lastCopiedText) { copyToClipboard("", false); } @@ -162,6 +173,11 @@ chrome.alarms.onAlarm.addListener(async (alarm) => { if (current === lastCopiedText) { await keepAlive(); } + } else if (alarm.name === "clearAuthRequest") { + console.debug("clearAuthRequest fired", { current, lastCopiedText }); + if (currentAuthRequest !== null) { + currentAuthRequest = null; + } } }); From 448c0c01896c094d5a7d1a69c485b8fc1d44d9ef Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Tue, 25 Mar 2025 18:10:14 -0600 Subject: [PATCH 18/35] fix search for auth to filter on BrowserpassURL().domain instead of URL().host, browserpass/browserpass-extension#320 --- src/popup/interface.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/popup/interface.js b/src/popup/interface.js index 0c24ff98..61a4a409 100644 --- a/src/popup/interface.js +++ b/src/popup/interface.js @@ -3,6 +3,7 @@ module.exports = Interface; const m = require("mithril"); const Moment = require("moment"); const SearchInterface = require("./searchinterface"); +const BrowserpassURL = require("@browserpass/url"); const layout = require("./layoutInterface"); const helpers = require("../helpers"); let overrideDefaultSearchOnce = true; @@ -231,8 +232,8 @@ function search(searchQuery) { const authUrl = overrideDefaultSearchOnce && helpers.parseAuthUrl(this.settings.tab.url); if (overrideDefaultSearchOnce && this.settings.authRequested && authUrl) { - const authUrlInfo = new URL(authUrl); - this.results = helpers.filterSortLogins(this.logins, authUrlInfo.host, true); + const authUrlInfo = new BrowserpassURL(authUrl); + this.results = helpers.filterSortLogins(this.logins, authUrlInfo.domain, true); } else { this.results = helpers.filterSortLogins(this.logins, searchQuery, this.currentDomainOnly); } From bb094de222b4f322507708c95a37adbe355bb00c Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Tue, 25 Mar 2025 22:49:31 -0600 Subject: [PATCH 19/35] extend modal dialog functionality to confirm/acknowledgement, browserpass/browserpass-extension#320 --- src/popup/modalDialog.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/popup/modalDialog.js b/src/popup/modalDialog.js index 9d394b1b..fe53a0b8 100644 --- a/src/popup/modalDialog.js +++ b/src/popup/modalDialog.js @@ -41,15 +41,17 @@ let Modal = { return m("dialog", { id: modalId }, [ m(".modal-content", {}, m.trust(modalContent)), m(".modal-actions", {}, [ - m( - "button.cancel", - { - onclick: () => { - buttonClick(false); - }, - }, - cancelButtonText - ), + cancelButtonText + ? m( + "button.cancel", + { + onclick: () => { + buttonClick(false); + }, + }, + cancelButtonText + ) + : null, m( "button.confirm", { @@ -84,6 +86,8 @@ let Modal = { if (typeof cancelText == "string" && cancelText.length) { cancelButtonText = cancelText; + } else if (cancelText === false) { + cancelButtonText = undefined; } else { cancelButtonText = CANCEL; } From dbb6a5fa13666eccd05094bf7d254bf9b618573e Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Tue, 25 Mar 2025 22:56:58 -0600 Subject: [PATCH 20/35] remove obsolete basic auth injection handler and add deprecation notice of url open shortcut, browserpass/browserpass-extension#320 --- src/background.js | 77 ------------------------------------ src/helpers.js | 2 + src/popup/interface.js | 11 +++++- src/popup/searchinterface.js | 13 +++++- 4 files changed, 23 insertions(+), 80 deletions(-) diff --git a/src/background.js b/src/background.js index 3594d18a..7ddf5e3e 100644 --- a/src/background.js +++ b/src/background.js @@ -781,70 +781,6 @@ async function getFullSettings() { return settings; } -/** - * Handle modal authentication requests (e.g. HTTP basic) - * - * @deprecated 3.10.0 no longer supported by Chrome, due to removal - * of blocking webRequest - * @since 3.0.0 - * - * @param object requestDetails Auth request details - * @return object Authentication credentials or {} - */ -function handleModalAuth(requestDetails) { - var launchHost = requestDetails.url.match(/:\/\/([^\/]+)/)[1]; - - // don't attempt authentication against the same login more than once - if (!this.login.allowFill) { - return {}; - } - this.login.allowFill = false; - - // don't attempt authentication outside the main frame - if (requestDetails.type !== "main_frame") { - return {}; - } - - // ensure the auth domain is the same, or ask the user for permissions to continue - if (launchHost !== requestDetails.challenger.host) { - var message = - "You are about to send login credentials to a domain that is different than " + - "the one you launched from the browserpass extension. Do you wish to proceed?\n\n" + - "Realm: " + - requestDetails.realm + - "\n" + - "Launched URL: " + - this.url + - "\n" + - "Authentication URL: " + - requestDetails.url; - if (!confirm(message)) { - return {}; - } - } - - // ask the user before sending credentials over an insecure connection - if (!requestDetails.url.match(/^https:/i)) { - var message = - "You are about to send login credentials via an insecure connection!\n\n" + - "Are you sure you want to do this? If there is an attacker watching your " + - "network traffic, they may be able to see your username and password.\n\n" + - "URL: " + - requestDetails.url; - if (!confirm(message)) { - return {}; - } - } - - // supply credentials - return { - authCredentials: { - username: this.login.fields.login, - password: this.login.fields.secret, - }, - }; -} - /** * Handle a message from elsewhere within the extension * @@ -1028,19 +964,6 @@ async function handleMessage(settings, message, sendResponse) { ? await chrome.tabs.update(settings.tab.id, { url: url }) : await chrome.tabs.create({ url: url }); - if (authListeners[tab.id]) { - chrome.tabs.onUpdated.removeListener(authListeners[tab.id]); - delete authListeners[tab.id]; - } - authListeners[tab.id] = handleModalAuth.bind({ - url: url, - login: message.login, - }); - chrome.webRequest.onAuthRequired.addListener( - authListeners[tab.id], - { urls: ["*://*/*"], tabId: tab.id }, - ["blocking"] - ); sendResponse({ status: "ok" }); } catch (e) { sendResponse({ diff --git a/src/helpers.js b/src/helpers.js index 68dbd94a..16de3bf9 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -19,12 +19,14 @@ const fieldsPrefix = { const containsSymbolsRegEx = RegExp(/[\p{P}\p{S}]/, "u"); const LATEST_NATIVE_APP_VERSION = 3001000; const AUTH_URL_QUERY_PARAM = "authUrl"; +const LAUNCH_URL_DEPRECATION_MESSAGE = `"Ctrl+G" and "Ctrl+Shift+G" shortcuts are deprecated and will be removed in a future version. You no longer need to open websites that require basic auth using these shortcuts, open websites normally and Browserpass will open a popup for you to choose the credentials.`; module.exports = { containsSymbolsRegEx, fieldsPrefix, AUTH_URL_QUERY_PARAM, LATEST_NATIVE_APP_VERSION, + LAUNCH_URL_DEPRECATION_MESSAGE, deepCopy, filterSortLogins, getPopupUrl, diff --git a/src/popup/interface.js b/src/popup/interface.js index 61a4a409..475e9e3e 100644 --- a/src/popup/interface.js +++ b/src/popup/interface.js @@ -5,6 +5,7 @@ const Moment = require("moment"); const SearchInterface = require("./searchinterface"); const BrowserpassURL = require("@browserpass/url"); const layout = require("./layoutInterface"); +const dialog = require("./modalDialog"); const helpers = require("../helpers"); let overrideDefaultSearchOnce = true; @@ -318,7 +319,15 @@ function keyHandler(e) { break; case "KeyG": if (e.ctrlKey) { - this.doAction(e.shiftKey ? "launchInNewTab" : "launch"); + const event = e; + const target = this; + dialog.open( + helpers.LAUNCH_URL_DEPRECATION_MESSAGE, + function () { + target.doAction(event.shiftKey ? "launchInNewTab" : "launch"); + }, + false + ); } break; case "KeyO": diff --git a/src/popup/searchinterface.js b/src/popup/searchinterface.js index b87f6c9e..4cc6a3f1 100644 --- a/src/popup/searchinterface.js +++ b/src/popup/searchinterface.js @@ -1,6 +1,7 @@ module.exports = SearchInterface; const BrowserpassURL = require("@browserpass/url"); +const dialog = require("./modalDialog"); const helpers = require("../helpers"); const m = require("mithril"); @@ -132,8 +133,16 @@ function view(ctl, params) { case "KeyG": if (e.ctrlKey && e.target.selectionStart == e.target.selectionEnd) { e.preventDefault(); - self.popup.results[0].doAction( - e.shiftKey ? "launchInNewTab" : "launch" + const event = e; + const target = self.popup.results[0]; + dialog.open( + helpers.LAUNCH_URL_DEPRECATION_MESSAGE, + function () { + target.doAction( + event.shiftKey ? "launchInNewTab" : "launch" + ); + }, + false ); } break; From fe10fc39efb213e7ebe5286465af48969dfd4d19 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Tue, 25 Mar 2025 23:24:58 -0600 Subject: [PATCH 21/35] add a little style + format to auth deprecation message and override agent styles, browserpass/browserpass-extension#320 --- src/helpers.js | 2 +- src/popup/popup.less | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/helpers.js b/src/helpers.js index 16de3bf9..4a764015 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -19,7 +19,7 @@ const fieldsPrefix = { const containsSymbolsRegEx = RegExp(/[\p{P}\p{S}]/, "u"); const LATEST_NATIVE_APP_VERSION = 3001000; const AUTH_URL_QUERY_PARAM = "authUrl"; -const LAUNCH_URL_DEPRECATION_MESSAGE = `"Ctrl+G" and "Ctrl+Shift+G" shortcuts are deprecated and will be removed in a future version. You no longer need to open websites that require basic auth using these shortcuts, open websites normally and Browserpass will open a popup for you to choose the credentials.`; +const LAUNCH_URL_DEPRECATION_MESSAGE = `

"Ctrl+G" and "Ctrl+Shift+G" shortcuts are deprecated and will be removed in a future version.

It is no longer necessary to open websites which require basic auth using these shortcuts. Navigate websites normally and Browserpass will automatically open a popup for you to choose the credentials.

`; module.exports = { containsSymbolsRegEx, diff --git a/src/popup/popup.less b/src/popup/popup.less index 60968771..30efe8fa 100644 --- a/src/popup/popup.less +++ b/src/popup/popup.less @@ -616,6 +616,10 @@ dialog#browserpass-modal { .modal-content { margin-bottom: 15px; white-space: pre-wrap; + + p { + margin: 0; + } } .modal-actions { From eb124703e8a16a4781ef2f220b3dafc05ca9012a Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Tue, 25 Mar 2025 23:40:14 -0600 Subject: [PATCH 22/35] fix copy pasta bug in alarm handler for clearAuthRequest, also correctly resolve/cancel the request when closing deatched popup, browserpass/browserpass-extension#320 --- src/background.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/background.js b/src/background.js index 7ddf5e3e..88000f29 100644 --- a/src/background.js +++ b/src/background.js @@ -161,7 +161,7 @@ async function keepAlive() { // handle fired alarms chrome.alarms.onAlarm.addListener(async (alarm) => { if (alarm.name === "clearClipboard") { - console.debug("clearClipboard fired", { current, lastCopiedText }); + console.debug("clearClipboard fired", { lastCopiedText }); if ((await readFromClipboard()) === lastCopiedText) { copyToClipboard("", false); } @@ -174,9 +174,9 @@ chrome.alarms.onAlarm.addListener(async (alarm) => { await keepAlive(); } } else if (alarm.name === "clearAuthRequest") { - console.debug("clearAuthRequest fired", { current, lastCopiedText }); + console.debug("clearAuthRequest fired", { currentAuthRequest }); if (currentAuthRequest !== null) { - currentAuthRequest = null; + resolveAuthRequest({ cancel: true }, currentAuthRequest.url); } } }); From 0c58ec08270a8deb9d70c97f33b639b1dcb2ff3f Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Fri, 28 Mar 2025 18:11:29 -0600 Subject: [PATCH 23/35] remove excess console.{log,debug} browserpass/browserpass-extension#320 --- src/background.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/background.js b/src/background.js index 88000f29..adfab686 100644 --- a/src/background.js +++ b/src/background.js @@ -116,7 +116,6 @@ async function createAuthRequestModal(url, callback, details) { }); function onPopupClose(windowId) { - console.debug("onPopupClose", { windowId, currentAuthRequest }); const waitingRequestId = (currentAuthRequest && currentAuthRequest.popup && currentAuthRequest.popup.id) || false; @@ -161,20 +160,17 @@ async function keepAlive() { // handle fired alarms chrome.alarms.onAlarm.addListener(async (alarm) => { if (alarm.name === "clearClipboard") { - console.debug("clearClipboard fired", { lastCopiedText }); if ((await readFromClipboard()) === lastCopiedText) { copyToClipboard("", false); } lastCopiedText = null; } else if (alarm.name === "keepAlive") { const current = await readFromClipboard(); - console.debug("keepAlive fired", { current, lastCopiedText }); // stop if either value changes if (current === lastCopiedText) { await keepAlive(); } } else if (alarm.name === "clearAuthRequest") { - console.debug("clearAuthRequest fired", { currentAuthRequest }); if (currentAuthRequest !== null) { resolveAuthRequest({ cancel: true }, currentAuthRequest.url); } From ba3af028f2edb737f9280a526a8f7eadfa6edc96 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Fri, 28 Mar 2025 18:20:10 -0600 Subject: [PATCH 24/35] fix regresion bug in saveRecent, browserpass/browserpass-extension#320 --- src/background.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/background.js b/src/background.js index adfab686..f4d44d8b 100644 --- a/src/background.js +++ b/src/background.js @@ -368,8 +368,8 @@ async function saveRecent(settings, login, remove = false) { var ignoreInterval = 60000; // 60 seconds - don't increment counter twice within this window // save store timestamp - const ts = `recent${login.store.id}`; - chrome.storage.local.set({ ts: JSON.stringify(Date.now()) }); + const ts = `recent:${login.store.id}`; + chrome.storage.local.set(ts, JSON.stringify(Date.now())); // update login usage count & timestamp if (Date.now() > login.recent.when + ignoreInterval) { From 02ba8ca306ec888f8f649dad79d7f92305968054 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Fri, 28 Mar 2025 18:21:34 -0600 Subject: [PATCH 25/35] remove another extra console.log browserpass/browserpass-extension#320 --- src/background.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/background.js b/src/background.js index f4d44d8b..2b33a0a9 100644 --- a/src/background.js +++ b/src/background.js @@ -642,7 +642,6 @@ async function getLocalSettings() { if (Object.prototype.hasOwnProperty.call(items, key)) { value = items[key]; } - console.info(`getLocalSettings(), response for ${key}=`, value); if (value !== null && Boolean(value)) { try { From 4be440cddf0a5d67539eca8f00ce356a47ae47b1 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Fri, 28 Mar 2025 18:54:47 -0600 Subject: [PATCH 26/35] use simple isChrome approach, browserpass/browserpass-extension#320 --- src/helpers.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/helpers.js b/src/helpers.js index 4a764015..5bd60b9e 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -92,13 +92,7 @@ function getSetting(key, login, settings) { * @since 3.10.0 */ function isChrome() { - // return chrome.browser.runtime.getURL('/').startsWith('chrome') - const ua = navigator.userAgent; - const matches = ua.match(/(chrom)/i) || []; - if (Object.keys(matches).length > 2 && /chrom/i.test(matches[1])) { - return true; - } - return false; + return chrome.runtime.getURL("/").startsWith("chrom"); } /** From ad0d892b90ce6dfa3e2e9cb4cd12a0e858386ecc Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Fri, 28 Mar 2025 18:57:55 -0600 Subject: [PATCH 27/35] fix save recent for v3 manifest storage setItem, browserpass/browserpass-extension#320 --- src/background.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/background.js b/src/background.js index 2b33a0a9..4aada4e0 100644 --- a/src/background.js +++ b/src/background.js @@ -368,8 +368,9 @@ async function saveRecent(settings, login, remove = false) { var ignoreInterval = 60000; // 60 seconds - don't increment counter twice within this window // save store timestamp - const ts = `recent:${login.store.id}`; - chrome.storage.local.set(ts, JSON.stringify(Date.now())); + const obj = {}; + obj[`recent:${login.store.id}`] = JSON.stringify(Date.now()); + chrome.storage.local.set(obj); // update login usage count & timestamp if (Date.now() > login.recent.when + ignoreInterval) { From 2023c90b45af7e0a8c8539ee5d50dace4d32c6a0 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Sat, 29 Mar 2025 07:33:09 -0600 Subject: [PATCH 28/35] adjust syntax for searchInterface to pass window url to parseAuthUrl, browserpass/browserpass-extension#320 --- src/popup/searchinterface.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/popup/searchinterface.js b/src/popup/searchinterface.js index 4cc6a3f1..69650245 100644 --- a/src/popup/searchinterface.js +++ b/src/popup/searchinterface.js @@ -34,7 +34,7 @@ function view(ctl, params) { var self = this; let url = ""; - const authUrl = helpers.parseAuthUrl((window && `${window.location.href}`) || null); + const authUrl = helpers.parseAuthUrl(window?.location?.href ?? null); if (this.popup.settings.authRequested && authUrl) { url = new BrowserpassURL(authUrl); } else { From c2167889a317536a555d64e3b4402f3e22b89dc6 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Sat, 29 Mar 2025 14:13:35 -0600 Subject: [PATCH 29/35] check for non https protocol when filling fields of auth request, warn user, and proceed only based on confirm, browserpass/browserpass-extension#320 --- src/background.js | 8 ++++---- src/helpers.js | 24 ++++++++++++++++++++++-- src/helpers.ui.js | 30 +++++++++++++++++++++++++++++- src/popup/addEditInterface.js | 2 +- src/popup/colors.less | 20 ++++++++++++++++++++ src/popup/interface.js | 2 +- src/popup/modalDialog.js | 31 ++++++++++++++++++++++++++++--- 7 files changed, 105 insertions(+), 12 deletions(-) diff --git a/src/background.js b/src/background.js index 4aada4e0..d44e5e64 100644 --- a/src/background.js +++ b/src/background.js @@ -1233,11 +1233,11 @@ async function receiveMessage(message, sender, sendResponse) { */ async function clearUsageData() { // clear local storage - localStorage.removeItem("foreignFills"); - localStorage.removeItem("recent"); - Object.keys(localStorage).forEach((key) => { + chrome.storage.local.remove("foreignFills"); + chrome.storage.local.remove("recent"); + Object.keys(chrome.storage.local.getKeys()).forEach((key) => { if (key.startsWith("recent:")) { - localStorage.removeItem(key); + chrome.storage.local.remove(key); } }); diff --git a/src/helpers.js b/src/helpers.js index 5bd60b9e..f4012d34 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -19,7 +19,7 @@ const fieldsPrefix = { const containsSymbolsRegEx = RegExp(/[\p{P}\p{S}]/, "u"); const LATEST_NATIVE_APP_VERSION = 3001000; const AUTH_URL_QUERY_PARAM = "authUrl"; -const LAUNCH_URL_DEPRECATION_MESSAGE = `

"Ctrl+G" and "Ctrl+Shift+G" shortcuts are deprecated and will be removed in a future version.

It is no longer necessary to open websites which require basic auth using these shortcuts. Navigate websites normally and Browserpass will automatically open a popup for you to choose the credentials.

`; +const LAUNCH_URL_DEPRECATION_MESSAGE = `

Deprecation

"Ctrl+G" and "Ctrl+Shift+G" shortcuts are deprecated and will be removed in a future version.

It is no longer necessary to open websites which require basic auth using these shortcuts. Navigate websites normally and Browserpass will automatically open a popup for you to choose the credentials.

`; module.exports = { containsSymbolsRegEx, @@ -37,6 +37,7 @@ module.exports = { parseAuthUrl, prepareLogin, prepareLogins, + unsecureRequestWarning, }; //----------------------------------- Function definitions ----------------------------------// @@ -92,7 +93,13 @@ function getSetting(key, login, settings) { * @since 3.10.0 */ function isChrome() { - return chrome.runtime.getURL("/").startsWith("chrom"); + // return chrome.runtime.getURL("/").startsWith("chrom"); + const ua = navigator.userAgent; + const matches = ua.match(/(chrom)/i) || []; + if (Object.keys(matches).length > 2 && /chrom/i.test(matches[1])) { + return true; + } + return false; } /** @@ -488,3 +495,16 @@ function sortUnique(array, comparator) { .sort(comparator) .filter((elem, index, arr) => index == !arr.length || arr[index - 1] != elem); } + +/** + * Returns warning html string with unsecure url specified + * @returns html string + */ +function unsecureRequestWarning(url) { + return ( + "

Warning: Are you sure you want to do this?

" + + "

You are about to send login credentials via an insecure connection!

" + + "

If there is an attacker watching your network traffic, they may be able to see your username and password.

" + + `

URL: ${url}

` + ); +} diff --git a/src/helpers.ui.js b/src/helpers.ui.js index 77edc32d..1fa18889 100644 --- a/src/helpers.ui.js +++ b/src/helpers.ui.js @@ -2,8 +2,9 @@ "use strict"; const m = require("mithril"); -const helpers = require("./helpers"); +const dialog = require("./popup/modalDialog"); const notify = require("./popup/notifications"); +const helpers = require("./helpers"); const containsNumbersRegEx = RegExp(/[0-9]/); const containsSymbolsRegEx = RegExp(/[\p{P}\p{S}]/, "u"); @@ -79,6 +80,33 @@ function handleError(error, type = "error") { * @return void */ async function withLogin(action, params = {}) { + try { + const url = helpers.parseAuthUrl(this.settings?.tab?.url ?? null); + const askToProceed = + action === "fill" && + this.settings?.authRequested && + !params?.confirmedAlready && + !url?.match(/^https:/i); + if (askToProceed) { + const that = this; + that.doAction = withLogin.bind({ + settings: this.settings, + login: this.login, + }); + dialog.open( + { message: helpers.unsecureRequestWarning(url), type: "warning" }, + function () { + // proceed + params.confirmedAlready = true; + that.doAction(action, params); + } + ); + return; + } + } catch (e) { + console.error(e); + } + try { switch (action) { case "fill": diff --git a/src/popup/addEditInterface.js b/src/popup/addEditInterface.js index d13e6047..dca5d188 100644 --- a/src/popup/addEditInterface.js +++ b/src/popup/addEditInterface.js @@ -3,10 +3,10 @@ const Login = require("./models/Login"); const Settings = require("./models/Settings"); const Tree = require("./models/Tree"); const notify = require("./notifications"); +const dialog = require("./modalDialog"); const helpers = require("../helpers"); const helpersUI = require("../helpers.ui"); const layout = require("./layoutInterface"); -const dialog = require("./modalDialog"); module.exports = AddEditInterface; diff --git a/src/popup/colors.less b/src/popup/colors.less index 53cee572..c5e41224 100644 --- a/src/popup/colors.less +++ b/src/popup/colors.less @@ -265,5 +265,25 @@ background-color: @ntfy-warning-bgcolor; border: 1px solid @ntfy-warning-border; } + + &.warning { + color: @ntfy-warning-color; + background-color: @ntfy-warning-bgcolor; + border: 1px solid @ntfy-warning-border; + } + &.error { + color: @ntfy-error-color; + background-color: @ntfy-error-bgcolor; + border: 1px solid @ntfy-error-border; + } + + &.warning, + &.error { + button { + color: @ntfy-info-color; + background-color: @ntfy-info-bgcolor; + border: 1px solid @ntfy-info-border; + } + } } } diff --git a/src/popup/interface.js b/src/popup/interface.js index 475e9e3e..9c7f819e 100644 --- a/src/popup/interface.js +++ b/src/popup/interface.js @@ -4,9 +4,9 @@ const m = require("mithril"); const Moment = require("moment"); const SearchInterface = require("./searchinterface"); const BrowserpassURL = require("@browserpass/url"); -const layout = require("./layoutInterface"); const dialog = require("./modalDialog"); const helpers = require("../helpers"); +const layout = require("./layoutInterface"); let overrideDefaultSearchOnce = true; /** diff --git a/src/popup/modalDialog.js b/src/popup/modalDialog.js index fe53a0b8..62ea4908 100644 --- a/src/popup/modalDialog.js +++ b/src/popup/modalDialog.js @@ -69,21 +69,44 @@ let Modal = { * * @since 3.8.0 * - * @param {string} message message or html to render in main body of dialog + * @param {string} request object, with type, or string message (html) to render in main body of dialog * @param {function} callback function which accepts a single boolean argument * @param {string} cancelText text to display on the negative response button * @param {string} confirmText text to display on the positive response button */ open: ( - message = "", + request = "", callback = (resp = false) => {}, cancelText = CANCEL, confirmText = CONFIRM ) => { - if (!message.length || typeof callback !== "function") { + if (typeof callback !== "function") { return null; } + let message = ""; + let type = "info"; + switch (typeof request) { + case "string": + if (!request.length) { + return null; + } + message = request; + break; + case "object": + if (typeof request?.message !== "string") { + return null; + } + message = request.message; + + if (["info", "warning", "error"].includes(request?.type)) { + type = request.type; + } + break; + default: + return null; + } + if (typeof cancelText == "string" && cancelText.length) { cancelButtonText = cancelText; } else if (cancelText === false) { @@ -99,6 +122,8 @@ let Modal = { } modalElement = document.getElementById(modalId); + modalElement.classList.remove(...modalElement.classList); + modalElement.classList.add([type]); callBackFn = callback; modalContent = message; modalElement.showModal(); From 399eccba61ed08704b587e3ae1626019d668e3eb Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Sun, 30 Mar 2025 19:14:54 -0600 Subject: [PATCH 30/35] confirmed must use user agent method for isChrome is certain situations, browserpass/browserpass-extension#320 --- src/helpers.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/helpers.js b/src/helpers.js index f4012d34..d022aa62 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -93,7 +93,6 @@ function getSetting(key, login, settings) { * @since 3.10.0 */ function isChrome() { - // return chrome.runtime.getURL("/").startsWith("chrom"); const ua = navigator.userAgent; const matches = ua.match(/(chrom)/i) || []; if (Object.keys(matches).length > 2 && /chrom/i.test(matches[1])) { From 2b5b83d4eb8d2509ce12115b55fa77d52ae7249d Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Sun, 30 Mar 2025 20:10:59 -0600 Subject: [PATCH 31/35] first version of redraw modal height for dialog and notifications which are clipped, browserpass/browserpass-extension#320 --- src/helpers.redraw.js | 43 ++++++++++++++++++++++++++++++++++++++ src/popup/modalDialog.js | 3 +++ src/popup/notifications.js | 9 +++++++- src/popup/popup.less | 8 +++++++ 4 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 src/helpers.redraw.js diff --git a/src/helpers.redraw.js b/src/helpers.redraw.js new file mode 100644 index 00000000..a2bd1aef --- /dev/null +++ b/src/helpers.redraw.js @@ -0,0 +1,43 @@ +"use strict"; + +const m = require("mithril"); + +module.exports = { + increaseModalHeight, +}; + +/** + * Increases modal window height using a document child reference height + * Maximum height is 1000px + * @param referenceElement an htmlElement returned from one of document.getElementBy... methods + * @since 3.10.0 + */ +function increaseModalHeight(referenceElement) { + console.debug(`increaseModalHeight(referenceElement...${referenceElement && "present"})`, { + referenceElement, + exists: Boolean(referenceElement), + height: referenceElement.clientHeight, + }); + if (!referenceElement) { + return; + } + const rootContentEl = document.getRootNode().getElementsByClassName("layout")[0] ?? null; + console.debug(`increaseModalHeight()`, { + rootContentEl, + exists: Boolean(rootContentEl), + height: rootContentEl.clientHeight, + }); + if (rootContentEl) { + let count = 0; + while ( + rootContentEl.clientHeight < 1000 && + rootContentEl.clientHeight < referenceElement?.clientHeight + 50 + ) { + console.log(count, rootContentEl, referenceElement); + rootContentEl.classList.remove(...rootContentEl.classList); + rootContentEl.classList.add(...["layout", `mh-${count}`]); + m.redraw(); + count += 1; + } + } +} diff --git a/src/popup/modalDialog.js b/src/popup/modalDialog.js index 62ea4908..58f25d94 100644 --- a/src/popup/modalDialog.js +++ b/src/popup/modalDialog.js @@ -1,4 +1,5 @@ const m = require("mithril"); +const redraw = require("../helpers.redraw"); const modalId = "browserpass-modal"; const CANCEL = "Cancel"; @@ -128,6 +129,8 @@ let Modal = { modalContent = message; modalElement.showModal(); m.redraw(); + + redraw.increaseModalHeight(modalElement); }, }; diff --git a/src/popup/notifications.js b/src/popup/notifications.js index 92b21c03..b61dea18 100644 --- a/src/popup/notifications.js +++ b/src/popup/notifications.js @@ -5,7 +5,9 @@ */ const m = require("mithril"); +const redraw = require("../helpers.redraw"); const uuidPrefix = RegExp(/^([a-z0-9]){8}-/); +const NOTIFY_CLASS = "m-notifications"; /** * Generate a globally unique id @@ -85,11 +87,16 @@ function addError(text, timeout = 5000) { } let Notifications = { + onupdate: function () { + setTimeout(() => { + redraw.increaseModalHeight(document.getElementsByClassName(NOTIFY_CLASS)[0]); + }, 25); + }, view(vnode) { let ui = vnode.state; return state.list ? m( - ".m-notifications", + `.${NOTIFY_CLASS}`, state.list.map((msg) => { return m("div", { key: msg.id }, m(Notification, msg)); //wrap in div with key for proper dom updates }) diff --git a/src/popup/popup.less b/src/popup/popup.less index 30efe8fa..92df5850 100644 --- a/src/popup/popup.less +++ b/src/popup/popup.less @@ -659,3 +659,11 @@ dialog#browserpass-modal { opacity: 0; } } + +.generate-heights(@start, @end, @i: 0, @step: 25) when ((@start + @i * @step) =< @end) { + .mh-@{i} { + min-height: (1px * @start) + (1px * @i * @step); + } + .generate-heights(@start, @end, (@i + 1)); +} +.generate-heights(300, 1000); From a93759a6115342e865856f241b37248518578671 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Sun, 30 Mar 2025 21:01:14 -0600 Subject: [PATCH 32/35] minor adjustment to refernce diff for notifications, browserpass/browserpass-extension#320 --- src/helpers.redraw.js | 7 +++++-- src/popup/notifications.js | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/helpers.redraw.js b/src/helpers.redraw.js index a2bd1aef..f9176648 100644 --- a/src/helpers.redraw.js +++ b/src/helpers.redraw.js @@ -12,7 +12,10 @@ module.exports = { * @param referenceElement an htmlElement returned from one of document.getElementBy... methods * @since 3.10.0 */ -function increaseModalHeight(referenceElement) { +function increaseModalHeight(referenceElement, heightDiff = 50) { + if (typeof heightDiff != "number") { + heightDiff = 50; + } console.debug(`increaseModalHeight(referenceElement...${referenceElement && "present"})`, { referenceElement, exists: Boolean(referenceElement), @@ -31,7 +34,7 @@ function increaseModalHeight(referenceElement) { let count = 0; while ( rootContentEl.clientHeight < 1000 && - rootContentEl.clientHeight < referenceElement?.clientHeight + 50 + rootContentEl.clientHeight < referenceElement?.clientHeight + heightDiff ) { console.log(count, rootContentEl, referenceElement); rootContentEl.classList.remove(...rootContentEl.classList); diff --git a/src/popup/notifications.js b/src/popup/notifications.js index b61dea18..b4e4dc3a 100644 --- a/src/popup/notifications.js +++ b/src/popup/notifications.js @@ -89,7 +89,7 @@ function addError(text, timeout = 5000) { let Notifications = { onupdate: function () { setTimeout(() => { - redraw.increaseModalHeight(document.getElementsByClassName(NOTIFY_CLASS)[0]); + redraw.increaseModalHeight(document.getElementsByClassName(NOTIFY_CLASS)[0], 25); }, 25); }, view(vnode) { From a49daaecdf40740cc365f9ff92828804eee0e216 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Sun, 30 Mar 2025 21:08:53 -0600 Subject: [PATCH 33/35] remove console.debug, browserpass/browserpass-extension#320 --- src/helpers.redraw.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/helpers.redraw.js b/src/helpers.redraw.js index f9176648..8fdd08db 100644 --- a/src/helpers.redraw.js +++ b/src/helpers.redraw.js @@ -16,27 +16,16 @@ function increaseModalHeight(referenceElement, heightDiff = 50) { if (typeof heightDiff != "number") { heightDiff = 50; } - console.debug(`increaseModalHeight(referenceElement...${referenceElement && "present"})`, { - referenceElement, - exists: Boolean(referenceElement), - height: referenceElement.clientHeight, - }); if (!referenceElement) { return; } const rootContentEl = document.getRootNode().getElementsByClassName("layout")[0] ?? null; - console.debug(`increaseModalHeight()`, { - rootContentEl, - exists: Boolean(rootContentEl), - height: rootContentEl.clientHeight, - }); if (rootContentEl) { let count = 0; while ( rootContentEl.clientHeight < 1000 && rootContentEl.clientHeight < referenceElement?.clientHeight + heightDiff ) { - console.log(count, rootContentEl, referenceElement); rootContentEl.classList.remove(...rootContentEl.classList); rootContentEl.classList.add(...["layout", `mh-${count}`]); m.redraw(); From aadca90eede218dcf42fabad38f3067798cdf337 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Mon, 31 Mar 2025 09:41:48 -0600 Subject: [PATCH 34/35] ship with simple url approach, but keep reference to user agent approach temporarily, browserpass/browserpass-extension#320 --- src/helpers.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/helpers.js b/src/helpers.js index d022aa62..f9f7c3ad 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -93,12 +93,13 @@ function getSetting(key, login, settings) { * @since 3.10.0 */ function isChrome() { - const ua = navigator.userAgent; - const matches = ua.match(/(chrom)/i) || []; - if (Object.keys(matches).length > 2 && /chrom/i.test(matches[1])) { - return true; - } - return false; + return chrome.runtime.getURL("/").startsWith("chrom"); + // const ua = navigator.userAgent; + // const matches = ua.match(/(chrom)/i) || []; + // if (Object.keys(matches).length > 2 && /chrom/i.test(matches[1])) { + // return true; + // } + // return false; } /** From 6257ec0fe21cb2c75e8f276cc57fad470a902d08 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Mon, 31 Mar 2025 09:49:21 -0600 Subject: [PATCH 35/35] Add note about why keeping commented block and need to determine which one to keep or remove, browserpass/browserpass-extension#320 --- src/helpers.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/helpers.js b/src/helpers.js index f9f7c3ad..50304746 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -94,6 +94,17 @@ function getSetting(key, login, settings) { */ function isChrome() { return chrome.runtime.getURL("/").startsWith("chrom"); + /** + * Alternate approach to checking if current browser is chrome or + * chromium based. + * + * @TODO: remove one of these two after probationary period + * to determine which will approach will best suite browserpass + * purposes in the wild. + * + * Above: .getURL("/") will error on "chrome://" protocols + * Below: check user agent, can be altered depending vendor + */ // const ua = navigator.userAgent; // const matches = ua.match(/(chrom)/i) || []; // if (Object.keys(matches).length > 2 && /chrom/i.test(matches[1])) {