Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b9640e8cb | |||
| 85fa47037d | |||
| 32f36a1100 | |||
|
|
9d528aeaa0 | ||
| 6f1e2a814d | |||
| 4b307fd776 | |||
| 5d65b863f6 | |||
| 8d39bd8ed0 | |||
| 898e0b9e42 | |||
| 81385fbf88 | |||
| 4cb4b44690 | |||
| 2f12a09a1b | |||
|
|
b216ec9928 | ||
|
|
da4a99a6ef | ||
| f98cc4a863 | |||
| 5a1d6c7390 | |||
| c511e49725 | |||
| 8469c01b13 | |||
| 7162129220 | |||
| 017bda025e | |||
| 39582e0f26 | |||
| 86e926e268 | |||
| e341963675 | |||
| 7a7d2fd724 | |||
| 65821a4e47 | |||
| 0caa8a66e1 | |||
|
|
35f49d24b3 | ||
| 09e0d85d97 | |||
| 416a4d9581 | |||
| 6dfcab813d | |||
| da72cb46eb | |||
| 36db5056a3 | |||
| c3fab882f2 | |||
| 74b1010881 | |||
| 562a84d984 | |||
| 7800eadfaa | |||
| c64f886188 | |||
| e759c13d64 | |||
| 1d71ca6861 | |||
| 65ca3a3d3d | |||
| 136920404d | |||
| ef82f2640f | |||
| 65f8f0f21e | |||
| a180cb62d7 | |||
| b4b54ec57c | |||
| 48571b31c1 | |||
| bc2a510b8d | |||
| 82cef032eb | |||
| d48d1e94a9 | |||
| cef1194ad0 | |||
| 89465f4c68 | |||
| 6fa296ebc4 | |||
| 241867e93c | |||
| 6d2e202aff | |||
| 32fd8355e1 | |||
|
|
527fc52539 | ||
| 29e2c8337c | |||
| 9b9c36070b | |||
| 0cc706d260 | |||
| 89ccd0575f | |||
|
|
ba75b73503 | ||
|
|
487283626f | ||
|
|
6604fbb6e1 | ||
|
|
cc66b612ef | ||
| 81b6116a9f | |||
| 075dd914f1 | |||
|
|
1d22f425e9 | ||
|
|
b255a8cd90 | ||
|
|
b330af258f | ||
|
|
e16fa0c218 | ||
| 3108195ce3 | |||
|
|
3f0636976e | ||
|
|
e9c6208b54 | ||
| f88a0d1f29 | |||
| 636d403396 | |||
| 423799bfd7 | |||
| 6a86ab989c | |||
| 601ddfc629 | |||
| d395807c98 | |||
| 2757b81e64 | |||
| 9dc74cb942 | |||
| 714f0f570f | |||
| 5cfb47c470 | |||
| e0f8670a58 | |||
| 035576998d | |||
| a5b3dec5b0 | |||
| f5a0da3f69 | |||
|
|
f8472d7ca4 | ||
|
|
de0a443ebb | ||
| 53db16b832 | |||
| b0ea92b71b |
79
ChangeLog
Normal file
@@ -0,0 +1,79 @@
|
||||
**v0.9 :**
|
||||
|
||||
Server
|
||||
|
||||
* Add support for user & url parameters from gPass popup
|
||||
* Fix some minor bugs
|
||||
|
||||
Addon
|
||||
* 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
|
||||
* Add Privacy Policy information
|
||||
|
||||
CLI
|
||||
|
||||
**v0.8.1 :**
|
||||
|
||||
Server
|
||||
|
||||
* Scroll to page bottom when user adds a new password
|
||||
* Run a simpler algorithm for wildcard domains
|
||||
|
||||
Addon
|
||||
|
||||
* Add Privacy Policy information
|
||||
|
||||
CLI
|
||||
|
||||
* Run a simpler algorithm for wildcard domains
|
||||
|
||||
**v0.8 :**
|
||||
|
||||
Server
|
||||
|
||||
* Clear master keys and reset passwords after 15 minutes of inactivity
|
||||
* Set USE_SHADOW_LOGINS by default
|
||||
* New crypto scheme (Use CBC chaining and fix a security problem with salt) and protocol v4. not backward compatible with v3
|
||||
* Add QUnit tests
|
||||
* New password form is now on top of the page
|
||||
* Add a button to go to the top of the page when scrolling
|
||||
* Add simple password button
|
||||
* Rework password generation for most user friendly passwords (less special characters, more letters)
|
||||
|
||||
Addon
|
||||
|
||||
* New webextension for Firefox is provided. It shares most of code with Chrome extension and use native crypto API
|
||||
* Block connection when masterkey is sent in clear (password replacement failed). **Doesn't work with Firefox**
|
||||
|
||||
CLI
|
||||
|
||||
* Add command line interface (CLI)
|
||||
|
||||
**v0.7 :**
|
||||
|
||||
Server
|
||||
|
||||
* Fix a bug for Chrome browser (doesn't support default parameters)
|
||||
* Display an error message when a query fails
|
||||
* You can now export clear password database (only unciphered passwords)
|
||||
* New database version : 2
|
||||
* Add two new protections : REQUESTS_MIN_DELAY and MAX_PASSWORDS_PER_REQUEST (see conf.php)
|
||||
* Remove '\' character from password generation
|
||||
|
||||
Addon
|
||||
|
||||
* Addon is now compatible with more websites
|
||||
* Use jpm building tool instead of cfx for Firefox Addon
|
||||
|
||||
**v0.6 : **
|
||||
|
||||
Addon
|
||||
|
||||
* Add support for "@_masterkey" input
|
||||
47
PrivacyPolicy.md
Normal file
@@ -0,0 +1,47 @@
|
||||
gPass web browser extension Privacy Policy
|
||||
------------------------------------------
|
||||
|
||||
|
||||
## Information we collect ##
|
||||
|
||||
The gPass extension collect three information once invoked :
|
||||
* Site address URL
|
||||
* Login name
|
||||
* Master key
|
||||
|
||||
|
||||
## How we use information we collect ##
|
||||
|
||||
Once collected, site address and login name are encrypted by a derived version of your master key.
|
||||
It's then sent to the server (password server) you configured in extension configuration page for comparison.
|
||||
|
||||
This server has been set up by the user himself (recommended) or by a provider he trust in.
|
||||
|
||||
The database that the server access to do comparisons only contains the crypted
|
||||
version of your information. They are never decrypted in the server side.
|
||||
|
||||
If a comparison match, the real password is sent back to your extension were
|
||||
it's unencrypted using the same key (derived masterkey).
|
||||
|
||||
Finally, the application context is cleared and nothing is kept in memory
|
||||
nor written anywhere.
|
||||
|
||||
|
||||
## Accessing and updating your personal information ##
|
||||
|
||||
As a user, you can add, edit and delete your ciphered information through
|
||||
the web interface of the password server.
|
||||
|
||||
During these operations, no clear information is sent to the server.
|
||||
|
||||
|
||||
## Information we share ##
|
||||
|
||||
Nothing is shared with anyone. Nor on extension side, nor on server side.
|
||||
|
||||
|
||||
## Information security ##
|
||||
|
||||
Information transmitted to the server are done through an HTTPS AJAX request.
|
||||
Data are encrypted using AES 256 CBC algorithm and the master key is prior
|
||||
derived using PKBDF2 algorithm.
|
||||
7
README
@@ -1,7 +0,0 @@
|
||||
gPass : global Password.
|
||||
|
||||
Copyright (C) 2013-2014 Grégory Soutadé
|
||||
|
||||
Licence : GPL v3
|
||||
|
||||
See http://indefero.soutade.fr/p/gpass/ for further information
|
||||
101
README.md
Normal file
@@ -0,0 +1,101 @@
|
||||
gPass : global Password for Firefox and Chrome
|
||||
==============================================
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
Everyday we have a lot of passwords to manage corresponding to a lot of accounts we use. It's hard to remain all of these, moreover if we don't use it often. So, what most people do is to generate only a subset of passwords easy to remain. This implies two common errors :
|
||||
|
||||
* Password are not very strong
|
||||
* We use them for multiple 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/masterkey 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 or leak your masterkey) ! So it's important to choose a 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 !
|
||||
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
The first thing to do is to populate your database (from your/a password server) with website address/login/password/master key values. You can use "*" character to access to all subdomains of a specific website (ie *.google.com). If you want to make a strong password, there is a password generator. After that, configure your addon in "tools -> addons -> gPass -> preferences" in Firefox or "More tools -> extensions -> gPass -> options" in Chrome to point to your password server (+ username). **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 (only if gPass icon is green !). 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 automatic submit. This allows to support more websites.**
|
||||
|
||||
Another option is to enter your credentials in the new popup menu by clicking on gPass icon. If it's possible, gPass will auto fill password field, if not result password is stored into your clipboard. Popup path is a safest method as website page will never see your masterkey. There is also an option to disable addon for a specific website (it's a local configuration, so it must be done for each browser).
|
||||
|
||||
|
||||
Technical details
|
||||
-----------------
|
||||
|
||||
The two main columns in database are "login" and "password".
|
||||
login is compounded by "domain;login", salted and encrypted with AES 256-CBC
|
||||
|
||||
The real key that encrypts these fields is PBKDF2 (hmac-sha256, masterkey, password_server_url, 1000, 256), IV is PBKDF2 (hmac-sha256, password_server_url, masterkey, 1000, 256)
|
||||
|
||||
PBKDF2 level can be changed by user.
|
||||
|
||||
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 (valid SSL/TLS certificate) 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's the default server configuration for fresh installed addon (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 it again.
|
||||
|
||||
**Server side is available [here](http://indefero.soutade.fr/p/gpass/downloads)**
|
||||
|
||||
Version 0.6 introduces shadow logins. It's a protection again illegal database dump and purge but requires twice computation. Database update is transparent.
|
||||
|
||||
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). It's enabled by default.
|
||||
|
||||
|
||||
Client
|
||||
------
|
||||
|
||||
Just install the package. You can have debug information by setting DEBUG in main.js.
|
||||
|
||||
|
||||
Command line interface
|
||||
----------------------
|
||||
|
||||
A command line interface is also available with the following usage :
|
||||
|
||||
Usage: ./gpass_cli [-f config_file] [-p server_port] [-c CA_certificate_path] [-l PBKDF2_level] [-s gpass_server] [-v] -d domain -u username
|
||||
|
||||
You can save recurrent parameters into a configuration file. Default config file is found at $HOME/.local/share/gpass/gpass.ini
|
||||
|
||||
The dependencies are libcurl and OpenSSL (-dev packages : ie _libcurl4-openssl-dev_ and _libssl-dev_)
|
||||
|
||||
A sample configuration file is available _gpass.ini.sample_
|
||||
|
||||
|
||||
Version Information
|
||||
-------------------
|
||||
|
||||
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.**
|
||||
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
All the code is licensed under GPL v3. Source code is available [here](http://indefero.soutade.fr/p/gpass).
|
||||
|
||||
|
||||
Mailing list
|
||||
------------
|
||||
|
||||
You can subscribe to the announce list @ https://pannous.soutade.fr/lists/gpass
|
||||
582
chrome_addon/background.js
Normal file
@@ -0,0 +1,582 @@
|
||||
/*
|
||||
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)
|
||||
{
|
||||
// debug(JSON.stringify(details));
|
||||
if (details.requestBody)
|
||||
{
|
||||
if (details.requestBody.formData)
|
||||
{
|
||||
for (var key in details.requestBody.formData)
|
||||
{
|
||||
for(var idx in details.requestBody.formData[key])
|
||||
{
|
||||
value = details.requestBody.formData[key][idx];
|
||||
if (value.startsWith("@@") ||
|
||||
value.startsWith("@_"))
|
||||
return {cancel: true};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// 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};
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
return {cancel: false};
|
||||
}
|
||||
|
||||
function url_unblock_callback(details)
|
||||
{
|
||||
return {cancel: false};
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
enabled = (key_present == null);
|
||||
// Do switch
|
||||
enabled = !enabled;
|
||||
if (enabled)
|
||||
{
|
||||
parameters = {type:"blockForms"};
|
||||
debug("Now enabled");
|
||||
}
|
||||
else
|
||||
{
|
||||
parameters = {type:"unblockForms"};
|
||||
debug("Now disabled");
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
64
chrome_addon/compat.js
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
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 get_preference(key)
|
||||
{
|
||||
// Inspired from https://github.com/akiomik/chrome-storage-promise/
|
||||
var promise = new Promise((resolve, reject) => {
|
||||
chrome.storage.local.get(key, (items) => {
|
||||
let err = chrome.runtime.lastError;
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(items);
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(function (pref) {
|
||||
if (!pref.hasOwnProperty(key))
|
||||
{
|
||||
if (default_preferences.hasOwnProperty(key))
|
||||
return default_preferences[key];
|
||||
else
|
||||
return null;
|
||||
}
|
||||
return pref[key];
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
function set_preference(key, value)
|
||||
{
|
||||
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
chrome_addon/icons
Symbolic link
@@ -0,0 +1 @@
|
||||
../resources/icons/
|
||||
381
chrome_addon/lib/main.js
Normal file
@@ -0,0 +1,381 @@
|
||||
/*
|
||||
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 gpass_enabled = true;
|
||||
|
||||
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(options).then(
|
||||
function created(notification_id)
|
||||
{
|
||||
window.setTimeout(function() {
|
||||
browser.notifications.clear(notification_id);
|
||||
}, 2000);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
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_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;
|
||||
|
||||
debug("on_submit");
|
||||
|
||||
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
|
||||
type_filters.push("password");
|
||||
all_logins = try_get_name(fields, type_filters, false);
|
||||
|
||||
if (!logins.length)
|
||||
logins = all_logins;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
// Look for <input type="password" value="@@...">
|
||||
for (var i=0; i<fields.length; i++)
|
||||
{
|
||||
var field = fields[i];
|
||||
|
||||
if (field.getAttribute("type") == "password")
|
||||
{
|
||||
password = field.value;
|
||||
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);
|
||||
|
||||
parameters = {
|
||||
type:"password",
|
||||
logins:logins,
|
||||
domain:domain,
|
||||
mkey:mkey,
|
||||
options:{field_id:i}
|
||||
};
|
||||
|
||||
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 set_password(form, field, password, do_submit)
|
||||
{
|
||||
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 (var a=0; a<fields.length; a++)
|
||||
{
|
||||
var field = fields[a];
|
||||
if (field.getAttribute("type") == "password")
|
||||
{
|
||||
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 (managed_forms.length && do_block)
|
||||
block_url("<all_urls>");
|
||||
}
|
||||
|
||||
function unblock_all_forms()
|
||||
{
|
||||
debug("unblock all forms");
|
||||
|
||||
on_blur(null);
|
||||
|
||||
for(var i=0; i<managed_forms.length; i++)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (managed_forms.length)
|
||||
unblock_url("<all_urls>");
|
||||
|
||||
gpass_enabled = false;
|
||||
}
|
||||
|
||||
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("");
|
||||
295
chrome_addon/lib/misc.js
Normal file
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
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};
|
||||
|
||||
var browser = browser || chrome;
|
||||
var crypto = crypto || window.crypto;
|
||||
|
||||
function notify(text, data)
|
||||
{
|
||||
browser.runtime.sendMessage({type: "notification", options:{"message":text, "data":data}});
|
||||
}
|
||||
|
||||
function block_url(url)
|
||||
{
|
||||
debug("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));
|
||||
}
|
||||
|
||||
// https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
|
||||
function str2ab2(str) {
|
||||
var chars = []
|
||||
for (var i=0, strLen=str.length; i < strLen; i++) {
|
||||
chars.push(str.charCodeAt(i));
|
||||
}
|
||||
return new Uint8Array(chars);
|
||||
}
|
||||
|
||||
function str2ab(str) {
|
||||
var buf = new ArrayBuffer(str.length);
|
||||
// var buf = new ArrayBuffer(str.length*2); // 2 bytes for each char
|
||||
var bufView = new Uint8Array(buf);
|
||||
for (var i=0, strLen=str.length; i < strLen; i++) {
|
||||
bufView[i] = str.charCodeAt(i);
|
||||
}
|
||||
return bufView;
|
||||
}
|
||||
|
||||
function crypto_pbkdf2(mkey, salt, level)
|
||||
{
|
||||
AESCBC = {
|
||||
name: "AES-CBC",
|
||||
length: 256,
|
||||
}
|
||||
|
||||
var key = str2ab(mkey);
|
||||
return crypto.subtle.importKey("raw", key, {name: "PBKDF2"}, false, ["deriveBits", "deriveKey"])
|
||||
.then(function(key){
|
||||
//sha-256
|
||||
return crypto.subtle.deriveKey({
|
||||
name: "PBKDF2",
|
||||
salt: str2ab(salt),
|
||||
iterations: level,
|
||||
hash: "SHA-256",
|
||||
}, key, AESCBC, false, ["encrypt", "decrypt", "unwrapKey", "wrapKey"])
|
||||
.then(function(key) {
|
||||
return key;
|
||||
})
|
||||
.catch(function(err){
|
||||
console.log("Error derive key " + err);
|
||||
});
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.log("Error import key" + err);
|
||||
});
|
||||
}
|
||||
|
||||
function simple_pbkdf2(mkey, salt, level)
|
||||
{
|
||||
AESCBC = {
|
||||
name: "AES-CBC",
|
||||
length: 256,
|
||||
}
|
||||
|
||||
var key = str2ab(mkey);
|
||||
return crypto.subtle.importKey("raw", key, {name: "PBKDF2"}, false, ["deriveBits", "deriveKey"])
|
||||
.then(function(key){
|
||||
//sha-256
|
||||
return crypto.subtle.deriveKey({
|
||||
name: "PBKDF2",
|
||||
salt: str2ab(salt),
|
||||
iterations: level,
|
||||
hash: "SHA-256",
|
||||
}, key, AESCBC, true, ["unwrapKey", "wrapKey"])
|
||||
.then(function(key) {
|
||||
return crypto.subtle.exportKey("raw", key)
|
||||
.then(function (key) {
|
||||
return ab2str(key);
|
||||
});
|
||||
})
|
||||
.catch(function(err){
|
||||
console.log("Error derive key " + err);
|
||||
});
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.log("Error import key" + err);
|
||||
});
|
||||
}
|
||||
|
||||
function _encrypt(mkey, iv, data)
|
||||
{
|
||||
while ((data.length % 16))
|
||||
data += "\0";
|
||||
|
||||
data = str2ab(data);
|
||||
|
||||
promise = mkey.then(function(mkey){
|
||||
return crypto.subtle.encrypt({
|
||||
name: "AES-CBC",
|
||||
iv: iv
|
||||
}, mkey, data)})
|
||||
.then(function(encrypted) {
|
||||
return ab2str(encrypted);
|
||||
})
|
||||
.catch(function(encryption) {
|
||||
console.log("Encryption rejected " + encryption);
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
async function _decrypt(mkey, iv, data)
|
||||
{
|
||||
while ((data.length % 16))
|
||||
data += "\0";
|
||||
|
||||
nulliv = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
pkcs7_padding = new Uint8Array([16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16]);
|
||||
pkcs7_padding = await _encrypt(mkey, nulliv, ab2str(pkcs7_padding));
|
||||
|
||||
data = str2ab(data + pkcs7_padding);
|
||||
|
||||
nulliv = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
|
||||
promise = mkey.then(function(mkey){
|
||||
return crypto.subtle.decrypt({
|
||||
name: "AES-CBC",
|
||||
iv: iv
|
||||
}, mkey, data)})
|
||||
.then(function(decrypted) {
|
||||
return ab2str(decrypted);
|
||||
})
|
||||
.catch(function(decryption) {
|
||||
console.log("Decryption rejected " + decryption);
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
async function encrypt_ecb(mkey, data)
|
||||
{
|
||||
var result = "";
|
||||
|
||||
debug("Encrypt 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)
|
||||
{
|
||||
res = await _encrypt(mkey, nulliv, data.slice(0, 16));
|
||||
// Remove PKCS7 padding
|
||||
result += res.slice(0, 16);
|
||||
data = data.slice(16);
|
||||
}
|
||||
res = await _encrypt(mkey, nulliv, data);
|
||||
result += res.slice(0, 16);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function decrypt_ecb(mkey, data)
|
||||
{
|
||||
var result = "";
|
||||
|
||||
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)
|
||||
{
|
||||
res = await _decrypt(mkey, nulliv, data.slice(0, 16));
|
||||
// Remove PKCS7 padding
|
||||
result += res.slice(0, 16);
|
||||
data = data.slice(16);
|
||||
}
|
||||
res = await _decrypt(mkey, nulliv, data);
|
||||
result += res.slice(0, 16);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function encrypt_cbc(mkey, iv, 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)
|
||||
{
|
||||
debug("Decrypt CBC " + data);
|
||||
|
||||
var result = await _decrypt(mkey, str2ab(iv), data);
|
||||
|
||||
// Remove PKCS7 padding
|
||||
return result.slice(0, result.length-16);
|
||||
}
|
||||
|
||||
async function digest(data)
|
||||
{
|
||||
return crypto.subtle.digest("SHA-256", str2ab(data)).then(function (hash) {
|
||||
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);
|
||||
}
|
||||
32
chrome_addon/lib/parseuri.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// parseUri 1.2.2
|
||||
// (c) Steven Levithan <stevenlevithan.com>
|
||||
// MIT License
|
||||
|
||||
parseURI = {
|
||||
|
||||
parseUri : function (str) {
|
||||
var o = {
|
||||
strictMode: false,
|
||||
key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],
|
||||
q: {
|
||||
name: "queryKey",
|
||||
parser: /(?:^|&)([^&=]*)=?([^&]*)/g
|
||||
},
|
||||
parser: {
|
||||
strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
|
||||
loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
|
||||
}},
|
||||
m = o.parser[o.strictMode ? "strict" : "loose"].exec(str),
|
||||
uri = {},
|
||||
i = 14;
|
||||
|
||||
while (i--) uri[o.key[i]] = m[i] || "";
|
||||
|
||||
uri[o.q.name] = {};
|
||||
uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
|
||||
if ($1) uri[o.q.name][$1] = $2;
|
||||
});
|
||||
|
||||
return uri;
|
||||
}
|
||||
};
|
||||
45
chrome_addon/manifest.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
|
||||
"name": "gPass",
|
||||
"short_name": "gPass",
|
||||
"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/misc.js", "lib/main.js"],
|
||||
"run_at" : "document_idle",
|
||||
"all_frames" : true
|
||||
}
|
||||
],
|
||||
|
||||
"background": {
|
||||
"persistent": true,
|
||||
"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",
|
||||
"clipboardWrite",
|
||||
"contextMenus"
|
||||
]
|
||||
}
|
||||
21
chrome_addon/options.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>gPass</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<b>Account URL</b> URL of your gPass account <input id="account_url" type="text"/><br />
|
||||
<b>WARNING</b> It should be a valid HTTPS URL because navigator doesn't like mixed content (HTTPS/HTTP). If not, requests will silentely failed. If you have an auto-signed certificate, add it to trusted ones.<br/>
|
||||
<br/>
|
||||
<b>PBKDF2 level</b> Number of iterations used to derivate master key <input id="pbkdf2" type="number"/><br />
|
||||
<br/>
|
||||
<br/>
|
||||
<b>Crypto v1 compatible </b> Compatible with old crypto schema (AES ECB). Use it for encrypted passwords with server <= 0.7 <input id="crypto_v1_compatible" type="checkbox"/><br />
|
||||
<br/>
|
||||
<input type="button" id="save" value="Save"/>
|
||||
|
||||
<script type="text/javascript" src="options.js">
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
40
chrome_addon/options.js
Normal file
@@ -0,0 +1,40 @@
|
||||
var default_preferences = {"pbkdf2_level": 1000,
|
||||
"account_url": "https://gpass-demo.soutade.fr/demo",
|
||||
"crypto_v1_compatible": true};
|
||||
|
||||
function save() {
|
||||
var account_url = document.getElementById('account_url').value;
|
||||
var pbkdf2 = document.getElementById('pbkdf2').value;
|
||||
var crypto_v1_compatible = document.getElementById('crypto_v1_compatible').checked;
|
||||
|
||||
chrome.storage.local.set({
|
||||
'account_url': account_url,
|
||||
'pbkdf2': pbkdf2,
|
||||
'crypto_v1_compatible': crypto_v1_compatible,
|
||||
}, function() {
|
||||
alert('Saved');
|
||||
});
|
||||
}
|
||||
|
||||
chrome.storage.local.get(null, function(prefs) {
|
||||
if (!prefs.hasOwnProperty("account_url"))
|
||||
account_url = default_preferences['account_url'];
|
||||
else
|
||||
account_url = prefs['account_url'];
|
||||
|
||||
if (!prefs.hasOwnProperty("pbkdf2_level"))
|
||||
pbkdf2 = default_preferences['pbkdf2_level'];
|
||||
else
|
||||
pbkdf2 = prefs['pbkdf2_level'];
|
||||
|
||||
if (!prefs.hasOwnProperty("crypto_v1_compatible"))
|
||||
crypto_v1_compatible = default_preferences['crypto_v1_compatible'];
|
||||
else
|
||||
crypto_v1_compatible = prefs['crypto_v1_compatible'];
|
||||
|
||||
document.getElementById('account_url').value = account_url;
|
||||
document.getElementById('pbkdf2').value = pbkdf2;
|
||||
document.getElementById('crypto_v1_compatible').checked = crypto_v1_compatible;
|
||||
});
|
||||
|
||||
document.getElementById('save').addEventListener("click", save);
|
||||
1
chrome_addon/popup/compat.js
Symbolic link
@@ -0,0 +1 @@
|
||||
../compat.js
|
||||
1
chrome_addon/popup/misc.js
Symbolic link
@@ -0,0 +1 @@
|
||||
../lib/misc.js
|
||||
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
@@ -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);
|
||||
13
cli/Makefile
Normal file
@@ -0,0 +1,13 @@
|
||||
CC=gcc
|
||||
CFLAGS=-Wall -O2
|
||||
LDFLAGS= -lcrypto -lcurl
|
||||
TARGET=gpass_cli
|
||||
SRCS=main.c ini.c
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(SRCS)
|
||||
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
|
||||
|
||||
clean:
|
||||
rm -f $(TARGET) *.o *~
|
||||
6
cli/gpass.ini.sample
Normal file
@@ -0,0 +1,6 @@
|
||||
[params]
|
||||
# ca_path=./ca_path/ca_authority.pem
|
||||
# server=https://demo-gpass.soutade.fr/demo
|
||||
# pbkdf2_level=1000
|
||||
# server_port=443
|
||||
# verify_ssl_peer=1
|
||||
194
cli/ini.c
Normal file
@@ -0,0 +1,194 @@
|
||||
/* inih -- simple .INI file parser
|
||||
|
||||
inih is released under the New BSD license (see LICENSE.txt). Go to the project
|
||||
home page for more info:
|
||||
|
||||
https://github.com/benhoyt/inih
|
||||
|
||||
*/
|
||||
|
||||
#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS)
|
||||
#define _CRT_SECURE_NO_WARNINGS
|
||||
#endif
|
||||
|
||||
#include <stdio.h>
|
||||
#include <ctype.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "ini.h"
|
||||
|
||||
#if !INI_USE_STACK
|
||||
#include <stdlib.h>
|
||||
#endif
|
||||
|
||||
#define MAX_SECTION 50
|
||||
#define MAX_NAME 50
|
||||
|
||||
/* Strip whitespace chars off end of given string, in place. Return s. */
|
||||
static char* rstrip(char* s)
|
||||
{
|
||||
char* p = s + strlen(s);
|
||||
while (p > s && isspace((unsigned char)(*--p)))
|
||||
*p = '\0';
|
||||
return s;
|
||||
}
|
||||
|
||||
/* Return pointer to first non-whitespace char in given string. */
|
||||
static char* lskip(const char* s)
|
||||
{
|
||||
while (*s && isspace((unsigned char)(*s)))
|
||||
s++;
|
||||
return (char*)s;
|
||||
}
|
||||
|
||||
/* Return pointer to first char (of chars) or inline comment in given string,
|
||||
or pointer to null at end of string if neither found. Inline comment must
|
||||
be prefixed by a whitespace character to register as a comment. */
|
||||
static char* find_chars_or_comment(const char* s, const char* chars)
|
||||
{
|
||||
#if INI_ALLOW_INLINE_COMMENTS
|
||||
int was_space = 0;
|
||||
while (*s && (!chars || !strchr(chars, *s)) &&
|
||||
!(was_space && strchr(INI_INLINE_COMMENT_PREFIXES, *s))) {
|
||||
was_space = isspace((unsigned char)(*s));
|
||||
s++;
|
||||
}
|
||||
#else
|
||||
while (*s && (!chars || !strchr(chars, *s))) {
|
||||
s++;
|
||||
}
|
||||
#endif
|
||||
return (char*)s;
|
||||
}
|
||||
|
||||
/* Version of strncpy that ensures dest (size bytes) is null-terminated. */
|
||||
static char* strncpy0(char* dest, const char* src, size_t size)
|
||||
{
|
||||
strncpy(dest, src, size);
|
||||
dest[size - 1] = '\0';
|
||||
return dest;
|
||||
}
|
||||
|
||||
/* See documentation in header file. */
|
||||
int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler,
|
||||
void* user)
|
||||
{
|
||||
/* Uses a fair bit of stack (use heap instead if you need to) */
|
||||
#if INI_USE_STACK
|
||||
char line[INI_MAX_LINE];
|
||||
#else
|
||||
char* line;
|
||||
#endif
|
||||
char section[MAX_SECTION] = "";
|
||||
char prev_name[MAX_NAME] = "";
|
||||
|
||||
char* start;
|
||||
char* end;
|
||||
char* name;
|
||||
char* value;
|
||||
int lineno = 0;
|
||||
int error = 0;
|
||||
|
||||
#if !INI_USE_STACK
|
||||
line = (char*)malloc(INI_MAX_LINE);
|
||||
if (!line) {
|
||||
return -2;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Scan through stream line by line */
|
||||
while (reader(line, INI_MAX_LINE, stream) != NULL) {
|
||||
lineno++;
|
||||
|
||||
start = line;
|
||||
#if INI_ALLOW_BOM
|
||||
if (lineno == 1 && (unsigned char)start[0] == 0xEF &&
|
||||
(unsigned char)start[1] == 0xBB &&
|
||||
(unsigned char)start[2] == 0xBF) {
|
||||
start += 3;
|
||||
}
|
||||
#endif
|
||||
start = lskip(rstrip(start));
|
||||
|
||||
if (*start == ';' || *start == '#') {
|
||||
/* Per Python configparser, allow both ; and # comments at the
|
||||
start of a line */
|
||||
}
|
||||
#if INI_ALLOW_MULTILINE
|
||||
else if (*prev_name && *start && start > line) {
|
||||
/* Non-blank line with leading whitespace, treat as continuation
|
||||
of previous name's value (as per Python configparser). */
|
||||
if (!handler(user, section, prev_name, start) && !error)
|
||||
error = lineno;
|
||||
}
|
||||
#endif
|
||||
else if (*start == '[') {
|
||||
/* A "[section]" line */
|
||||
end = find_chars_or_comment(start + 1, "]");
|
||||
if (*end == ']') {
|
||||
*end = '\0';
|
||||
strncpy0(section, start + 1, sizeof(section));
|
||||
*prev_name = '\0';
|
||||
}
|
||||
else if (!error) {
|
||||
/* No ']' found on section line */
|
||||
error = lineno;
|
||||
}
|
||||
}
|
||||
else if (*start) {
|
||||
/* Not a comment, must be a name[=:]value pair */
|
||||
end = find_chars_or_comment(start, "=:");
|
||||
if (*end == '=' || *end == ':') {
|
||||
*end = '\0';
|
||||
name = rstrip(start);
|
||||
value = lskip(end + 1);
|
||||
#if INI_ALLOW_INLINE_COMMENTS
|
||||
end = find_chars_or_comment(value, NULL);
|
||||
if (*end)
|
||||
*end = '\0';
|
||||
#endif
|
||||
rstrip(value);
|
||||
|
||||
/* Valid name[=:]value pair found, call handler */
|
||||
strncpy0(prev_name, name, sizeof(prev_name));
|
||||
if (!handler(user, section, name, value) && !error)
|
||||
error = lineno;
|
||||
}
|
||||
else if (!error) {
|
||||
/* No '=' or ':' found on name[=:]value line */
|
||||
error = lineno;
|
||||
}
|
||||
}
|
||||
|
||||
#if INI_STOP_ON_FIRST_ERROR
|
||||
if (error)
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !INI_USE_STACK
|
||||
free(line);
|
||||
#endif
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
/* See documentation in header file. */
|
||||
int ini_parse_file(FILE* file, ini_handler handler, void* user)
|
||||
{
|
||||
return ini_parse_stream((ini_reader)fgets, file, handler, user);
|
||||
}
|
||||
|
||||
/* See documentation in header file. */
|
||||
int ini_parse(const char* filename, ini_handler handler, void* user)
|
||||
{
|
||||
FILE* file;
|
||||
int error;
|
||||
|
||||
file = fopen(filename, "r");
|
||||
if (!file)
|
||||
return -1;
|
||||
error = ini_parse_file(file, handler, user);
|
||||
fclose(file);
|
||||
return error;
|
||||
}
|
||||
93
cli/ini.h
Normal file
@@ -0,0 +1,93 @@
|
||||
/* inih -- simple .INI file parser
|
||||
|
||||
inih is released under the New BSD license (see LICENSE.txt). Go to the project
|
||||
home page for more info:
|
||||
|
||||
https://github.com/benhoyt/inih
|
||||
|
||||
*/
|
||||
|
||||
#ifndef __INI_H__
|
||||
#define __INI_H__
|
||||
|
||||
/* Make this header file easier to include in C++ code */
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
/* Typedef for prototype of handler function. */
|
||||
typedef int (*ini_handler)(void* user, const char* section,
|
||||
const char* name, const char* value);
|
||||
|
||||
/* Typedef for prototype of fgets-style reader function. */
|
||||
typedef char* (*ini_reader)(char* str, int num, void* stream);
|
||||
|
||||
/* Parse given INI-style file. May have [section]s, name=value pairs
|
||||
(whitespace stripped), and comments starting with ';' (semicolon). Section
|
||||
is "" if name=value pair parsed before any section heading. name:value
|
||||
pairs are also supported as a concession to Python's configparser.
|
||||
|
||||
For each name=value pair parsed, call handler function with given user
|
||||
pointer as well as section, name, and value (data only valid for duration
|
||||
of handler call). Handler should return nonzero on success, zero on error.
|
||||
|
||||
Returns 0 on success, line number of first error on parse error (doesn't
|
||||
stop on first error), -1 on file open error, or -2 on memory allocation
|
||||
error (only when INI_USE_STACK is zero).
|
||||
*/
|
||||
int ini_parse(const char* filename, ini_handler handler, void* user);
|
||||
|
||||
/* Same as ini_parse(), but takes a FILE* instead of filename. This doesn't
|
||||
close the file when it's finished -- the caller must do that. */
|
||||
int ini_parse_file(FILE* file, ini_handler handler, void* user);
|
||||
|
||||
/* Same as ini_parse(), but takes an ini_reader function pointer instead of
|
||||
filename. Used for implementing custom or string-based I/O. */
|
||||
int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler,
|
||||
void* user);
|
||||
|
||||
/* Nonzero to allow multi-line value parsing, in the style of Python's
|
||||
configparser. If allowed, ini_parse() will call the handler with the same
|
||||
name for each subsequent line parsed. */
|
||||
#ifndef INI_ALLOW_MULTILINE
|
||||
#define INI_ALLOW_MULTILINE 1
|
||||
#endif
|
||||
|
||||
/* Nonzero to allow a UTF-8 BOM sequence (0xEF 0xBB 0xBF) at the start of
|
||||
the file. See http://code.google.com/p/inih/issues/detail?id=21 */
|
||||
#ifndef INI_ALLOW_BOM
|
||||
#define INI_ALLOW_BOM 1
|
||||
#endif
|
||||
|
||||
/* Nonzero to allow inline comments (with valid inline comment characters
|
||||
specified by INI_INLINE_COMMENT_PREFIXES). Set to 0 to turn off and match
|
||||
Python 3.2+ configparser behaviour. */
|
||||
#ifndef INI_ALLOW_INLINE_COMMENTS
|
||||
#define INI_ALLOW_INLINE_COMMENTS 1
|
||||
#endif
|
||||
#ifndef INI_INLINE_COMMENT_PREFIXES
|
||||
#define INI_INLINE_COMMENT_PREFIXES ";"
|
||||
#endif
|
||||
|
||||
/* Nonzero to use stack, zero to use heap (malloc/free). */
|
||||
#ifndef INI_USE_STACK
|
||||
#define INI_USE_STACK 1
|
||||
#endif
|
||||
|
||||
/* Stop parsing on first error (default is to keep parsing). */
|
||||
#ifndef INI_STOP_ON_FIRST_ERROR
|
||||
#define INI_STOP_ON_FIRST_ERROR 0
|
||||
#endif
|
||||
|
||||
/* Maximum line length for any line in INI file. */
|
||||
#ifndef INI_MAX_LINE
|
||||
#define INI_MAX_LINE 200
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __INI_H__ */
|
||||
719
cli/main.c
Normal file
@@ -0,0 +1,719 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <signal.h>
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include <openssl/opensslv.h>
|
||||
#include <openssl/evp.h>
|
||||
|
||||
#include "ini.h"
|
||||
|
||||
#define STRNCMP(a, b) strncmp(a, b, sizeof(b)-1)
|
||||
|
||||
#define DEFAULT_CONFIG_FILE ".local/share/gpass/gpass.ini"
|
||||
|
||||
#define DEFAULT_PBKDF2_LEVEL 1000
|
||||
#define MASTER_KEY_LENGTH (256/8)
|
||||
#define GLOBAL_IV_LENGTH 16
|
||||
#define BLOCK_SIZE (128/8)
|
||||
#define DEFAULT_SERVER_PORT 443
|
||||
#define SERVER_PROTOCOL 4
|
||||
#define RESPONSE_SIZE 2048
|
||||
#define MAX_SUBDOMAINS 10
|
||||
#define DISPLAY_TIME 30 // 30 seconds
|
||||
|
||||
struct gpass_parameters {
|
||||
unsigned pbkdf2_level;
|
||||
char *server;
|
||||
char *salt;
|
||||
char *domain;
|
||||
char *username;
|
||||
char *orig_master_key;
|
||||
unsigned char *derived_master_key;
|
||||
unsigned server_port;
|
||||
unsigned verbose;
|
||||
char *ca_path;
|
||||
unsigned verify_ssl_peer;
|
||||
unsigned port_set;
|
||||
unsigned crypto_v1_compatible;
|
||||
unsigned char *global_iv;
|
||||
} ;
|
||||
|
||||
#if OPENSSL_VERSION_NUMBER >= 0x10010000
|
||||
// OpenSSL >= 1.1
|
||||
static EVP_MD_CTX * s_md_ctx;
|
||||
#else
|
||||
static EVP_MD_CTX * s_md_ctx;
|
||||
static EVP_MD_CTX ss_md_ctx;
|
||||
#define EVP_MD_CTX_new(...) &ss_md_ctx
|
||||
#define EVP_MD_CTX_free(...)
|
||||
#endif
|
||||
static const EVP_MD * s_md_256;
|
||||
|
||||
static EVP_CIPHER_CTX * s_cipher_ctx;
|
||||
static int s_stop_display = 0;
|
||||
|
||||
static void signal_handler(int signum)
|
||||
{
|
||||
s_stop_display = 1;
|
||||
}
|
||||
|
||||
static void display_password(char* password, int time)
|
||||
{
|
||||
int print_len = 0;
|
||||
|
||||
for (; time && !s_stop_display; time--)
|
||||
{
|
||||
print_len = printf("\r(%02d) Password found: %s", time, password);
|
||||
fflush(stdout);
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
// Clear line
|
||||
print_len++; // For C or Z
|
||||
printf("\r");
|
||||
while (print_len--)
|
||||
printf(" ");
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
static int digest(unsigned char** out, unsigned char* in, unsigned size)
|
||||
{
|
||||
*out = NULL;
|
||||
EVP_DigestInit(s_md_ctx, s_md_256);
|
||||
EVP_DigestUpdate(s_md_ctx, in, size);
|
||||
*out = malloc(32);
|
||||
return EVP_DigestFinal(s_md_ctx, *out, NULL);
|
||||
}
|
||||
|
||||
static void derive_master_key(struct gpass_parameters* params)
|
||||
{
|
||||
if (!params->derived_master_key)
|
||||
params->derived_master_key = malloc(MASTER_KEY_LENGTH);
|
||||
|
||||
if (!params->global_iv)
|
||||
params->global_iv = malloc(GLOBAL_IV_LENGTH);
|
||||
|
||||
PKCS5_PBKDF2_HMAC(params->orig_master_key, strlen(params->orig_master_key),
|
||||
(unsigned char*)params->salt, strlen(params->salt),
|
||||
params->pbkdf2_level, EVP_sha256(),
|
||||
MASTER_KEY_LENGTH, params->derived_master_key);
|
||||
|
||||
PKCS5_PBKDF2_HMAC(params->salt, strlen(params->salt),
|
||||
(unsigned char*)params->orig_master_key, strlen(params->orig_master_key),
|
||||
params->pbkdf2_level, EVP_sha256(),
|
||||
GLOBAL_IV_LENGTH, params->global_iv);
|
||||
}
|
||||
|
||||
static void bin_to_hex(unsigned char* bin, unsigned char* hex, unsigned bin_size)
|
||||
{
|
||||
unsigned char tmp;
|
||||
|
||||
for (; bin_size--; bin++)
|
||||
{
|
||||
tmp = (*bin >> 4) & 0xf;
|
||||
if (tmp <= 9)
|
||||
*hex++ = '0' + tmp;
|
||||
else
|
||||
*hex++ = 'a' + (tmp-10);
|
||||
|
||||
tmp = *bin & 0xf;
|
||||
if (tmp <= 9)
|
||||
*hex++ = '0' + tmp;
|
||||
else
|
||||
*hex++ = 'a' + (tmp-10);
|
||||
}
|
||||
}
|
||||
|
||||
static void hex_to_bin(unsigned char* bin, unsigned char* hex, long hex_size)
|
||||
{
|
||||
unsigned char tmp;
|
||||
|
||||
// Round to 2
|
||||
hex_size &= ~1;
|
||||
|
||||
for (; hex_size; hex_size-=2, bin++)
|
||||
{
|
||||
tmp = *hex++;
|
||||
if (tmp >= '0' && tmp <= '9')
|
||||
*bin = (tmp - '0') << 4;
|
||||
else if (tmp >= 'a' && tmp <= 'f')
|
||||
*bin = ((tmp - 'a')+10) << 4;
|
||||
else
|
||||
*bin = ((tmp - 'A')+10) << 4;
|
||||
|
||||
tmp = *hex++;
|
||||
if (tmp >= '0' && tmp <= '9')
|
||||
*bin |= (tmp - '0');
|
||||
else if (tmp >= 'a' && tmp <= 'f')
|
||||
*bin |= ((tmp - 'a')+10);
|
||||
else
|
||||
*bin |= ((tmp - 'A')+10);
|
||||
}
|
||||
}
|
||||
|
||||
static void encrypt_domain_v1(struct gpass_parameters* params, char* domain,
|
||||
unsigned char** res, unsigned* out_size)
|
||||
{
|
||||
unsigned size = 2+strlen(domain)+1+strlen(params->username);
|
||||
unsigned char* buffer, *tmp;
|
||||
|
||||
if (params->verbose)
|
||||
printf("%s: %s\n", __func__, domain);
|
||||
|
||||
if ((size % BLOCK_SIZE))
|
||||
size = ((size/BLOCK_SIZE)+1)*BLOCK_SIZE;
|
||||
|
||||
buffer = malloc(size+1); // Cause snprintf() add a final \0
|
||||
memset(buffer, 0, size+1);
|
||||
|
||||
snprintf((char*)buffer, size+1, "@@%s;%s", domain, params->username);
|
||||
|
||||
tmp = malloc(size);
|
||||
*res = malloc(size*2);
|
||||
|
||||
EVP_EncryptInit(s_cipher_ctx, EVP_aes_256_ecb(), params->derived_master_key, NULL);
|
||||
EVP_CipherUpdate(s_cipher_ctx, tmp, (int*)out_size, buffer, size);
|
||||
|
||||
bin_to_hex(tmp, *res, size);
|
||||
|
||||
*out_size *= 2;
|
||||
|
||||
free(buffer);
|
||||
free(tmp);
|
||||
}
|
||||
|
||||
static void encrypt_domain(struct gpass_parameters* params, char* domain,
|
||||
unsigned char** res, unsigned* out_size)
|
||||
{
|
||||
unsigned size = strlen(domain)+1+strlen(params->username);
|
||||
unsigned padded_size;
|
||||
unsigned char* buffer, *tmp;
|
||||
|
||||
if (params->verbose)
|
||||
printf("%s: %s\n", __func__, domain);
|
||||
|
||||
if ((size % BLOCK_SIZE))
|
||||
size = ((size/BLOCK_SIZE)+1)*BLOCK_SIZE;
|
||||
padded_size = size;
|
||||
|
||||
size += 16; // For digest
|
||||
|
||||
buffer = malloc(size);
|
||||
memset(buffer, 0, size);
|
||||
|
||||
snprintf((char*)buffer, size, "%s;%s", domain, params->username);
|
||||
|
||||
// Append digest
|
||||
digest(&tmp, buffer, padded_size);
|
||||
memcpy(&buffer[padded_size], &tmp[8], 16);
|
||||
free(tmp);
|
||||
|
||||
tmp = malloc(size);
|
||||
*res = malloc(size*2);
|
||||
|
||||
EVP_EncryptInit(s_cipher_ctx, EVP_aes_256_cbc(), params->derived_master_key, params->global_iv);
|
||||
EVP_CipherUpdate(s_cipher_ctx, tmp, (int*)out_size, buffer, size);
|
||||
|
||||
bin_to_hex(tmp, *res, size);
|
||||
|
||||
*out_size *= 2;
|
||||
|
||||
free(buffer);
|
||||
free(tmp);
|
||||
}
|
||||
|
||||
static void append_to_request(char** request, char* new_req, unsigned new_req_size)
|
||||
{
|
||||
static int cur_req_idx = 0;
|
||||
int size_added;
|
||||
|
||||
if (!cur_req_idx)
|
||||
{
|
||||
*request = malloc(3+new_req_size+1);
|
||||
snprintf(*request, 3+new_req_size+1, "k0=%s", new_req);
|
||||
}
|
||||
else
|
||||
{
|
||||
size_added = 4+new_req_size;
|
||||
if (cur_req_idx >= 10)
|
||||
size_added++;
|
||||
|
||||
*request = realloc(*request, strlen(*request)+1+size_added);
|
||||
|
||||
snprintf(&((*request)[strlen(*request)]), size_added+1, "&k%d=%s",
|
||||
cur_req_idx, new_req);
|
||||
}
|
||||
|
||||
cur_req_idx++;
|
||||
}
|
||||
|
||||
static char* wildcard_domain(char* domain)
|
||||
{
|
||||
int cur_level = 1;
|
||||
char* level_ptr[MAX_SUBDOMAINS], *tmp, *res = NULL;
|
||||
int level_length[MAX_SUBDOMAINS];
|
||||
|
||||
memset(level_ptr, 0, sizeof(level_ptr));
|
||||
memset(level_length, 0, sizeof(level_length));
|
||||
level_ptr[0] = domain;
|
||||
|
||||
for (tmp=domain; *tmp && cur_level < MAX_SUBDOMAINS; tmp++)
|
||||
{
|
||||
if (*tmp == '.')
|
||||
{
|
||||
level_ptr[cur_level] = tmp+1;
|
||||
level_length[cur_level-1] = tmp - level_ptr[cur_level-1];
|
||||
cur_level++;
|
||||
}
|
||||
}
|
||||
|
||||
// Too much levels
|
||||
if (cur_level >= MAX_SUBDOMAINS)
|
||||
{
|
||||
fprintf(stderr, "Error: Too much levels for domain %s\n", domain);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Final level
|
||||
level_length[cur_level-1] = tmp - level_ptr[cur_level-1];
|
||||
|
||||
tmp = NULL;
|
||||
if (cur_level > 2)
|
||||
{
|
||||
// Standard root domain (zzz.xxx.com) or more
|
||||
tmp = level_ptr[1];
|
||||
}
|
||||
// Simple xxx.com
|
||||
else if (cur_level == 2)
|
||||
tmp = level_ptr[0];
|
||||
|
||||
if (tmp)
|
||||
{
|
||||
res = malloc(2+strlen(tmp)+1);
|
||||
sprintf(res, "*.%s", tmp);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata)
|
||||
{
|
||||
if ((size*nmemb) > RESPONSE_SIZE)
|
||||
{
|
||||
fprintf(stderr, "Error curl response is too big (%d bytes, max %d bytes)\n",
|
||||
(int)(size*nmemb), RESPONSE_SIZE);
|
||||
}
|
||||
else
|
||||
memcpy(userdata, ptr, size*nmemb);
|
||||
|
||||
return size*nmemb;
|
||||
}
|
||||
|
||||
static int ask_server(struct gpass_parameters* params)
|
||||
{
|
||||
char* wc_domain, *saveptr, *token, *cur_ptr;
|
||||
unsigned char* enc_domain;
|
||||
unsigned enc_size, matched_key = 0, crypto_v1_index = 1;
|
||||
char* request = NULL;
|
||||
int ret = -1, res, len;
|
||||
CURL *curl;
|
||||
char response[RESPONSE_SIZE];
|
||||
unsigned char password[256];
|
||||
|
||||
if (params->verbose)
|
||||
printf("Username: %s\n", params->username);
|
||||
|
||||
encrypt_domain(params, params->domain, &enc_domain, &enc_size);
|
||||
append_to_request(&request, (char*)enc_domain, enc_size);
|
||||
free(enc_domain);
|
||||
|
||||
wc_domain = wildcard_domain(params->domain);
|
||||
if (wc_domain)
|
||||
{
|
||||
crypto_v1_index++;
|
||||
encrypt_domain(params, wc_domain, &enc_domain, &enc_size);
|
||||
append_to_request(&request, (char*)enc_domain, enc_size);
|
||||
free(enc_domain);
|
||||
}
|
||||
|
||||
if (params->crypto_v1_compatible)
|
||||
{
|
||||
encrypt_domain_v1(params, params->domain, &enc_domain, &enc_size);
|
||||
append_to_request(&request, (char*)enc_domain, enc_size);
|
||||
free(enc_domain);
|
||||
if (wc_domain)
|
||||
{
|
||||
encrypt_domain_v1(params, wc_domain, &enc_domain, &enc_size);
|
||||
append_to_request(&request, (char*)enc_domain, enc_size);
|
||||
free(enc_domain);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (params->verbose)
|
||||
printf("Request: %s\n", request);
|
||||
|
||||
curl = curl_easy_init();
|
||||
curl_easy_setopt(curl, CURLOPT_URL, params->server);
|
||||
curl_easy_setopt(curl, CURLOPT_PORT, params->server_port);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, params->verify_ssl_peer);
|
||||
if (params->ca_path)
|
||||
curl_easy_setopt(curl, CURLOPT_CAINFO, params->ca_path);
|
||||
curl_easy_setopt(curl, CURLOPT_POST, 1);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request);
|
||||
if (params->verbose)
|
||||
curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)response);
|
||||
|
||||
res = curl_easy_perform(curl);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (res != CURLE_OK)
|
||||
{
|
||||
fprintf(stderr, "curl_easy_perform() failed: %s\n",
|
||||
curl_easy_strerror(res));
|
||||
goto end;
|
||||
}
|
||||
|
||||
token = strtok_r(response, "\n", &saveptr);
|
||||
|
||||
while (token)
|
||||
{
|
||||
if (params->verbose)
|
||||
printf("Parse %s\n", token);
|
||||
cur_ptr = token;
|
||||
if (!strcmp(token, "<end>"))
|
||||
break;
|
||||
else if (!STRNCMP(token, "protocol"))
|
||||
{
|
||||
cur_ptr += sizeof("protocol"); // includes "="
|
||||
if (STRNCMP(cur_ptr, "gpass-"))
|
||||
{
|
||||
fprintf(stderr, "Error: Unknown server protocol %s\n", token);
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
cur_ptr += sizeof("gpass-")-1;
|
||||
if (atoi(cur_ptr) > SERVER_PROTOCOL)
|
||||
{
|
||||
fprintf(stderr, "Error: Cannot handle server protocol %s\n", token);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!STRNCMP(token, "pass"))
|
||||
{
|
||||
cur_ptr += sizeof("pass"); // includes "="
|
||||
|
||||
if ((strlen(cur_ptr)/2) > sizeof(password))
|
||||
{
|
||||
fprintf(stderr, "Error: retrieved password is too big !\n");
|
||||
goto end;
|
||||
}
|
||||
|
||||
hex_to_bin(password, (unsigned char*)cur_ptr, strlen(cur_ptr));
|
||||
|
||||
if (matched_key >= crypto_v1_index)
|
||||
{
|
||||
// Crypto v1
|
||||
EVP_DecryptInit(s_cipher_ctx, EVP_aes_256_ecb(), params->derived_master_key, NULL);
|
||||
EVP_CipherUpdate(s_cipher_ctx, password, &res, password, strlen(cur_ptr)/2);
|
||||
// Remove salt
|
||||
password[strlen((char*)password)-3] = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
EVP_DecryptInit(s_cipher_ctx, EVP_aes_256_cbc(), params->derived_master_key, params->global_iv);
|
||||
EVP_CipherUpdate(s_cipher_ctx, password, &res, password, strlen(cur_ptr)/2);
|
||||
// Remove salt
|
||||
len = strlen((char*)password);
|
||||
memmove(password, &password[3], len-3);
|
||||
password[len-3] = 0;
|
||||
}
|
||||
display_password((char*)password, DISPLAY_TIME);
|
||||
ret = 0;
|
||||
goto end;
|
||||
}
|
||||
else if (!STRNCMP(token, "pbkdf2_level"))
|
||||
{
|
||||
cur_ptr += sizeof("pbkdf2_level"); // includes "="
|
||||
|
||||
if (atoi(cur_ptr) != params->pbkdf2_level)
|
||||
{
|
||||
params->pbkdf2_level = atoi(cur_ptr);
|
||||
ret = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (!STRNCMP(token, "matched_key"))
|
||||
{
|
||||
cur_ptr += sizeof("matched_key"); // includes "="
|
||||
|
||||
matched_key = atoi(cur_ptr);
|
||||
}
|
||||
else
|
||||
{
|
||||
fprintf(stderr, "Error: Unknown server response %s\n", token);
|
||||
break;
|
||||
}
|
||||
token = strtok_r(NULL, "\n", &saveptr);
|
||||
}
|
||||
|
||||
if (ret)
|
||||
printf("Password not found\n");
|
||||
|
||||
end:
|
||||
free(request);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void init_parameters(struct gpass_parameters* params)
|
||||
{
|
||||
memset (params, 0, sizeof(*params));
|
||||
params->pbkdf2_level = DEFAULT_PBKDF2_LEVEL;
|
||||
params->server_port = DEFAULT_SERVER_PORT;
|
||||
params->verify_ssl_peer = 1;
|
||||
params->crypto_v1_compatible = 1; // For now, in the next version it must a command line parameter
|
||||
}
|
||||
|
||||
static void release_parameters(struct gpass_parameters* params)
|
||||
{
|
||||
if (params->server) free(params->server);
|
||||
if (params->salt) free(params->salt);
|
||||
if (params->domain) free(params->domain);
|
||||
if (params->username) free(params->username);
|
||||
if (params->orig_master_key) free(params->orig_master_key);
|
||||
if (params->derived_master_key) free(params->derived_master_key);
|
||||
if( params->ca_path) free(params->ca_path);
|
||||
if (params->global_iv) free(params->global_iv);
|
||||
}
|
||||
|
||||
static int check_parameters(struct gpass_parameters* params)
|
||||
{
|
||||
if (!params->server)
|
||||
{
|
||||
fprintf(stderr, "Error: server not set\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!params->domain)
|
||||
{
|
||||
fprintf(stderr, "Error: gpass domain not set\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!params->username)
|
||||
{
|
||||
fprintf(stderr, "Error: username not set\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int gpass_ini_handler(void* user, const char* section,
|
||||
const char* name, const char* value)
|
||||
{
|
||||
struct gpass_parameters* params = (struct gpass_parameters*) user;
|
||||
|
||||
if (!STRNCMP(name, "ca_path"))
|
||||
{
|
||||
if (params->ca_path) free(params->ca_path);
|
||||
params->ca_path = strdup(value);
|
||||
}
|
||||
else if (!STRNCMP(name, "pbkdf2_level"))
|
||||
params->pbkdf2_level = atoi(value);
|
||||
else if (!STRNCMP(name, "verify_ssl_peer"))
|
||||
params->verify_ssl_peer = atoi(value);
|
||||
else if (!STRNCMP(name, "server_port"))
|
||||
{
|
||||
params->server_port = atoi(value);
|
||||
params->port_set = 1;
|
||||
}
|
||||
else if (!STRNCMP(name, "server"))
|
||||
{
|
||||
if (params->server) free(params->server);
|
||||
params->server = strdup(value);
|
||||
}
|
||||
else
|
||||
fprintf(stderr, "Error: Unknown key '%s' in config file\n", name);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void usage(char* program_name)
|
||||
{
|
||||
fprintf(stderr, "Usage: %s [-f config_file] [-p server_port] [-c CA_certificate_path] [-l PBKDF2_level] [-s gpass_server] [-v] -d domain -u username\n",
|
||||
program_name);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
struct gpass_parameters params;
|
||||
int opt, ret = 0;
|
||||
char* tmp;
|
||||
char* config_file, *home;
|
||||
|
||||
if (argc == 1)
|
||||
usage(argv[0]);
|
||||
|
||||
init_parameters(¶ms);
|
||||
|
||||
home = getenv("HOME");
|
||||
if (home)
|
||||
{
|
||||
config_file = malloc(strlen(home)+1+sizeof(DEFAULT_CONFIG_FILE));
|
||||
sprintf(config_file, "%s/" DEFAULT_CONFIG_FILE, home);
|
||||
|
||||
ini_parse(config_file, gpass_ini_handler, ¶ms);
|
||||
|
||||
free(config_file);
|
||||
}
|
||||
|
||||
while ((opt = getopt(argc, argv, "c:d:f:l:np:s:u:vh")) != -1) {
|
||||
switch (opt) {
|
||||
case 'c':
|
||||
if (params.ca_path) free(params.ca_path);
|
||||
params.ca_path = strdup(optarg);
|
||||
break;
|
||||
case 'd':
|
||||
if (params.domain) free(params.domain);
|
||||
params.domain = strdup(optarg);
|
||||
break;
|
||||
case 'f':
|
||||
ini_parse(optarg, gpass_ini_handler, ¶ms);
|
||||
break;
|
||||
case 'l':
|
||||
params.pbkdf2_level = atoi(optarg);
|
||||
break;
|
||||
case 'n':
|
||||
params.verify_ssl_peer = 0;
|
||||
break;
|
||||
case 'p':
|
||||
params.server_port = atoi(optarg);
|
||||
params.port_set = 1;
|
||||
break;
|
||||
case 's':
|
||||
if (params.server) free(params.server);
|
||||
params.server = strdup(optarg);
|
||||
break;
|
||||
case 'u':
|
||||
if (params.username) free(params.username);
|
||||
params.username = strdup(optarg);
|
||||
break;
|
||||
case 'v':
|
||||
params.verbose++;
|
||||
break;
|
||||
case 'h':
|
||||
case '?':
|
||||
default: /* '?' */
|
||||
usage(argv[0]);
|
||||
}
|
||||
}
|
||||
|
||||
ret = check_parameters(¶ms);
|
||||
|
||||
if (ret)
|
||||
goto end;
|
||||
|
||||
// Manage server, server_port and salt
|
||||
if (!STRNCMP(params.server, "http://"))
|
||||
{
|
||||
if (!params.port_set)
|
||||
params.server_port = 80;
|
||||
params.salt = strdup(¶ms.server[7]);
|
||||
}
|
||||
else if (!STRNCMP(params.server, "https://"))
|
||||
{
|
||||
if (!params.port_set)
|
||||
params.server_port = 443;
|
||||
params.salt = strdup(¶ms.server[8]);
|
||||
}
|
||||
|
||||
// Manage domain
|
||||
if (!STRNCMP(params.domain, "http://"))
|
||||
{
|
||||
tmp = strdup(¶ms.domain[7]);
|
||||
free(params.domain);
|
||||
params.domain = tmp;
|
||||
}
|
||||
else if (!STRNCMP(params.domain, "https://"))
|
||||
{
|
||||
tmp = strdup(¶ms.domain[8]);
|
||||
free(params.domain);
|
||||
params.domain = tmp;
|
||||
}
|
||||
|
||||
// Remove query part of domain (a.com[/XXXX])
|
||||
for (tmp=params.domain; *tmp; tmp++)
|
||||
{
|
||||
if (*tmp == '/')
|
||||
{
|
||||
*tmp = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
s_md_ctx = EVP_MD_CTX_new();
|
||||
s_md_256 = EVP_sha256();
|
||||
EVP_DigestInit(s_md_ctx, s_md_256);
|
||||
|
||||
s_cipher_ctx = EVP_CIPHER_CTX_new();
|
||||
|
||||
// Let's go
|
||||
tmp = getpass("Enter master key: ");
|
||||
|
||||
if (!tmp)
|
||||
goto end;
|
||||
|
||||
params.orig_master_key = strdup(tmp);
|
||||
derive_master_key(¶ms);
|
||||
|
||||
// Ctrl+C
|
||||
signal(SIGINT, signal_handler);
|
||||
// Ctrl+Z
|
||||
signal(SIGTSTP, signal_handler);
|
||||
|
||||
ret = ask_server(¶ms);
|
||||
|
||||
// try again with new parameters
|
||||
if (ret > 0)
|
||||
{
|
||||
derive_master_key(¶ms);
|
||||
ask_server(¶ms);
|
||||
}
|
||||
|
||||
end:
|
||||
release_parameters(¶ms);
|
||||
|
||||
if (s_md_ctx) EVP_MD_CTX_free(s_md_ctx);
|
||||
if (s_cipher_ctx) EVP_CIPHER_CTX_free(s_cipher_ctx);
|
||||
|
||||
return ret;
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
gPass : global Password
|
||||
=======================
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
Everyday we have a lot of passwords to manage corresponding to a lot of accounts we use. It's hard to remain all of these, moreover if we don't use it often. So, what most people do is to generate only a subset of passwords easy to remain. This implies two common errors :
|
||||
|
||||
* Password are not very strong
|
||||
* We use them for multiple 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 master key) 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 stored information (server side) is encrypted. 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 bruteforce 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 home hostable (be careful on server down !). Moreover, with gPass, you can have multiple master keys !
|
||||
|
||||
|
||||
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 subdomains 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" to point to your password server (+ username). Be careful, login and password are case sensitive.
|
||||
|
||||
When you're in a login form and you want to use gPass, type your login and fill "@@masterkey" in password field. Then submit and password will automatically be replaced by the one in the database (after addon decrypt it).
|
||||
|
||||
|
||||
Technical details
|
||||
-----------------
|
||||
|
||||
The two columns in database are "login" and "password".
|
||||
login is compounded by "@@domain;login" encrypted with AES 256
|
||||
password is salted and encrypted with AES 256
|
||||
|
||||
The key that encrypt these fields is PBKDF2(hmac-sha256, masterkey, password_server_url, 1000, 256)
|
||||
|
||||
For now, the only addons made is for firefox. 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 ressources. For enhanced security, it's better to put the password server under https and add authentication to admin panel. Server side is written in PHP (with SQLite3 for database component).
|
||||
|
||||
Configuration parameters are in conf.php
|
||||
|
||||
A demonstration server is available [here](http://gpass-demo.soutade.fr). It's the default server of XPI package (user demo).
|
||||
|
||||
**Warning** The master key derivation is partially based on account URL. So it's linked to your server information. Currently there is no simple way to export/import a full gPass database.
|
||||
|
||||
Client
|
||||
------
|
||||
|
||||
Just install xpi package. You can have debug information by setting DEBUG in main.js (use it with firefox addon sdk).
|
||||
|
||||
|
||||
Licence
|
||||
-------
|
||||
|
||||
All the code is licenced under GPL v3. Source code is available [here](http://indefero.soutade.fr/p/gpass).
|
||||
1
firefox_addon/README.md
Symbolic link
@@ -0,0 +1 @@
|
||||
../README.md
|
||||
BIN
firefox_addon/data/gpass_icon_64.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
42
firefox_addon/lib/hmac.js
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
Copyright (C) 2013 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 hmac = Cc["@mozilla.org/security/hmac;1"]
|
||||
.createInstance(Ci.nsICryptoHMAC);
|
||||
|
||||
exports.hmac = {
|
||||
hmac_init : function (key) {
|
||||
var keyObject = Cc["@mozilla.org/security/keyobjectfactory;1"]
|
||||
.getService(Ci.nsIKeyObjectFactory)
|
||||
.keyFromString(Ci.nsIKeyObject.HMAC, key);
|
||||
hmac.init(hmac.SHA256, keyObject);
|
||||
},
|
||||
|
||||
hmac_digest : function (message) {
|
||||
var data = new Uint8Array(message.length);
|
||||
for(i=0; i<message.length; i++)
|
||||
data[i] = message.charCodeAt(i);
|
||||
hmac.update(data, data.length);
|
||||
res = hmac.finish(false);
|
||||
hmac.reset();
|
||||
return res;
|
||||
}
|
||||
};
|
||||
@@ -19,15 +19,16 @@
|
||||
|
||||
var {Cc, Ci} = require("chrome");
|
||||
var notifications = require("sdk/notifications");
|
||||
|
||||
var pkdbf2 = require("pkdbf2").pkdbf2;
|
||||
var aes = require("jsaes").aes;
|
||||
var parseURI = require("parseuri").parseURI;
|
||||
var self = require("sdk/self");
|
||||
var prefSet = require("sdk/simple-prefs");
|
||||
|
||||
var pkdbf2 = require("lib/pkdbf2").pkdbf2;
|
||||
var aes = require("lib/jsaes").aes;
|
||||
var parseURI = require("lib/parseuri").parseURI;
|
||||
var DEBUG = false;
|
||||
var pkdbf2_level = prefSet.prefs.pkdbf2_level;
|
||||
var pkdbf2_level = getPref("pkdbf2_level");
|
||||
var protocol_version = 3;
|
||||
SERVER = { OK : 0, FAILED : 1, RESTART_REQUEST : 2};
|
||||
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) {
|
||||
@@ -41,7 +42,7 @@ function a2hex(str) {
|
||||
var hex = '';
|
||||
for (var i = 0; i < str.length; i++)
|
||||
{
|
||||
c = str.charCodeAt(i).toString(16);
|
||||
var c = str.charCodeAt(i).toString(16);
|
||||
if (c.length == 1) c = "0" + c;
|
||||
hex += c;
|
||||
}
|
||||
@@ -54,20 +55,43 @@ function debug(s)
|
||||
console.log(s);
|
||||
}
|
||||
|
||||
function notify(text, data)
|
||||
{
|
||||
var icon = self.data.url("gpass_icon_64.png");
|
||||
notifications.notify({
|
||||
title: "gPass",
|
||||
text: text,
|
||||
data: data,
|
||||
iconUrl: icon,
|
||||
});
|
||||
}
|
||||
|
||||
function getPref(key)
|
||||
{
|
||||
return prefSet.prefs[key];
|
||||
}
|
||||
|
||||
function setPref(key, value)
|
||||
{
|
||||
prefSet.prefs[key] = value;
|
||||
}
|
||||
|
||||
function generate_request(domain, login, mkey)
|
||||
{
|
||||
v = "@@" + domain + ";" + login;
|
||||
var v = "@@" + domain + ";" + login;
|
||||
debug("will encrypt " + v);
|
||||
debug("with " + a2hex(mkey));
|
||||
enc = aes.encryptLongString(v, aes.init(mkey));
|
||||
var enc = aes.encryptLongString(v, aes.init(mkey));
|
||||
aes.finish();
|
||||
debug("res " + enc);
|
||||
debug("res " + a2hex(enc));
|
||||
|
||||
return enc;
|
||||
}
|
||||
|
||||
function ask_server(form, field, logins, domain, wdomain, mkey, salt)
|
||||
function ask_server(form, field, logins, domain, wdomain, mkey, salt, submit)
|
||||
{
|
||||
var a, b;
|
||||
|
||||
mkey = pkdbf2.pkdbf2(mkey, salt, pkdbf2_level, 256/8);
|
||||
|
||||
keys = "";
|
||||
@@ -89,33 +113,28 @@ function ask_server(form, field, logins, domain, wdomain, mkey, salt)
|
||||
|
||||
debug("Keys " + keys);
|
||||
|
||||
// Need to do a synchronous request
|
||||
var gPassRequest = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
|
||||
createInstance(Ci.nsIXMLHttpRequest);
|
||||
|
||||
var ret = SERVER.OK;
|
||||
|
||||
// gPassRequest.addEventListener("progress", function(evt) { ; }, false);
|
||||
gPassRequest.addEventListener("load", function(evt) {
|
||||
ciphered_password = "";
|
||||
server_pkdbf2_level = 0;
|
||||
server_version = 0;
|
||||
var ciphered_password = "";
|
||||
var server_pkdbf2_level = 0;
|
||||
var server_version = 0;
|
||||
|
||||
r = this.responseText.split("\n");
|
||||
var r = this.responseText.split("\n");
|
||||
debug("resp " + r);
|
||||
|
||||
for(a=0; a<r.length; a++)
|
||||
for(var a=0; a<r.length; a++)
|
||||
{
|
||||
debug("Analyse " + r[a]);
|
||||
|
||||
params = r[a].split("=");
|
||||
if (params.length != 2 && params[0] != "<end>")
|
||||
{
|
||||
notifications.notify({
|
||||
title: "gPasss",
|
||||
text: "Error : It seems that it's not a gPass server",
|
||||
data: this.responseText,
|
||||
});
|
||||
notify("Error : It seems that it's not a gPass server",
|
||||
this.responseText);
|
||||
ret = SERVER.FAILED;
|
||||
break;
|
||||
}
|
||||
@@ -127,11 +146,8 @@ function ask_server(form, field, logins, domain, wdomain, mkey, salt)
|
||||
|
||||
if (params[1].indexOf("gpass-") != 0)
|
||||
{
|
||||
notifications.notify({
|
||||
title: "gPasss",
|
||||
text: "Error : It seems that it's not a gPass server",
|
||||
data: this.responseText,
|
||||
});
|
||||
notify("Error : It seems that it's not a gPass server",
|
||||
this.responseText);
|
||||
ret = SERVER.FAILED;
|
||||
break;
|
||||
}
|
||||
@@ -140,11 +156,8 @@ function ask_server(form, field, logins, domain, wdomain, mkey, salt)
|
||||
|
||||
if (server_protocol_version > protocol_version)
|
||||
{
|
||||
notifications.notify({
|
||||
title: "gPasss",
|
||||
text: "Protocol version not supported, please upgrade your addon",
|
||||
data: "Protocol version not supported, please upgrade your addon",
|
||||
});
|
||||
notify("Protocol version not supported, please upgrade your addon",
|
||||
"Protocol version not supported, please upgrade your addon");
|
||||
ret = SERVER.FAILED;
|
||||
}
|
||||
else
|
||||
@@ -171,7 +184,7 @@ function ask_server(form, field, logins, domain, wdomain, mkey, salt)
|
||||
{
|
||||
debug("New pkdbf2 level " + server_pkdbf2_level);
|
||||
pkdbf2_level = server_pkdbf2_level;
|
||||
prefSet.prefs.pkdbf2_level = server_pkdbf2_level;
|
||||
setPref("pkdbf2_level", pkdbf2_level);
|
||||
ret = SERVER.RESTART_REQUEST;
|
||||
}
|
||||
break;
|
||||
@@ -180,11 +193,8 @@ function ask_server(form, field, logins, domain, wdomain, mkey, salt)
|
||||
default:
|
||||
debug("Unknown command " + params[0]);
|
||||
|
||||
notifications.notify({
|
||||
title: "gPasss",
|
||||
text: "Error : It seems that it's not a gPass server",
|
||||
data: this.responseText,
|
||||
});
|
||||
notify("Error : It seems that it's not a gPass server",
|
||||
this.responseText);
|
||||
ret = SERVER.FAILED;
|
||||
break;
|
||||
}
|
||||
@@ -204,8 +214,16 @@ function ask_server(form, field, logins, domain, wdomain, mkey, salt)
|
||||
debug("Clear password " + clear_password);
|
||||
field.value = clear_password;
|
||||
// Remove gPass event listener and submit again with clear password
|
||||
form.removeEventListener("submit", on_sumbit, true);
|
||||
form.submit();
|
||||
if (submit)
|
||||
{
|
||||
form.removeEventListener("submit", on_sumbit, true);
|
||||
form.submit();
|
||||
}
|
||||
else
|
||||
{
|
||||
notify("Password successfully replaced",
|
||||
"Password successfully replaced");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -213,25 +231,18 @@ function ask_server(form, field, logins, domain, wdomain, mkey, salt)
|
||||
|
||||
ret = SERVER.FAILED;
|
||||
|
||||
notifications.notify({
|
||||
title: "gPasss",
|
||||
text: "No password found in database",
|
||||
data: "No password found in database",
|
||||
});
|
||||
notify("No password found in database",
|
||||
"No password found in database");
|
||||
}
|
||||
}, false);
|
||||
gPassRequest.addEventListener("error", function(evt) {
|
||||
debug("error");
|
||||
ret = false;
|
||||
notifications.notify({
|
||||
title: "gPasss",
|
||||
text: "Error",
|
||||
data: "Error",
|
||||
});
|
||||
|
||||
notify("Error",
|
||||
"Error");
|
||||
}, false);
|
||||
debug("connect to " + prefSet.prefs.account_url);
|
||||
gPassRequest.open("POST", prefSet.prefs.account_url, true);
|
||||
debug("connect to " + getPref("account_url"));
|
||||
gPassRequest.open("POST", getPref("account_url"), true);
|
||||
gPassRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
|
||||
gPassRequest.send(keys);
|
||||
|
||||
@@ -240,7 +251,7 @@ function ask_server(form, field, logins, domain, wdomain, mkey, salt)
|
||||
|
||||
function wildcard_domain(domain)
|
||||
{
|
||||
parts = domain.split(".");
|
||||
var parts = domain.split(".");
|
||||
|
||||
if (parts.length >= 3)
|
||||
{
|
||||
@@ -261,49 +272,77 @@ function wildcard_domain(domain)
|
||||
return "";
|
||||
}
|
||||
|
||||
function on_sumbit(e)
|
||||
function _add_name(logins, name)
|
||||
{
|
||||
var form = this;
|
||||
var fields = form.getElementsByTagName("input");
|
||||
for(var i=0; i<logins.length; i++)
|
||||
if (logins[i] == name) return ;
|
||||
logins.push(name);
|
||||
}
|
||||
|
||||
domain = parseURI.parseUri(form.ownerDocument.baseURI);
|
||||
domain = domain["host"];
|
||||
wdomain = wildcard_domain(domain);
|
||||
function try_get_name(fields, type_filters, match)
|
||||
{
|
||||
var user = null;
|
||||
var all_logins = new Array();
|
||||
|
||||
salt = parseURI.parseUri(prefSet.prefs.account_url);
|
||||
salt = salt["host"] + salt["path"];
|
||||
|
||||
debug("salt " + salt);
|
||||
|
||||
user = null;
|
||||
all_logins = new Array;
|
||||
|
||||
// Get all <input type="text"> && <input type="email">
|
||||
for (i=0; i<fields.length; i++)
|
||||
for (var i=0; i<fields.length; i++)
|
||||
{
|
||||
var field = fields[i];
|
||||
if (field.getAttribute("type") == "text" || field.getAttribute("type") == "email")
|
||||
|
||||
for (var a=0; a<type_filters.length; a++)
|
||||
{
|
||||
if (field.hasAttribute("name") && field.value != "")
|
||||
if ((match && field.getAttribute("type") == type_filters[a]) ||
|
||||
(!match && field.getAttribute("type") != type_filters[a]))
|
||||
{
|
||||
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;
|
||||
all_logins.push(field.value);
|
||||
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)
|
||||
logins = new Array(user);
|
||||
return new Array(user);
|
||||
else
|
||||
logins = all_logins;
|
||||
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);
|
||||
|
||||
var salt = parseURI.parseUri(getPref("account_url"));
|
||||
salt = salt["host"] + salt["path"];
|
||||
|
||||
debug("salt " + salt);
|
||||
|
||||
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 (i=0; i<fields.length; i++)
|
||||
for (var i=0; i<fields.length; i++)
|
||||
{
|
||||
var field = fields[i];
|
||||
|
||||
@@ -311,26 +350,26 @@ function on_sumbit(e)
|
||||
{
|
||||
debug(field.value);
|
||||
password = field.value;
|
||||
if (password.indexOf("@@") != 0)
|
||||
if (password.indexOf("@@") != 0 && password.indexOf("@_") != 0)
|
||||
continue;
|
||||
|
||||
mkey = password.substring(2);
|
||||
|
||||
var ret = ask_server(form, field, logins, domain, wdomain, mkey, salt);
|
||||
e.preventDefault();
|
||||
|
||||
var ret = ask_server(form, field, logins, domain, wdomain, mkey, salt, (password.indexOf("@@") == 0));
|
||||
|
||||
switch(ret)
|
||||
{
|
||||
case SERVER.OK:
|
||||
e.preventDefault();
|
||||
break;
|
||||
case SERVER.FAILED:
|
||||
if (logins !== all_logins)
|
||||
{
|
||||
ret = ask_server(form, field, all_logins, domain, wdomain, mkey, salt);
|
||||
ret = ask_server(form, field, all_logins, domain, wdomain, mkey, salt, (password.indexOf("@@") == 0));
|
||||
if (ret == SERVER.OK)
|
||||
break;
|
||||
}
|
||||
e.preventDefault();
|
||||
break;
|
||||
case SERVER.RESTART_REQUEST:
|
||||
i = -1; // Restart loop
|
||||
@@ -344,9 +383,9 @@ function on_sumbit(e)
|
||||
|
||||
function document_loaded(event)
|
||||
{
|
||||
doc = event.target;
|
||||
var doc = event.target;
|
||||
// If there is a password in the form, add a "submit" listener
|
||||
for(i=0; i<doc.forms.length; i++)
|
||||
for(var i=0; i<doc.forms.length; i++)
|
||||
{
|
||||
var form = doc.forms[i];
|
||||
var fields = form.getElementsByTagName("input");
|
||||
|
||||
@@ -17,27 +17,7 @@
|
||||
along with gPass. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
var {Cc, Ci} = require("chrome");
|
||||
|
||||
var hmac = Cc["@mozilla.org/security/hmac;1"]
|
||||
.createInstance(Ci.nsICryptoHMAC);
|
||||
|
||||
function hmac_init(key) {
|
||||
var keyObject = Cc["@mozilla.org/security/keyobjectfactory;1"]
|
||||
.getService(Ci.nsIKeyObjectFactory)
|
||||
.keyFromString(Ci.nsIKeyObject.HMAC, key);
|
||||
hmac.init(hmac.SHA256, keyObject);
|
||||
}
|
||||
|
||||
function hmac_digest(message) {
|
||||
var data = new Uint8Array(message.length);
|
||||
for(i=0; i<message.length; i++)
|
||||
data[i] = message.charCodeAt(i);
|
||||
hmac.update(data, data.length);
|
||||
res = hmac.finish(false);
|
||||
hmac.reset();
|
||||
return res;
|
||||
}
|
||||
var hmac = require("lib/hmac").hmac;
|
||||
|
||||
exports.pkdbf2 = {
|
||||
|
||||
@@ -48,10 +28,10 @@ exports.pkdbf2 = {
|
||||
var temp_res = "";
|
||||
var temp_res2 = "";
|
||||
|
||||
hmac_init(password);
|
||||
hmac.hmac_init(password);
|
||||
for (i=1; result.length < outlen; i++)
|
||||
{
|
||||
temp = hmac_digest(salt +
|
||||
temp = hmac.hmac_digest(salt +
|
||||
String.fromCharCode((i & 0xff000000) >> 24) +
|
||||
String.fromCharCode((i & 0x00ff0000) >> 16) +
|
||||
String.fromCharCode((i & 0x0000ff00) >> 8) +
|
||||
@@ -61,7 +41,7 @@ exports.pkdbf2 = {
|
||||
|
||||
for(a=1; a<iterations; a++)
|
||||
{
|
||||
temp2 = hmac_digest(temp);
|
||||
temp2 = hmac.hmac_digest(temp);
|
||||
temp_res2 = "";
|
||||
for(b = 0; b<temp_res.length; b++)
|
||||
temp_res2 += String.fromCharCode(temp_res.charCodeAt(b) ^ temp2.charCodeAt(b));
|
||||
@@ -74,4 +54,4 @@ exports.pkdbf2 = {
|
||||
|
||||
return result.substr(0, outlen);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
{
|
||||
"name": "gpass",
|
||||
"fullName": "gPass",
|
||||
"id": "jid1-eNs887pPJU8aNg",
|
||||
"id": "jid1-eNs887pPJU8aNg@jetpack",
|
||||
"main": "lib/main.js",
|
||||
"description": "gPass : global password manager",
|
||||
"author": "Grégory Soutadé",
|
||||
"license": "GNU GPL v3",
|
||||
"version": "0.4",
|
||||
"homepage" : "http://indefero.soutade.fr/p/gpass",
|
||||
"icon" : "data/gpass_icon_64.png",
|
||||
"version": "0.7.0",
|
||||
"preferences": [
|
||||
{
|
||||
"name": "account_url",
|
||||
|
||||
582
firefox_webextension/background.js
Normal file
@@ -0,0 +1,582 @@
|
||||
/*
|
||||
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)
|
||||
{
|
||||
// debug(JSON.stringify(details));
|
||||
if (details.requestBody)
|
||||
{
|
||||
if (details.requestBody.formData)
|
||||
{
|
||||
for (var key in details.requestBody.formData)
|
||||
{
|
||||
for(var idx in details.requestBody.formData[key])
|
||||
{
|
||||
value = details.requestBody.formData[key][idx];
|
||||
if (value.startsWith("@@") ||
|
||||
value.startsWith("@_"))
|
||||
return {cancel: true};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// 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};
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
return {cancel: false};
|
||||
}
|
||||
|
||||
function url_unblock_callback(details)
|
||||
{
|
||||
return {cancel: false};
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
enabled = (key_present == null);
|
||||
// Do switch
|
||||
enabled = !enabled;
|
||||
if (enabled)
|
||||
{
|
||||
parameters = {type:"blockForms"};
|
||||
debug("Now enabled");
|
||||
}
|
||||
else
|
||||
{
|
||||
parameters = {type:"unblockForms"};
|
||||
debug("Now disabled");
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
58
firefox_webextension/compat.js
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
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 get_preference(key)
|
||||
{
|
||||
return browser.storage.local.get(key)
|
||||
.then(
|
||||
function (pref) {
|
||||
if (!pref.hasOwnProperty(key))
|
||||
{
|
||||
if (default_preferences.hasOwnProperty(key))
|
||||
return default_preferences[key];
|
||||
else
|
||||
return null;
|
||||
}
|
||||
return pref[key];
|
||||
}
|
||||
,
|
||||
function (err) {
|
||||
console.log("Error getting preference " + err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function set_preference(key, value)
|
||||
{
|
||||
browser.storage.local.set({[key]:value});
|
||||
}
|
||||
|
||||
function delete_preference(key)
|
||||
{
|
||||
browser.storage.local.remove(key);
|
||||
}
|
||||
|
||||
function send_tab_message(tab_id, parameters, callback)
|
||||
{
|
||||
browser.tabs.sendMessage(tab_id, parameters).then(
|
||||
function cb(response) {
|
||||
callback(response);
|
||||
}
|
||||
);
|
||||
}
|
||||
1
firefox_webextension/icons
Symbolic link
@@ -0,0 +1 @@
|
||||
../chrome_addon/icons
|
||||
381
firefox_webextension/lib/main.js
Normal file
@@ -0,0 +1,381 @@
|
||||
/*
|
||||
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 gpass_enabled = true;
|
||||
|
||||
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(options).then(
|
||||
function created(notification_id)
|
||||
{
|
||||
window.setTimeout(function() {
|
||||
browser.notifications.clear(notification_id);
|
||||
}, 2000);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
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_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;
|
||||
|
||||
debug("on_submit");
|
||||
|
||||
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
|
||||
type_filters.push("password");
|
||||
all_logins = try_get_name(fields, type_filters, false);
|
||||
|
||||
if (!logins.length)
|
||||
logins = all_logins;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
// Look for <input type="password" value="@@...">
|
||||
for (var i=0; i<fields.length; i++)
|
||||
{
|
||||
var field = fields[i];
|
||||
|
||||
if (field.getAttribute("type") == "password")
|
||||
{
|
||||
password = field.value;
|
||||
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);
|
||||
|
||||
parameters = {
|
||||
type:"password",
|
||||
logins:logins,
|
||||
domain:domain,
|
||||
mkey:mkey,
|
||||
options:{field_id:i}
|
||||
};
|
||||
|
||||
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 set_password(form, field, password, do_submit)
|
||||
{
|
||||
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 (var a=0; a<fields.length; a++)
|
||||
{
|
||||
var field = fields[a];
|
||||
if (field.getAttribute("type") == "password")
|
||||
{
|
||||
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 (managed_forms.length && do_block)
|
||||
block_url("<all_urls>");
|
||||
}
|
||||
|
||||
function unblock_all_forms()
|
||||
{
|
||||
debug("unblock all forms");
|
||||
|
||||
on_blur(null);
|
||||
|
||||
for(var i=0; i<managed_forms.length; i++)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (managed_forms.length)
|
||||
unblock_url("<all_urls>");
|
||||
|
||||
gpass_enabled = false;
|
||||
}
|
||||
|
||||
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("");
|
||||
295
firefox_webextension/lib/misc.js
Normal file
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
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};
|
||||
|
||||
var browser = browser || chrome;
|
||||
var crypto = crypto || window.crypto;
|
||||
|
||||
function notify(text, data)
|
||||
{
|
||||
browser.runtime.sendMessage({type: "notification", options:{"message":text, "data":data}});
|
||||
}
|
||||
|
||||
function block_url(url)
|
||||
{
|
||||
debug("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));
|
||||
}
|
||||
|
||||
// https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
|
||||
function str2ab2(str) {
|
||||
var chars = []
|
||||
for (var i=0, strLen=str.length; i < strLen; i++) {
|
||||
chars.push(str.charCodeAt(i));
|
||||
}
|
||||
return new Uint8Array(chars);
|
||||
}
|
||||
|
||||
function str2ab(str) {
|
||||
var buf = new ArrayBuffer(str.length);
|
||||
// var buf = new ArrayBuffer(str.length*2); // 2 bytes for each char
|
||||
var bufView = new Uint8Array(buf);
|
||||
for (var i=0, strLen=str.length; i < strLen; i++) {
|
||||
bufView[i] = str.charCodeAt(i);
|
||||
}
|
||||
return bufView;
|
||||
}
|
||||
|
||||
function crypto_pbkdf2(mkey, salt, level)
|
||||
{
|
||||
AESCBC = {
|
||||
name: "AES-CBC",
|
||||
length: 256,
|
||||
}
|
||||
|
||||
var key = str2ab(mkey);
|
||||
return crypto.subtle.importKey("raw", key, {name: "PBKDF2"}, false, ["deriveBits", "deriveKey"])
|
||||
.then(function(key){
|
||||
//sha-256
|
||||
return crypto.subtle.deriveKey({
|
||||
name: "PBKDF2",
|
||||
salt: str2ab(salt),
|
||||
iterations: level,
|
||||
hash: "SHA-256",
|
||||
}, key, AESCBC, false, ["encrypt", "decrypt", "unwrapKey", "wrapKey"])
|
||||
.then(function(key) {
|
||||
return key;
|
||||
})
|
||||
.catch(function(err){
|
||||
console.log("Error derive key " + err);
|
||||
});
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.log("Error import key" + err);
|
||||
});
|
||||
}
|
||||
|
||||
function simple_pbkdf2(mkey, salt, level)
|
||||
{
|
||||
AESCBC = {
|
||||
name: "AES-CBC",
|
||||
length: 256,
|
||||
}
|
||||
|
||||
var key = str2ab(mkey);
|
||||
return crypto.subtle.importKey("raw", key, {name: "PBKDF2"}, false, ["deriveBits", "deriveKey"])
|
||||
.then(function(key){
|
||||
//sha-256
|
||||
return crypto.subtle.deriveKey({
|
||||
name: "PBKDF2",
|
||||
salt: str2ab(salt),
|
||||
iterations: level,
|
||||
hash: "SHA-256",
|
||||
}, key, AESCBC, true, ["unwrapKey", "wrapKey"])
|
||||
.then(function(key) {
|
||||
return crypto.subtle.exportKey("raw", key)
|
||||
.then(function (key) {
|
||||
return ab2str(key);
|
||||
});
|
||||
})
|
||||
.catch(function(err){
|
||||
console.log("Error derive key " + err);
|
||||
});
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.log("Error import key" + err);
|
||||
});
|
||||
}
|
||||
|
||||
function _encrypt(mkey, iv, data)
|
||||
{
|
||||
while ((data.length % 16))
|
||||
data += "\0";
|
||||
|
||||
data = str2ab(data);
|
||||
|
||||
promise = mkey.then(function(mkey){
|
||||
return crypto.subtle.encrypt({
|
||||
name: "AES-CBC",
|
||||
iv: iv
|
||||
}, mkey, data)})
|
||||
.then(function(encrypted) {
|
||||
return ab2str(encrypted);
|
||||
})
|
||||
.catch(function(encryption) {
|
||||
console.log("Encryption rejected " + encryption);
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
async function _decrypt(mkey, iv, data)
|
||||
{
|
||||
while ((data.length % 16))
|
||||
data += "\0";
|
||||
|
||||
nulliv = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
pkcs7_padding = new Uint8Array([16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16]);
|
||||
pkcs7_padding = await _encrypt(mkey, nulliv, ab2str(pkcs7_padding));
|
||||
|
||||
data = str2ab(data + pkcs7_padding);
|
||||
|
||||
nulliv = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
|
||||
promise = mkey.then(function(mkey){
|
||||
return crypto.subtle.decrypt({
|
||||
name: "AES-CBC",
|
||||
iv: iv
|
||||
}, mkey, data)})
|
||||
.then(function(decrypted) {
|
||||
return ab2str(decrypted);
|
||||
})
|
||||
.catch(function(decryption) {
|
||||
console.log("Decryption rejected " + decryption);
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
async function encrypt_ecb(mkey, data)
|
||||
{
|
||||
var result = "";
|
||||
|
||||
debug("Encrypt 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)
|
||||
{
|
||||
res = await _encrypt(mkey, nulliv, data.slice(0, 16));
|
||||
// Remove PKCS7 padding
|
||||
result += res.slice(0, 16);
|
||||
data = data.slice(16);
|
||||
}
|
||||
res = await _encrypt(mkey, nulliv, data);
|
||||
result += res.slice(0, 16);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function decrypt_ecb(mkey, data)
|
||||
{
|
||||
var result = "";
|
||||
|
||||
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)
|
||||
{
|
||||
res = await _decrypt(mkey, nulliv, data.slice(0, 16));
|
||||
// Remove PKCS7 padding
|
||||
result += res.slice(0, 16);
|
||||
data = data.slice(16);
|
||||
}
|
||||
res = await _decrypt(mkey, nulliv, data);
|
||||
result += res.slice(0, 16);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function encrypt_cbc(mkey, iv, 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)
|
||||
{
|
||||
debug("Decrypt CBC " + data);
|
||||
|
||||
var result = await _decrypt(mkey, str2ab(iv), data);
|
||||
|
||||
// Remove PKCS7 padding
|
||||
return result.slice(0, result.length-16);
|
||||
}
|
||||
|
||||
async function digest(data)
|
||||
{
|
||||
return crypto.subtle.digest("SHA-256", str2ab(data)).then(function (hash) {
|
||||
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);
|
||||
}
|
||||
32
firefox_webextension/lib/parseuri.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// parseUri 1.2.2
|
||||
// (c) Steven Levithan <stevenlevithan.com>
|
||||
// MIT License
|
||||
|
||||
parseURI = {
|
||||
|
||||
parseUri : function (str) {
|
||||
var o = {
|
||||
strictMode: false,
|
||||
key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],
|
||||
q: {
|
||||
name: "queryKey",
|
||||
parser: /(?:^|&)([^&=]*)=?([^&]*)/g
|
||||
},
|
||||
parser: {
|
||||
strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
|
||||
loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
|
||||
}},
|
||||
m = o.parser[o.strictMode ? "strict" : "loose"].exec(str),
|
||||
uri = {},
|
||||
i = 14;
|
||||
|
||||
while (i--) uri[o.key[i]] = m[i] || "";
|
||||
|
||||
uri[o.q.name] = {};
|
||||
uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
|
||||
if ($1) uri[o.q.name][$1] = $2;
|
||||
});
|
||||
|
||||
return uri;
|
||||
}
|
||||
};
|
||||
49
firefox_webextension/manifest.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
|
||||
"name": "gPass",
|
||||
"short_name": "gPass",
|
||||
"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/misc.js", "lib/main.js"],
|
||||
"run_at" : "document_idle",
|
||||
"all_frames" : true
|
||||
}
|
||||
],
|
||||
|
||||
"background": {
|
||||
"persistent": true,
|
||||
"scripts": ["lib/parseuri.js", "lib/misc.js", "compat.js", "background.js"]
|
||||
},
|
||||
|
||||
"options_ui": {
|
||||
"page":"options.html",
|
||||
"browser_style": true
|
||||
},
|
||||
|
||||
"browser_action": {
|
||||
"default_icon": "icons/gpass_icon_32.png",
|
||||
"default_title": "Get your password",
|
||||
"default_popup": "popup/popup.html",
|
||||
"browser_style": true
|
||||
},
|
||||
|
||||
"permissions": [
|
||||
"<all_urls>",
|
||||
"activeTab",
|
||||
"notifications",
|
||||
"webRequest",
|
||||
"webRequestBlocking",
|
||||
"tabs",
|
||||
"storage",
|
||||
"clipboardWrite",
|
||||
"menus"
|
||||
]
|
||||
}
|
||||
21
firefox_webextension/options.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>gPass</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<b>Account URL</b> URL of your gPass account <input id="account_url" type="text"/><br />
|
||||
<b>WARNING</b> It should be a valid HTTPS URL because navigator doesn't like mixed content (HTTPS/HTTP). If not, requests will silentely failed. If you have an auto-signed certificate, add it to trusted ones.<br/>
|
||||
<br/>
|
||||
<b>PBKDF2 level</b> Number of iterations used to derivate master key <input id="pbkdf2" type="number"/><br />
|
||||
<br/>
|
||||
<br/>
|
||||
<b>Crypto v1 compatible </b> Compatible with old crypto schema (AES ECB). Use it for encrypted passwords with server <= 0.7 <input id="crypto_v1_compatible" type="checkbox"/><br />
|
||||
<br/>
|
||||
<input type="button" id="save" value="Save"/>
|
||||
|
||||
<script type="text/javascript" src="options.js">
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
42
firefox_webextension/options.js
Normal file
@@ -0,0 +1,42 @@
|
||||
var default_preferences = {"pbkdf2_level": 1000,
|
||||
"account_url": "https://gpass-demo.soutade.fr/demo",
|
||||
"crypto_v1_compatible": true};
|
||||
|
||||
function save() {
|
||||
var account_url = document.getElementById('account_url').value;
|
||||
var pbkdf2 = document.getElementById('pbkdf2').value;
|
||||
var crypto_v1_compatible = document.getElementById('crypto_v1_compatible').checked;
|
||||
|
||||
browser.storage.local.set({
|
||||
"account_url":account_url,
|
||||
"pbkdf2_level":pbkdf2,
|
||||
"crypto_v1_compatible": crypto_v1_compatible,
|
||||
})
|
||||
.then(function ok() { alert("Saved"); },
|
||||
function err() { alert("Cannot save your preferences");}
|
||||
);
|
||||
}
|
||||
|
||||
function restoreOptions()
|
||||
{
|
||||
document.getElementById('account_url').value = default_preferences['account_url'];
|
||||
document.getElementById('pbkdf2').value = default_preferences['pbkdf2_level'];
|
||||
document.getElementById('crypto_v1_compatible').checked = default_preferences["crypto_v1_compatible"];
|
||||
|
||||
browser.storage.local.get().then(
|
||||
function(prefs)
|
||||
{
|
||||
if (prefs.hasOwnProperty("account_url"))
|
||||
document.getElementById('account_url').value = prefs["account_url"];
|
||||
|
||||
if (prefs.hasOwnProperty("pbkdf2_level"))
|
||||
document.getElementById('pbkdf2').value = prefs["pbkdf2_level"];
|
||||
|
||||
if (prefs.hasOwnProperty("crypto_v1_compatible"))
|
||||
document.getElementById('crypto_v1_compatible').checked = prefs["crypto_v1_compatible"];
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
document.getElementById('save').addEventListener("click", save);
|
||||
document.addEventListener("DOMContentLoaded", restoreOptions);
|
||||
1
firefox_webextension/popup/compat.js
Symbolic link
@@ -0,0 +1 @@
|
||||
../compat.js
|
||||
1
firefox_webextension/popup/misc.js
Symbolic link
@@ -0,0 +1 @@
|
||||
../lib/misc.js
|
||||
15
firefox_webextension/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
firefox_webextension/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);
|
||||
@@ -8,10 +8,16 @@
|
||||
AuthType Basic
|
||||
AuthName "Private"
|
||||
AuthUserFile /private/_pwd/user
|
||||
Require valid-user
|
||||
Options +FollowSymlinks
|
||||
# Uncomment to remove password for internal network
|
||||
# <RequireAny>
|
||||
# Require ip 192.168.0.0/24
|
||||
Require valid-user
|
||||
# </RequireAny>
|
||||
</Location>
|
||||
|
||||
<Location "/users/">
|
||||
Options +FollowSymlinks
|
||||
Allow from all
|
||||
Satisfy Any
|
||||
</Location>
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
BIN
resources/gpass_activated_icon.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
resources/gpass_activated_icon.xcf
Normal file
BIN
resources/gpass_disabled_icon.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
resources/gpass_disabled_icon.xcf
Normal file
BIN
resources/gpass_icon.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
resources/gpass_icon.xcf
Normal file
BIN
resources/icons/gpass_activated_icon_128.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
resources/icons/gpass_activated_icon_16.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
resources/icons/gpass_activated_icon_32.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
resources/icons/gpass_activated_icon_64.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
resources/icons/gpass_disabled_icon_128.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
resources/icons/gpass_disabled_icon_16.png
Normal file
|
After Width: | Height: | Size: 785 B |
BIN
resources/icons/gpass_disabled_icon_32.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
resources/icons/gpass_disabled_icon_64.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
resources/icons/gpass_icon_128.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
resources/icons/gpass_icon_16.png
Normal file
|
After Width: | Height: | Size: 488 B |
BIN
resources/icons/gpass_icon_32.png
Normal file
|
After Width: | Height: | Size: 918 B |
BIN
resources/icons/gpass_icon_64.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
resources/usage.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
resources/usage.xcf
Normal file
|
Before Width: | Height: | Size: 38 KiB |
72
server/_user
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
/*
|
||||
Copyright (C) 2013-2014 Grégory Soutadé
|
||||
Copyright (C) 2013-2020 Grégory Soutadé
|
||||
|
||||
This file is part of gPass.
|
||||
|
||||
@@ -22,44 +22,72 @@ include("conf.php");
|
||||
|
||||
function load_database()
|
||||
{
|
||||
global $REQUESTS_MIN_DELAY;
|
||||
|
||||
try {
|
||||
$db = new SQLite3("./gpass.bdd", SQLITE3_OPEN_READONLY);
|
||||
$db = new SQLite3("./gpass.bdd", SQLITE3_OPEN_READWRITE);
|
||||
}
|
||||
catch(Exception $e)
|
||||
{
|
||||
die("<b>Unable to load database for user $user !</b><br/>");
|
||||
return null;
|
||||
}
|
||||
|
||||
list($usec, $sec) = explode(" ", microtime());
|
||||
$usec = $usec + $sec*1000;
|
||||
|
||||
try {
|
||||
$last_time = $db->querySingle("SELECT last_access_time FROM conf");
|
||||
if ($last_time <= $usec &&
|
||||
($usec - $last_time) < $REQUESTS_MIN_DELAY)
|
||||
{
|
||||
// Brute force ??
|
||||
$db->close();
|
||||
return null;
|
||||
}
|
||||
$db->query("UPDATE conf SET last_access_time=$usec");
|
||||
$db->close();
|
||||
$db = new SQLite3("./gpass.bdd", SQLITE3_OPEN_READONLY);
|
||||
}
|
||||
catch(Exception $e)
|
||||
{
|
||||
$db->close();
|
||||
die("<b>Unable to load database for user $user !</b><br/>");
|
||||
return null;
|
||||
}
|
||||
|
||||
return $db;
|
||||
}
|
||||
|
||||
$PROTOCOL_VERSION = 3;
|
||||
$PROTOCOL_VERSION = 4;
|
||||
|
||||
$db = load_database();
|
||||
|
||||
$res = "";
|
||||
|
||||
$statement = $db->prepare("SELECT password FROM gpass WHERE login=:login");
|
||||
|
||||
echo "protocol=gpass-$PROTOCOL_VERSION\n";
|
||||
if ($PKDBF2_LEVEL != 1000)
|
||||
echo "pkdbf2_level=$PKDBF2_LEVEL\n";
|
||||
if ($PBKDF2_LEVEL != 1000)
|
||||
echo "pbkdf2_level=$PBKDF2_LEVEL\n";
|
||||
|
||||
for ($i=0; isset($_POST["k$i"]); $i++)
|
||||
if ($db)
|
||||
{
|
||||
$statement->bindValue(":login", $_POST["k$i"]);
|
||||
$result = $statement->execute();
|
||||
$row = $result->fetchArray(SQLITE3_ASSOC);
|
||||
$result->finalize();
|
||||
if (isset($row["password"]))
|
||||
{
|
||||
echo "pass=" . $row["password"] . "\n";
|
||||
break;
|
||||
}
|
||||
}
|
||||
$statement = $db->prepare("SELECT password FROM gpass WHERE login=:login");
|
||||
|
||||
$statement->close();
|
||||
for ($i=0; $i<$MAX_PASSWORDS_PER_REQUEST && isset($_POST["k$i"]); $i++)
|
||||
{
|
||||
$statement->bindValue(":login", addslashes($_POST["k$i"]));
|
||||
$result = $statement->execute();
|
||||
$row = $result->fetchArray(SQLITE3_ASSOC);
|
||||
$result->finalize();
|
||||
if (isset($row["password"]))
|
||||
{
|
||||
echo "matched_key=" . $i . "\n";
|
||||
echo "pass=" . $row["password"] . "\n";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$statement->close();
|
||||
}
|
||||
|
||||
echo "<end>";
|
||||
|
||||
?>
|
||||
?>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
/*
|
||||
Copyright (C) 2013-2014 Grégory Soutadé
|
||||
Copyright (C) 2013-2017 Grégory Soutadé
|
||||
|
||||
This file is part of gPass.
|
||||
|
||||
@@ -29,7 +29,7 @@ $VIEW_CIPHERED_PASSWORDS=true;
|
||||
$ADMIN_MODE=true;
|
||||
|
||||
/*
|
||||
Number of iterations for PKDBF2 algorithm.
|
||||
Number of iterations for PBKDF2 algorithm.
|
||||
Minimum recommended level is 1000, but you can increase
|
||||
this value to have a better security (need more computation
|
||||
power).
|
||||
@@ -37,5 +37,52 @@ $ADMIN_MODE=true;
|
||||
!! Warning !! This impact master keys. So if you change
|
||||
this value with existings masterkeys, they will unusable !
|
||||
*/
|
||||
$PKDBF2_LEVEL=1000;
|
||||
$PBKDF2_LEVEL=1000;
|
||||
|
||||
/*
|
||||
This is a security feature : It protects from database dump
|
||||
and database purge without authentication.
|
||||
When get all entries, instead of returning logins/passwords,
|
||||
it returns "shadow logins". These are random values.
|
||||
Shadow logins must be encrypted using masterkey and salt
|
||||
(to generate a unique PBKDF2 derivation) that result in an access tokens.
|
||||
With this access token, user has the right to get
|
||||
encrypted login/password values and remove them.
|
||||
It's a kind of challenge but requires more cpu bandwidth
|
||||
(one derivation + two decryption for each password !).
|
||||
|
||||
This option is backward compatible with old version < 0.6
|
||||
*/
|
||||
$USE_SHADOW_LOGINS=1;
|
||||
|
||||
/*
|
||||
Protection against DDoS.
|
||||
Each request can contains multiple password combinations
|
||||
(to support wildcards for example) and multiple names.
|
||||
Currently only two passwords are sent from addon :
|
||||
www.example.com
|
||||
*.example.com
|
||||
But, on future we may also consider 'www.example.*', '*.example.*' and lower case username.
|
||||
For maximum security, you can set it to 2 or 4 if you want to be backward compatible
|
||||
with addons/extions <= 0.7.
|
||||
*/
|
||||
$MAX_PASSWORDS_PER_REQUEST=10;
|
||||
|
||||
/*
|
||||
Protection against brute force.
|
||||
Minimum delay (in milliseconds) between two requests.
|
||||
*/
|
||||
$REQUESTS_MIN_DELAY=1000;
|
||||
|
||||
/*
|
||||
Clear master keys and reset passwords after 15 minutes of inactivity
|
||||
*/
|
||||
$CLEAR_TIME=15*60*1000;
|
||||
|
||||
/*
|
||||
The first crypto schema use an AES-ECB process to encrypt logins.
|
||||
It's used until version 0.7.
|
||||
Since version 0.8, we use AES-CBC + SHA256.
|
||||
*/
|
||||
$CRYPTO_V1_COMPATIBLE=1;
|
||||
?>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
/*
|
||||
Copyright (C) 2013 Grégory Soutadé
|
||||
Copyright (C) 2013-2019 Grégory Soutadé
|
||||
|
||||
This file is part of gPass.
|
||||
|
||||
@@ -20,80 +20,21 @@
|
||||
|
||||
/*
|
||||
login is stored as :
|
||||
@@url;login
|
||||
url;login + 16 bytes padding * \0 + sha256(url;login + padding)[8:24]
|
||||
|
||||
Password is salted (3 random characters) and encrypted
|
||||
|
||||
All is encrypted with AES256 and key : PKDBF2(hmac_sha256, master key, url, 1000)
|
||||
All is encrypted with AES256-CBC and key PBKDF2(hmac_sha256, master key, server url, 1000)
|
||||
level is server configurable
|
||||
iv is PBKDF2(hmac_sha256, server url, master key, 1000)[0:16]
|
||||
*/
|
||||
$MAX_ENTRY_LEN = 512;
|
||||
$USERS_PATH = "./users/";
|
||||
$TARGET_DB_VERSION = 2;
|
||||
|
||||
function open_crypto($mkey)
|
||||
function sanitize($val)
|
||||
{
|
||||
if (!isset($_SESSION['td']))
|
||||
{
|
||||
$td = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_ECB, '');
|
||||
|
||||
if ($td == false)
|
||||
die("Unable to open mcrypt");
|
||||
|
||||
$ret = mcrypt_generic_init($td, hex2bin($mkey), '0000000000000000');
|
||||
|
||||
if ($ret < 0)
|
||||
{
|
||||
echo "<div class=\"error\">Unable to set key $ret</div>";
|
||||
return null;
|
||||
}
|
||||
|
||||
$_SESSION['td'] = $td;
|
||||
}
|
||||
else
|
||||
$td = $_SESSION['td'];
|
||||
|
||||
return $td;
|
||||
}
|
||||
|
||||
function decrypt($mkey, $val, $salted)
|
||||
{
|
||||
$td = open_crypto($mkey);
|
||||
|
||||
if ($td == null) return;
|
||||
|
||||
$val = mdecrypt_generic($td, hex2bin($val));
|
||||
|
||||
// Remove 0 added by encrypt
|
||||
$val = str_replace("\0", '', $val);
|
||||
|
||||
// Remove salt
|
||||
if ($salted)
|
||||
$val = substr($val, 0, strlen($val)-3);
|
||||
|
||||
return $val;
|
||||
}
|
||||
|
||||
function encrypt($mkey, $val, $salted)
|
||||
{
|
||||
global $MAX_ENTRY_LEN;
|
||||
|
||||
$td = open_crypto($mkey);
|
||||
|
||||
if ($td == null) return;
|
||||
|
||||
if ($salted)
|
||||
{
|
||||
$val .= dechex(rand(256,4095)); //between 0x100 and 0xfff
|
||||
}
|
||||
|
||||
$val = mcrypt_generic($td, $val);
|
||||
|
||||
if (strlen($val) > $MAX_ENTRY_LEN)
|
||||
{
|
||||
echo "<div class=\"error\">Value to encrypt is too long</div>";
|
||||
return null;
|
||||
}
|
||||
|
||||
return bin2hex($val);
|
||||
return (isset($_POST[$val])) ? addslashes($_POST[$val]) : "";
|
||||
}
|
||||
|
||||
// From http://php.net/manual/en/function.copy.php
|
||||
@@ -147,6 +88,65 @@ function create_user($user)
|
||||
return false;
|
||||
}
|
||||
|
||||
function _migrate_0($user, $db)
|
||||
{
|
||||
try {
|
||||
$db->query("ALTER TABLE gpass ADD access_token VARCHAR(32)");
|
||||
$db->query("ALTER TABLE gpass ADD shadow_login VARCHAR(32)");
|
||||
$db->query("ALTER TABLE gpass ADD salt VARCHAR(32)");
|
||||
|
||||
$db->query("CREATE TABLE db_version(version INTEGER)");
|
||||
$db->query("INSERT INTO db_version (version) VALUES (1)");
|
||||
}
|
||||
catch(Exception $e)
|
||||
{
|
||||
$db->close();
|
||||
echo "<div class=\"error\">Unable to load database for user $user ! : $e</div>";
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function _migrate_1($user, $db)
|
||||
{
|
||||
try {
|
||||
$db->query("CREATE TABLE conf(db_version INTEGER, last_access_time INTEGER)");
|
||||
$db->query("INSERT INTO conf VALUES(2, 0)");
|
||||
}
|
||||
catch(Exception $e)
|
||||
{
|
||||
$db->close();
|
||||
echo "<div class=\"error\">Unable to load database for user $user ! : $e</div>";
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function migrate_database($user, $db)
|
||||
{
|
||||
global $TARGET_DB_VERSION;
|
||||
|
||||
$migration_functions = ['_migrate_0', '_migrate_1'];
|
||||
|
||||
$version = $db->querySingle("SELECT db_version FROM conf");
|
||||
if ($version == NULL || $version == -1)
|
||||
{
|
||||
$version = $db->querySingle("SELECT version FROM db_version");
|
||||
if ($version == NULL || $version == -1)
|
||||
$version = 0;
|
||||
}
|
||||
|
||||
for($i=$version; $i<$TARGET_DB_VERSION; $i++)
|
||||
{
|
||||
if ($migration_functions[$i]($user, $db))
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function load_database($user)
|
||||
{
|
||||
global $USERS_PATH;
|
||||
@@ -160,14 +160,20 @@ function load_database($user)
|
||||
return null;
|
||||
}
|
||||
|
||||
if (migrate_database($user, $db))
|
||||
return null;
|
||||
|
||||
// New access need to reset crypto
|
||||
unset($_SESSION['td']);
|
||||
|
||||
return $db;
|
||||
}
|
||||
|
||||
function add_entry($user, $login, $password)
|
||||
function add_entry($user, $login, $password,
|
||||
$shadow_login, $salt, $access_token)
|
||||
{
|
||||
global $USE_SHADOW_LOGINS;
|
||||
|
||||
$db = load_database($user);
|
||||
|
||||
if ($db == null)
|
||||
@@ -176,25 +182,45 @@ function add_entry($user, $login, $password)
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($USE_SHADOW_LOGINS && (strlen($shadow_login) != 32 ||
|
||||
strlen($salt) != 32 || strlen($access_token) != 32))
|
||||
{
|
||||
$db->close();
|
||||
echo "Shadow login not configured";
|
||||
return false;
|
||||
}
|
||||
|
||||
$count = $db->querySingle("SELECT COUNT(*) FROM gpass WHERE login='" . $login . "'");
|
||||
|
||||
if ($count != 0)
|
||||
if ($count != NULL && $count != 0)
|
||||
{
|
||||
echo "Entry already exists";
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = $db->query("INSERT INTO gpass ('login', 'password') VALUES ('" . $login . "', '" . $password . "')");
|
||||
$result = $db->exec("INSERT INTO gpass ('login', 'password', 'shadow_login', 'salt', 'access_token') VALUES
|
||||
('" . $login . "', '" . $password . "', '" . $shadow_login . "', '" . $salt . "', '" . $access_token . "')");
|
||||
|
||||
/* error_log("INSERT INTO gpass ('login', 'password', 'shadow_login', 'salt', 'access_token') VALUES */
|
||||
/* ('" . $login . "', '" . $password . "', '" . $shadow_login . "', '" . $salt . "', '" . $access_token . "')"); */
|
||||
$db->close();
|
||||
|
||||
echo "OK";
|
||||
|
||||
return true;
|
||||
if (!$result)
|
||||
{
|
||||
echo "Error " . $db->lastErrorMsg();
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
echo "OK";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function delete_entry($user, $login)
|
||||
function delete_entry($user, $login, $access_token)
|
||||
{
|
||||
global $USE_SHADOW_LOGINS;
|
||||
|
||||
$db = load_database($user);
|
||||
|
||||
if ($db == null)
|
||||
@@ -203,37 +229,107 @@ function delete_entry($user, $login)
|
||||
return false;
|
||||
}
|
||||
|
||||
$db->query("DELETE FROM gpass WHERE login='" . $login . "'");
|
||||
if ($USE_SHADOW_LOGINS)
|
||||
{
|
||||
$db_ac = $db->querySingle("SELECT access_token FROM gpass WHERE login='" . $login . "'");
|
||||
if ($db_ac != NULL && strcmp($db_ac, $access_token))
|
||||
{
|
||||
$db->close();
|
||||
echo "Bad access token";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$result = $db->exec("DELETE FROM gpass WHERE login='" . $login . "'");
|
||||
|
||||
if (!$result)
|
||||
{
|
||||
echo "Error " . $db->lastErrorMsg();
|
||||
$ret = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
echo "OK";
|
||||
$ret = true;
|
||||
}
|
||||
|
||||
|
||||
$db->close();
|
||||
|
||||
echo "OK";
|
||||
|
||||
return true;
|
||||
return $ret;
|
||||
}
|
||||
|
||||
function update_entry($user, $mkey, $old_login, $url, $login, $password)
|
||||
function update_entry($user, $mkey, $old_login, $url, $login, $password, $shadow_login, $salt, $old_access_token, $new_access_token)
|
||||
{
|
||||
if (delete_entry($user, $old_login))
|
||||
return add_entry($user, $mkey, $url, $login, $password);
|
||||
if (delete_entry($user, $old_login, $old_access_token))
|
||||
return add_entry($user, $mkey, $url, $login, $password, $shadow_login, $salt, $new_access_token);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function list_entries($user)
|
||||
{
|
||||
global $USE_SHADOW_LOGINS;
|
||||
|
||||
$db = load_database($user);
|
||||
|
||||
if ($db == null) return;
|
||||
|
||||
$result = $db->query("SELECT * FROM gpass");
|
||||
|
||||
echo "entries\n";
|
||||
$first = false;
|
||||
header('Content-Type: application/json');
|
||||
echo "{ \"entries\" : [\n";
|
||||
|
||||
while (($row = $result->fetchArray()))
|
||||
{
|
||||
echo $row['login'] . ";" . $row['password'] . "\n";
|
||||
if ($first) echo ",";
|
||||
else $first = true;
|
||||
if (!strlen($row['shadow_login']) || !$USE_SHADOW_LOGINS)
|
||||
echo "{\"login\" : \"" . $row['login'] . "\", \"password\" : \"" . $row['password'] . "\" }\n";
|
||||
else
|
||||
echo "{\"shadow_login\" : \"" . $row['shadow_login'] . "\", \"salt\" : \"" . $row['salt'] . "\" }\n";
|
||||
}
|
||||
|
||||
echo "]}";
|
||||
|
||||
$db->close();
|
||||
}
|
||||
|
||||
?>
|
||||
function get_secure_entries($user, $access_tokens)
|
||||
{
|
||||
$db = load_database($user);
|
||||
|
||||
if ($db == null) return;
|
||||
|
||||
$query = "SELECT access_token, login, password FROM gpass WHERE access_token IN (";
|
||||
$first = false;
|
||||
|
||||
foreach (preg_split("/,/", $access_tokens) as $ac)
|
||||
{
|
||||
/* error_log($ac); */
|
||||
if ($first) $query .= ", ";
|
||||
else $first = true;
|
||||
$query .= "'$ac'";
|
||||
}
|
||||
$query .= ")";
|
||||
|
||||
//error_log($query);
|
||||
$result = $db->query($query);
|
||||
|
||||
header('Content-Type: application/json');
|
||||
$first = false;
|
||||
echo "{ \"entries\" : [\n";
|
||||
|
||||
while (($row = $result->fetchArray()))
|
||||
{
|
||||
if ($first) echo ",";
|
||||
else $first = true;
|
||||
echo "{\"access_token\" : \"" . $row['access_token'] . "\", \"login\" : \"" . $row['login'] . "\", \"password\" : \"" . $row['password'] . "\" }\n";
|
||||
}
|
||||
|
||||
echo "]}";
|
||||
|
||||
$db->close();
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
127
server/index.php
@@ -1,72 +1,111 @@
|
||||
<?php
|
||||
/*
|
||||
Copyright (C) 2013-2014 Grégory Soutadé
|
||||
|
||||
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/>.
|
||||
*/
|
||||
|
||||
include('functions.php');
|
||||
|
||||
include('conf.php');
|
||||
include('functions.php');
|
||||
|
||||
session_start();
|
||||
|
||||
$user = "";
|
||||
$user = '';
|
||||
|
||||
if ($ADMIN_MODE && isset($_POST['create_user']))
|
||||
{
|
||||
if (create_user($_POST['user']))
|
||||
$user = addslashes($_POST['user']);
|
||||
if (create_user($user))
|
||||
$user = $_POST['user'];
|
||||
else
|
||||
$user = '';
|
||||
}
|
||||
else
|
||||
{
|
||||
$user = sanitize('user');
|
||||
$login = sanitize('login');
|
||||
$shadow_login = sanitize('shadow_login');
|
||||
$password = sanitize('password');
|
||||
$access_token = sanitize('access_token');
|
||||
$access_tokens = sanitize('access_tokens');
|
||||
$salt = sanitize('salt');
|
||||
|
||||
if (isset($_POST['get_secure_passwords']) && isset($_POST['user']) &&
|
||||
isset($_POST['access_tokens']))
|
||||
return get_secure_entries($user, $access_tokens);
|
||||
|
||||
if (isset($_POST['get_passwords']) && isset($_POST['user']))
|
||||
return list_entries($_POST['user']);
|
||||
return list_entries($user);
|
||||
|
||||
if (isset($_POST['add_entry']) && isset($_POST['user']) &&
|
||||
isset($_POST['login']) && isset($_POST['password']))
|
||||
return add_entry($_POST['user'], $_POST['login'], $_POST['password']);
|
||||
if (isset($_POST['add_entry']) && isset($_POST['user']) &&
|
||||
isset($_POST['login']) && isset($_POST['password']) &&
|
||||
isset($_POST['shadow_login']) && isset($_POST['salt']) &&
|
||||
isset($_POST['access_token']) )
|
||||
return add_entry($user,
|
||||
$login,
|
||||
$password,
|
||||
$shadow_login,
|
||||
$salt,
|
||||
$access_token);
|
||||
|
||||
if (isset($_POST['delete_entry']) && isset($_POST['user']) &&
|
||||
isset($_POST['login']))
|
||||
return delete_entry($_POST['user'], $_POST['login']);
|
||||
if (isset($_POST['delete_entry']) && isset($_POST['user']) &&
|
||||
isset($_POST['login']) && isset($_POST['access_token']))
|
||||
return delete_entry($user,
|
||||
$login,
|
||||
$access_token);
|
||||
}
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" >
|
||||
<link rel="stylesheet" type="text/css" href="ressources/gpass.css" />
|
||||
<link rel="icon" type="image/png" href="resources/favicon.png" />
|
||||
<link rel="stylesheet" type="text/css" href="resources/gpass.css" />
|
||||
<script language="javascript">
|
||||
<?php
|
||||
echo "pkdbf2_level=$PKDBF2_LEVEL;\n";
|
||||
echo "pbkdf2_level=$PBKDF2_LEVEL; use_shadow_logins=$USE_SHADOW_LOGINS;\n";
|
||||
echo "CLEAR_TIME=$CLEAR_TIME; // Clear master key after 15 minutes\n";
|
||||
echo "CRYPTO_V1_COMPATIBLE=$CRYPTO_V1_COMPATIBLE;\n";
|
||||
?>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
window.onscroll = function(ev) {
|
||||
document.getElementById("buttonTop").className = (window.pageYOffset > 500) ? "cVisible" : "cInvisible";
|
||||
};
|
||||
});
|
||||
function scrollToTop()
|
||||
{
|
||||
if (window.pageYOffset == 0)
|
||||
return;
|
||||
target = (window.innerHeight) ? window.innerHeight/5 : 200;
|
||||
toScroll = (window.pageYOffset > target) ? target : window.pageYOffset;
|
||||
window.scrollBy(0, -toScroll);
|
||||
|
||||
setTimeout(scrollToTop, 24);
|
||||
}
|
||||
</script>
|
||||
<script src="ressources/jsaes.js"></script>
|
||||
<script src="ressources/jssha256.js"></script>
|
||||
<script src="ressources/hmac.js"></script>
|
||||
<script src="ressources/pkdbf2.js"></script>
|
||||
<script src="ressources/gpass.js"></script>
|
||||
<script src="ressources/pwdmeter.js"></script>
|
||||
<script src="resources/misc.js"></script>
|
||||
<script src="resources/gpass.js"></script>
|
||||
<script src="resources/pwdmeter.js"></script>
|
||||
<title>gPass : global Password</title>
|
||||
</head>
|
||||
<body onload="start();">
|
||||
<div><a id="buttonTop" class="cInvisible" onclick="scrollToTop();"></a></div>
|
||||
<div id="logo">
|
||||
<a href="http://indefero.soutade.fr/p/gpass"><img src="ressources/gpass.png" alt="logo"/></a>
|
||||
<a href="http://indefero.soutade.fr/p/gpass"><img src="resources/gpass.png" alt="logo"/></a>
|
||||
</div>
|
||||
|
||||
<div id="admin" <?php if (!$ADMIN_MODE) echo "style=\"display:none\"";?> >
|
||||
@@ -111,8 +150,6 @@ else
|
||||
echo "<div id=\"addon_address\">Current addon address is : https://" . $_SERVER['SERVER_NAME'] . "/" . $user . "</div>\n";
|
||||
}
|
||||
?>
|
||||
<div id="passwords">
|
||||
</div>
|
||||
<div id="add_new_password">
|
||||
<?php
|
||||
global $user;
|
||||
@@ -121,17 +158,47 @@ if ($user != "")
|
||||
{
|
||||
echo "<b>Add a new password</b><br/>\n";
|
||||
|
||||
echo 'URL <input type="text" name="url"/>';
|
||||
echo 'login <input type="text" name="login" />';
|
||||
echo 'URL <input type="text" name="url" value="' . (filter_input(INPUT_GET, "url", FILTER_SANITIZE_SPECIAL_CHARS) ?: "") . '"/>';
|
||||
echo 'login <input type="text" name="login" value="' . (filter_input(INPUT_GET, "user", FILTER_SANITIZE_SPECIAL_CHARS) ?: "") . '"/>';
|
||||
echo 'password <input id="new_password" type="text" name="password"/>';
|
||||
echo 'master key <input type="text" name="mkey" onkeypress="if (event.keyCode == 13) add_password();" onkeyup="chkPass(this.value);"/>';
|
||||
echo '<input type="button" value="Generate password" onClick="generate_password();"/>';
|
||||
echo '<input type="button" value="Generate simple password" onClick="generate_simple_password();"/>';
|
||||
echo "<input type=\"button\" name=\"add\" value=\"Add\" onclick=\"add_password();\"/>";
|
||||
echo "<br />";
|
||||
echo '<div><a href="http://en.wikipedia.org/wiki/Password_strength">Master key strength</a><div id="scorebarBorder"><div id="score">0%</div><div id="scorebar"> </div></div></div>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<div id="passwords">
|
||||
</div>
|
||||
<div id="update_masterkey">
|
||||
<?php
|
||||
global $user;
|
||||
|
||||
if ($user != "")
|
||||
{
|
||||
echo "<b>Update Masterkey</b><br/>\n";
|
||||
|
||||
echo 'Old master key <input type="text" id="oldmkey"/>';
|
||||
echo 'New master key <input type="text" id="newmkey" onkeyup="chkPass(this.value);"/>';
|
||||
echo '<input type="button" value="Update masterkey" onClick="update_masterkey();"/>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<div id="export_database">
|
||||
<?php
|
||||
global $user;
|
||||
|
||||
if ($user != "")
|
||||
{
|
||||
echo "<b>Export</b><br/>\n";
|
||||
|
||||
echo '<input type="button" value="Export" onclick="export_database();"/>';
|
||||
echo '<a id="export_link">Download</a>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
CREATE TABLE gpass(login VARCHAR(512) PRIMARY KEY, password VARCHAR(512));
|
||||
CREATE TABLE gpass(login VARCHAR(512) PRIMARY KEY, password VARCHAR(512), shadow_login VARCHAR(32), salt VARCHAR(32), access_token VARCHAR(32));
|
||||
CREATE TABLE conf(db_version INTEGER, last_access_time INTEGER);
|
||||
INSERT INTO conf VALUES (2, 0);
|
||||
|
||||
BIN
server/ref/gpass.bdd
Executable file → Normal file
|
Before Width: | Height: | Size: 676 B After Width: | Height: | Size: 676 B |
BIN
server/resources/favicon.png
Executable file
|
After Width: | Height: | Size: 1.1 KiB |
@@ -71,6 +71,27 @@ body {
|
||||
margin : 15px;
|
||||
}
|
||||
|
||||
#update_masterkey {
|
||||
border-style:solid;
|
||||
border-width:5px;
|
||||
border-color:yellow;
|
||||
padding : 15px;
|
||||
margin : 15px;
|
||||
}
|
||||
|
||||
#export_database {
|
||||
border-style:solid;
|
||||
border-width:5px;
|
||||
border-color:pink;
|
||||
padding : 15px;
|
||||
margin : 15px;
|
||||
}
|
||||
|
||||
#export_link {
|
||||
display:none;
|
||||
visibility:hidden;
|
||||
}
|
||||
|
||||
.error {
|
||||
text-align:center;
|
||||
color:red;
|
||||
@@ -99,10 +120,43 @@ body {
|
||||
}
|
||||
|
||||
#scorebar {
|
||||
background-image: url(/ressources/bg_strength_gradient.jpg);
|
||||
background-image: url(/resources/bg_strength_gradient.jpg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 0;
|
||||
position:absolute;
|
||||
width: 100px;
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* From http://www.trucsweb.com/tutoriels/javascript/retour-haut/ */
|
||||
a#buttonTop{
|
||||
border-radius:3px;
|
||||
padding:0 10px 10px 10px;
|
||||
font-size:3em;
|
||||
text-align:center;
|
||||
color:#fff;
|
||||
background:rgba(0, 0, 0, 0.25);
|
||||
position:fixed;
|
||||
right:3%;
|
||||
opacity:1;
|
||||
z-index:99999;
|
||||
transition:all ease-in 0.2s;
|
||||
backface-visibility: hidden;
|
||||
-webkit-backface-visibility: hidden;
|
||||
text-decoration: none;
|
||||
}
|
||||
a#buttonTop:before{ content: "\25b2"; }
|
||||
a#buttonTop:hover{
|
||||
background:rgba(0, 0, 0, 1);
|
||||
transition:all ease-in 0.2s;
|
||||
}
|
||||
a#buttonTop.cInvisible{
|
||||
bottom:-35px;
|
||||
opacity:0;
|
||||
transition:all ease-in 0.5s;
|
||||
}
|
||||
|
||||
a#buttonTop.cVisible{
|
||||
bottom:20px;
|
||||
opacity:1;
|
||||
}
|
||||
@@ -50,30 +50,63 @@ Element.prototype.removeAllChilds = function() {
|
||||
this.removeChild(this.childNodes[0]);
|
||||
};
|
||||
|
||||
function _generate_random(size, symbols)
|
||||
{
|
||||
forbidden = new Array('\\');
|
||||
|
||||
function generate_password()
|
||||
var res = "";
|
||||
while (res.length < size)
|
||||
{
|
||||
a = Math.floor(Math.random() * (symbols.length/2)) * 2;
|
||||
diff = symbols[a+1] - symbols[a];
|
||||
r = Math.floor(Math.random()*diff);
|
||||
if (isNaN(r+symbols[a]))
|
||||
continue;
|
||||
character = String.fromCharCode(r + symbols[a]);
|
||||
forbid = false;
|
||||
for (var j=0; j<forbidden.length; j++)
|
||||
{
|
||||
if (character == forbidden[j])
|
||||
{
|
||||
forbid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (forbid) continue;
|
||||
res += character;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
function generate_random(size, only_ascii)
|
||||
{
|
||||
// symbols 32 - 47 / 58 - 64 / 91 - 96 / 123 - 126
|
||||
// numbers 48 - 57
|
||||
// upper 65 - 90
|
||||
// lower 97 - 122
|
||||
// Give priority to letters (65 - 122 duplicated in front and end of array)
|
||||
var symbols = new Array(65, 90, 97, 122, 40, 47, 48, 57, 65, 90, 97, 122, 123, 126, 65, 90, 97, 122);
|
||||
// Give priority to letters (65 - 122 duplicated)
|
||||
var symbols;
|
||||
if (only_ascii)
|
||||
symbols = new Array(32, 47, 48, 57, 58, 64, 91, 96, 123, 126, 65, 90, 97, 122, 65, 90, 97, 122, 48, 57);
|
||||
else
|
||||
symbols = new Array(1, 255);
|
||||
|
||||
field = document.getElementById("new_password");
|
||||
return _generate_random(size, symbols);
|
||||
}
|
||||
|
||||
var res = "";
|
||||
while (res.length < 16)
|
||||
{
|
||||
a = Math.round(Math.random() * (symbols.length/2) * 2);
|
||||
diff = symbols[a+1] - symbols[a];
|
||||
r = Math.round(Math.random()*diff);
|
||||
if (isNaN(r+symbols[a]))
|
||||
continue;
|
||||
res += String.fromCharCode(r + symbols[a]);
|
||||
}
|
||||
function generate_password()
|
||||
{
|
||||
document.getElementById("new_password").value = generate_random(16, true);
|
||||
}
|
||||
|
||||
field.value = res;
|
||||
function generate_simple_password()
|
||||
{
|
||||
// ! ( ) * + - . _
|
||||
// numbers 48 - 57
|
||||
// upper 65 - 90
|
||||
// lower 97 - 122
|
||||
symbols = new Array(33, 33, 40, 43, 45, 46, 95, 95, 48, 57, 65, 90, 97, 122, 48, 57, 65, 90, 97, 122, 48, 57, 48, 57, 65, 90, 97, 122);
|
||||
document.getElementById("new_password").value = _generate_random(8, symbols);
|
||||
}
|
||||
|
||||
function url_domain(data) {
|
||||
@@ -100,89 +133,209 @@ function a2hex(str) {
|
||||
return hex;
|
||||
}
|
||||
|
||||
function derive_mkey(user, mkey)
|
||||
async function derive_mkey(user, mkey)
|
||||
{
|
||||
url = url_domain(document.URL) + "/" + user;
|
||||
mkey = a2hex(pkdbf2(mkey, url, pkdbf2_level, 256/8));
|
||||
return mkey;
|
||||
url = url_domain(server_url) + "/" + user;
|
||||
global_iv = simple_pbkdf2(url, mkey, pbkdf2_level);
|
||||
return crypto_pbkdf2(mkey, url, pbkdf2_level);
|
||||
}
|
||||
|
||||
var passwords;
|
||||
var passwords = null;
|
||||
var current_user = "";
|
||||
var current_mkey = "";
|
||||
var clearTimer = null;
|
||||
var global_iv = null;
|
||||
var server_url = window.location.href.split('?')[0];
|
||||
|
||||
function PasswordEntry (ciphered_login, ciphered_password) {
|
||||
function PasswordEntry (ciphered_login, ciphered_password, salt, shadow_login) {
|
||||
this.ciphered_login = ciphered_login;
|
||||
this.ciphered_password = ciphered_password;
|
||||
this.unciphered = false;
|
||||
this.clear_url = "";
|
||||
this.clear_login = "";
|
||||
this.clear_password = "";
|
||||
this.masterkey = "";
|
||||
this.masterkey = null;
|
||||
this.salt = salt;
|
||||
this.shadow_login = shadow_login;
|
||||
this.access_token = "";
|
||||
|
||||
this.decrypt = function(masterkey)
|
||||
this.reset = function()
|
||||
{
|
||||
if (masterkey == this.masterkey && this.unciphered == true)
|
||||
this.unciphered = false;
|
||||
this.clear_url = "";
|
||||
this.clear_login = "";
|
||||
this.clear_password = "";
|
||||
this.masterkey = null;
|
||||
this.salt = salt;
|
||||
}
|
||||
|
||||
this.reset_master_key = function()
|
||||
{
|
||||
this.masterkey = null;
|
||||
}
|
||||
|
||||
this.encrypt = async function(masterkey)
|
||||
{
|
||||
if (masterkey == this.masterkey)
|
||||
return true;
|
||||
|
||||
if (masterkey == "" || this.unciphered == true)
|
||||
if (masterkey == null || this.clear_url == "" || this.clear_login == "")
|
||||
return false;
|
||||
|
||||
aes = new AES();
|
||||
a_masterkey = aes.init(hex2a(masterkey));
|
||||
login = aes.decryptLongString(hex2a(this.ciphered_login), a_masterkey);
|
||||
login = login.replace(/\0*$/, "");
|
||||
if (login.indexOf("@@") != 0)
|
||||
{
|
||||
aes.finish();
|
||||
var ciphered_login = this.clear_url + ";" + this.clear_login;
|
||||
while ((ciphered_login.length % 16))
|
||||
ciphered_login += "\0";
|
||||
var computed_hash = await digest(ciphered_login);
|
||||
ciphered_login += computed_hash.slice(8, 24);
|
||||
var iv = await global_iv;
|
||||
iv = iv.slice(0, 16);
|
||||
|
||||
// Add salt
|
||||
var ciphered_password = generate_random(3, false) + this.clear_password ;
|
||||
|
||||
this.ciphered_login = a2hex(await encrypt_cbc(masterkey, iv, ciphered_login));
|
||||
this.ciphered_password = a2hex(await encrypt_cbc(masterkey, iv, ciphered_password));
|
||||
|
||||
this.unciphered = true;
|
||||
this.masterkey = masterkey;
|
||||
|
||||
if (use_shadow_logins)
|
||||
await this.generate_access_token(masterkey);
|
||||
}
|
||||
|
||||
this.decrypt = async function(masterkey)
|
||||
{
|
||||
if (masterkey == null)
|
||||
return false;
|
||||
|
||||
if (masterkey == this.masterkey)
|
||||
return (this.unciphered == true);
|
||||
|
||||
var old = false;
|
||||
var iv = await global_iv;
|
||||
iv = iv.slice(0, 16);
|
||||
var login = await decrypt_cbc(masterkey, iv, hex2a(this.ciphered_login));
|
||||
|
||||
var computed_digest = await digest(login.slice(0, login.length-16))
|
||||
computed_digest = computed_digest.slice(8, 24);
|
||||
|
||||
if (login.indexOf(computed_digest) == login.length-16)
|
||||
{
|
||||
login = login.slice(0, login.length-16).replace(/\0*$/, "");
|
||||
}
|
||||
// Remove @@
|
||||
login = login.substring(2);
|
||||
else if (CRYPTO_V1_COMPATIBLE)
|
||||
{
|
||||
login = await decrypt_ecb(masterkey, hex2a(this.ciphered_login));
|
||||
if (login.indexOf("@@") != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
login = login.replace(/\0*$/, "");
|
||||
// Remove @@
|
||||
login = login.substring(2);
|
||||
old = true;
|
||||
}
|
||||
else
|
||||
return false;
|
||||
|
||||
infos = login.split(";");
|
||||
this.clear_url = infos[0];
|
||||
this.clear_login = infos[1];
|
||||
this.clear_password = aes.decryptLongString(hex2a(this.ciphered_password), a_masterkey);
|
||||
if (old)
|
||||
{
|
||||
this.clear_password = await decrypt_ecb(masterkey, hex2a(this.ciphered_password));
|
||||
// Remove salt
|
||||
this.clear_password = this.clear_password.replace(/\0*$/, "");
|
||||
this.clear_password = this.clear_password.substr(0, this.clear_password.length-3);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.clear_password = await decrypt_cbc(masterkey, iv, hex2a(this.ciphered_password));
|
||||
// Remove salt
|
||||
this.clear_password = this.clear_password.replace(/\0*$/, "");
|
||||
this.clear_password = this.clear_password.substr(3, this.clear_password.length);
|
||||
}
|
||||
this.unciphered = true;
|
||||
this.masterkey = masterkey;
|
||||
aes.finish();
|
||||
|
||||
// Remove salt
|
||||
this.clear_password = this.clear_password.replace(/\0*$/, "");
|
||||
this.clear_password = this.clear_password.substr(0, this.clear_password.length-3);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
this.isUnciphered = function(masterkey)
|
||||
{
|
||||
return (this.unciphered == true && masterkey == this.masterkey && masterkey != "")
|
||||
return (this.unciphered == true && masterkey == this.masterkey && masterkey != null)
|
||||
}
|
||||
|
||||
this.isCiphered = function(masterkey)
|
||||
{
|
||||
return !(this.isUnciphered(masterkey));
|
||||
}
|
||||
|
||||
this.shadow_login_to_access_token = async function(masterkey)
|
||||
{
|
||||
this.access_token = await encrypt_ecb(masterkey, hex2a(this.shadow_login));
|
||||
this.access_token = a2hex(this.access_token);
|
||||
}
|
||||
|
||||
this.generate_access_token = async function(masterkey)
|
||||
{
|
||||
this.salt = a2hex(generate_random(16, false));
|
||||
this.shadow_login = a2hex(generate_random(16, false));
|
||||
|
||||
return await this.shadow_login_to_access_token(masterkey);
|
||||
}
|
||||
}
|
||||
|
||||
function clearMasterKey()
|
||||
{
|
||||
current_mkey = null;
|
||||
|
||||
for(i=0; i<passwords.length; i++)
|
||||
{
|
||||
passwords[i].reset();
|
||||
}
|
||||
}
|
||||
|
||||
function stopClearTimer()
|
||||
{
|
||||
if (clearTimer)
|
||||
{
|
||||
clearTimeout(clearTimer);
|
||||
clearTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startClearTimer()
|
||||
{
|
||||
stopClearTimer();
|
||||
clearTimer = setTimeout(
|
||||
function()
|
||||
{
|
||||
clearMasterKey();
|
||||
change_master_key(false);
|
||||
scrollToTop();
|
||||
}
|
||||
, CLEAR_TIME);
|
||||
}
|
||||
|
||||
function list_all_entries(user)
|
||||
{
|
||||
passwords = new Array();
|
||||
|
||||
|
||||
req = new XMLHttpRequest();
|
||||
req.addEventListener("load", function(evt) {
|
||||
entries = this.responseText.split("\n");
|
||||
if (entries[0] == "entries")
|
||||
j = JSON.parse(this.responseText);
|
||||
for(i=0; i<j.entries.length; i++)
|
||||
{
|
||||
for(i=1; i<entries.length; i++)
|
||||
{
|
||||
if (entries[i] == "") continue;
|
||||
entry = entries[i].split(";");
|
||||
passwords.push(new PasswordEntry(entry[0], entry[1]));
|
||||
}
|
||||
if (j.entries[i].hasOwnProperty('login'))
|
||||
p = new PasswordEntry(j.entries[i].login, j.entries[i].password, "", "");
|
||||
else
|
||||
p = new PasswordEntry("", "", j.entries[i].salt, j.entries[i].shadow_login);
|
||||
passwords.push(p);
|
||||
}
|
||||
}, false);
|
||||
req.open("POST", document.documentURI, false);
|
||||
}
|
||||
, false);
|
||||
req.open("POST", server_url, false);
|
||||
req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
|
||||
req.send("get_passwords=1&user=" + user);
|
||||
}
|
||||
@@ -219,15 +372,80 @@ function update_stats()
|
||||
div.appendChild(document.createElement("br"));
|
||||
}
|
||||
|
||||
function change_master_key(warning_unciphered)
|
||||
// Remove all password without credentials
|
||||
async function put_ciphered_credentials(passwords, masterkey)
|
||||
{
|
||||
var nb_unciphered = 0;
|
||||
for(i=0; i<passwords.length; i++)
|
||||
for(var i=0; i<passwords.length; i++)
|
||||
{
|
||||
if (passwords[i].decrypt(current_mkey))
|
||||
nb_unciphered++;
|
||||
await passwords[i].generate_access_token(masterkey);
|
||||
remove_password_server(current_user, passwords[i].ciphered_login, '');
|
||||
add_password_server(current_user, passwords[i]);
|
||||
}
|
||||
}
|
||||
|
||||
async function get_ciphered_credentials(masterkey)
|
||||
{
|
||||
access_tokens = '';
|
||||
old_passwords = new Array();
|
||||
|
||||
for(var i=0; i<passwords.length; i++)
|
||||
{
|
||||
// Already got
|
||||
if (passwords[i].ciphered_login.length)
|
||||
{
|
||||
if (!passwords[i].access_token.length)
|
||||
{
|
||||
res = await passwords[i].decrypt(masterkey);
|
||||
if(res)
|
||||
old_passwords.push(passwords[i]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
await passwords[i].shadow_login_to_access_token(masterkey);
|
||||
if (access_tokens.length) access_tokens += ",";
|
||||
access_tokens += passwords[i].access_token;
|
||||
}
|
||||
|
||||
if (old_passwords.length)
|
||||
await put_ciphered_credentials(old_passwords, masterkey);
|
||||
|
||||
if (!access_tokens.length)
|
||||
return;
|
||||
|
||||
req = new XMLHttpRequest();
|
||||
req.addEventListener("load", function(evt) {
|
||||
j = JSON.parse(this.responseText);
|
||||
for(i=0; i<j.entries.length; i++)
|
||||
{
|
||||
for (k=0; k<passwords.length; k++)
|
||||
{
|
||||
if (passwords[k].access_token == j.entries[i].access_token)
|
||||
{
|
||||
passwords[k].ciphered_login = j.entries[i].login;
|
||||
passwords[k].ciphered_password = j.entries[i].password;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, false);
|
||||
req.open("POST", server_url, false);
|
||||
req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
|
||||
req.send("get_secure_passwords=1&user=" + current_user + "&access_tokens=" + access_tokens);
|
||||
}
|
||||
|
||||
async function change_master_key(warning_unciphered)
|
||||
{
|
||||
var nb_unciphered = 0;
|
||||
|
||||
if (current_mkey && use_shadow_logins)
|
||||
await get_ciphered_credentials(current_mkey);
|
||||
|
||||
for(i=0; i<passwords.length; i++)
|
||||
{
|
||||
if (await passwords[i].decrypt(current_mkey))
|
||||
nb_unciphered++;
|
||||
}
|
||||
|
||||
if (!nb_unciphered && warning_unciphered)
|
||||
alert("No password unciphered with this master key !");
|
||||
@@ -305,7 +523,6 @@ function change_master_key(warning_unciphered)
|
||||
ciph_login = document.createElement("input");
|
||||
ciph_login.setAttribute("name", "ciphered_login");
|
||||
ciph_login.setAttribute("type", "hidden");
|
||||
ciph_login.setAttribute("login", passwords[i].ciphered_login);
|
||||
div.appendChild(ciph_login);
|
||||
|
||||
div.appendChild(document.createTextNode("URL"));
|
||||
@@ -313,7 +530,6 @@ function change_master_key(warning_unciphered)
|
||||
url.setAttribute("class", "hash");
|
||||
url.setAttribute("type", "text");
|
||||
url.setAttribute("name", "URL");
|
||||
url.setAttribute("value", passwords[i].ciphered_login);
|
||||
div.appendChild(url);
|
||||
|
||||
div.appendChild(document.createTextNode("password"));
|
||||
@@ -321,7 +537,6 @@ function change_master_key(warning_unciphered)
|
||||
password.setAttribute("class", "hash");
|
||||
password.setAttribute("type", "text");
|
||||
password.setAttribute("name", "password");
|
||||
password.setAttribute("value", passwords[i].ciphered_password);
|
||||
div.appendChild(password);
|
||||
|
||||
delete_button = document.createElement("input");
|
||||
@@ -331,6 +546,19 @@ function change_master_key(warning_unciphered)
|
||||
div.appendChild(delete_button);
|
||||
|
||||
password_div.appendChild(div);
|
||||
|
||||
if (passwords[i].ciphered_login.length)
|
||||
{
|
||||
ciph_login.setAttribute("login", passwords[i].ciphered_login);
|
||||
url.setAttribute("value", passwords[i].ciphered_login);
|
||||
password.setAttribute("value", passwords[i].ciphered_password);
|
||||
}
|
||||
else
|
||||
{
|
||||
ciph_login.setAttribute("login", passwords[i].shadow_login);
|
||||
url.setAttribute("value", passwords[i].shadow_login);
|
||||
// password empty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,14 +576,14 @@ function update_master_key(warning_unciphered)
|
||||
{
|
||||
current_user = user;
|
||||
|
||||
document.title = "gPass : global Password - " + current_user;
|
||||
document.title = "gPass - " + current_user;
|
||||
|
||||
list_all_entries(current_user);
|
||||
|
||||
addon_address = document.getElementById("addon_address");
|
||||
addon_address.removeAllChilds();
|
||||
|
||||
addon_address.appendChild(document.createTextNode("Current addon address is : " + document.documentURI + current_user));
|
||||
addon_address.appendChild(document.createTextNode("Current addon address is : " + server_url + current_user));
|
||||
|
||||
warning_unciphered = false;
|
||||
}
|
||||
@@ -363,10 +591,22 @@ function update_master_key(warning_unciphered)
|
||||
current_mkey = document.getElementById("master_key").value;
|
||||
|
||||
if (current_mkey != "")
|
||||
{
|
||||
for(i=0; i<passwords.length; i++)
|
||||
{
|
||||
passwords[i].reset_master_key();
|
||||
}
|
||||
current_mkey = derive_mkey(current_user, current_mkey);
|
||||
startClearTimer();
|
||||
}
|
||||
else
|
||||
{
|
||||
current_mkey = null;
|
||||
// Disable warning on empty master key (clear passwords from others)
|
||||
warning_unciphered = false;
|
||||
stopClearTimer();
|
||||
clearMasterKey();
|
||||
}
|
||||
|
||||
change_master_key(warning_unciphered);
|
||||
}
|
||||
@@ -391,14 +631,14 @@ function add_password_server(user, pentry)
|
||||
else
|
||||
alert(resp);
|
||||
}, false);
|
||||
req.open("POST", document.documentURI, false);
|
||||
req.open("POST", server_url, false);
|
||||
req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
|
||||
req.send("add_entry=1&user=" + user + "&login=" + pentry.ciphered_login + "&password=" + pentry.ciphered_password);
|
||||
req.send("add_entry=1&user=" + user + "&login=" + pentry.ciphered_login + "&password=" + pentry.ciphered_password + "&shadow_login=" + pentry.shadow_login + "&salt=" + pentry.salt + "&access_token=" + pentry.access_token);
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
function construct_pentry(user, url, password, login, mkey, derive_masterkey)
|
||||
async function construct_pentry(user, url, password, login, mkey, derive_masterkey)
|
||||
{
|
||||
var ret = null;
|
||||
|
||||
@@ -427,7 +667,7 @@ function construct_pentry(user, url, password, login, mkey, derive_masterkey)
|
||||
}
|
||||
|
||||
if (derive_masterkey)
|
||||
mkey = derive_mkey(current_user, mkey);
|
||||
mkey = derive_mkey(user, mkey);
|
||||
|
||||
for(i=0; i<passwords.length; i++)
|
||||
{
|
||||
@@ -442,32 +682,16 @@ function construct_pentry(user, url, password, login, mkey, derive_masterkey)
|
||||
}
|
||||
}
|
||||
|
||||
ciphered_login = "@@" + url + ";" + login;
|
||||
|
||||
// Add salt
|
||||
for(i=0; i<3; i++)
|
||||
{
|
||||
password += String.fromCharCode((Math.random() * 128)+1);
|
||||
}
|
||||
|
||||
ciphered_password = password;
|
||||
|
||||
aes = new AES();
|
||||
a_masterkey = aes.init(hex2a(mkey));
|
||||
ciphered_login = a2hex(aes.encryptLongString(ciphered_login, a_masterkey));
|
||||
ciphered_password = a2hex(aes.encryptLongString(ciphered_password, a_masterkey));
|
||||
|
||||
pentry = new PasswordEntry(ciphered_login, ciphered_password);
|
||||
pentry.unciphered = true;
|
||||
pentry = new PasswordEntry("", "", "", "");
|
||||
pentry.clear_url = url;
|
||||
pentry.clear_login = login;
|
||||
pentry.clear_password = password.substr(0, password.length-3);
|
||||
pentry.masterkey = mkey;
|
||||
pentry.clear_password = password;
|
||||
await pentry.encrypt(mkey);
|
||||
|
||||
return pentry;
|
||||
}
|
||||
|
||||
function remove_password_server(user, login)
|
||||
function remove_password_server(user, login, access_token)
|
||||
{
|
||||
var ok = false;
|
||||
|
||||
@@ -479,9 +703,9 @@ function remove_password_server(user, login)
|
||||
else
|
||||
alert(resp);
|
||||
}, false);
|
||||
req.open("POST", document.documentURI, false);
|
||||
req.open("POST", server_url, false);
|
||||
req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
|
||||
req.send("delete_entry=1&user=" + user + "&login=" + login);
|
||||
req.send("delete_entry=1&user=" + user + "&login=" + login + "&access_token=" + access_token);
|
||||
|
||||
return ok;
|
||||
}
|
||||
@@ -509,32 +733,43 @@ function add_password()
|
||||
mkey = inputs[i].value;
|
||||
}
|
||||
|
||||
pentry = construct_pentry(current_user, url, password, login, mkey, true)
|
||||
construct_pentry(current_user, url, password, login, mkey, true).then(
|
||||
function (pentry) {
|
||||
if (pentry == null) return false;
|
||||
|
||||
if (pentry == null) return;
|
||||
res = add_password_server(current_user, pentry);
|
||||
|
||||
res = add_password_server(current_user, pentry);
|
||||
if (!res) return false;
|
||||
|
||||
if (!res) return false;
|
||||
for(i=0; i<passwords.length; i++)
|
||||
{
|
||||
passwords[i].reset_master_key();
|
||||
}
|
||||
|
||||
current_mkey = pentry.masterkey;
|
||||
|
||||
passwords.push(pentry);
|
||||
passwords.push(pentry);
|
||||
|
||||
change_master_key(false);
|
||||
current_mkey = pentry.masterkey;
|
||||
change_master_key(false);
|
||||
|
||||
for(i=0; i<inputs.length; i++)
|
||||
{
|
||||
if (inputs[i].getAttribute("type") == "text" ||
|
||||
inputs[i].getAttribute("type") == "password")
|
||||
inputs[i].value = "";
|
||||
}
|
||||
for(i=0; i<inputs.length; i++)
|
||||
{
|
||||
if (inputs[i].getAttribute("type") == "text" ||
|
||||
inputs[i].getAttribute("type") == "password")
|
||||
inputs[i].value = "";
|
||||
}
|
||||
|
||||
startClearTimer();
|
||||
});
|
||||
|
||||
window.scrollTo(0,document.body.scrollHeight);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function delete_entry(entry_number)
|
||||
{
|
||||
startClearTimer();
|
||||
|
||||
entry = document.getElementById(entry_number);
|
||||
|
||||
if (entry == null) {
|
||||
@@ -563,7 +798,8 @@ function delete_entry(entry_number)
|
||||
var found = -1;
|
||||
for(i=0; i<passwords.length; i++)
|
||||
{
|
||||
if (passwords[i].ciphered_login == ciphered_login.getAttribute("login"))
|
||||
if (passwords[i].ciphered_login == ciphered_login.getAttribute("login") ||
|
||||
passwords[i].shadow_login == ciphered_login.getAttribute("login"))
|
||||
{
|
||||
found = i;
|
||||
break;
|
||||
@@ -579,7 +815,7 @@ function delete_entry(entry_number)
|
||||
if(!confirm("Are you sure want to delete this entry ?"))
|
||||
return;
|
||||
|
||||
ok = remove_password_server(current_user, ciphered_login.getAttribute("login"));
|
||||
ok = remove_password_server(current_user, ciphered_login.getAttribute("login"), passwords[i].access_token);
|
||||
|
||||
if (!ok) return;
|
||||
|
||||
@@ -599,6 +835,8 @@ function update_entry(entry_number)
|
||||
var mkey = "";
|
||||
var ciphered_login;
|
||||
|
||||
startClearTimer();
|
||||
|
||||
entry = document.getElementById(entry_number);
|
||||
|
||||
if (entry == null) {
|
||||
@@ -640,18 +878,123 @@ function update_entry(entry_number)
|
||||
if(!confirm("Are you sure want to update this entry ?"))
|
||||
return;
|
||||
|
||||
pentry = construct_pentry(current_user, url, password, login, current_mkey, false);
|
||||
construct_pentry(current_user, url, password, login, current_mkey, false).then(
|
||||
function (pentry) {
|
||||
if (pentry == null) return;
|
||||
|
||||
if (pentry == null) return;
|
||||
ok = remove_password_server(current_user, passwords[found].ciphered_login, passwords[found].access_token);
|
||||
if (!ok) return;
|
||||
|
||||
ok = remove_password_server(current_user, passwords[found].ciphered_login);
|
||||
if (!ok) return;
|
||||
ok = add_password_server(current_user, pentry);
|
||||
if (!ok) return;
|
||||
|
||||
ok = add_password_server(current_user, pentry);
|
||||
if (!ok) return;
|
||||
passwords[found] = pentry;
|
||||
ciphered_login.setAttribute("login", pentry.ciphered_login);
|
||||
|
||||
passwords[found] = pentry;
|
||||
ciphered_login.setAttribute("login", pentry.ciphered_login);
|
||||
|
||||
alert("Entry updated");
|
||||
alert("Entry updated");
|
||||
});
|
||||
}
|
||||
|
||||
async function update_masterkey()
|
||||
{
|
||||
var url = "";
|
||||
var login = "";
|
||||
var password = "";
|
||||
var mkey = "";
|
||||
var ciphered_login;
|
||||
|
||||
oldmkey = document.getElementById("oldmkey").value;
|
||||
newmkey = document.getElementById("newmkey").value;
|
||||
|
||||
if (newmkey == "" || oldmkey == "")
|
||||
{
|
||||
alert("Cannot set an empty masterkey");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!confirm("Are you sure want to update the masterkey ?"))
|
||||
return;
|
||||
|
||||
oldmkey = derive_mkey(current_user, oldmkey);
|
||||
old_global_iv = global_iv;
|
||||
current_mkey = derive_mkey(current_user, newmkey);
|
||||
new_global_iv = global_iv;
|
||||
|
||||
var found = 0;
|
||||
for(i=0; i<passwords.length; i++)
|
||||
{
|
||||
global_iv = old_global_iv;
|
||||
if (await passwords[i].decrypt(oldmkey))
|
||||
{
|
||||
ok = remove_password_server(current_user, passwords[i].ciphered_login, passwords[i].access_token);
|
||||
if (!ok)
|
||||
{
|
||||
alert("Error updating password");
|
||||
break;
|
||||
}
|
||||
|
||||
if (use_shadow_logins)
|
||||
await passwords[i].generate_access_token(current_mkey);
|
||||
|
||||
global_iv = new_global_iv;
|
||||
await passwords[i].encrypt(current_mkey);
|
||||
ok = add_password_server(current_user, passwords[i]);
|
||||
|
||||
if (!ok)
|
||||
{
|
||||
alert("Error updating password");
|
||||
break;
|
||||
}
|
||||
found++;
|
||||
}
|
||||
}
|
||||
|
||||
if (found == 0)
|
||||
alert("No password found with this masterkey");
|
||||
else
|
||||
{
|
||||
alert(found + " passwords updated");
|
||||
change_master_key(false);
|
||||
}
|
||||
}
|
||||
|
||||
function makeText(text) {
|
||||
var data = new Blob([text], {type: 'application/xml'});
|
||||
|
||||
textFile = window.URL.createObjectURL(data);
|
||||
|
||||
// returns a URL you can use as a href
|
||||
return textFile;
|
||||
};
|
||||
|
||||
var text_link = null;
|
||||
function export_database()
|
||||
{
|
||||
startClearTimer();
|
||||
|
||||
link = document.getElementById("export_link");
|
||||
|
||||
if (text_link != null) window.URL.revokeObjectURL(text_link);
|
||||
|
||||
text = "<passwords user=\"" + current_user + "\" addon_address=\"" + server_url + current_user + "\">\n";
|
||||
for(i=0; i<passwords.length; i++)
|
||||
{
|
||||
if (!passwords[i].unciphered) continue;
|
||||
text += "\t<password_entry>\n"
|
||||
text += "\t\t<url value=\"" + passwords[i].clear_url + "\"/>\n";
|
||||
text += "\t\t<login value=\"" + passwords[i].clear_login + "\"/>\n";
|
||||
text += "\t\t<password><![CDATA[" + passwords[i].clear_password.replace("]]>", "]]\\>", "g") + "]]></password>\n";
|
||||
text += "\t</password_entry>\n"
|
||||
}
|
||||
text += "</passwords>\n";
|
||||
|
||||
text_link = makeText(text);
|
||||
link.href = text_link;
|
||||
|
||||
link.style.display = "inline";
|
||||
link.style.visibility = "visible";
|
||||
|
||||
alert_msg = "Click on download link to get all current unciphered passwords\n\n";
|
||||
alert_msg += "\"]]>\" sequence has been replaced by \"]]\\>\"";
|
||||
alert(alert_msg);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
220
server/resources/misc.js
Normal file
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
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 default_preferences = {"pbkdf2_level": 1000,
|
||||
"account_url": "https://gpass-demo.soutade.fr/demo"};
|
||||
|
||||
var browser = browser;
|
||||
if (typeof chrome !== 'undefined')
|
||||
browser = chrome;
|
||||
var crypto = crypto || window.crypto;
|
||||
|
||||
// https://stackoverflow.com/questions/6965107/converting-between-strings-and-arraybuffers
|
||||
function ab2str(buf) {
|
||||
return String.fromCharCode.apply(null, new Uint8Array(buf));
|
||||
}
|
||||
|
||||
// https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
|
||||
function str2ab2(str) {
|
||||
var chars = []
|
||||
for (var i=0, strLen=str.length; i < strLen; i++) {
|
||||
chars.push(str.charCodeAt(i));
|
||||
}
|
||||
return new Uint8Array(chars);
|
||||
}
|
||||
|
||||
function str2ab(str) {
|
||||
var buf = new ArrayBuffer(str.length); // 2 bytes for each char
|
||||
// var buf = new ArrayBuffer(str.length*2); // 2 bytes for each char
|
||||
var bufView = new Uint8Array(buf);
|
||||
for (var i=0, strLen=str.length; i < strLen; i++) {
|
||||
bufView[i] = str.charCodeAt(i);
|
||||
}
|
||||
return bufView;
|
||||
}
|
||||
|
||||
function crypto_pbkdf2(mkey, salt, level)
|
||||
{
|
||||
AESCBC = {
|
||||
name: "AES-CBC",
|
||||
length: 256,
|
||||
}
|
||||
|
||||
var key = str2ab(mkey);
|
||||
return crypto.subtle.importKey("raw", key, {name: "PBKDF2"}, false, ["deriveBits", "deriveKey"])
|
||||
.then(function(key){
|
||||
//sha-256
|
||||
return crypto.subtle.deriveKey({
|
||||
name: "PBKDF2",
|
||||
salt: str2ab(salt),
|
||||
iterations: level,
|
||||
hash: "SHA-256",
|
||||
}, key, AESCBC, false, ["encrypt", "decrypt", "unwrapKey", "wrapKey"])
|
||||
.then(function(key) {
|
||||
return key;
|
||||
})
|
||||
.catch(function(err){
|
||||
console.log("Error derive key " + err);
|
||||
});
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.log("Error import key" + err);
|
||||
});
|
||||
}
|
||||
|
||||
function simple_pbkdf2(mkey, salt, level)
|
||||
{
|
||||
AESCBC = {
|
||||
name: "AES-CBC",
|
||||
length: 256,
|
||||
}
|
||||
|
||||
var key = str2ab(mkey);
|
||||
return crypto.subtle.importKey("raw", key, {name: "PBKDF2"}, false, ["deriveBits", "deriveKey"])
|
||||
.then(function(key){
|
||||
//sha-256
|
||||
return crypto.subtle.deriveKey({
|
||||
name: "PBKDF2",
|
||||
salt: str2ab(salt),
|
||||
iterations: level,
|
||||
hash: "SHA-256",
|
||||
}, key, AESCBC, true, ["unwrapKey", "wrapKey"])
|
||||
.then(function(key) {
|
||||
return crypto.subtle.exportKey("raw", key)
|
||||