gPass/chrome_addon/background.js

566 lines
14 KiB
JavaScript

/*
Copyright (C) 2013-2022 Grégory Soutadé
This file is part of gPass.
gPass is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
gPass is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with gPass. If not, see <http://www.gnu.org/licenses/>.
*/
import {parseUri} from "./lib/parseuri.js";
import {SERVER, GPASS_ICON, wildcard_domain, simple_pbkdf2, crypto_pbkdf2,
encrypt_ecb, encrypt_cbc, decrypt_cbc, digest, a2hex, hex2a, debug} from "./lib/misc.js";
import {get_preference, set_preference, delete_preference} from "./compat.js";
var browser = browser || chrome;
var protocol_version = 4;
var account_url = null;
function _notification(message, data)
{
if (message !== data)
message += data;
var options = {
type: "basic",
title : "gPass",
message : message,
iconUrl:"icons/gpass_icon_64.png"
};
browser.notifications.create("gPass", options, function(){});
}
async function generate_request(domain, login, mkey, iv)
{
var v = domain + ";" + login;
debug("will encrypt " + v);
while ((v.length % 16))
v += "\0";
var hash = await digest(crypto, v);
v += hash.slice(8, 24);
var enc = encrypt_cbc(mkey, iv, v);
return enc;
}
async function ask_server(logins, domain, wdomain, mkey, sendResponse, options)
{
var account_url = await get_preference("account_url");
var salt = parseUri(account_url);
salt = salt["host"] + salt["path"];
debug("salt " + salt);
var pbkdf2_level = await get_preference("pbkdf2_level");
var global_iv = await simple_pbkdf2(crypto, salt, mkey, pbkdf2_level);
global_iv = global_iv.slice(0, 16);
var mkey = crypto_pbkdf2(crypto, mkey, salt, pbkdf2_level);
debug("global_iv " + a2hex(global_iv));
var keys = "";
var key_index;
var a;
for(key_index=0, a=0; a<logins.length; a++, key_index++)
{
var enc = await generate_request(domain, logins[a], mkey, global_iv);
keys += (keys.length != 0) ? "&" : "";
keys += "k" + key_index + "=" + a2hex(enc);
if (wdomain != "")
{
enc = await generate_request(wdomain, logins[a], mkey, global_iv);
keys += (keys.length != 0) ? "&" : "";
keys += "k" + (++key_index) + "=" + a2hex(enc);
}
}
debug("Keys " + keys);
var ret = SERVER.OK;
debug("connect to " + account_url);
const headers = {'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8'};
const request = new Request(account_url, {method: 'POST', headers: headers, body: keys});
const response = await fetch(request).catch((e) => {
debug(e);
_notification("Network error : check your server address", "");
});
if (response === undefined)
{
ret = SERVER.ERROR;
sendResponse({"value": ret, options:options});
return;
}
const responseText = await response.text();
var ciphered_password = "";
var server_pbkdf2_level = 0;
var server_version = 0;
var matched_key = 0;
var r = responseText.split("\n");
debug("resp " + r);
for(var a=0; a<r.length; a++)
{
debug("Analyse " + r[a]);
var params = r[a].split("=");
if (params.length != 2 && params[0] != "<end>")
{
_notification("Error : It seems that it's not a gPass server",
responseText);
ret = SERVER.FAILED;
break;
}
switch(params[0])
{
case "protocol":
debug("protocol : " + params[1]);
if (params[1].indexOf("gpass-") != 0)
{
_notification("Error : It seems that it's not a gPass server",
responseText);
ret = SERVER.FAILED;
break;
}
var server_protocol_version = params[1].match(/\d+/)[0];
if (server_protocol_version > protocol_version)
{
_notification("Protocol version not supported, please upgrade your addon", "");
ret = SERVER.FAILED;
}
else
{
switch (server_protocol_version)
{
case 2:
server_pbkdf2_level = 1000;
break;
case 3:
// Version 3 : nothing special to do
case 4:
// Version 4 : nothing special to do
break;
}
}
break;
case "matched_key":
matched_key = params[1];
case "pass":
ciphered_password = params[1];
break;
case "pkdbf2_level":
case "pbkdf2_level":
server_pbkdf2_level = parseInt(params[1].match(/\d+/)[0], 10);
if (server_pbkdf2_level != NaN &&
server_pbkdf2_level != pbkdf2_level &&
server_pbkdf2_level >= 1000) // Minimum level for PBKDF2 !
{
debug("New pbkdf2 level " + server_pbkdf2_level);
pbkdf2_level = server_pbkdf2_level;
set_preference("pbkdf2_level", pbkdf2_level, null);
ret = SERVER.RESTART_REQUEST;
}
break;
case "<end>":
break;
default:
debug("Unknown command " + params[0]);
_notification("Error : It seems that it's not a gPass server",
responseText);
ret = SERVER.FAILED;
break;
}
}
if (ret != SERVER.OK)
{
sendResponse({"value": ret, options:options});
return;
}
if (ciphered_password != "")
{
debug("Ciphered password : " + ciphered_password);
var clear_password = await decrypt_cbc(mkey, global_iv, hex2a(ciphered_password));
clear_password = clear_password.replace(/\0*$/, "");
clear_password = clear_password.substr(3, clear_password.length);
debug("Clear password " + clear_password);
sendResponse({"value": ret, "password":clear_password, "options":options});
}
else
{
debug("No password found");
ret = SERVER.FAILED;
_notification("No password found in database", "")
sendResponse({"value": ret, "options":options});
}
return;
}
function update_gpass_icon(iconId, tabId)
{
debug("update_gpass_icon");
var icon_infos = {"tabId":tabId};
var icon_name = "";
switch (iconId)
{
case GPASS_ICON.NORMAL: break;
case GPASS_ICON.DISABLED:
icon_name = "_disabled";
break;
case GPASS_ICON.ACTIVATED:
icon_name = "_activated";
break;
default:
}
debug(icon_name);
var icon_infos = {};
icon_infos["path"] = {
16:"icons/gpass" + icon_name + "_icon_16.png",
32:"icons/gpass" + icon_name + "_icon_32.png",
64:"icons/gpass" + icon_name + "_icon_64.png",
128:"icons/gpass" + icon_name + "_icon_128.png",
};
if (browser.browserAction)
browser.browserAction.setIcon(icon_infos);
else
browser.action.setIcon(icon_infos);
}
async function is_gpass_enabled(uri)
{
return await get_preference("always_disabled").then(
function(always_disabled) {
if (always_disabled)
{
debug("Always disabled");
return new Promise(function(resolve, reject) {
resolve(1); // null -> enabled, 1 -> disabled
});
}
else
{
debug("Check for enable");
var domain = parseUri(uri);
domain = domain["host"];
debug("Is gpass enabled for " + domain + " ?");
return get_preference("disable-" + domain);
}
}
);
}
function save_gpass_enable_config(uri, enable)
{
var domain = parseUri(uri);
domain = domain["host"];
var key = "disable-" + domain;
if (enable)
{
debug("Enable gpass for " + domain);
delete_preference(key);
}
else
{
debug("Disable gpass for " + domain);
set_preference(key, true, null);
}
}
function _query_tabs_is_gpass_enabled(tabs, sendResponse)
{
if (tabs.length)
{
is_gpass_enabled(tabs[0].url).then(
function (key_present) {
var enabled = (key_present == null);
update_gpass_icon((enabled)?GPASS_ICON.NORMAL:GPASS_ICON.DISABLED, tabs[0].id);
sendResponse({"enabled":enabled});
}
);
}
else
{
debug("No cur tab");
sendResponse({"enabled":true});
}
return true;
}
function _query_tabs_update_icon(tabs, iconId)
{
if (tabs.length)
{
update_gpass_icon(iconId, tabs[0].id);
}
}
function update_enable(enabled, tab, saveConfig)
{
var parameters;
if (enabled)
{
parameters = {type:"blockForms"};
saveConfig = true;// Force save when enable website
debug("Now enabled");
}
else
{
parameters = {type:"unblockForms"};
debug("Now disabled");
}
if (saveConfig)
save_gpass_enable_config(tab.url, enabled);
update_gpass_icon((enabled)?GPASS_ICON.NORMAL:GPASS_ICON.DISABLED, tab.id);
browser.tabs.sendMessage(tab.id, parameters);
}
function gpass_switch_enable(tab)
{
debug("Switch enable");
is_gpass_enabled(tab.url).then(
function (key_present)
{
var enabled = (key_present == null);
// Do switch
enabled = !enabled;
update_enable(enabled, tab, true);
});
}
function createMenus(browser)
{
var title;
debug("Create menus");
/* Not supported by Chrome */
if (browser.menus.onShown)
title = 'Disable gPass for this website';
else
title = 'Disable or enable gPass for this website';
/* Enable/disable */
browser.menus.create({
id: 'switch_enable',
title: title,
contexts: ['action'],
}, () => {console.log(chrome.runtime.lastError);});
/* Not supported by Chrome */
if (browser.menus.onShown)
title = 'Disable gPass for ALL websites';
else
title = 'Disable or enable gPass for ALL websites';
/* Always enable/disable */
browser.menus.create({
id: 'always_disable',
title: title,
contexts: ['action']
});
}
function extension_load()
{
browser.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
if (request.type == "password")
{
var domain = parseUri(request.domain);
domain = domain["host"];
var wdomain = wildcard_domain(domain);
ask_server(request.logins, domain,
wdomain, request.mkey,
sendResponse, request.options);
return true;
}
else if (request.type == "notification")
{
_notification(request.options.message, request.options.data);
}
else if (request.type == "getServerAddress")
{
get_preference("account_url").then(
function (address) {
sendResponse({"value" : address});
});
return true;
}
else if (request.type == "getPopupClipboard")
{
get_preference("popup_clipboard").then(
function (value) {
sendResponse({"value" : value});
});
return true;
}
else if (request.type == "is_gpass_enabled")
{
browser.tabs.query({active:true, currentWindow:true}).then( (tabs) =>
{
_query_tabs_is_gpass_enabled(tabs, sendResponse);
});
return true;
}
else if (request.type == "update_icon")
{
debug("update_icon");
browser.tabs.query({active:true, currentWindow:true}).then( (tabs) =>
{
_query_tabs_update_icon(tabs, request.icon_id);
});
}
else
{
debug("Unknown message " + request.type);
}
}
);
/* Chrome */
if (!browser.menus && browser.contextMenus)
{
browser.menus = browser.contextMenus;
}
browser.runtime.onInstalled.addListener(() => {
createMenus(browser)
});
browser.menus.onClicked.addListener(
function(info, tab) {
switch (info.menuItemId) {
case 'always_disable':
get_preference('always_disabled').then(
function (always_disabled) {
debug('Change always disable');
debug(always_disabled);
always_disabled = !always_disabled;
set_preference('always_disabled', always_disabled,
function(error)
{
browser.tabs.query({active:true, currentWindow:true},
(tabs) => {
for (var i=0; i<tabs.length; i++)
update_enable(!always_disabled, tabs[i], false);
});
}
);
}
);
break;
case 'switch_enable':
gpass_switch_enable(tab);
break;
}
}
);
/* Firefox only */
if (browser.menus.onShown)
{
browser.menus.onShown.addListener(
function(info, tab) {
is_gpass_enabled(tab.url).then(
function (key_present) {
enabled = (key_present == null);
if (enabled)
title = 'Disable gPass for this website';
else
title = 'Enable gPass for this website';
browser.menus.update("switch_enable",
{
"title":title
}
);
browser.menus.refresh();
}
);
}
);
browser.menus.onShown.addListener(
function(info, tab) {
get_preference('always_disabled').then(
function (always_disabled) {
if (always_disabled)
title = 'Enable gPass for ALL websites';
else
title = 'Disable gPass for ALL websites';
browser.menus.update("always_disable",
{
"title":title
}
);
browser.menus.refresh();
}
);
}
);
}
}
async function self_test()
{
mkey = crypto_pbkdf2("password", "salt", 4096);
res = await encrypt_ecb(mkey, "DDDDDDDDDDDDDDDD");
reference = new Uint8Array([0xc4, 0x76, 0x01, 0x07, 0xa1, 0xc0, 0x2f, 0x22, 0xee, 0xbe, 0x60,
0xff, 0x65, 0x33, 0x5b, 0x9e]);
if (res != ab2str(reference))
{
console.log("Self test ERROR !");
}
else
console.log("Self test OK !");
}
//self_test();
extension_load();