From 40893dbbfef597cdadde598deb077e800705c6fc Mon Sep 17 00:00:00 2001 From: Omar Zeidan Date: Fri, 19 Apr 2019 21:35:55 +0200 Subject: [PATCH 1/3] Initial implementation for adding passwords --- src/background-interject.js | 56 +++++++++++++++++++++++ src/background.js | 82 +++++++++++++++++++++++++++++++++- src/inject.js | 1 - src/popup/add-interface.js | 89 +++++++++++++++++++++++++++++++++++++ src/popup/popup.js | 39 ++++++++++++++++ src/popup/popup.less | 42 +++++++++++++++++ 6 files changed, 307 insertions(+), 2 deletions(-) create mode 100644 src/background-interject.js create mode 100644 src/popup/add-interface.js diff --git a/src/background-interject.js b/src/background-interject.js new file mode 100644 index 00000000..2b2e4ae6 --- /dev/null +++ b/src/background-interject.js @@ -0,0 +1,56 @@ +module.exports = { extractFormData: extractFormData }; + +const USERNAME_KEYS = ["username", "user", "login"]; +const EMAIL_KEYS = ["email", "e-mail"]; +const PASSWORD_KEYS = ["password", "passwd", "pass", "secret"]; + +function findFormDataEntries(formData, keys) { + let found = []; + + for (key in formData) { + if (keys.includes(key)) { + let entry = formData[key]; + if (entry != "") found.push(entry[0]); + } + } + + return found; +} + +function checkPasswords(passwords) { + if (passwords.length < 2) return true; + + let compare = passwords[0]; + + for (let password of passwords) { + if (password != compare) return false; + } + + return true; +} + +function extractFormData(formData) { + let passwords = findFormDataEntries(formData, PASSWORD_KEYS); + let usernames = findFormDataEntries(formData, USERNAME_KEYS); + let emails = findFormDataEntries(formData, EMAIL_KEYS); + + if (!checkPasswords(passwords)) return null; + + if (passwords.length === 0) return null; + + if (usernames.length === 0 && emails.length === 0) return null; + + let credentials = { password: passwords[0] }; + if (usernames.length > 0) { + credentials.login = usernames[0]; + + if (emails.length > 0) { + credentials.email = emails[0]; + } + } else { + credentials.login = emails[0]; + credentials.email = ""; + } + + return credentials; +} diff --git a/src/background.js b/src/background.js index f9b7d03d..54e5dd81 100644 --- a/src/background.js +++ b/src/background.js @@ -5,6 +5,7 @@ require("chrome-extension-async"); var TldJS = require("tldjs"); var sha1 = require("sha1"); var idb = require("idb"); +var Interject = require("./background-interject.js"); // native application id var appID = "com.github.browserpass.native"; @@ -51,8 +52,50 @@ chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) { chrome.runtime.onInstalled.addListener(onExtensionInstalled); +let recentCredentials = {}; +let dismissedCredentials = new Set(); + +chrome.webRequest.onBeforeRequest.addListener( + async function(details) { + if (details.method == "POST") { + let formData = details.requestBody.formData; + if (!formData) { + return; + } + + let credentials = Interject.extractFormData(formData); + if (!credentials) return; + + let currentDomain = new URL(details.url).hostname; + credentials.domain = currentDomain; + + let path = currentDomain + "/" + credentials.login + ".gpg"; + credentials.path = path; + + if (await fileExists(path)) return; + + let id = hashCredentials(credentials); + if (recentCredentials.hasOwnProperty(id)) return; + if (dismissedCredentials.has(id)) return; + + credentials.path = path; + recentCredentials[id] = credentials; + } + }, + { urls: [""] }, + ["requestBody"] +); + //----------------------------------- Function definitions ----------------------------------// +async function fileExists(path) { + let settings = await getFullSettings(); + let response = await hostAction(settings, "exists", { + file: path + }); + + return response.data.exists; +} /** * Get the deepest available domain component of a path * @@ -600,6 +643,13 @@ async function handleMessage(settings, message, sendResponse) { // route action switch (message.action) { + case "listCredentials": + sendResponse({ + status: "ok", + credentials: Object.values(recentCredentials) + }); + // recentCredentials = []; + break; case "getSettings": sendResponse({ status: "ok", @@ -705,7 +755,7 @@ async function handleMessage(settings, message, sendResponse) { sendResponse({ status: "ok", filledFields: filledFields }); } catch (e) { try { - sendResponse({ + await sendResponse({ status: "error", message: e.toString() }); @@ -728,6 +778,32 @@ async function handleMessage(settings, message, sendResponse) { }); } break; + case "create": + try { + response = await hostAction(settings, "create", { + storeID: settings.stores.default.id, + credentials: message.credentials, + file: message.credentials.path + }); + if (response.status != "ok") { + throw new Error(JSON.stringify(response)); + } + let id = hashCredentials(message.credentials); + delete recentCredentials[id]; + sendResponse({ status: "ok" }); + } catch (e) { + sendResponse({ + status: "error", + message: "Unable to create password file" + e.toString() + }); + } + break; + case "dismiss": + let id = hashCredentials(message.credentials); + dismissedCredentials.add(id); + delete recentCredentials[id]; + sendResponse({ status: "ok" }); + break; default: sendResponse({ status: "error", @@ -880,6 +956,10 @@ async function receiveMessage(message, sender, sendResponse) { } } +function hashCredentials(credentials) { + return credentials.login + credentials.email + credentials.password; +} + /** * Clear usage data * diff --git a/src/inject.js b/src/inject.js index afe33acd..f854c98b 100644 --- a/src/inject.js +++ b/src/inject.js @@ -99,7 +99,6 @@ if (!request.allowNoSecret && !find(PASSWORD_FIELDS, loginForm)) { return result; } - // ensure the origin is the same, or ask the user for permissions to continue if (window.location.origin !== request.origin) { if (!request.allowForeign || request.foreignFills[window.location.origin] === false) { diff --git a/src/popup/add-interface.js b/src/popup/add-interface.js new file mode 100644 index 00000000..51c2ada1 --- /dev/null +++ b/src/popup/add-interface.js @@ -0,0 +1,89 @@ +module.exports = AddInterface; +var m = require("mithril"); + +function AddInterface(credentials, settings) { + // public methods + this.attach = attach; + this.view = view; + + // fields + this.settings = settings; + this.credentials = credentials; + this.dismissCredential = dismissCredential; +} + +function attach(element) { + m.mount(element, this); +} + +function view(ctl, params) { + let credentials = this.credentials.length > 0 ? this.credentials[0] : undefined; + + var nodes = []; + + nodes.push(m("div.label.title", "Save credentials to store?")); + + nodes.push(m("div.label", "Path:")); + nodes.push( + m("input.credential", { + type: "text", + value: credentials ? credentials.path : "" + }) + ); + + nodes.push(m("div.label", "Username:")); + nodes.push( + m("input.credential", { + type: "text", + value: credentials ? credentials.login : "" + }) + ); + + nodes.push(m("div.label", "Password:")); + nodes.push( + m("input.credential", { + type: "password", + value: credentials ? credentials.password : "" + }) + ); + + if (credentials.email) { + nodes.push(m("div.label", "E-Mail:")); + nodes.push( + m("input.credential", { + type: "text", + value: credentials ? credentials.email : "" + }) + ); + } + + nodes.push( + m("div.buttons", [ + m( + "button.storeButton", + { + onclick: async function(e) { + await credentials.doAction("create"); + } + }, + "Yes" + ), + m( + "button.storeButton", + { + onclick: async function(e) { + await credentials.doAction("dismiss"); + } + }, + "No" + ) + ]) + ); + + return nodes; +} + +function dismissCredential() { + this.credentials.shift(); + return this.credentials.length; +} diff --git a/src/popup/popup.js b/src/popup/popup.js index eda6af71..cbd22537 100644 --- a/src/popup/popup.js +++ b/src/popup/popup.js @@ -5,6 +5,7 @@ require("chrome-extension-async"); var TldJS = require("tldjs"); var sha1 = require("sha1"); var Interface = require("./interface"); +var AddInterface = require("./add-interface"); run(); @@ -81,6 +82,24 @@ async function run() { throw new Error("Unable to retrieve current tab information"); } + response = await chrome.runtime.sendMessage({ action: "listCredentials" }); + if (response.status != "ok") { + throw new Error(response.message); + } + + if (response.credentials.length > 0) { + var popup = new AddInterface(response.credentials, settings); + for (let credential of response.credentials) { + credential.doAction = withCredential.bind({ + settings: settings, + credentials: credential, + interface: popup + }); + } + popup.attach(document.body); + return; + } + // get list of logins response = await chrome.runtime.sendMessage({ action: "listFiles" }); if (response.status != "ok") { @@ -172,3 +191,23 @@ async function withLogin(action) { handleError(e); } } + +async function withCredential(action) { + const credentials = JSON.parse(JSON.stringify(this.credentials)); + let response = await chrome.runtime.sendMessage({ + action: action, + credentials: credentials + }); + + switch (action) { + case "dismiss": + window.close(); + break; + case "create": + if (response.status != "ok") handleError(Error(response.message)); + + let credentialsLeft = this.interface.dismissCredential(); + if (credentialsLeft === 0) window.close(); + break; + } +} diff --git a/src/popup/popup.less b/src/popup/popup.less index 4be78a2e..9834d142 100644 --- a/src/popup/popup.less +++ b/src/popup/popup.less @@ -234,3 +234,45 @@ body { color: @error-text-color; } } + +.buttons { + display: flex; + flex-direction: row; + height: @login-height; +} + +.storeButton { + width: 100%; + border: none; + background-color: @bg-color; + font-size: 16px; + overflow: hidden; + color: @text-color; + outline: none; +} + +.storeButton:hover, +.storeButton:focus { + background-color: @hover-bg-color; +} + +.label, +.credential { + font-size: 16px; +} + +.title { + text-align: center; + font-size: 20px; + width: 100%; + background-color: @hover-bg-color; +} + +textarea:focus, +input:focus { + outline: none; +} + +*:focus { + outline: none; +} From d3a541f05f262cf9173ccebf8f8fb97f27cb4b89 Mon Sep 17 00:00:00 2001 From: Omar Zeidan Date: Sat, 20 Apr 2019 17:53:35 +0200 Subject: [PATCH 2/3] Choose a store when saving password --- src/background.js | 2 +- src/popup/add-interface.js | 11 ++++++++++- src/popup/popup.js | 5 +++-- src/popup/popup.less | 17 +++++++++++++++++ 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/background.js b/src/background.js index 54e5dd81..02b91c6b 100644 --- a/src/background.js +++ b/src/background.js @@ -781,7 +781,7 @@ async function handleMessage(settings, message, sendResponse) { case "create": try { response = await hostAction(settings, "create", { - storeID: settings.stores.default.id, + storeID: message.storeID, credentials: message.credentials, file: message.credentials.path }); diff --git a/src/popup/add-interface.js b/src/popup/add-interface.js index 51c2ada1..8aedaaab 100644 --- a/src/popup/add-interface.js +++ b/src/popup/add-interface.js @@ -23,6 +23,14 @@ function view(ctl, params) { nodes.push(m("div.label.title", "Save credentials to store?")); + nodes.push(m("div.label", "Store:")); + let select = m( + "select", + Object.values(this.settings.stores).map(store => + m("option", { storeID: store.id }, store.name) + ) + ); + nodes.push(select); nodes.push(m("div.label", "Path:")); nodes.push( m("input.credential", { @@ -63,7 +71,8 @@ function view(ctl, params) { "button.storeButton", { onclick: async function(e) { - await credentials.doAction("create"); + let storeID = select.dom.selectedOptions[0].getAttribute("storeID"); + await credentials.doAction("create", storeID); } }, "Yes" diff --git a/src/popup/popup.js b/src/popup/popup.js index cbd22537..55f56a94 100644 --- a/src/popup/popup.js +++ b/src/popup/popup.js @@ -192,11 +192,12 @@ async function withLogin(action) { } } -async function withCredential(action) { +async function withCredential(action, storeID) { const credentials = JSON.parse(JSON.stringify(this.credentials)); let response = await chrome.runtime.sendMessage({ action: action, - credentials: credentials + credentials: credentials, + storeID: storeID }); switch (action) { diff --git a/src/popup/popup.less b/src/popup/popup.less index 9834d142..37cdb3bc 100644 --- a/src/popup/popup.less +++ b/src/popup/popup.less @@ -276,3 +276,20 @@ input:focus { *:focus { outline: none; } + +select { + // -moz-appearance: none; + // -webkit-appearance: none; + appearance: none; + border: none; + width: 100%; + // height: 20px; + background-color: #fff; + color: #000; + font-size: 16px; +} + +select, +.credential { + height: 25px; +} From c6dbcae116c94bbcf82a64526647702a4ba1c11a Mon Sep 17 00:00:00 2001 From: Omar Zeidan Date: Tue, 30 Apr 2019 17:39:40 +0200 Subject: [PATCH 3/3] Smarter extraction of credentials from request --- src/background-interject.js | 9 ++++++++- src/background.js | 3 +++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/background-interject.js b/src/background-interject.js index 2b2e4ae6..433235ce 100644 --- a/src/background-interject.js +++ b/src/background-interject.js @@ -8,7 +8,14 @@ function findFormDataEntries(formData, keys) { let found = []; for (key in formData) { - if (keys.includes(key)) { + // strip keys of the form user[x] + let strippedKey = key; + let match = /\[.*\]/.exec(key); + if (match) { + strippedKey = key.substring(match.index + 1, match.index + match[0].length - 1); + } + + if (keys.includes(strippedKey)) { let entry = formData[key]; if (entry != "") found.push(entry[0]); } diff --git a/src/background.js b/src/background.js index 02b91c6b..ed2d4832 100644 --- a/src/background.js +++ b/src/background.js @@ -58,6 +58,9 @@ let dismissedCredentials = new Set(); chrome.webRequest.onBeforeRequest.addListener( async function(details) { if (details.method == "POST") { + if (!details.requestBody) { + return; + } let formData = details.requestBody.formData; if (!formData) { return;