diff --git a/.gitignore b/.gitignore index 47ef6b0..746119e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,4 @@ /chrome /src/node_modules /src/js -/src/*.js /src/*.log -!/src/*.src.js diff --git a/Makefile b/Makefile index 0e08122..ef206b1 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,8 @@ CHROME_FILES := manifest.json \ popup/*.svg CHROME_FILES := $(wildcard $(addprefix src/,$(CHROME_FILES))) \ src/js/background.dist.js \ - src/js/popup.dist.js + src/js/popup.dist.js \ + src/js/inject.dist.js CHROME_FILES := $(patsubst src/%,chrome/%,$(CHROME_FILES)) .PHONY: chrome diff --git a/src/Makefile b/src/Makefile index 5b42482..6a5528e 100644 --- a/src/Makefile +++ b/src/Makefile @@ -5,7 +5,7 @@ CLEAN_FILES := js PRETTIER_FILES := $(wildcard *.js popup/*.js *.css popup/*.css) .PHONY: all -all: deps prettier js/background.dist.js js/popup.dist.js +all: deps prettier js/background.dist.js js/popup.dist.js js/inject.dist.js .PHONY: deps deps: @@ -23,6 +23,10 @@ js/popup.dist.js: $(BROWSERIFY) popup/*.js [ -d js ] || mkdir -p js $(BROWSERIFY) -o js/popup.dist.js popup/popup.js +js/inject.dist.js: $(BROWSERIFY) inject.js + [ -d js ] || mkdir -p js + $(BROWSERIFY) -o js/inject.dist.js inject.js + .PHONY: clean clean: rm -rf $(CLEAN_FILES) diff --git a/src/background.js b/src/background.js index 93a7a07..7945619 100644 --- a/src/background.js +++ b/src/background.js @@ -60,6 +60,12 @@ async function handleMessage(settings, message, sendResponse) { sendResponse({ status: "error", message: "Action is missing" }); } + // fetch file & parse fields if a login entry is present + if (typeof message.login !== "undefined") { + await parseFields(settings, message.login); + } + + // route action switch (message.action) { case "getSettings": sendResponse({ @@ -76,7 +82,53 @@ async function handleMessage(settings, message, sendResponse) { var response = await hostAction(settings, "list"); sendResponse(response.data.files); } catch (e) { - console.log(e); + sendResponse({ + status: "error", + message: "Unable to enumerate password files" + e.toString() + }); + } + break; + case "launch": + try { + var tab = (await browser.tabs.query({ active: true, currentWindow: true }))[0]; + var url = message.login.fields.url ? message.login.fields.url : response.login.url; + if (!url.match(/:\/\//)) { + url = "http://" + url; + } + chrome.tabs.update(tab.id, { url: url }); + sendResponse({ status: "ok" }); + } catch (e) { + sendResponse({ + status: "error", + message: "Unable to launch URL: " + e.toString() + }); + } + break; + case "fill": + try { + var tab = (await browser.tabs.query({ active: true, currentWindow: true }))[0]; + await browser.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 browser.tabs.executeScript(tab.id, { + code: `window.browserpass.fillLogin(${fillFields});` + }); + sendResponse({ status: "ok" }); + } catch (e) { + sendResponse({ + status: "error", + message: "Unable to fill credentials: " + e.toString() + }); } break; default: @@ -110,6 +162,65 @@ function hostAction(settings, action, params = {}) { return browser.runtime.sendNativeMessage(appID, request); } +/** + * Fetch file & parse fields + * + * @since 3.0.0 + * + * @param object settings Settings object + * @param object login Login object + * @return void + */ +async function parseFields(settings, login) { + var response = await hostAction(settings, "fetch", { + store: login.store, + file: login.login + ".gpg" + }); + if (response.status != "ok") { + throw new Error(JSON.stringify(response)); // TODO handle host error + } + + // save raw data inside login + login.raw = response.data.contents; + + // parse lines + login.fields = { + secret: ["secret", "password", "pass"], + login: ["login", "username", "user", "email"], + url: ["url", "uri", "website", "site", "link", "launch"] + }; + var lines = login.raw.split(/[\r\n]+/).filter(line => line.trim().length > 0); + lines.forEach(function(line) { + // split key / value + var parts = line + .split(":", 2) + .map(value => value.trim()) + .filter(value => value.length); + if (parts.length != 2) { + return; + } + + // assign to fields + for (var key in login.fields) { + if (Array.isArray(login.fields[key]) && login.fields[key].indexOf(parts[0]) >= 0) { + login.fields[key] = parts[1]; + break; + } + } + }); + + // clean up unassigned fields + for (var key in login.fields) { + if (Array.isArray(login.fields[key])) { + if (key == "secret" && lines.length) { + login.fields.secret = lines[0]; + } else { + login.fields[key] = null; + } + } + } +} + /** * Wrap inbound messages to fetch native configuration * diff --git a/src/inject.js b/src/inject.js new file mode 100644 index 0000000..a0186e9 --- /dev/null +++ b/src/inject.js @@ -0,0 +1,18 @@ +(function() { + /** + * Fill password + * + * @since 3.0.0 + * + * @param object login Login fields + * @return void + */ + function fillLogin(login) { + alert("Fill login: " + JSON.stringify(login)); + } + + // set window object + window.browserpass = { + fillLogin: fillLogin + }; +})(); diff --git a/src/popup/interface.js b/src/popup/interface.js index b229bc7..89af53c 100644 --- a/src/popup/interface.js +++ b/src/popup/interface.js @@ -23,7 +23,7 @@ function Interface(settings, logins) { this.settings = settings; this.logins = logins; this.results = []; - this.active = true; + this.active = !settings.tab.url.match(/^chrome:\/\//); this.searchPart = new SearchInterface(this); // initialise with empty search @@ -69,6 +69,7 @@ function view(ctl, params) { result.doAction("fill"); }, onkeydown: function(e) { + e.preventDefault(); switch (e.code) { case "ArrowDown": if (e.target.nextSibling) { @@ -87,6 +88,9 @@ function view(ctl, params) { case "Enter": result.doAction("fill"); break; + case "KeyG": + result.doAction("launch"); + break; } } }, @@ -104,7 +108,7 @@ function view(ctl, params) { title: "Copy username", onclick: function(e) { e.stopPropagation(); - result.doAction("copyUser"); + result.doAction("copyUsername"); } }), m("div.action.launch", { diff --git a/src/popup/popup.css b/src/popup/popup.css index 9256a73..b9d7f8f 100644 --- a/src/popup/popup.css +++ b/src/popup/popup.css @@ -1,6 +1,7 @@ html, body { min-width: 260px; + overflow-x: hidden; white-space: nowrap; } @@ -25,6 +26,7 @@ body { flex-shrink: 0; min-height: 29px; padding: 6px; + width: 100%; } .part:last-child { diff --git a/src/popup/popup.js b/src/popup/popup.js index 104295c..de3daae 100644 --- a/src/popup/popup.js +++ b/src/popup/popup.js @@ -33,11 +33,14 @@ browser.tabs.query({ active: true, currentWindow: true }, async function(tabs) { * @since 3.0.0 * * @param Error error Error object + * @param string type Error type */ -function handleError(error) { - console.log(error); +function handleError(error, type = "error") { + if (type == "error") { + console.log(error); + } var errorNode = document.createElement("div"); - errorNode.setAttribute("class", "part error"); + errorNode.setAttribute("class", "part " + type); errorNode.textContent = error.toString(); document.body.innerHTML = ""; document.body.appendChild(errorNode); @@ -115,9 +118,31 @@ async function run(settings) { */ async function withLogin(action) { try { + // replace popup with a "please wait" notice + switch (action) { + case "fill": + handleError("Filling login details...", "notice"); + break; + case "launch": + handleError("Launching URL...", "notice"); + break; + case "copyPassword": + handleError("Copying password to clipboard...", "notice"); + break; + case "copyUsername": + handleError("Copying username to clipboard...", "notice"); + break; + default: + handleError("Please wait...", "notice"); + break; + } + + // hand off action to background script var response = await browser.runtime.sendMessage({ action: action, login: this }); if (response.status != "ok") { throw new Error(response.message); + } else { + window.close(); } } catch (e) { handleError(e); diff --git a/src/popup/searchinterface.js b/src/popup/searchinterface.js index 0720af4..b11fa21 100644 --- a/src/popup/searchinterface.js +++ b/src/popup/searchinterface.js @@ -32,17 +32,16 @@ function view(ctl, params) { return m( "form.part.search", { - onsubmit: function(e) { - e.preventDefault(); - }, onkeydown: function(e) { switch (e.code) { case "ArrowDown": + e.preventDefault(); if (self.popup.results.length) { document.querySelector("*[tabindex]").focus(); } break; case "Enter": + e.preventDefault(); if (self.popup.results.length) { self.popup.results[0].doAction("fill"); }