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:
Grégory Soutadé 2020-01-18 15:20:13 +01:00
parent 81385fbf88
commit 898e0b9e42
22 changed files with 2558 additions and 483 deletions

View File

@ -11,7 +11,7 @@ Everyday we have a lot of passwords to manage corresponding to a lot of accounts
The best way to avoid these errors is to have a unique strong password for each account. gPass helps to reach this goal : you keep a subset of passwords (called masterkey) and for each login/password tuple you chose, gPass returns the real password by querying a password server.
To have a high level of security, all information is stored encrypted (server side). Nothing is stored on client. The decryption is done on the fly when it's needed and only with user input. So, a hacker can get your password database, it will not be able to see any information (except if it brute force your masterkey) ! So it's important to choose to strong masterkey !
To have a high level of security, all information is stored encrypted (server side). Nothing is stored on client. The decryption is done on the fly when it's needed and only with user input. So, a hacker can get your password database, it will not be able to see any information (except if it brute force or leak your masterkey) ! So it's important to choose to strong masterkey !
This addon is like [last pass](https://lastpass.com/) one, but I wanted it to be open source and self hostable (be careful on server down !). Moreover, with gPass, you can have multiple master keys !
@ -19,17 +19,20 @@ This addon is like [last pass](https://lastpass.com/) one, but I wanted it to be
Usage
-----
The first thing to do is to populate your database (from your/a password server) with website/login/password/master key values. You can use "*" character to access to all sub domains of a specific website. If you want to make strong password, there is a password generator. After that, configure your addon in "tools -> addons -> gPass -> preferences" in Firefox or "addons -> gPass -> options" in Chrome to point to your password server (+ username). Be careful, login and password are case sensitive.
The first thing to do is to populate your database (from your/a password server) with website/login/password/master key values. You can use "*" character to access to all sub domains of a specific website. If you want to make strong password, there is a password generator. After that, configure your addon in "tools -> addons -> gPass -> preferences" in Firefox or "addons -> gPass -> options" in Chrome to point to your password server (+ username). For firefox users, don't forget to enable addon within private mode. Be careful, login and password are case sensitive !
When you're in a login form and you want to use gPass, type your login (case sensitive !) and fill "@@masterkey" in password field. Then submit and password will automatically be replaced by the one in the database (after addon decrypt it).
**You can also type "@_masterkey" to only replace your password without submitting and manually submit. This allows to support more websites.**
Another option is to enter your credentials in the new popup menu. If found, password will be stored in your clipboard.
Technical details
-----------------
The two columns in database are "login" and "password".
login is compounded by "domain;login" is salted and encrypted with AES 256-CBC
login is compounded by "domain;login", salted and encrypted with AES 256-CBC
The key that encrypt these fields is PBKDF2 (hmac-sha256, masterkey, password_server_url, 1000, 256), IV is PBKDF2 (hmac-sha256, password_server_url, masterkey, 1000, 256)
@ -41,11 +44,11 @@ Server side is written in PHP (with SQLite3 for database component).
Server
------
To host a password server, you need a webserver. Just copy server files in a directory read/write for web server user (www-data). A sample apache2 configuration file is available in resources. Since v0.8 and the use of Crypto API, it's manadatory to have an HTTPS access to the server. Without that, the decryption will fails.
To host a password server, you need a webserver. Just copy server files in a directory read/write for web server user (www-data). A sample apache2 configuration file is available in resources. Since v0.8 and the use of Crypto API, **it's manadatory to have an HTTPS access to the server**. Without that, the decryption will fails.
Configuration parameters are in conf.php
A demonstration server is available [here](https://gpass-demo.soutade.fr). It may not works with HTTPS content because it uses a self-signed SSL certificate. If so, explicitly add the certificate to your browser. It's the default server of package (user demo).
A demonstration server is available [here](https://gpass-demo.soutade.fr). It's the default server of package (user demo).
**Warning** The master key derivation is partially based on account URL. So it's linked to your current server information. You can't move databases from servers with different URLs, you need to export them and import again.
@ -55,11 +58,13 @@ Version 0.6 introduces shadow logins. It's a protection again illegal database d
The principle is to generate a random value (shadow login) that must be encrypted with the masterkey to get an access token. This access token allows to get the true (but encrypted) login/password couple. It's a kind of challenge : if I can encrypt the shadow login, I know the masterkey ! For security reason, the derivation of masterkey for deciphering passwords is different than for encrypting shadow logins (it uses its own salt).
Client
------
Just install the package. You can have debug information by setting DEBUG in main.js.
Command line interface
----------------------
@ -73,17 +78,16 @@ The dependencies are libcurl and OpenSSL (-dev packages : ie _libcurl4-openssl-d
A sample configuration file is available _gpass.ini.sample_
Version Information
-------------------
Current version is 0.8. **(not compatible with 0.7)**
Current version is 0.9. **(not compatible with 0.7)**
Firefox will remove support for addons, so the gPass addon code is not supported since v0.8, please migrate to webextension.
Transition from v0.7 to v0.8 : **Please update your masterkey (even with the same one) to gain a security level of your passwords's wallet.**
**This version is incompatible from 0.1**. Please use [this script](http://soutade.fr/files/gpass_migrate_0_1.php) to migrate.
License
-------

View File

@ -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();

View File

@ -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);
}

View File

@ -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();

View File

@ -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":