
* Handle no-url-in-file case * Handle null case * Fix split (use lookbehind) * Get user from path
429 lines
13 KiB
JavaScript
429 lines
13 KiB
JavaScript
//-------------------------------- Background initialisation --------------------------------//
|
|
"use strict";
|
|
|
|
require("chrome-extension-async");
|
|
|
|
// native application id
|
|
var appID = "com.github.browserpass.native";
|
|
|
|
// default settings
|
|
var defaultSettings = {
|
|
gpgPath: null,
|
|
stores: {}
|
|
};
|
|
|
|
var authListeners = {};
|
|
|
|
// watch for tab updates
|
|
chrome.tabs.onUpdated.addListener(function(tab, info) {
|
|
// ignore non-complete status
|
|
if (info.status !== "complete") {
|
|
return;
|
|
}
|
|
|
|
// unregister any auth listeners for this tab
|
|
if (authListeners[tab.id]) {
|
|
chrome.tabs.onAuthRequired.removeListener(authListeners[tab.id]);
|
|
delete authListeners[tab.id];
|
|
}
|
|
});
|
|
|
|
// handle incoming messages
|
|
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
|
|
receiveMessage(message, sender, sendResponse);
|
|
|
|
// allow async responses after this function returns
|
|
return true;
|
|
});
|
|
|
|
//----------------------------------- Function definitions ----------------------------------//
|
|
|
|
/**
|
|
* Copy text to clipboard
|
|
*
|
|
* @since 3.0.0
|
|
*
|
|
* @param string text Text to copy
|
|
* @return void
|
|
*/
|
|
function copyToClipboard(text) {
|
|
document.addEventListener(
|
|
"copy",
|
|
function(e) {
|
|
e.clipboardData.setData("text/plain", text);
|
|
e.preventDefault();
|
|
},
|
|
{ once: true }
|
|
);
|
|
document.execCommand("copy");
|
|
}
|
|
|
|
/**
|
|
* Get Local settings from the extension
|
|
*
|
|
* @since 3.0.0
|
|
*
|
|
* @return object Local settings from the extension
|
|
*/
|
|
function getLocalSettings() {
|
|
var settings = Object.assign({}, defaultSettings);
|
|
for (var key in settings) {
|
|
var value = localStorage.getItem(key);
|
|
if (value !== null) {
|
|
settings[key] = JSON.parse(value);
|
|
}
|
|
}
|
|
|
|
return settings;
|
|
}
|
|
|
|
/**
|
|
* Handle modal authentication requests (e.g. HTTP basic)
|
|
*
|
|
* @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 lauched 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
|
|
*
|
|
* @since 3.0.0
|
|
*
|
|
* @param object settings Settings object
|
|
* @param mixed message Incoming message
|
|
* @param function(mixed) sendResponse Callback to send response
|
|
* @return void
|
|
*/
|
|
async function handleMessage(settings, message, sendResponse) {
|
|
// check that action is present
|
|
if (typeof message !== "object" || !message.hasOwnProperty("action")) {
|
|
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({
|
|
status: "ok",
|
|
settings: settings
|
|
});
|
|
break;
|
|
case "saveSettings":
|
|
saveSettings(message.settings);
|
|
sendResponse({ status: "ok" });
|
|
break;
|
|
case "listFiles":
|
|
try {
|
|
var response = await hostAction(settings, "list");
|
|
sendResponse(response.data.files);
|
|
} catch (e) {
|
|
sendResponse({
|
|
status: "error",
|
|
message: "Unable to enumerate password files" + e.toString()
|
|
});
|
|
}
|
|
break;
|
|
case "copyPassword":
|
|
try {
|
|
copyToClipboard(message.login.fields.secret);
|
|
sendResponse({ status: "ok" });
|
|
} catch (e) {
|
|
sendResponse({
|
|
status: "error",
|
|
message: "Unable to copy password"
|
|
});
|
|
}
|
|
break;
|
|
case "copyUsername":
|
|
try {
|
|
copyToClipboard(message.login.fields.login);
|
|
sendResponse({ status: "ok" });
|
|
} catch (e) {
|
|
sendResponse({
|
|
status: "error",
|
|
message: "Unable to copy username"
|
|
});
|
|
}
|
|
break;
|
|
|
|
case "launch":
|
|
try {
|
|
var tab = (await chrome.tabs.query({ active: true, currentWindow: true }))[0];
|
|
var url = message.login.fields.url || message.login.domain;
|
|
if (!url) {
|
|
throw new Error("No URL is defined for this entry");
|
|
}
|
|
if (!url.match(/:\/\//)) {
|
|
url = "http://" + 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"]
|
|
);
|
|
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 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" });
|
|
} catch (e) {
|
|
sendResponse({
|
|
status: "error",
|
|
message: "Unable to fill credentials: " + e.toString()
|
|
});
|
|
}
|
|
break;
|
|
default:
|
|
sendResponse({
|
|
status: "error",
|
|
message: "Unknown action: " + message.action
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send a request to the host app
|
|
*
|
|
* @since 3.0.0
|
|
*
|
|
* @param object settings Live settings object
|
|
* @param string action Action to run
|
|
* @param params object Additional params to pass to the host app
|
|
* @return Promise
|
|
*/
|
|
function hostAction(settings, action, params = {}) {
|
|
var request = {
|
|
settings: settings,
|
|
action: action
|
|
};
|
|
for (var key in params) {
|
|
request[key] = params[key];
|
|
}
|
|
|
|
return chrome.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.name,
|
|
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(/(?<=^[^:]+):/)
|
|
.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 if (key == "login") {
|
|
login.fields[key] = login.login.match(/([^\/]+)$/)[1];
|
|
} else {
|
|
login.fields[key] = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wrap inbound messages to fetch native configuration
|
|
*
|
|
* @since 3.0.0
|
|
*
|
|
* @param mixed message Incoming message
|
|
* @param MessageSender sender Message sender
|
|
* @param function(mixed) sendResponse Callback to send response
|
|
* @return void
|
|
*/
|
|
async function receiveMessage(message, sender, sendResponse) {
|
|
// restrict messages to this extension only
|
|
if (sender.id !== chrome.runtime.id) {
|
|
// silently exit without responding when the source is foreign
|
|
return;
|
|
}
|
|
|
|
var settings = getLocalSettings();
|
|
try {
|
|
var configureSettings = Object.assign(settings, { defaultStore: {} });
|
|
var response = await chrome.runtime.sendNativeMessage(appID, {
|
|
settings: configureSettings,
|
|
action: "configure"
|
|
});
|
|
settings.version = response.version;
|
|
if (settings.stores.length) {
|
|
// there are user-configured stores present
|
|
for (var key in settings.stores) {
|
|
if (response.data.storeSettings.hasOwnProperty(key)) {
|
|
var fileSettings = JSON.parse(response.data.storeSettings[key]);
|
|
if (typeof settings.stores[key].settings !== "object") {
|
|
settings.stores[key].settings = {};
|
|
}
|
|
var storeSettings = settings.stores[key].settings;
|
|
for (var settingKey in fileSettings) {
|
|
if (!storeSettings.hasOwnProperty(settingKey)) {
|
|
storeSettings[settingKey] = fileSettings[settingKey];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// no user-configured stores, so use the default store
|
|
settings.stores.default = {
|
|
name: "default",
|
|
path: response.data.defaultStore.path,
|
|
settings: response.data.defaultStore.settings
|
|
};
|
|
}
|
|
handleMessage(settings, message, sendResponse);
|
|
} catch (e) {
|
|
// handle error
|
|
console.log(e);
|
|
sendResponse({ status: "error", message: e.toString() });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save settings
|
|
*
|
|
* @since 3.0.0
|
|
*
|
|
* @param object Final settings object
|
|
* @return void
|
|
*/
|
|
function saveSettings(settings) {
|
|
for (var key in defaultSettings) {
|
|
if (settings.hasOwnProperty(key)) {
|
|
localStorage.setItem(key, JSON.stringify(settings[key]));
|
|
}
|
|
}
|
|
}
|