diff --git a/src/background.js b/src/background.js index a6947a5..c4822b2 100644 --- a/src/background.js +++ b/src/background.js @@ -60,6 +60,105 @@ function copyToClipboard(text) { document.execCommand("copy"); } +/** + * Call injected form-fill code + * + * @param object tab Target tab + * @param object fillRequest Fill request details + * @param boolean allFrames Dispatch to all frames + * @param boolean allowForeign Allow foreign-origin iframes + * @param boolean allowNoSecret Allow forms that don't contain a password field + * @return array list of filled fields + */ +async function dispatchFill( + tab, + fillRequest, + allFrames = false, + allowForeign = false, + allowNoSecret = false +) { + fillRequest = Object.assign({}, fillRequest, { + allowForeign: allowForeign, + allowNoSecret: allowNoSecret + }); + + var filledFields = await chrome.tabs.executeScript(tab.id, { + allFrames: allFrames, + code: `window.browserpass.fillLogin(${JSON.stringify(fillRequest)});` + }); + + // simplify the list of filled fields + filledFields = filledFields + .reduce((fields, addFields) => fields.concat(addFields), []) + .reduce(function(fields, field) { + if (!fields.includes(field)) { + fields.push(field); + } + return fields; + }, []); + + return filledFields; +} + +/** + * Fill form fields + * + * @param object tab Target tab + * @param object login Login object + * @param array fields List of fields to fill + * @return array List of filled fields + */ +async function fillFields(tab, login, fields) { + // check that required fields are present + for (var field of fields) { + if (login.fields[field] === null) { + throw new Error(`Required field is missing: ${field}`); + } + } + + // inject script + await chrome.tabs.executeScript(tab.id, { + allFrames: true, + file: "js/inject.dist.js" + }); + + // build fill request + var fillRequest = { + origin: new URL(tab.url).origin, + login: login, + fields: fields + }; + + // fill form via injected script + var filledFields = await dispatchFill(tab, fillRequest); + + // try again using same-origin frames if we couldn't fill a password field + if (!filledFields.includes("secret")) { + filledFields = filledFields.concat(await dispatchFill(tab, fillRequest, true)); + } + + // try again using all available frames if we couldn't fill a password field + if (!filledFields.includes("secret")) { + filledFields = filledFields.concat(await dispatchFill(tab, fillRequest, true, true)); + } + + // try again using same-origin frames, and don't require a password field + if (!filledFields.length) { + filledFields = filledFields.concat(await dispatchFill(tab, fillRequest, true, false, true)); + } + + // try again using all available frames, and don't require a password field + if (!filledFields.length) { + filledFields = filledFields.concat(await dispatchFill(tab, fillRequest, true, true, true)); + } + + if (!filledFields.length) { + throw new Error(`No fillable forms available for fields: ${fields.join(", ")}`); + } + + return filledFields; +} + /** * Get Local settings from the extension * @@ -239,29 +338,26 @@ async function handleMessage(settings, message, sendResponse) { break; case "fill": try { - var tab = (await chrome.tabs.query({ active: true, currentWindow: true }))[0]; - await chrome.tabs.executeScript(tab.id, { file: "js/inject.dist.js" }); - // check login fields - if (message.login.fields.login === null) { - throw new Error("No login is available"); - } - if (message.login.fields.secret === null) { - throw new Error("No password is available"); - } - var fillFields = JSON.stringify({ - login: message.login.fields.login, - secret: message.login.fields.secret - }); - // fill form via injected script - await chrome.tabs.executeScript(tab.id, { - code: `window.browserpass.fillLogin(${fillFields});` - }); - sendResponse({ status: "ok" }); + // get tab info + var targetTab = (await chrome.tabs.query({ active: true, currentWindow: true }))[0]; + + // dispatch initial fill request + var filledFields = await fillFields(targetTab, message.login, ["login", "secret"]); + + // no need to check filledFields, because fillFields() already throws an error if empty + sendResponse({ status: "ok", filledFields: filledFields }); } catch (e) { - sendResponse({ - status: "error", - message: "Unable to fill credentials: " + e.toString() - }); + try { + sendResponse({ + status: "error", + message: e.toString() + }); + } catch (e) { + // TODO An error here is typically a closed message port, due to a popup taking focus + // away from the extension menu and the menu closing as a result. Need to investigate + // whether triggering the extension menu from the background script is possible. + console.log(e); + } } break; default: @@ -335,7 +431,10 @@ async function parseFields(settings, login) { // assign to fields for (var key in login.fields) { - if (Array.isArray(login.fields[key]) && login.fields[key].indexOf(parts[0]) >= 0) { + if ( + Array.isArray(login.fields[key]) && + login.fields[key].indexOf(parts[0].toLowerCase()) >= 0 + ) { login.fields[key] = parts[1]; break; } diff --git a/src/inject.js b/src/inject.js index 512f8b3..0dabfc0 100644 --- a/src/inject.js +++ b/src/inject.js @@ -73,24 +73,60 @@ * * @since 3.0.0 * - * @param object login Login fields - * @param bool autoSubmit Whether to autosubmit the login form + * @param object request Form fill request * @return void */ - function fillLogin(login, autoSubmit = false) { + function fillLogin(request) { + var autoSubmit = false; + var filledFields = []; + + // get the login form var loginForm = form(); - update(USERNAME_FIELDS, login.login, loginForm); - update(PASSWORD_FIELDS, login.secret, loginForm); + // don't attempt to fill non-secret forms unless non-secret filling is allowed + if (!find(PASSWORD_FIELDS, loginForm) && !request.allowNoSecret) { + return filledFields; + } + // ensure the origin is the same, or ask the user for permissions to continue + if (window.location.origin !== request.origin) { + var message = + "You have requested to fill login credentials into an embedded document from a " + + "different origin than the main document in this tab. Do you wish to proceed?\n\n" + + `Tab origin: ${request.origin}\n` + + `Embedded origin: ${window.location.origin}`; + if (!request.allowForeign || !confirm(message)) { + return filledFields; + } + } + + // fill login field + if ( + request.fields.includes("login") && + update(USERNAME_FIELDS, request.login.fields.login, loginForm) + ) { + filledFields.push("login"); + } + + // fill secret field + if ( + request.fields.includes("secret") && + update(PASSWORD_FIELDS, request.login.fields.secret, loginForm) + ) { + filledFields.push("secret"); + } + + // check for multiple password fields in the login form var password_inputs = queryAllVisible(document, PASSWORD_FIELDS, loginForm); if (password_inputs.length > 1) { // There is likely a field asking for OTP code, so do not submit form just yet password_inputs[1].select(); } else { window.requestAnimationFrame(function() { - // Try to submit the form, or focus on the submit button (based on user settings) + // try to locate the submit button var submit = find(SUBMIT_FIELDS, loginForm); + + // Try to submit the form, or focus on the submit button (based on user settings) if (submit) { if (autoSubmit) { submit.click(); @@ -115,6 +151,9 @@ } }); } + + // finished filling things successfully + return filledFields; } /** diff --git a/src/manifest.json b/src/manifest.json index b6889ff..f534b7d 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -25,9 +25,7 @@ "permissions": [ "activeTab", "nativeMessaging", - "notifications" - ], - "optional_permissions": [ + "notifications", "webRequest", "webRequestBlocking", "http://*/*", diff --git a/src/popup/popup.js b/src/popup/popup.js index 9fb6ead..57c0bcc 100644 --- a/src/popup/popup.js +++ b/src/popup/popup.js @@ -178,13 +178,6 @@ async function withLogin(action) { handleError("Filling login details...", "notice"); break; case "launch": - var havePermission = await chrome.permissions.request({ - permissions: ["webRequest", "webRequestBlocking"], - origins: ["http://*/*", "https://*/*"] - }); - if (!havePermission) { - throw new Error("Browserpass requires additional permissions to proceed"); - } handleError("Launching URL...", "notice"); break; case "copyPassword": @@ -207,6 +200,7 @@ async function withLogin(action) { throw new Error(response.message); } else { switch (action) { + // fall through to update recent case "fill": case "copyPassword": case "copyUsername":