gPass/firefox_addon/lib/main.js

284 lines
7.7 KiB
JavaScript

/*
Copyright (C) 2013-2014 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 {Cc, Ci} = require("chrome");
var notifications = require("sdk/notifications");
// http://www.timdown.co.uk/jshashtable/
var Hashtable = require("jshashtable-3.0").Hashtable;
var pkdbf2 = require("pkdbf2").pkdbf2;
var aes = require("jsaes").aes;
var parseURI = require("parseuri").parseURI;
var prefSet = require("simple-prefs");
var DEBUG = true;
// 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++)
{
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);
}
function wildcard_domain(domain)
{
parts = domain.split(".");
if (parts.length >= 3)
{
// Seems to be a two level root domain (ie zzz.xxx.co.uk ...)
if (parts[parts.length-2].lenght <= 3)
{
if (parts.length > 3)
return "*" + "." + parts[parts.length-3] + "." + parts[parts.length-2] + "." + parts[parts.length-1];
}
// Standard root domain (zzz.xxx.com)
else
return "*" + "." + parts[parts.length-2] + "." + parts[parts.length-1];
}
return "";
}
function on_sumbit(e)
{
var form = this;
var fields = form.getElementsByTagName("input");
var my_map = new Hashtable();
domain = parseURI.parseUri(form.ownerDocument.baseURI);
domain = domain["host"];
wdomain = wildcard_domain(domain);
salt = parseURI.parseUri(prefSet.prefs["account_url"]);
salt = salt["host"] + salt["path"];
debug("salt " + salt);
// Get all <input type="text"> && <input type="email">
for (i=0; i<fields.length; i++)
{
var field = fields[i];
if (field.getAttribute("type") == "text" || field.getAttribute("type") == "email")
{
if (field.hasAttribute("name") && field.value != "")
my_map.put(field.getAttribute("name"), field.value);
}
}
// Look for <input type="password" value="@@...">
for (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)
continue;
mkey = password.substring(2);
mkey = pkdbf2.pkdbf2(mkey, salt, 1000, 256/8);
user = null;
// Subset of common user field
if (my_map.containsKey("user")) user = my_map.get("user");
else if (my_map.containsKey("usr")) user = my_map.get("usr");
else if (my_map.containsKey("username")) user = my_map.get("username");
else if (my_map.containsKey("login")) user = my_map.get("login");
// If no one found, use all
logins = (user != null) ? new Array(user) : my_map.values();
keys = "";
for(a=0; a<logins.length; a++)
{
v = "@@" + domain + ";" + logins[a];
debug("will encrypt " + v);
debug("with " + a2hex(mkey));
enc = aes.encryptLongString(v, aes.init(mkey));
aes.finish();
debug("res " + enc);
keys += (keys.length != 0) ? "&" : "";
keys += "k" + a + "=" + a2hex(enc);
if (wdomain != "")
{
v = "@@" + wdomain + ";" + logins[a];
debug("will encrypt " + v);
debug("with " + a2hex(mkey));
enc = aes.encryptLongString(v, aes.init(mkey));
aes.finish();
debug("res " + enc);
keys += (keys.length != 0) ? "&" : "";
keys += "k" + a + "=" + a2hex(enc);
}
}
debug(keys);
// Need to do a synchronous request
var gPassRequest = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
createInstance(Ci.nsIXMLHttpRequest);
var ret = true;
// gPassRequest.addEventListener("progress", function(evt) { ; }, false);
gPassRequest.addEventListener("load", function(evt) {
r = this.responseText.split("\n");
debug("resp " + r);
protocol = r[0].split("=");
if ((protocol.length == 2 && protocol[1] != "gpass-2") || protocol.length != 2)
{
ret = false;
if (protocol.length == 2 && protocol[1].startsWith("gpass"))
{
notifications.notify({
title: "gPasss",
text: "Protocol version not supported, please upgrade your addon",
data: "Protocol version not supported, please upgrade your addon",
});
}
else
{
notifications.notify({
title: "gPasss",
text: "Error : It seems that it's not a gPass server",
data: this.responseText,
});
}
}
else
{
if (r[1] != "<end>" && r[1].startsWith("pass="))
{
ciphered_password = r[1].split("=");
ciphered_password = ciphered_password[1];
debug("Ciphered password : " + ciphered_password);
clear_password = aes.decryptLongString(hex2a(ciphered_password), aes.init(mkey));
aes.finish();
// Remove salt
clear_password = clear_password.replace(/\0*$/, "");
clear_password = clear_password.substr(0, clear_password.length-3);
debug("Clear password " + clear_password);
field.value = clear_password;
}
else
{
debug("No password found");
ret = false;
notifications.notify({
title: "gPasss",
text: "No password found in database",
data: "No password found in database",
});
}
}
}, false);
gPassRequest.addEventListener("error", function(evt) {
debug("error");
ret = false;
notifications.notify({
title: "gPasss",
text: "Error",
data: "Error",
});
}, false);
debug("connect to " + prefSet.prefs["account_url"]);
gPassRequest.open("POST", prefSet.prefs["account_url"], false);
gPassRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
gPassRequest.send(keys);
if (!ret)
{
e.preventDefault();
return ret;
}
}
}
}
function document_loaded(event)
{
doc = event.target;
// If there is a password in the form, add a "submit" listener
for(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")
{
form.addEventListener("submit", on_sumbit);
break;
}
}
}
}
var httpRequestObserver =
{
observe: function(subject, topic, data)
{
if (topic == "content-document-global-created")
{
subject.addEventListener("DOMContentLoaded", document_loaded, false);
}
}
};
var observerService = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
observerService.addObserver(httpRequestObserver, "content-document-global-created", false);
function self_test()
{
if((res = a2hex(hmac256("Jefe", "what do ya want for nothing?"))) !=
"5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843")
console.log("HMAC256 failed " + res);
if((res = a2hex(pkdbf2.pkdbf2("password", "salt", 4096, 256/8))) !=
"c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a")
console.log("PKDBF2 failed " + res);
else
console.log("All is OK ! " + mkey);
}