gPass

gPass Git Source Tree

Root/chrome_addon/lib/main.js

1/*
2 Copyright (C) 2013-2017 Grégory Soutadé
3
4 This file is part of gPass.
5
6 gPass is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10
11 gPass is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with gPass. If not, see <http://www.gnu.org/licenses/>.
18*/
19
20var DEBUG = false;
21var protocol_version = 4;
22var account_url = null;
23var crypto_v2_logins_size = 0;
24
25SERVER = {OK : 0, FAILED : 1, RESTART_REQUEST : 2};
26
27// http://stackoverflow.com/questions/3745666/how-to-convert-from-hex-to-ascii-in-javascript
28function hex2a(hex) {
29 var str = '';
30 for (var i = 0; i < hex.length; i += 2)
31 str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
32 return str;
33}
34
35function a2hex(_str_) {
36 var hex = '';
37 for (var i = 0; i < _str_.length; i++)
38 {
39var c = _str_.charCodeAt(i).toString(16);
40if (c.length == 1) c = "0" + c;
41 hex += c;
42 }
43 return hex;
44}
45
46function debug(s)
47{
48 if (DEBUG)
49console.log(s);
50}
51
52async function generate_request(domain, login, mkey, iv, old)
53{
54 if (old)
55 {
56var v = "@@" + domain + ";" + login;
57debug("will encrypt " + v);
58enc = encrypt_ecb(mkey, v);
59 }
60 else
61 {
62var v = domain + ";" + login;
63debug("will encrypt " + v);
64while ((v.length % 16))
65 v += "\0";
66hash = await digest(v);
67v += hash.slice(8, 24);
68enc = encrypt_cbc(mkey, iv, v);
69 }
70 return enc;
71}
72
73async function ask_server(form, field, logins, domain, wdomain, mkey, submit)
74{
75 account_url = await getPref("account_url");
76
77 var salt = parseURI.parseUri(account_url);
78 salt = salt["host"] + salt["path"];
79
80 debug("salt " + salt);
81
82 pbkdf2_level = await getPref("pbkdf2_level");
83
84 global_iv = await simple_pbkdf2(salt, mkey, pbkdf2_level);
85 global_iv = global_iv.slice(0, 16);
86 mkey = crypto_pbkdf2(mkey, salt, pbkdf2_level);
87
88 debug("global_iv " + a2hex(global_iv));
89
90 keys = "";
91 for(key_index=0, a=0; a<logins.length; a++, key_index++)
92 {
93enc = await generate_request(domain, logins[a], mkey, global_iv, 0);
94keys += (keys.length != 0) ? "&" : "";
95keys += "k" + key_index + "=" + a2hex(enc);
96
97if (wdomain != "")
98{
99 enc = await generate_request(wdomain, logins[a], mkey, global_iv, 0);
100 keys += (keys.length != 0) ? "&" : "";
101 keys += "k" + (++key_index) + "=" + a2hex(enc);
102}
103 }
104
105 crypto_v2_logins_size = key_index;
106 if (await getPref("crypto_v1_compatible"))
107 {
108for(a=0; a<logins.length; a++, key_index++)
109{
110 enc = await generate_request(domain, logins[a], mkey, global_iv, 1);
111 keys += (keys.length != 0) ? "&" : "";
112 keys += "k" + key_index + "=" + a2hex(enc);
113
114 if (wdomain != "")
115 {
116enc = await generate_request(wdomain, logins[a], mkey, global_iv, 1);
117keys += (keys.length != 0) ? "&" : "";
118keys += "k" + (++key_index) + "=" + a2hex(enc);
119 }
120}
121 }
122 debug("Keys " + keys);
123
124 var gPassRequest = new XMLHttpRequest();
125
126 var ret = SERVER.OK;
127
128 // gPassRequest.addEventListener("progress", function(evt) { ; }, false);
129 gPassRequest.addEventListener("load", async function(evt) {
130var ciphered_password = "";
131var server_pbkdf2_level = 0;
132var server_version = 0;
133var matched_key = 0;
134
135var r = this.responseText.split("\n");
136debug("resp " + r);
137
138for(var a=0; a<r.length; a++)
139{
140 debug("Analyse " + r[a]);
141
142 params = r[a].split("=");
143 if (params.length != 2 && params[0] != "<end>")
144 {
145notify("Error : It seems that it's not a gPass server",
146 this.responseText);
147ret = SERVER.FAILED;
148break;
149 }
150
151 switch(params[0])
152 {
153 case "protocol":
154debug("protocol : " + params[1]);
155
156if (params[1].indexOf("gpass-") != 0)
157{
158 notify("Error : It seems that it's not a gPass server",
159 this.responseText);
160 ret = SERVER.FAILED;
161 break;
162}
163
164server_protocol_version = params[1].match(/\d+/)[0];
165
166if (server_protocol_version > protocol_version)
167{
168 notify("Protocol version not supported, please upgrade your addon",
169 "Protocol version not supported, please upgrade your addon");
170 ret = SERVER.FAILED;
171}
172else
173{
174 switch (server_protocol_version)
175 {
176 case 2:
177server_pbkdf2_level = 1000;
178break;
179 case 3:
180// Version 3 : nothing special to do
181 case 4:
182// Version 4 : nothing special to do
183break;
184 }
185}
186break;
187 case "matched_key":
188matched_key = params[1];
189 case "pass":
190ciphered_password = params[1];
191break;
192 case "pkdbf2_level":
193 case "pbkdf2_level":
194server_pbkdf2_level = parseInt(params[1].match(/\d+/)[0], 10);
195if (server_pbkdf2_level != NaN &&
196 server_pbkdf2_level != pbkdf2_level &&
197 server_pbkdf2_level >= 1000) // Minimum level for PBKDF2 !
198{
199 debug("New pbkdf2 level " + server_pbkdf2_level);
200 pbkdf2_level = server_pbkdf2_level;
201 setPref("pbkdf2_level", pbkdf2_level);
202 ret = SERVER.RESTART_REQUEST;
203}
204break;
205 case "<end>":
206break;
207 default:
208debug("Unknown command " + params[0]);
209
210notify("Error : It seems that it's not a gPass server",
211 this.responseText);
212ret = SERVER.FAILED;
213break;
214 }
215}
216
217if (ret != SERVER.OK)
218{
219 return;
220}
221
222if (ciphered_password != "")
223{
224 debug("Ciphered password : " + ciphered_password);
225 if (matched_key >= crypto_v2_logins_size)
226// Crypto v1
227 {
228clear_password = await decrypt_ecb(mkey, hex2a(ciphered_password));
229// Remove trailing \0 and salt
230clear_password = clear_password.replace(/\0*$/, "");
231clear_password = clear_password.substr(0, clear_password.length-3);
232 }
233 else
234 {
235clear_password = await decrypt_cbc(mkey, global_iv, hex2a(ciphered_password));
236clear_password = clear_password.replace(/\0*$/, "");
237clear_password = clear_password.substr(3, clear_password.length);
238 }
239 debug("Clear password " + clear_password);
240 field.value = clear_password;
241 // Remove gPass event listener and submit again with clear password
242 if (submit)
243 {
244form.removeEventListener("submit", on_sumbit, true);
245// Propagate change
246change_cb = field.onchange;
247if (change_cb)
248 change_cb();
249// Try to type "enter"
250var evt = new KeyboardEvent("keydown");
251delete evt.which;
252evt.which = 13;
253field.dispatchEvent(evt);
254// Submit form
255form.submit();
256 }
257 else
258 {
259notify("Password successfully replaced",
260 "Password successfully replaced");
261 }
262}
263else
264{
265 debug("No password found");
266
267 ret = SERVER.FAILED;
268
269 notify("No password found in database",
270 "No password found in database");
271}
272 }, false);
273 gPassRequest.addEventListener("error", function(evt) {
274debug("error");
275ret = false;
276notify("Error",
277 "Error");
278 }, false);
279 debug("connect to " + account_url);
280 gPassRequest.open("POST", account_url, true);
281 gPassRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
282 gPassRequest.send(keys);
283
284 return ret;
285}
286
287function wildcard_domain(domain)
288{
289 var parts = domain.split(".");
290
291 // Standard root domain (zzz.xxx.com) or more
292 if (parts.length > 2)
293 {
294res = "*.";
295for (i=1; i<parts.length; i++)
296 res += parts[i] + ".";
297// Remove last "."
298return res.substr(0, res.length-1);
299 }
300 // Simple xxx.com
301 else if (parts.length == 2)
302return "*." + domain;
303
304 return "";
305}
306
307function _add_name(logins, name)
308{
309 for(var i=0; i<logins.length; i++)
310if (logins[i] == name) return ;
311 logins.push(name);
312}
313
314function try_get_name(fields, type_filters, match)
315{
316 var user = null;
317 var all_logins = new Array();
318
319 for (var i=0; i<fields.length; i++)
320 {
321var field = fields[i];
322
323for (var a=0; a<type_filters.length; a++)
324{
325 if ((match && field.getAttribute("type") == type_filters[a]) ||
326(!match && field.getAttribute("type") != type_filters[a]))
327 {
328if (field.hasAttribute("name") && field.value != "")
329{
330 name = field.getAttribute("name");
331 // Subset of common user field
332 if (name == "user") user = field.value;
333 else if (name == "usr") user = field.value;
334 else if (name == "username") user = field.value;
335 else if (name == "login") user = field.value;
336
337 _add_name(all_logins, field.value);
338}
339 }
340}
341 }
342
343 if (user != null)
344return new Array(user);
345 else
346return all_logins;
347}
348
349function on_sumbit(e)
350{
351 var form = this;
352 var fields = form.getElementsByTagName("input");
353
354 var domain = parseURI.parseUri(form.ownerDocument.baseURI);
355 domain = domain["host"];
356 var wdomain = wildcard_domain(domain);
357
358 type_filters = new Array();
359 // Get all <input type="text"> && <input type="email">
360 type_filters.push("text");
361 type_filters.push("email");
362 logins = try_get_name(fields, type_filters, true);
363
364 // Get all other fields except text, email and password
365 if (!logins.length)
366 {
367type_filters.push("password");
368logins = try_get_name(fields, type_filters, false);
369 }
370
371 // Look for <input type="password" value="@@...">
372 for (var i=0; i<fields.length; i++)
373 {
374var field = fields[i];
375
376if (field.getAttribute("type") == "password")
377{
378 debug(field.value);
379 password = field.value;
380 if (password.indexOf("@@") != 0 && password.indexOf("@_") != 0)
381continue;
382
383 // Remove current value to limit master key stealing
384 field.value = "";
385
386 mkey = password.substring(2);
387
388 e.preventDefault();
389
390 var ret = ask_server(form, field, logins, domain, wdomain, mkey, (password.indexOf("@@") == 0));
391
392 ret.then(function(ret){
393switch(ret)
394{
395 case SERVER.OK:
396 break;
397 case SERVER.FAILED:
398 if (logins !== all_logins)
399 {
400ask_server(form, field, all_logins, domain, wdomain, mkey, (password.indexOf("@@") == 0));
401 }
402 break;
403 case SERVER.RESTART_REQUEST:
404 i = -1; // Restart loop
405 break;
406 }
407});
408}
409 }
410
411 return false;
412}
413
414function document_loaded(doc)
415{
416 var has_login_form = false;
417
418 // If there is a password in the form, add a "submit" listener
419 for(var i=0; i<doc.forms.length; i++)
420 {
421var form = doc.forms[i];
422var fields = form.getElementsByTagName("input");
423for (a=0; a<fields.length; a++)
424{
425 var field = fields[a];
426 if (field.getAttribute("type") == "password")
427 {
428block_url(form.action);
429old_cb = form.onsubmit;
430if (old_cb)
431 form.removeEventListener("submit", old_cb);
432form.addEventListener("submit", on_sumbit);
433if (old_cb)
434 form.addEventListener("submit", old_cb);
435has_login_form = true;
436break;
437 }
438}
439 }
440
441 /* Request can be sent to another URL... */
442 if (has_login_form)
443block_url("<all_urls>");
444}
445
446document_loaded(document);
447
448async function self_test()
449{
450 mkey = crypto_pbkdf2("password", "salt", 4096);
451 res = await encrypt_ecb(mkey, "DDDDDDDDDDDDDDDD");
452
453 reference = new Uint8Array([0xc4, 0x76, 0x01, 0x07, 0xa1, 0xc0, 0x2f, 0x22, 0xee, 0xbe, 0x60,
4540xff, 0x65, 0x33, 0x5b, 0x9e]);
455 if (res != ab2str(reference))
456 {
457 console.log("Self test ERROR !");
458 }
459 else
460 console.log("Self test OK !");
461}
462
463console.log("Welcome to gPass web extension v0.8.2 !");
464console.log("Privacy Policy can be found at http://indefero.soutade.fr/p/gpass/source/tree/master/PrivacyPolicy.md");
465console.log("");
466
467//self_test();

Archive Download this file