466 lines
11 KiB
JavaScript
466 lines
11 KiB
JavaScript
/*
|
|
Copyright (C) 2013-2017 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;
|
|
|
|
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)
|
|
{
|
|
if (DEBUG)
|
|
console.log(s);
|
|
}
|
|
|
|
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(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 != "")
|
|
{
|
|
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 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)
|
|
{
|
|
for(var i=0; i<logins.length; i++)
|
|
if (logins[i] == name) return ;
|
|
logins.push(name);
|
|
}
|
|
|
|
function try_get_name(fields, type_filters, match)
|
|
{
|
|
var user = null;
|
|
var all_logins = new Array();
|
|
|
|
for (var i=0; i<fields.length; i++)
|
|
{
|
|
var field = fields[i];
|
|
|
|
for (var a=0; a<type_filters.length; a++)
|
|
{
|
|
if ((match && field.getAttribute("type") == type_filters[a]) ||
|
|
(!match && field.getAttribute("type") != type_filters[a]))
|
|
{
|
|
if (field.hasAttribute("name") && field.value != "")
|
|
{
|
|
name = field.getAttribute("name");
|
|
// Subset of common user field
|
|
if (name == "user") user = field.value;
|
|
else if (name == "usr") user = field.value;
|
|
else if (name == "username") user = field.value;
|
|
else if (name == "login") user = field.value;
|
|
|
|
_add_name(all_logins, field.value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (user != null)
|
|
return new Array(user);
|
|
else
|
|
return all_logins;
|
|
}
|
|
|
|
function on_sumbit(e)
|
|
{
|
|
var form = this;
|
|
var fields = form.getElementsByTagName("input");
|
|
|
|
var domain = parseURI.parseUri(form.ownerDocument.baseURI);
|
|
domain = domain["host"];
|
|
var wdomain = wildcard_domain(domain);
|
|
|
|
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);
|
|
|
|
// Get all other fields except text, email and password
|
|
if (!logins.length)
|
|
{
|
|
type_filters.push("password");
|
|
logins = try_get_name(fields, type_filters, false);
|
|
}
|
|
|
|
// Look for <input type="password" value="@@...">
|
|
for (var i=0; i<fields.length; i++)
|
|
{
|
|
var field = fields[i];
|
|
|
|
if (field.getAttribute("type") == "password")
|
|
{
|
|
debug(field.value);
|
|
password = field.value;
|
|
if (password.indexOf("@@") != 0 && password.indexOf("@_") != 0)
|
|
continue;
|
|
|
|
// Remove current value to limit master key stealing
|
|
field.value = "";
|
|
|
|
mkey = password.substring(2);
|
|
|
|
e.preventDefault();
|
|
|
|
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;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function document_loaded(doc)
|
|
{
|
|
var has_login_form = false;
|
|
|
|
// 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++)
|
|
{
|
|
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;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Request can be sent to another URL... */
|
|
if (has_login_form)
|
|
block_url("<all_urls>");
|
|
}
|
|
|
|
document_loaded(document);
|
|
|
|
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 !");
|
|
}
|
|
|
|
console.log("Welcome to gPass web extension v0.8.1 !");
|
|
console.log("Privacy Policy can be found at http://indefero.soutade.fr/p/gpass/source/tree/master/PrivacyPolicy.md");
|
|
console.log("");
|
|
|
|
//self_test();
|