Big update:
* Move all core functions from mains.js to background.js * Use message interface for IPCs between main.js, background.js and popup.js * Add popup interface : * safest method to compute masterkey * Direct access to our own gPass server with auto URL and username fill * Add some specific menus : * Access to gPass settings * Allow to disable extension * Update gPass icon when a password field has focus and gPass is ready to work
This commit is contained in:
@@ -1,25 +1,261 @@
|
||||
/*
|
||||
Copyright (C) 2013-2017 Grégory Soutadé
|
||||
|
||||
Copyright (C) 2013-2020 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/>.
|
||||
*/
|
||||
|
||||
var browser = browser || chrome;
|
||||
var protocol_version = 4;
|
||||
var account_url = null;
|
||||
var crypto_v2_logins_size = 0;
|
||||
|
||||
function _notification(message, data)
|
||||
{
|
||||
if (message !== data)
|
||||
message += data;
|
||||
|
||||
options = {
|
||||
type: "basic",
|
||||
title : "gPass",
|
||||
message : message,
|
||||
iconUrl:browser.extension.getURL("icons/gpass_icon_64.png")
|
||||
};
|
||||
|
||||
browser.notifications.create("gPass", options, function(){});
|
||||
|
||||
window.setTimeout(function() {browser.notifications.clear("gPass", function(){})}, 2000);
|
||||
}
|
||||
|
||||
async function generate_request(domain, login, mkey, iv, old)
|
||||
{
|
||||
if (old)
|
||||
{
|
||||
var v = "@@" + domain + ";" + login;
|
||||
debug("will encrypt " + v);
|
||||
enc = encrypt_ecb(mkey, v);
|
||||
}
|
||||
else
|
||||
{
|
||||
var v = domain + ";" + login;
|
||||
debug("will encrypt " + v);
|
||||
while ((v.length % 16))
|
||||
v += "\0";
|
||||
hash = await digest(v);
|
||||
v += hash.slice(8, 24);
|
||||
enc = encrypt_cbc(mkey, iv, v);
|
||||
}
|
||||
return enc;
|
||||
}
|
||||
|
||||
async function ask_server(logins, domain, wdomain, mkey, sendResponse, options)
|
||||
{
|
||||
account_url = await get_preference("account_url");
|
||||
|
||||
var salt = parseURI.parseUri(account_url);
|
||||
salt = salt["host"] + salt["path"];
|
||||
|
||||
debug("salt " + salt);
|
||||
|
||||
pbkdf2_level = await get_preference("pbkdf2_level");
|
||||
|
||||
global_iv = await simple_pbkdf2(salt, mkey, pbkdf2_level);
|
||||
global_iv = global_iv.slice(0, 16);
|
||||
mkey = crypto_pbkdf2(mkey, salt, pbkdf2_level);
|
||||
|
||||
debug("global_iv " + a2hex(global_iv));
|
||||
|
||||
keys = "";
|
||||
for(key_index=0, a=0; a<logins.length; a++, key_index++)
|
||||
{
|
||||
enc = await generate_request(domain, logins[a], mkey, global_iv, 0);
|
||||
keys += (keys.length != 0) ? "&" : "";
|
||||
keys += "k" + key_index + "=" + a2hex(enc);
|
||||
|
||||
if (wdomain != "")
|
||||
{
|
||||
enc = await generate_request(wdomain, logins[a], mkey, global_iv, 0);
|
||||
keys += (keys.length != 0) ? "&" : "";
|
||||
keys += "k" + (++key_index) + "=" + a2hex(enc);
|
||||
}
|
||||
}
|
||||
|
||||
crypto_v2_logins_size = key_index;
|
||||
if (await get_preference("crypto_v1_compatible"))
|
||||
{
|
||||
for(a=0; a<logins.length; a++, key_index++)
|
||||
{
|
||||
enc = await generate_request(domain, logins[a], mkey, global_iv, 1);
|
||||
keys += (keys.length != 0) ? "&" : "";
|
||||
keys += "k" + key_index + "=" + a2hex(enc);
|
||||
|
||||
if (wdomain != "")
|
||||
{
|
||||
enc = await generate_request(wdomain, logins[a], mkey, global_iv, 1);
|
||||
keys += (keys.length != 0) ? "&" : "";
|
||||
keys += "k" + (++key_index) + "=" + a2hex(enc);
|
||||
}
|
||||
}
|
||||
}
|
||||
debug("Keys " + keys);
|
||||
|
||||
var gPassRequest = new XMLHttpRequest();
|
||||
|
||||
var ret = SERVER.OK;
|
||||
|
||||
// gPassRequest.addEventListener("progress", function(evt) { ; }, false);
|
||||
gPassRequest.addEventListener("load", async function(evt) {
|
||||
var ciphered_password = "";
|
||||
var clear_password = "";
|
||||
var server_pbkdf2_level = 0;
|
||||
var server_version = 0;
|
||||
var matched_key = 0;
|
||||
|
||||
var r = this.responseText.split("\n");
|
||||
debug("resp " + r);
|
||||
|
||||
for(var a=0; a<r.length; a++)
|
||||
{
|
||||
debug("Analyse " + r[a]);
|
||||
|
||||
params = r[a].split("=");
|
||||
if (params.length != 2 && params[0] != "<end>")
|
||||
{
|
||||
_notification("Error : It seems that it's not a gPass server",
|
||||
this.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",
|
||||
this.responseText);
|
||||
ret = SERVER.FAILED;
|
||||
break;
|
||||
}
|
||||
|
||||
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);
|
||||
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",
|
||||
this.responseText);
|
||||
ret = SERVER.FAILED;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ret != SERVER.OK)
|
||||
{
|
||||
sendResponse({"value": ret, options:options});
|
||||
return;
|
||||
}
|
||||
|
||||
if (ciphered_password != "")
|
||||
{
|
||||
debug("Ciphered password : " + ciphered_password);
|
||||
if (matched_key >= crypto_v2_logins_size)
|
||||
// Crypto v1
|
||||
{
|
||||
clear_password = await decrypt_ecb(mkey, hex2a(ciphered_password));
|
||||
// Remove trailing \0 and salt
|
||||
clear_password = clear_password.replace(/\0*$/, "");
|
||||
clear_password = clear_password.substr(0, clear_password.length-3);
|
||||
}
|
||||
else
|
||||
{
|
||||
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);
|
||||
}
|
||||
else
|
||||
{
|
||||
debug("No password found");
|
||||
|
||||
ret = SERVER.FAILED;
|
||||
|
||||
_notification("No password found in database", "")
|
||||
}
|
||||
|
||||
sendResponse({"value": ret, "password":clear_password, "options":options});
|
||||
}, false);
|
||||
gPassRequest.addEventListener("error", function(evt) {
|
||||
debug("error");
|
||||
ret = false;
|
||||
_notification("Error");
|
||||
}, false);
|
||||
debug("connect to " + account_url);
|
||||
gPassRequest.open("POST", account_url, true);
|
||||
gPassRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
|
||||
gPassRequest.send(keys);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function url_block_callback(details)
|
||||
{
|
||||
// console.log(JSON.stringify(details));
|
||||
// debug(JSON.stringify(details));
|
||||
if (details.requestBody)
|
||||
{
|
||||
if (details.requestBody.formData)
|
||||
@@ -40,56 +276,307 @@ function url_block_callback(details)
|
||||
// Analyse POST parameters
|
||||
if (details.method == "POST" && details.requestBody.raw)
|
||||
{
|
||||
alert(details.requestBody.raw);
|
||||
var postedString = decodeURIComponent(String.fromCharCode.apply(null,
|
||||
new Uint8Array(details.requestBody.raw[0].bytes)));
|
||||
if (postedString.indexOf("=@@") != -1 ||
|
||||
postedString.indexOf("=@_") != -1)
|
||||
return {cancel: true};
|
||||
alert(details.requestBody.raw);
|
||||
var postedString = decodeURIComponent(String.fromCharCode.apply(null,
|
||||
new Uint8Array(details.requestBody.raw[0].bytes)));
|
||||
if (postedString.indexOf("=@@") != -1 ||
|
||||
postedString.indexOf("=@_") != -1)
|
||||
return {cancel: true};
|
||||
}
|
||||
*/
|
||||
*/
|
||||
}
|
||||
|
||||
return {cancel: false};
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener(
|
||||
function(request, sender, sendResponse) {
|
||||
function url_unblock_callback(details)
|
||||
{
|
||||
return {cancel: false};
|
||||
}
|
||||
|
||||
if (request.type == "notification")
|
||||
function update_gpass_icon(iconId, tabId)
|
||||
{
|
||||
debug("update_gpass_icon");
|
||||
|
||||
icon_infos = {"tabId":tabId};
|
||||
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:
|
||||
}
|
||||
|
||||
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",
|
||||
};
|
||||
|
||||
browser.browserAction.setIcon(icon_infos);
|
||||
}
|
||||
|
||||
function is_gpass_enabled(uri)
|
||||
{
|
||||
var domain = parseURI.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.parseUri(uri);
|
||||
domain = domain["host"];
|
||||
|
||||
key = "disable-" + domain;
|
||||
if (enable)
|
||||
{
|
||||
debug("Enable gpass for " + domain);
|
||||
delete_preference(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
debug("Disable gpass for " + domain);
|
||||
set_preference(key, true);
|
||||
}
|
||||
}
|
||||
|
||||
function _block_url(tabs, callback)
|
||||
{
|
||||
options = {
|
||||
urls:[tabs[0].url],
|
||||
"types":["main_frame"]
|
||||
};
|
||||
|
||||
if (tabs.length)
|
||||
{
|
||||
options["tabId"] = tabs[0].id;
|
||||
options["windowId"] = tabs[0].windowId;
|
||||
}
|
||||
|
||||
browser.webRequest.onBeforeRequest.addListener(
|
||||
url_block_callback,
|
||||
options,
|
||||
["blocking", "requestBody"]);
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
function _query_tabs_block_url(tabs)
|
||||
{
|
||||
return _block_url(tabs, url_block_callback);
|
||||
}
|
||||
|
||||
function _query_tabs_unblock_url(tabs)
|
||||
{
|
||||
return _block_url(tabs, url_unblock_callback);
|
||||
}
|
||||
|
||||
function _query_tabs_is_gpass_enabled(tabs, sendResponse)
|
||||
{
|
||||
if (tabs.length)
|
||||
{
|
||||
is_gpass_enabled(tabs[0].url).then(
|
||||
function (key_present) {
|
||||
enabled = (key_present == null);
|
||||
update_gpass_icon((enabled)?GPASS_ICON.ENABLED: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 gpass_switch_enable(tab)
|
||||
{
|
||||
is_gpass_enabled(tab.url).then(
|
||||
function (key_present)
|
||||
{
|
||||
options = {
|
||||
type: "basic",
|
||||
title : "gPass",
|
||||
message : request.options.message,
|
||||
iconUrl:chrome.extension.getURL("icons/gpass_icon_64.png")
|
||||
};
|
||||
enabled = (key_present == null);
|
||||
// Do switch
|
||||
enabled = !enabled;
|
||||
if (enabled)
|
||||
{
|
||||
parameters = {type:"blockForms"};
|
||||
debug("Now enabled");
|
||||
}
|
||||
else
|
||||
{
|
||||
parameters = {type:"unblockForms"};
|
||||
debug("Now disabled");
|
||||
}
|
||||
|
||||
chrome.notifications.create("gPass", options, function(){});
|
||||
save_gpass_enable_config(tab.url, enabled);
|
||||
update_gpass_icon((enabled)?GPASS_ICON.ENABLED:GPASS_ICON.DISABLED, tab.id);
|
||||
browser.tabs.sendMessage(tab.id, parameters);
|
||||
});
|
||||
}
|
||||
|
||||
window.setTimeout(function() {chrome.notifications.clear("gPass", function(){})}, 2000);
|
||||
}
|
||||
else if (request.type == "block_url")
|
||||
{
|
||||
chrome.tabs.getCurrent(function cb(tab) {
|
||||
if (tab)
|
||||
{
|
||||
chrome.webRequest.onBeforeRequest.addListener(
|
||||
url_block_callback,
|
||||
{urls:[request.options.url],
|
||||
"types":["main_frame"],
|
||||
"tabId":tab.id,
|
||||
"windowId":tab.windowId
|
||||
},
|
||||
["blocking", "requestBody"]);
|
||||
}
|
||||
else
|
||||
{
|
||||
chrome.webRequest.onBeforeRequest.addListener(
|
||||
url_block_callback,
|
||||
{urls:[request.options.url], "types":["main_frame"]},
|
||||
["blocking", "requestBody"]);
|
||||
}
|
||||
});
|
||||
function extension_load()
|
||||
{
|
||||
browser.runtime.onMessage.addListener(
|
||||
function(request, sender, sendResponse) {
|
||||
if (request.type == "password")
|
||||
{
|
||||
var domain = parseURI.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 == "block_url")
|
||||
{
|
||||
browser.tabs.query({active:true, currentWindow:true}, _query_tabs_block_url);
|
||||
}
|
||||
else if (request.type == "unblock_url")
|
||||
{
|
||||
browser.tabs.query({active:true, currentWindow:true}, _query_tabs_unblock_url);
|
||||
}
|
||||
else if (request.type == "is_gpass_enabled")
|
||||
{
|
||||
browser.tabs.query({active:true, currentWindow:true},
|
||||
function cb(tabs) {
|
||||
_query_tabs_is_gpass_enabled(tabs, sendResponse);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
else if (request.type == "switch_enable")
|
||||
{
|
||||
debug("Switch enable");
|
||||
browser.tabs.query({active:true, currentWindow:true},
|
||||
function cb(tabs) {
|
||||
_query_tabs_switch_enable(tabs, sendResponse)
|
||||
});
|
||||
return true;
|
||||
}
|
||||
else if (request.type == "update_icon")
|
||||
{
|
||||
debug("update_icon");
|
||||
browser.tabs.query({active:true, currentWindow:true},
|
||||
function cb(tabs) {
|
||||
_query_tabs_update_icon(tabs, request.icon_id);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
debug("Unknown message " + request.type);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!browser.menus && browser.contextMenus)
|
||||
browser.menus = browser.contextMenus;
|
||||
|
||||
browser.menus.create({
|
||||
id: 'settings',
|
||||
title: 'gPass Settings',
|
||||
contexts: ['browser_action']
|
||||
});
|
||||
|
||||
/* Not supported by Chrome */
|
||||
if (browser.menus.onShown)
|
||||
title = 'disable gPass for this website';
|
||||
else
|
||||
title = 'disable or enable gPass for this website';
|
||||
|
||||
browser.menus.create({
|
||||
id: 'switch_enable',
|
||||
title: title,
|
||||
contexts: ['browser_action']
|
||||
});
|
||||
|
||||
browser.menus.onClicked.addListener(
|
||||
function(info, tab) {
|
||||
switch (info.menuItemId) {
|
||||
case 'settings':
|
||||
browser.runtime.openOptionsPage();
|
||||
break;
|
||||
|
||||
case 'switch_enable':
|
||||
gpass_switch_enable(tab);
|
||||
break;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
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();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
@@ -1,23 +1,23 @@
|
||||
/*
|
||||
Copyright (C) 2013-2017 Grégory Soutadé
|
||||
|
||||
Copyright (C) 2013-2020 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/>.
|
||||
*/
|
||||
|
||||
function getPref(key)
|
||||
function get_preference(key)
|
||||
{
|
||||
// Inspired from https://github.com/akiomik/chrome-storage-promise/
|
||||
var promise = new Promise((resolve, reject) => {
|
||||
@@ -32,14 +32,33 @@ function getPref(key)
|
||||
})
|
||||
.then(function (pref) {
|
||||
if (!pref.hasOwnProperty(key))
|
||||
return default_preferences[key];
|
||||
{
|
||||
if (default_preferences.hasOwnProperty(key))
|
||||
return default_preferences[key];
|
||||
else
|
||||
return null;
|
||||
}
|
||||
return pref[key];
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
function setPref(key, value)
|
||||
function set_preference(key, value)
|
||||
{
|
||||
chrome.storage.local.set({key:value}, function ok() {});
|
||||
pref = {[key]:value};
|
||||
chrome.storage.local.set(pref, function (result) {
|
||||
if (chrome.runtime.lastError)
|
||||
alert(chrome.runtime.lastError);
|
||||
});
|
||||
}
|
||||
|
||||
function delete_preference(key)
|
||||
{
|
||||
chrome.storage.local.remove(key);
|
||||
}
|
||||
|
||||
function send_tab_message(tab_id, parameters, callback)
|
||||
{
|
||||
chrome.tabs.sendMessage(tab_id, parameters, {}, callback);
|
||||
}
|
||||
|
@@ -1,305 +1,44 @@
|
||||
/*
|
||||
Copyright (C) 2013-2017 Grégory Soutadé
|
||||
|
||||
Copyright (C) 2013-2020 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/>.
|
||||
*/
|
||||
|
||||
var DEBUG = false;
|
||||
var protocol_version = 4;
|
||||
var account_url = null;
|
||||
var crypto_v2_logins_size = 0;
|
||||
var gpass_enabled = true;
|
||||
|
||||
SERVER = {OK : 0, FAILED : 1, RESTART_REQUEST : 2};
|
||||
|
||||
// http://stackoverflow.com/questions/3745666/how-to-convert-from-hex-to-ascii-in-javascript
|
||||
function hex2a(hex) {
|
||||
var str = '';
|
||||
for (var i = 0; i < hex.length; i += 2)
|
||||
str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
|
||||
return str;
|
||||
}
|
||||
|
||||
function a2hex(_str_) {
|
||||
var hex = '';
|
||||
for (var i = 0; i < _str_.length; i++)
|
||||
{
|
||||
var c = _str_.charCodeAt(i).toString(16);
|
||||
if (c.length == 1) c = "0" + c;
|
||||
hex += c;
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
function debug(s)
|
||||
function _notification(message, data)
|
||||
{
|
||||
if (DEBUG)
|
||||
console.log(s);
|
||||
}
|
||||
if (message !== data)
|
||||
message += data;
|
||||
|
||||
async function generate_request(domain, login, mkey, iv, old)
|
||||
{
|
||||
if (old)
|
||||
{
|
||||
var v = "@@" + domain + ";" + login;
|
||||
debug("will encrypt " + v);
|
||||
enc = encrypt_ecb(mkey, v);
|
||||
}
|
||||
else
|
||||
{
|
||||
var v = domain + ";" + login;
|
||||
debug("will encrypt " + v);
|
||||
while ((v.length % 16))
|
||||
v += "\0";
|
||||
hash = await digest(v);
|
||||
v += hash.slice(8, 24);
|
||||
enc = encrypt_cbc(mkey, iv, v);
|
||||
}
|
||||
return enc;
|
||||
}
|
||||
options = {
|
||||
type: "basic",
|
||||
title : "gPass",
|
||||
message : message,
|
||||
iconUrl:browser.extension.getURL("icons/gpass_icon_64.png")
|
||||
};
|
||||
|
||||
async function ask_server(form, field, logins, domain, wdomain, mkey, submit)
|
||||
{
|
||||
account_url = await getPref("account_url");
|
||||
|
||||
var salt = parseURI.parseUri(account_url);
|
||||
salt = salt["host"] + salt["path"];
|
||||
|
||||
debug("salt " + salt);
|
||||
|
||||
pbkdf2_level = await getPref("pbkdf2_level");
|
||||
|
||||
global_iv = await simple_pbkdf2(salt, mkey, pbkdf2_level);
|
||||
global_iv = global_iv.slice(0, 16);
|
||||
mkey = crypto_pbkdf2(mkey, salt, pbkdf2_level);
|
||||
|
||||
debug("global_iv " + a2hex(global_iv));
|
||||
|
||||
keys = "";
|
||||
for(key_index=0, a=0; a<logins.length; a++, key_index++)
|
||||
{
|
||||
enc = await generate_request(domain, logins[a], mkey, global_iv, 0);
|
||||
keys += (keys.length != 0) ? "&" : "";
|
||||
keys += "k" + key_index + "=" + a2hex(enc);
|
||||
|
||||
if (wdomain != "")
|
||||
browser.notifications.create(options).then(
|
||||
function created(notification_id)
|
||||
{
|
||||
enc = await generate_request(wdomain, logins[a], mkey, global_iv, 0);
|
||||
keys += (keys.length != 0) ? "&" : "";
|
||||
keys += "k" + (++key_index) + "=" + a2hex(enc);
|
||||
window.setTimeout(function() {
|
||||
browser.notifications.clear(notification_id);
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
crypto_v2_logins_size = key_index;
|
||||
if (await getPref("crypto_v1_compatible"))
|
||||
{
|
||||
for(a=0; a<logins.length; a++, key_index++)
|
||||
{
|
||||
enc = await generate_request(domain, logins[a], mkey, global_iv, 1);
|
||||
keys += (keys.length != 0) ? "&" : "";
|
||||
keys += "k" + key_index + "=" + a2hex(enc);
|
||||
|
||||
if (wdomain != "")
|
||||
{
|
||||
enc = await generate_request(wdomain, logins[a], mkey, global_iv, 1);
|
||||
keys += (keys.length != 0) ? "&" : "";
|
||||
keys += "k" + (++key_index) + "=" + a2hex(enc);
|
||||
}
|
||||
}
|
||||
}
|
||||
debug("Keys " + keys);
|
||||
|
||||
var gPassRequest = new XMLHttpRequest();
|
||||
|
||||
var ret = SERVER.OK;
|
||||
|
||||
// gPassRequest.addEventListener("progress", function(evt) { ; }, false);
|
||||
gPassRequest.addEventListener("load", async function(evt) {
|
||||
var ciphered_password = "";
|
||||
var server_pbkdf2_level = 0;
|
||||
var server_version = 0;
|
||||
var matched_key = 0;
|
||||
|
||||
var r = this.responseText.split("\n");
|
||||
debug("resp " + r);
|
||||
|
||||
for(var a=0; a<r.length; a++)
|
||||
{
|
||||
debug("Analyse " + r[a]);
|
||||
|
||||
params = r[a].split("=");
|
||||
if (params.length != 2 && params[0] != "<end>")
|
||||
{
|
||||
notify("Error : It seems that it's not a gPass server",
|
||||
this.responseText);
|
||||
ret = SERVER.FAILED;
|
||||
break;
|
||||
}
|
||||
|
||||
switch(params[0])
|
||||
{
|
||||
case "protocol":
|
||||
debug("protocol : " + params[1]);
|
||||
|
||||
if (params[1].indexOf("gpass-") != 0)
|
||||
{
|
||||
notify("Error : It seems that it's not a gPass server",
|
||||
this.responseText);
|
||||
ret = SERVER.FAILED;
|
||||
break;
|
||||
}
|
||||
|
||||
server_protocol_version = params[1].match(/\d+/)[0];
|
||||
|
||||
if (server_protocol_version > protocol_version)
|
||||
{
|
||||
notify("Protocol version not supported, please upgrade your addon",
|
||||
"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;
|
||||
setPref("pbkdf2_level", pbkdf2_level);
|
||||
ret = SERVER.RESTART_REQUEST;
|
||||
}
|
||||
break;
|
||||
case "<end>":
|
||||
break;
|
||||
default:
|
||||
debug("Unknown command " + params[0]);
|
||||
|
||||
notify("Error : It seems that it's not a gPass server",
|
||||
this.responseText);
|
||||
ret = SERVER.FAILED;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ret != SERVER.OK)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ciphered_password != "")
|
||||
{
|
||||
debug("Ciphered password : " + ciphered_password);
|
||||
if (matched_key >= crypto_v2_logins_size)
|
||||
// Crypto v1
|
||||
{
|
||||
clear_password = await decrypt_ecb(mkey, hex2a(ciphered_password));
|
||||
// Remove trailing \0 and salt
|
||||
clear_password = clear_password.replace(/\0*$/, "");
|
||||
clear_password = clear_password.substr(0, clear_password.length-3);
|
||||
}
|
||||
else
|
||||
{
|
||||
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);
|
||||
field.value = clear_password;
|
||||
// Remove gPass event listener and submit again with clear password
|
||||
if (submit)
|
||||
{
|
||||
form.removeEventListener("submit", on_sumbit, true);
|
||||
// Propagate change
|
||||
change_cb = field.onchange;
|
||||
if (change_cb)
|
||||
change_cb();
|
||||
// Try to type "enter"
|
||||
var evt = new KeyboardEvent("keydown");
|
||||
delete evt.which;
|
||||
evt.which = 13;
|
||||
field.dispatchEvent(evt);
|
||||
// Submit form
|
||||
form.submit();
|
||||
}
|
||||
else
|
||||
{
|
||||
notify("Password successfully replaced",
|
||||
"Password successfully replaced");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
debug("No password found");
|
||||
|
||||
ret = SERVER.FAILED;
|
||||
|
||||
notify("No password found in database",
|
||||
"No password found in database");
|
||||
}
|
||||
}, false);
|
||||
gPassRequest.addEventListener("error", function(evt) {
|
||||
debug("error");
|
||||
ret = false;
|
||||
notify("Error",
|
||||
"Error");
|
||||
}, false);
|
||||
debug("connect to " + account_url);
|
||||
gPassRequest.open("POST", account_url, true);
|
||||
gPassRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
|
||||
gPassRequest.send(keys);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function wildcard_domain(domain)
|
||||
{
|
||||
var parts = domain.split(".");
|
||||
|
||||
// Standard root domain (zzz.xxx.com) or more
|
||||
if (parts.length > 2)
|
||||
{
|
||||
res = "*.";
|
||||
for (i=1; i<parts.length; i++)
|
||||
res += parts[i];
|
||||
}
|
||||
// Simple xxx.com
|
||||
else if (parts.length == 2)
|
||||
return "*." + domain;
|
||||
|
||||
return "";
|
||||
);
|
||||
}
|
||||
|
||||
function _add_name(logins, name)
|
||||
@@ -344,14 +83,38 @@ function try_get_name(fields, type_filters, match)
|
||||
return all_logins;
|
||||
}
|
||||
|
||||
function on_focus(e)
|
||||
{
|
||||
if (gpass_enabled)
|
||||
{
|
||||
parameters = {
|
||||
type:"update_icon",
|
||||
icon_id:GPASS_ICON.ACTIVATED,
|
||||
};
|
||||
browser.runtime.sendMessage(parameters, {});
|
||||
}
|
||||
}
|
||||
|
||||
function on_blur(e)
|
||||
{
|
||||
if (gpass_enabled)
|
||||
{
|
||||
parameters = {
|
||||
type:"update_icon",
|
||||
icon_id:GPASS_ICON.NORMAL,
|
||||
};
|
||||
browser.runtime.sendMessage(parameters, {});
|
||||
}
|
||||
}
|
||||
|
||||
function on_sumbit(e)
|
||||
{
|
||||
var form = this;
|
||||
var fields = form.getElementsByTagName("input");
|
||||
var domain = form.ownerDocument.baseURI;
|
||||
var password_computed = false;
|
||||
|
||||
var domain = parseURI.parseUri(form.ownerDocument.baseURI);
|
||||
domain = domain["host"];
|
||||
var wdomain = wildcard_domain(domain);
|
||||
debug("on_submit");
|
||||
|
||||
type_filters = new Array();
|
||||
// Get all <input type="text"> && <input type="email">
|
||||
@@ -360,11 +123,13 @@ function on_sumbit(e)
|
||||
logins = try_get_name(fields, type_filters, true);
|
||||
|
||||
// Get all other fields except text, email and password
|
||||
type_filters.push("password");
|
||||
all_logins = try_get_name(fields, type_filters, false);
|
||||
|
||||
if (!logins.length)
|
||||
{
|
||||
type_filters.push("password");
|
||||
logins = try_get_name(fields, type_filters, false);
|
||||
}
|
||||
logins = all_logins;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
// Look for <input type="password" value="@@...">
|
||||
for (var i=0; i<fields.length; i++)
|
||||
@@ -373,93 +138,244 @@ function on_sumbit(e)
|
||||
|
||||
if (field.getAttribute("type") == "password")
|
||||
{
|
||||
debug(field.value);
|
||||
password = field.value;
|
||||
if (password.indexOf("@@") != 0 && password.indexOf("@_") != 0)
|
||||
if (!password.startsWith("@@") && !password.startsWith("@_"))
|
||||
continue;
|
||||
|
||||
// Remove current value to limit master key stealing
|
||||
field.value = "";
|
||||
|
||||
password_computed = true;
|
||||
do_submit = !password.startsWith("@_");
|
||||
mkey = password.substring(2);
|
||||
|
||||
e.preventDefault();
|
||||
parameters = {
|
||||
type:"password",
|
||||
logins:logins,
|
||||
domain:domain,
|
||||
mkey:mkey,
|
||||
options:{field_id:i}
|
||||
};
|
||||
|
||||
var ret = ask_server(form, field, logins, domain, wdomain, mkey, (password.indexOf("@@") == 0));
|
||||
|
||||
ret.then(function(ret){
|
||||
switch(ret)
|
||||
{
|
||||
case SERVER.OK:
|
||||
break;
|
||||
case SERVER.FAILED:
|
||||
if (logins !== all_logins)
|
||||
{
|
||||
ask_server(form, field, all_logins, domain, wdomain, mkey, (password.indexOf("@@") == 0));
|
||||
}
|
||||
break;
|
||||
case SERVER.RESTART_REQUEST:
|
||||
i = -1; // Restart loop
|
||||
break;
|
||||
}
|
||||
});
|
||||
browser.runtime.sendMessage(parameters, {},
|
||||
function (response) {
|
||||
debug(response);
|
||||
var field = fields[response.options.field_id];
|
||||
switch(response.value)
|
||||
{
|
||||
case SERVER.OK:
|
||||
set_password(form, field, response.password, do_submit)
|
||||
notify("Password successfully replaced", "");
|
||||
break;
|
||||
case SERVER.FAILED:
|
||||
if (logins.length != all_logins.length)
|
||||
{
|
||||
parameters[logins] = all_logins;
|
||||
browser.runtime.sendMessage(parameters);
|
||||
}
|
||||
break;
|
||||
case SERVER.RESTART_REQUEST:
|
||||
i = -1; // Restart loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!password_computed)
|
||||
{
|
||||
debug("No password computed");
|
||||
form.submit();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function document_loaded(doc)
|
||||
function set_password(form, field, password, do_submit)
|
||||
{
|
||||
var has_login_form = false;
|
||||
field.value = password;
|
||||
// Remove gPass event listener and submit again with clear password
|
||||
unblock_all_forms();
|
||||
if (do_submit)
|
||||
{
|
||||
// Propagate change
|
||||
change_cb = field.onchange;
|
||||
if (change_cb)
|
||||
change_cb();
|
||||
// Try to type "enter"
|
||||
var evt = new KeyboardEvent("keydown");
|
||||
delete evt.which;
|
||||
evt.which = 13;
|
||||
field.dispatchEvent(evt);
|
||||
// Submit form
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
|
||||
var managed_forms = new Array();
|
||||
|
||||
function block_all_forms(doc, do_block)
|
||||
{
|
||||
var first_time = (managed_forms.length == 0);
|
||||
var cur_focused_element = document.activeElement;
|
||||
|
||||
debug("block all forms");
|
||||
|
||||
gpass_enabled = do_block;
|
||||
|
||||
// If there is a password in the form, add a "submit" listener
|
||||
for(var i=0; i<doc.forms.length; i++)
|
||||
{
|
||||
var form = doc.forms[i];
|
||||
var fields = form.getElementsByTagName("input");
|
||||
for (a=0; a<fields.length; a++)
|
||||
for (var a=0; a<fields.length; a++)
|
||||
{
|
||||
var field = fields[a];
|
||||
if (field.getAttribute("type") == "password")
|
||||
{
|
||||
block_url(form.action);
|
||||
old_cb = form.onsubmit;
|
||||
if (old_cb)
|
||||
form.removeEventListener("submit", old_cb);
|
||||
form.addEventListener("submit", on_sumbit);
|
||||
if (old_cb)
|
||||
form.addEventListener("submit", old_cb);
|
||||
has_login_form = true;
|
||||
if (do_block)
|
||||
{
|
||||
block_url(form.action);
|
||||
old_cb = form.onsubmit;
|
||||
if (old_cb)
|
||||
form.removeEventListener("submit", old_cb);
|
||||
form.addEventListener("submit", on_sumbit);
|
||||
if (old_cb)
|
||||
form.addEventListener("submit", old_cb);
|
||||
field.addEventListener("focus", on_focus);
|
||||
field.addEventListener("blur", on_blur);
|
||||
if (cur_focused_element === field)
|
||||
on_focus(null);
|
||||
}
|
||||
if (first_time)
|
||||
managed_forms.push(form);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Request can be sent to another URL... */
|
||||
if (has_login_form)
|
||||
block_url("<all_urls>");
|
||||
if (managed_forms.length && do_block)
|
||||
block_url("<all_urls>");
|
||||
}
|
||||
|
||||
document_loaded(document);
|
||||
|
||||
async function self_test()
|
||||
function unblock_all_forms()
|
||||
{
|
||||
mkey = crypto_pbkdf2("password", "salt", 4096);
|
||||
res = await encrypt_ecb(mkey, "DDDDDDDDDDDDDDDD");
|
||||
debug("unblock all forms");
|
||||
|
||||
on_blur(null);
|
||||
|
||||
reference = new Uint8Array([0xc4, 0x76, 0x01, 0x07, 0xa1, 0xc0, 0x2f, 0x22, 0xee, 0xbe, 0x60,
|
||||
0xff, 0x65, 0x33, 0x5b, 0x9e]);
|
||||
if (res != ab2str(reference))
|
||||
for(var i=0; i<managed_forms.length; i++)
|
||||
{
|
||||
console.log("Self test ERROR !");
|
||||
var form = managed_forms[i];
|
||||
|
||||
form.removeEventListener("submit", on_sumbit);
|
||||
unblock_url(form.action);
|
||||
|
||||
var fields = form.getElementsByTagName("input");
|
||||
for (var a=0; a<fields.length; a++)
|
||||
{
|
||||
var field = fields[a];
|
||||
if (field.getAttribute("type") == "password")
|
||||
{
|
||||
field.removeEventListener("focus", on_focus);
|
||||
field.removeEventListener("blur", on_blur);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
console.log("Self test OK !");
|
||||
|
||||
if (managed_forms.length)
|
||||
unblock_url("<all_urls>");
|
||||
|
||||
gpass_enabled = false;
|
||||
}
|
||||
|
||||
console.log("Welcome to gPass web extension v0.8.1 !");
|
||||
browser.runtime.onMessage.addListener(
|
||||
function(request, sender, sendResponse) {
|
||||
|
||||
if (request.type == "getUsername")
|
||||
{
|
||||
debug("getUsername");
|
||||
if (managed_forms.length == 1)
|
||||
{
|
||||
fields = managed_forms[0].getElementsByTagName("input");
|
||||
|
||||
type_filters = new Array();
|
||||
// Get all <input type="text"> && <input type="email">
|
||||
type_filters.push("text");
|
||||
type_filters.push("email");
|
||||
logins = try_get_name(fields, type_filters, true);
|
||||
|
||||
if (logins.length == 1)
|
||||
sendResponse(logins[0]);
|
||||
else
|
||||
sendResponse("");
|
||||
}
|
||||
else
|
||||
sendResponse("");
|
||||
}
|
||||
else if (request.type == "setPassword")
|
||||
{
|
||||
debug("setPassword");
|
||||
var response = "";
|
||||
if (managed_forms.length == 1)
|
||||
{
|
||||
fields = managed_forms[0].getElementsByTagName("input");
|
||||
password_field = null;
|
||||
|
||||
for (a=0; a<fields.length; a++)
|
||||
{
|
||||
field = fields[a];
|
||||
if (field.getAttribute("type") == "password")
|
||||
{
|
||||
if (password_field == null)
|
||||
password_field = field;
|
||||
else
|
||||
{
|
||||
// More than one password field : abort
|
||||
password_field = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (password_field)
|
||||
{
|
||||
set_password(managed_forms[0], password_field,
|
||||
request.password, request.submit);
|
||||
response = "ok";
|
||||
}
|
||||
}
|
||||
sendResponse(response);
|
||||
return true;
|
||||
}
|
||||
else if (request.type == "blockForms")
|
||||
{
|
||||
block_all_forms(document, true);
|
||||
}
|
||||
else if (request.type == "unblockForms")
|
||||
{
|
||||
unblock_all_forms();
|
||||
}
|
||||
});
|
||||
|
||||
function document_loaded()
|
||||
{
|
||||
parameters = {
|
||||
"type": "is_gpass_enabled",
|
||||
};
|
||||
|
||||
browser.runtime.sendMessage(parameters, {},
|
||||
function (response) {
|
||||
if (response)
|
||||
block_all_forms(document, response.enabled);
|
||||
});
|
||||
}
|
||||
|
||||
document_loaded();
|
||||
|
||||
console.log("Welcome to gPass web extension v0.9 !");
|
||||
console.log("Privacy Policy can be found at http://indefero.soutade.fr/p/gpass/source/tree/master/PrivacyPolicy.md");
|
||||
console.log("");
|
||||
|
||||
//self_test();
|
||||
|
@@ -1,22 +1,27 @@
|
||||
/*
|
||||
Copyright (C) 2013-2017 Grégory Soutadé
|
||||
|
||||
Copyright (C) 2013-2020 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/>.
|
||||
*/
|
||||
|
||||
var DEBUG = false;
|
||||
|
||||
SERVER = {OK : 0, FAILED : 1, RESTART_REQUEST : 2};
|
||||
GPASS_ICON = {NORMAL:0, DISABLED:1, ACTIVATED:2};
|
||||
|
||||
var default_preferences = {"pbkdf2_level": 1000,
|
||||
"account_url": "https://gpass-demo.soutade.fr/demo",
|
||||
"crypto_v1_compatible": true};
|
||||
@@ -26,7 +31,7 @@ var crypto = crypto || window.crypto;
|
||||
|
||||
function notify(text, data)
|
||||
{
|
||||
browser.runtime.sendMessage({type: "notification", options:{"message":text}});
|
||||
browser.runtime.sendMessage({type: "notification", options:{"message":text, "data":data}});
|
||||
}
|
||||
|
||||
function block_url(url)
|
||||
@@ -35,6 +40,12 @@ function block_url(url)
|
||||
browser.runtime.sendMessage({type: "block_url", options:{"url":url}});
|
||||
}
|
||||
|
||||
function unblock_url(url)
|
||||
{
|
||||
debug("Unblock URL " + url);
|
||||
browser.runtime.sendMessage({type: "unblock_url", options:{"url":url}});
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/6965107/converting-between-strings-and-arraybuffers
|
||||
function ab2str(buf) {
|
||||
return String.fromCharCode.apply(null, new Uint8Array(buf));
|
||||
@@ -124,7 +135,7 @@ function _encrypt(mkey, iv, data)
|
||||
{
|
||||
while ((data.length % 16))
|
||||
data += "\0";
|
||||
|
||||
|
||||
data = str2ab(data);
|
||||
|
||||
promise = mkey.then(function(mkey){
|
||||
@@ -173,8 +184,8 @@ async function _decrypt(mkey, iv, data)
|
||||
async function encrypt_ecb(mkey, data)
|
||||
{
|
||||
var result = "";
|
||||
|
||||
console.log("Encrypt ECB " + data);
|
||||
|
||||
debug("Encrypt ECB " + data);
|
||||
|
||||
nulliv = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
|
||||
@@ -195,8 +206,8 @@ async function decrypt_ecb(mkey, data)
|
||||
{
|
||||
var result = "";
|
||||
|
||||
console.log("Decrypt ECB " + data);
|
||||
|
||||
debug("Decrypt ECB " + data);
|
||||
|
||||
nulliv = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
|
||||
while (data.length > 16)
|
||||
@@ -213,23 +224,23 @@ async function decrypt_ecb(mkey, data)
|
||||
}
|
||||
|
||||
async function encrypt_cbc(mkey, iv, data)
|
||||
{
|
||||
console.log("Encrypt CBC " + data);
|
||||
{
|
||||
debug("Encrypt CBC " + data);
|
||||
|
||||
var result = await _encrypt(mkey, str2ab(iv), data);
|
||||
|
||||
|
||||
// Remove PKCS7 padding
|
||||
return result.slice(0, result.length-16);
|
||||
}
|
||||
|
||||
async function decrypt_cbc(mkey, iv, data)
|
||||
{
|
||||
console.log("Decrypt CBC " + data);
|
||||
debug("Decrypt CBC " + data);
|
||||
|
||||
var result = await _decrypt(mkey, str2ab(iv), data);
|
||||
|
||||
// Remove PKCS7 padding
|
||||
return result.slice(0, result.length-16);
|
||||
return result.slice(0, result.length-16);
|
||||
}
|
||||
|
||||
async function digest(data)
|
||||
@@ -238,3 +249,47 @@ async function digest(data)
|
||||
return ab2str(hash);
|
||||
});
|
||||
}
|
||||
|
||||
function wildcard_domain(domain)
|
||||
{
|
||||
var parts = domain.split(".");
|
||||
|
||||
// Standard root domain (zzz.xxx.com) or more
|
||||
if (parts.length > 2)
|
||||
{
|
||||
res = "*.";
|
||||
for (i=1; i<parts.length; i++)
|
||||
res += parts[i] + ".";
|
||||
return res.substr(0, res.length-1);
|
||||
}
|
||||
// Simple xxx.com
|
||||
else if (parts.length == 2)
|
||||
return "*." + domain;
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
// http://stackoverflow.com/questions/3745666/how-to-convert-from-hex-to-ascii-in-javascript
|
||||
function hex2a(hex) {
|
||||
var str = '';
|
||||
for (var i = 0; i < hex.length; i += 2)
|
||||
str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
|
||||
return str;
|
||||
}
|
||||
|
||||
function a2hex(_str_) {
|
||||
var hex = '';
|
||||
for (var i = 0; i < _str_.length; i++)
|
||||
{
|
||||
var c = _str_.charCodeAt(i).toString(16);
|
||||
if (c.length == 1) c = "0" + c;
|
||||
hex += c;
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
function debug(s)
|
||||
{
|
||||
if (DEBUG)
|
||||
console.log(s);
|
||||
}
|
||||
|
@@ -3,16 +3,16 @@
|
||||
|
||||
"name": "gPass",
|
||||
"short_name": "gPass",
|
||||
"version": "0.8.1",
|
||||
"version": "0.9",
|
||||
"description": "gPass : global password manager",
|
||||
"icons" : {"16":"icons/gpass_icon_16.png", "32":"icons/gpass_icon_32.png", "64":"icons/gpass_icon_64.png", "128":"icons/gpass_icon_128.png"},
|
||||
"author" : "Grégory Soutadé",
|
||||
"homepage_url" : "http://indefero.soutade.fr/p/gpass",
|
||||
|
||||
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["lib/parseuri.js", "lib/misc.js", "compat.js", "lib/main.js"],
|
||||
"js": ["lib/misc.js", "lib/main.js"],
|
||||
"run_at" : "document_idle",
|
||||
"all_frames" : true
|
||||
}
|
||||
@@ -20,17 +20,26 @@
|
||||
|
||||
"background": {
|
||||
"persistent": true,
|
||||
"scripts": ["background.js"]
|
||||
"scripts": ["lib/parseuri.js", "lib/misc.js", "compat.js", "background.js"]
|
||||
},
|
||||
|
||||
"options_page": "options.html",
|
||||
|
||||
"browser_action": {
|
||||
"default_icon": {"32":"icons/gpass_icon_32.png"},
|
||||
"default_title": "Get your password",
|
||||
"default_popup": "popup/popup.html"
|
||||
},
|
||||
|
||||
"permissions": [
|
||||
"<all_urls>",
|
||||
"activeTab",
|
||||
"notifications",
|
||||
"webRequest",
|
||||
"webRequestBlocking",
|
||||
"tabs",
|
||||
"storage"
|
||||
"storage",
|
||||
"clipboardWrite",
|
||||
"contextMenus"
|
||||
]
|
||||
}
|
||||
|
1
chrome_addon/popup/compat.js
Symbolic link
1
chrome_addon/popup/compat.js
Symbolic link
@@ -0,0 +1 @@
|
||||
../compat.js
|
1
chrome_addon/popup/misc.js
Symbolic link
1
chrome_addon/popup/misc.js
Symbolic link
@@ -0,0 +1 @@
|
||||
../lib/misc.js
|
15
chrome_addon/popup/popup.html
Normal file
15
chrome_addon/popup/popup.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
<form id="passwordForm" autocomplete="off">
|
||||
Username <input type="text" id="gPassUsername" autofocus></input><br/> Master key <input type="password" id="gPassMasterKey"/><br/>
|
||||
<input id="getButton" type="submit" value="Get"/> <a id="serverLink" href="">Your server</a>
|
||||
</form>
|
||||
<script src="misc.js"></script>
|
||||
<script src="compat.js"></script>
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
144
chrome_addon/popup/popup.js
Normal file
144
chrome_addon/popup/popup.js
Normal file
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
Copyright (C) 2020 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/>.
|
||||
*/
|
||||
|
||||
var username_filled = false
|
||||
|
||||
function _query_tabs_get_password(tabs)
|
||||
{
|
||||
if (tabs.length <= 0) return;
|
||||
|
||||
var username = document.getElementById("gPassUsername").value;
|
||||
var mkey = document.getElementById("gPassMasterKey").value;
|
||||
|
||||
if (username == "" || mkey == "")
|
||||
return;
|
||||
|
||||
document.getElementById("gPassMasterKey").value = "";
|
||||
|
||||
var do_submit = !mkey.startsWith("@_") && username_filled;
|
||||
if (mkey.startsWith("@@") || mkey.startsWith("@_"))
|
||||
mkey = mkey.substring(2);
|
||||
|
||||
var domain = tabs[0].url;
|
||||
|
||||
var logins = new Array();
|
||||
logins.push(username);
|
||||
|
||||
parameters = {
|
||||
type:"password",
|
||||
logins:logins,
|
||||
domain:domain,
|
||||
mkey:mkey,
|
||||
options:{}
|
||||
};
|
||||
|
||||
browser.runtime.sendMessage(parameters, {},
|
||||
function (response)
|
||||
{
|
||||
debug("Get Response");
|
||||
if (response.value == SERVER.OK)
|
||||
{
|
||||
parameters = {
|
||||
"type":"setPassword",
|
||||
"password":response.password,
|
||||
"submit":do_submit
|
||||
};
|
||||
send_tab_message(tabs[0].id, parameters,
|
||||
function(arg)
|
||||
{
|
||||
debug("Response to setPassword " + arg);
|
||||
if (arg == "")
|
||||
{
|
||||
navigator.clipboard.writeText(response.password);
|
||||
notify("Password pasted into clipboard", "");
|
||||
}
|
||||
else
|
||||
notify("Password filled", "");
|
||||
window.close();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function get_password(evt)
|
||||
{
|
||||
debug('get_password');
|
||||
|
||||
evt.preventDefault();
|
||||
|
||||
browser.tabs.query({active:true, currentWindow:true}, _query_tabs_get_password);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
pform = document.getElementById("passwordForm");
|
||||
|
||||
if (pform != null)
|
||||
pform.onsubmit = get_password;
|
||||
|
||||
function _query_tabs_init(tabs)
|
||||
{
|
||||
debug("_query_tabs_init");
|
||||
|
||||
if (tabs.length != 1) return;
|
||||
|
||||
/* Fill username */
|
||||
parameters = {
|
||||
"type":"getUsername"
|
||||
};
|
||||
|
||||
send_tab_message(tabs[0].id, parameters,
|
||||
function (response)
|
||||
{
|
||||
if (response !== undefined && response != "")
|
||||
{
|
||||
document.getElementById("gPassUsername").value = response;
|
||||
document.getElementById("gPassMasterKey").focus();
|
||||
username_filled = true;
|
||||
}
|
||||
});
|
||||
|
||||
/* Setup server link address */
|
||||
parameters = {
|
||||
type:"getServerAddress"
|
||||
};
|
||||
|
||||
browser.runtime.sendMessage(parameters, {},
|
||||
function (response)
|
||||
{
|
||||
url = response.value;
|
||||
url = url.substring(0, url.lastIndexOf('/'));
|
||||
url += '?';
|
||||
url += 'url=' + encodeURI(tabs[0].url.split("?")[0]);
|
||||
url += '&user=' + document.getElementById("gPassUsername").value;
|
||||
link = document.getElementById("serverLink");
|
||||
link.href = url;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
browser.tabs.query({active:true, currentWindow:true}, _query_tabs_init);
|
Reference in New Issue
Block a user