From 898e0b9e42645366d15e3de5392c8cb83b392237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Soutad=C3=A9?= Date: Sat, 18 Jan 2020 15:20:13 +0100 Subject: [PATCH] Big update: * Move all core functions from mains.js to background.js * Use message interface for IPCs between main.js, background.js and popup.js * Add popup interface : * safest method to compute masterkey * Direct access to our own gPass server with auto URL and username fill * Add some specific menus : * Access to gPass settings * Allow to disable extension * Update gPass icon when a password field has focus and gPass is ready to work --- README.md | 20 +- chrome_addon/background.js | 581 +++++++++++++++++++++++-- chrome_addon/compat.js | 37 +- chrome_addon/lib/main.js | 592 +++++++++++--------------- chrome_addon/lib/misc.js | 87 +++- chrome_addon/manifest.json | 19 +- chrome_addon/popup/compat.js | 1 + chrome_addon/popup/misc.js | 1 + chrome_addon/popup/popup.html | 15 + chrome_addon/popup/popup.js | 144 +++++++ firefox_webextension/background.js | 591 +++++++++++++++++++++++-- firefox_webextension/compat.js | 37 +- firefox_webextension/lib | 1 - firefox_webextension/lib/main.js | 381 +++++++++++++++++ firefox_webextension/lib/misc.js | 295 +++++++++++++ firefox_webextension/lib/parseuri.js | 32 ++ firefox_webextension/manifest.json | 24 +- firefox_webextension/options.html | 22 +- firefox_webextension/popup/compat.js | 1 + firefox_webextension/popup/misc.js | 1 + firefox_webextension/popup/popup.html | 15 + firefox_webextension/popup/popup.js | 144 +++++++ 22 files changed, 2558 insertions(+), 483 deletions(-) create mode 120000 chrome_addon/popup/compat.js create mode 120000 chrome_addon/popup/misc.js create mode 100644 chrome_addon/popup/popup.html create mode 100644 chrome_addon/popup/popup.js delete mode 120000 firefox_webextension/lib create mode 100644 firefox_webextension/lib/main.js create mode 100644 firefox_webextension/lib/misc.js create mode 100644 firefox_webextension/lib/parseuri.js mode change 120000 => 100644 firefox_webextension/options.html create mode 120000 firefox_webextension/popup/compat.js create mode 120000 firefox_webextension/popup/misc.js create mode 100644 firefox_webextension/popup/popup.html create mode 100644 firefox_webextension/popup/popup.js diff --git a/README.md b/README.md index a0d5a49..34609fe 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Everyday we have a lot of passwords to manage corresponding to a lot of accounts The best way to avoid these errors is to have a unique strong password for each account. gPass helps to reach this goal : you keep a subset of passwords (called masterkey) and for each login/password tuple you chose, gPass returns the real password by querying a password server. -To have a high level of security, all information is stored encrypted (server side). Nothing is stored on client. The decryption is done on the fly when it's needed and only with user input. So, a hacker can get your password database, it will not be able to see any information (except if it brute force your masterkey) ! So it's important to choose to strong masterkey ! +To have a high level of security, all information is stored encrypted (server side). Nothing is stored on client. The decryption is done on the fly when it's needed and only with user input. So, a hacker can get your password database, it will not be able to see any information (except if it brute force or leak your masterkey) ! So it's important to choose to strong masterkey ! This addon is like [last pass](https://lastpass.com/) one, but I wanted it to be open source and self hostable (be careful on server down !). Moreover, with gPass, you can have multiple master keys ! @@ -19,17 +19,20 @@ This addon is like [last pass](https://lastpass.com/) one, but I wanted it to be Usage ----- -The first thing to do is to populate your database (from your/a password server) with website/login/password/master key values. You can use "*" character to access to all sub domains of a specific website. If you want to make strong password, there is a password generator. After that, configure your addon in "tools -> addons -> gPass -> preferences" in Firefox or "addons -> gPass -> options" in Chrome to point to your password server (+ username). Be careful, login and password are case sensitive. +The first thing to do is to populate your database (from your/a password server) with website/login/password/master key values. You can use "*" character to access to all sub domains of a specific website. If you want to make strong password, there is a password generator. After that, configure your addon in "tools -> addons -> gPass -> preferences" in Firefox or "addons -> gPass -> options" in Chrome to point to your password server (+ username). For firefox users, don't forget to enable addon within private mode. Be careful, login and password are case sensitive ! When you're in a login form and you want to use gPass, type your login (case sensitive !) and fill "@@masterkey" in password field. Then submit and password will automatically be replaced by the one in the database (after addon decrypt it). **You can also type "@_masterkey" to only replace your password without submitting and manually submit. This allows to support more websites.** +Another option is to enter your credentials in the new popup menu. If found, password will be stored in your clipboard. + + Technical details ----------------- The two columns in database are "login" and "password". -login is compounded by "domain;login" is salted and encrypted with AES 256-CBC +login is compounded by "domain;login", salted and encrypted with AES 256-CBC The key that encrypt these fields is PBKDF2 (hmac-sha256, masterkey, password_server_url, 1000, 256), IV is PBKDF2 (hmac-sha256, password_server_url, masterkey, 1000, 256) @@ -41,11 +44,11 @@ Server side is written in PHP (with SQLite3 for database component). Server ------ -To host a password server, you need a webserver. Just copy server files in a directory read/write for web server user (www-data). A sample apache2 configuration file is available in resources. Since v0.8 and the use of Crypto API, it's manadatory to have an HTTPS access to the server. Without that, the decryption will fails. +To host a password server, you need a webserver. Just copy server files in a directory read/write for web server user (www-data). A sample apache2 configuration file is available in resources. Since v0.8 and the use of Crypto API, **it's manadatory to have an HTTPS access to the server**. Without that, the decryption will fails. Configuration parameters are in conf.php -A demonstration server is available [here](https://gpass-demo.soutade.fr). It may not works with HTTPS content because it uses a self-signed SSL certificate. If so, explicitly add the certificate to your browser. It's the default server of package (user demo). +A demonstration server is available [here](https://gpass-demo.soutade.fr). It's the default server of package (user demo). **Warning** The master key derivation is partially based on account URL. So it's linked to your current server information. You can't move databases from servers with different URLs, you need to export them and import again. @@ -55,11 +58,13 @@ Version 0.6 introduces shadow logins. It's a protection again illegal database d The principle is to generate a random value (shadow login) that must be encrypted with the masterkey to get an access token. This access token allows to get the true (but encrypted) login/password couple. It's a kind of challenge : if I can encrypt the shadow login, I know the masterkey ! For security reason, the derivation of masterkey for deciphering passwords is different than for encrypting shadow logins (it uses its own salt). + Client ------ Just install the package. You can have debug information by setting DEBUG in main.js. + Command line interface ---------------------- @@ -73,17 +78,16 @@ The dependencies are libcurl and OpenSSL (-dev packages : ie _libcurl4-openssl-d A sample configuration file is available _gpass.ini.sample_ + Version Information ------------------- -Current version is 0.8. **(not compatible with 0.7)** +Current version is 0.9. **(not compatible with 0.7)** Firefox will remove support for addons, so the gPass addon code is not supported since v0.8, please migrate to webextension. Transition from v0.7 to v0.8 : **Please update your masterkey (even with the same one) to gain a security level of your passwords's wallet.** -**This version is incompatible from 0.1**. Please use [this script](http://soutade.fr/files/gpass_migrate_0_1.php) to migrate. - License ------- diff --git a/chrome_addon/background.js b/chrome_addon/background.js index 6689fdd..2fa30c1 100644 --- a/chrome_addon/background.js +++ b/chrome_addon/background.js @@ -1,25 +1,261 @@ /* - Copyright (C) 2013-2017 Grégory Soutadé - + Copyright (C) 2013-2020 Grégory Soutadé + This file is part of gPass. - + gPass is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - + gPass is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - + You should have received a copy of the GNU General Public License along with gPass. If not, see . */ +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 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 "": + break; + default: + debug("Unknown command " + params[0]); + + _notification("Error : It seems that it's not a gPass server", + this.responseText); + ret = SERVER.FAILED; + break; + } + } + + if (ret != SERVER.OK) + { + sendResponse({"value": ret, options:options}); + return; + } + + if (ciphered_password != "") + { + debug("Ciphered password : " + ciphered_password); + if (matched_key >= crypto_v2_logins_size) + // Crypto v1 + { + clear_password = await decrypt_ecb(mkey, hex2a(ciphered_password)); + // Remove trailing \0 and salt + clear_password = clear_password.replace(/\0*$/, ""); + clear_password = clear_password.substr(0, clear_password.length-3); + } + else + { + clear_password = await decrypt_cbc(mkey, global_iv, hex2a(ciphered_password)); + clear_password = clear_password.replace(/\0*$/, ""); + clear_password = clear_password.substr(3, clear_password.length); + } + debug("Clear password " + clear_password); + } + else + { + debug("No password found"); + + ret = SERVER.FAILED; + + _notification("No password found in database", "") + } + + sendResponse({"value": ret, "password":clear_password, "options":options}); + }, false); + gPassRequest.addEventListener("error", function(evt) { + debug("error"); + ret = false; + _notification("Error"); + }, false); + debug("connect to " + account_url); + gPassRequest.open("POST", account_url, true); + gPassRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'); + gPassRequest.send(keys); + + return true; +} + function url_block_callback(details) { - // console.log(JSON.stringify(details)); + // debug(JSON.stringify(details)); if (details.requestBody) { if (details.requestBody.formData) @@ -40,56 +276,307 @@ function url_block_callback(details) // Analyse POST parameters if (details.method == "POST" && details.requestBody.raw) { - alert(details.requestBody.raw); - var postedString = decodeURIComponent(String.fromCharCode.apply(null, - new Uint8Array(details.requestBody.raw[0].bytes))); - if (postedString.indexOf("=@@") != -1 || - postedString.indexOf("=@_") != -1) - return {cancel: true}; + alert(details.requestBody.raw); + var postedString = decodeURIComponent(String.fromCharCode.apply(null, + new Uint8Array(details.requestBody.raw[0].bytes))); + if (postedString.indexOf("=@@") != -1 || + postedString.indexOf("=@_") != -1) + return {cancel: true}; } -*/ + */ } return {cancel: false}; } -chrome.runtime.onMessage.addListener( - function(request, sender, sendResponse) { +function url_unblock_callback(details) +{ + return {cancel: false}; +} - if (request.type == "notification") +function update_gpass_icon(iconId, tabId) +{ + debug("update_gpass_icon"); + + icon_infos = {"tabId":tabId}; + icon_name = ""; + + switch (iconId) + { + case GPASS_ICON.NORMAL: break; + case GPASS_ICON.DISABLED: + icon_name = "_disabled"; + break; + case GPASS_ICON.ACTIVATED: + icon_name = "_activated"; + break; + default: + } + + icon_infos["path"] = { + 16:"icons/gpass" + icon_name + "_icon_16.png", + 32:"icons/gpass" + icon_name + "_icon_32.png", + 64:"icons/gpass" + icon_name + "_icon_64.png", + 128:"icons/gpass" + icon_name + "_icon_128.png", + }; + + browser.browserAction.setIcon(icon_infos); +} + +function is_gpass_enabled(uri) +{ + var domain = parseURI.parseUri(uri); + domain = domain["host"]; + debug("Is gpass enabled for " + domain + " ?"); + return get_preference("disable-" + domain); +} + +function save_gpass_enable_config(uri, enable) +{ + var domain = parseURI.parseUri(uri); + domain = domain["host"]; + + key = "disable-" + domain; + if (enable) + { + debug("Enable gpass for " + domain); + delete_preference(key); + } + else + { + debug("Disable gpass for " + domain); + set_preference(key, true); + } +} + +function _block_url(tabs, callback) +{ + options = { + urls:[tabs[0].url], + "types":["main_frame"] + }; + + if (tabs.length) + { + options["tabId"] = tabs[0].id; + options["windowId"] = tabs[0].windowId; + } + + browser.webRequest.onBeforeRequest.addListener( + url_block_callback, + options, + ["blocking", "requestBody"]); + + return true; + +} + +function _query_tabs_block_url(tabs) +{ + return _block_url(tabs, url_block_callback); +} + +function _query_tabs_unblock_url(tabs) +{ + return _block_url(tabs, url_unblock_callback); +} + +function _query_tabs_is_gpass_enabled(tabs, sendResponse) +{ + if (tabs.length) + { + is_gpass_enabled(tabs[0].url).then( + function (key_present) { + enabled = (key_present == null); + update_gpass_icon((enabled)?GPASS_ICON.ENABLED:GPASS_ICON.DISABLED, tabs[0].id); + sendResponse({"enabled":enabled}); + } + ); + } + else + { + debug("No cur tab"); + sendResponse({"enabled":true}); + } + + return true; +} + +function _query_tabs_update_icon(tabs, iconId) +{ + if (tabs.length) + { + update_gpass_icon(iconId, tabs[0].id); + } +} + +function gpass_switch_enable(tab) +{ + is_gpass_enabled(tab.url).then( + function (key_present) { - options = { - type: "basic", - title : "gPass", - message : request.options.message, - iconUrl:chrome.extension.getURL("icons/gpass_icon_64.png") - }; + enabled = (key_present == null); + // Do switch + enabled = !enabled; + if (enabled) + { + parameters = {type:"blockForms"}; + debug("Now enabled"); + } + else + { + parameters = {type:"unblockForms"}; + debug("Now disabled"); + } - chrome.notifications.create("gPass", options, function(){}); + save_gpass_enable_config(tab.url, enabled); + update_gpass_icon((enabled)?GPASS_ICON.ENABLED:GPASS_ICON.DISABLED, tab.id); + browser.tabs.sendMessage(tab.id, parameters); + }); +} - window.setTimeout(function() {chrome.notifications.clear("gPass", function(){})}, 2000); - } - else if (request.type == "block_url") - { - chrome.tabs.getCurrent(function cb(tab) { - if (tab) - { - chrome.webRequest.onBeforeRequest.addListener( - url_block_callback, - {urls:[request.options.url], - "types":["main_frame"], - "tabId":tab.id, - "windowId":tab.windowId - }, - ["blocking", "requestBody"]); - } - else - { - chrome.webRequest.onBeforeRequest.addListener( - url_block_callback, - {urls:[request.options.url], "types":["main_frame"]}, - ["blocking", "requestBody"]); - } - }); +function extension_load() +{ + browser.runtime.onMessage.addListener( + function(request, sender, sendResponse) { + if (request.type == "password") + { + var domain = parseURI.parseUri(request.domain); + domain = domain["host"]; + var wdomain = wildcard_domain(domain); + + ask_server(request.logins, domain, + wdomain, request.mkey, + sendResponse, request.options); + + return true; + } + else if (request.type == "notification") + { + _notification(request.options.message, request.options.data); + } + else if (request.type == "getServerAddress") + { + get_preference("account_url").then( + function (address) { + sendResponse({"value" : address}); + }); + return true; + } + else if (request.type == "block_url") + { + browser.tabs.query({active:true, currentWindow:true}, _query_tabs_block_url); + } + else if (request.type == "unblock_url") + { + browser.tabs.query({active:true, currentWindow:true}, _query_tabs_unblock_url); + } + else if (request.type == "is_gpass_enabled") + { + browser.tabs.query({active:true, currentWindow:true}, + function cb(tabs) { + _query_tabs_is_gpass_enabled(tabs, sendResponse); + }); + return true; + } + else if (request.type == "switch_enable") + { + debug("Switch enable"); + browser.tabs.query({active:true, currentWindow:true}, + function cb(tabs) { + _query_tabs_switch_enable(tabs, sendResponse) + }); + return true; + } + else if (request.type == "update_icon") + { + debug("update_icon"); + browser.tabs.query({active:true, currentWindow:true}, + function cb(tabs) { + _query_tabs_update_icon(tabs, request.icon_id); + }); + } + else + { + debug("Unknown message " + request.type); + } } + ); + + if (!browser.menus && browser.contextMenus) + browser.menus = browser.contextMenus; + + browser.menus.create({ + id: 'settings', + title: 'gPass Settings', + contexts: ['browser_action'] }); + + /* Not supported by Chrome */ + if (browser.menus.onShown) + title = 'disable gPass for this website'; + else + title = 'disable or enable gPass for this website'; + + browser.menus.create({ + id: 'switch_enable', + title: title, + contexts: ['browser_action'] + }); + + browser.menus.onClicked.addListener( + function(info, tab) { + switch (info.menuItemId) { + case 'settings': + browser.runtime.openOptionsPage(); + break; + + case 'switch_enable': + gpass_switch_enable(tab); + break; + } + } + ); + + if (browser.menus.onShown) + { + browser.menus.onShown.addListener( + function(info, tab) { + is_gpass_enabled(tab.url).then( + function (key_present) { + enabled = (key_present == null); + if (enabled) + title = 'disable gPass for this website'; + else + title = 'enable gPass for this website'; + browser.menus.update("switch_enable", + { + "title":title + } + ); + browser.menus.refresh(); + } + ); + } + ); + } +} + +async function self_test() +{ + mkey = crypto_pbkdf2("password", "salt", 4096); + res = await encrypt_ecb(mkey, "DDDDDDDDDDDDDDDD"); + + reference = new Uint8Array([0xc4, 0x76, 0x01, 0x07, 0xa1, 0xc0, 0x2f, 0x22, 0xee, 0xbe, 0x60, + 0xff, 0x65, 0x33, 0x5b, 0x9e]); + if (res != ab2str(reference)) + { + console.log("Self test ERROR !"); + } + else + console.log("Self test OK !"); +} + +//self_test(); + +extension_load(); diff --git a/chrome_addon/compat.js b/chrome_addon/compat.js index 58da9eb..729d202 100644 --- a/chrome_addon/compat.js +++ b/chrome_addon/compat.js @@ -1,23 +1,23 @@ /* - Copyright (C) 2013-2017 Grégory Soutadé - + Copyright (C) 2013-2020 Grégory Soutadé + This file is part of gPass. - + gPass is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - + gPass is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - + You should have received a copy of the GNU General Public License along with gPass. If not, see . */ -function getPref(key) +function get_preference(key) { // Inspired from https://github.com/akiomik/chrome-storage-promise/ var promise = new Promise((resolve, reject) => { @@ -32,14 +32,33 @@ function getPref(key) }) .then(function (pref) { if (!pref.hasOwnProperty(key)) - return default_preferences[key]; + { + if (default_preferences.hasOwnProperty(key)) + return default_preferences[key]; + else + return null; + } return pref[key]; }); return promise; } -function setPref(key, value) +function set_preference(key, value) { - chrome.storage.local.set({key:value}, function ok() {}); + pref = {[key]:value}; + chrome.storage.local.set(pref, function (result) { + if (chrome.runtime.lastError) + alert(chrome.runtime.lastError); + }); +} + +function delete_preference(key) +{ + chrome.storage.local.remove(key); +} + +function send_tab_message(tab_id, parameters, callback) +{ + chrome.tabs.sendMessage(tab_id, parameters, {}, callback); } diff --git a/chrome_addon/lib/main.js b/chrome_addon/lib/main.js index a56d9cc..5da14e7 100644 --- a/chrome_addon/lib/main.js +++ b/chrome_addon/lib/main.js @@ -1,305 +1,44 @@ /* - Copyright (C) 2013-2017 Grégory Soutadé - + Copyright (C) 2013-2020 Grégory Soutadé + This file is part of gPass. - + gPass is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - + gPass is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - + You should have received a copy of the GNU General Public License along with gPass. If not, see . */ -var DEBUG = false; -var protocol_version = 4; -var account_url = null; -var crypto_v2_logins_size = 0; +var gpass_enabled = true; -SERVER = {OK : 0, FAILED : 1, RESTART_REQUEST : 2}; - -// http://stackoverflow.com/questions/3745666/how-to-convert-from-hex-to-ascii-in-javascript -function hex2a(hex) { - var str = ''; - for (var i = 0; i < hex.length; i += 2) - str += String.fromCharCode(parseInt(hex.substr(i, 2), 16)); - return str; -} - -function a2hex(_str_) { - var hex = ''; - for (var i = 0; i < _str_.length; i++) - { - var c = _str_.charCodeAt(i).toString(16); - if (c.length == 1) c = "0" + c; - hex += c; - } - return hex; -} - -function debug(s) +function _notification(message, data) { - if (DEBUG) - console.log(s); -} + if (message !== data) + message += data; -async function generate_request(domain, login, mkey, iv, old) -{ - if (old) - { - var v = "@@" + domain + ";" + login; - debug("will encrypt " + v); - enc = encrypt_ecb(mkey, v); - } - else - { - var v = domain + ";" + login; - debug("will encrypt " + v); - while ((v.length % 16)) - v += "\0"; - hash = await digest(v); - v += hash.slice(8, 24); - enc = encrypt_cbc(mkey, iv, v); - } - return enc; -} + options = { + type: "basic", + title : "gPass", + message : message, + iconUrl:browser.extension.getURL("icons/gpass_icon_64.png") + }; -async function ask_server(form, field, logins, domain, wdomain, mkey, submit) -{ - account_url = await getPref("account_url"); - - var salt = parseURI.parseUri(account_url); - salt = salt["host"] + salt["path"]; - - debug("salt " + salt); - - pbkdf2_level = await getPref("pbkdf2_level"); - - global_iv = await simple_pbkdf2(salt, mkey, pbkdf2_level); - global_iv = global_iv.slice(0, 16); - mkey = crypto_pbkdf2(mkey, salt, pbkdf2_level); - - debug("global_iv " + a2hex(global_iv)); - - keys = ""; - for(key_index=0, a=0; a protocol_version) - { - notify("Protocol version not supported, please upgrade your addon", - "Protocol version not supported, please upgrade your addon"); - ret = SERVER.FAILED; - } - else - { - switch (server_protocol_version) - { - case 2: - server_pbkdf2_level = 1000; - break; - case 3: - // Version 3 : nothing special to do - case 4: - // Version 4 : nothing special to do - break; - } - } - break; - case "matched_key": - matched_key = params[1]; - case "pass": - ciphered_password = params[1]; - break; - case "pkdbf2_level": - case "pbkdf2_level": - server_pbkdf2_level = parseInt(params[1].match(/\d+/)[0], 10); - if (server_pbkdf2_level != NaN && - server_pbkdf2_level != pbkdf2_level && - server_pbkdf2_level >= 1000) // Minimum level for PBKDF2 ! - { - debug("New pbkdf2 level " + server_pbkdf2_level); - pbkdf2_level = server_pbkdf2_level; - setPref("pbkdf2_level", pbkdf2_level); - ret = SERVER.RESTART_REQUEST; - } - break; - case "": - break; - default: - debug("Unknown command " + params[0]); - - notify("Error : It seems that it's not a gPass server", - this.responseText); - ret = SERVER.FAILED; - break; - } - } - - if (ret != SERVER.OK) - { - return; - } - - if (ciphered_password != "") - { - debug("Ciphered password : " + ciphered_password); - if (matched_key >= crypto_v2_logins_size) - // Crypto v1 - { - clear_password = await decrypt_ecb(mkey, hex2a(ciphered_password)); - // Remove trailing \0 and salt - clear_password = clear_password.replace(/\0*$/, ""); - clear_password = clear_password.substr(0, clear_password.length-3); - } - else - { - clear_password = await decrypt_cbc(mkey, global_iv, hex2a(ciphered_password)); - clear_password = clear_password.replace(/\0*$/, ""); - clear_password = clear_password.substr(3, clear_password.length); - } - debug("Clear password " + clear_password); - field.value = clear_password; - // Remove gPass event listener and submit again with clear password - if (submit) - { - form.removeEventListener("submit", on_sumbit, true); - // Propagate change - change_cb = field.onchange; - if (change_cb) - change_cb(); - // Try to type "enter" - var evt = new KeyboardEvent("keydown"); - delete evt.which; - evt.which = 13; - field.dispatchEvent(evt); - // Submit form - form.submit(); - } - else - { - notify("Password successfully replaced", - "Password successfully replaced"); - } - } - else - { - debug("No password found"); - - ret = SERVER.FAILED; - - notify("No password found in database", - "No password found in database"); - } - }, false); - gPassRequest.addEventListener("error", function(evt) { - debug("error"); - ret = false; - notify("Error", - "Error"); - }, false); - debug("connect to " + account_url); - gPassRequest.open("POST", account_url, true); - gPassRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'); - gPassRequest.send(keys); - - return ret; -} - -function wildcard_domain(domain) -{ - var parts = domain.split("."); - - // Standard root domain (zzz.xxx.com) or more - if (parts.length > 2) - { - res = "*."; - for (i=1; i && @@ -360,11 +123,13 @@ function on_sumbit(e) logins = try_get_name(fields, type_filters, true); // Get all other fields except text, email and password + type_filters.push("password"); + all_logins = try_get_name(fields, type_filters, false); + if (!logins.length) - { - type_filters.push("password"); - logins = try_get_name(fields, type_filters, false); - } + logins = all_logins; + + e.preventDefault(); // Look for for (var i=0; i"); + if (managed_forms.length && do_block) + block_url(""); } -document_loaded(document); - -async function self_test() +function unblock_all_forms() { - mkey = crypto_pbkdf2("password", "salt", 4096); - res = await encrypt_ecb(mkey, "DDDDDDDDDDDDDDDD"); + debug("unblock all forms"); + + on_blur(null); - reference = new Uint8Array([0xc4, 0x76, 0x01, 0x07, 0xa1, 0xc0, 0x2f, 0x22, 0xee, 0xbe, 0x60, - 0xff, 0x65, 0x33, 0x5b, 0x9e]); - if (res != ab2str(reference)) + for(var i=0; i"); + + gpass_enabled = false; } -console.log("Welcome to gPass web extension v0.8.1 !"); +browser.runtime.onMessage.addListener( + function(request, sender, sendResponse) { + + if (request.type == "getUsername") + { + debug("getUsername"); + if (managed_forms.length == 1) + { + fields = managed_forms[0].getElementsByTagName("input"); + + type_filters = new Array(); + // Get all && + 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. */ +var DEBUG = false; + +SERVER = {OK : 0, FAILED : 1, RESTART_REQUEST : 2}; +GPASS_ICON = {NORMAL:0, DISABLED:1, ACTIVATED:2}; + var default_preferences = {"pbkdf2_level": 1000, "account_url": "https://gpass-demo.soutade.fr/demo", "crypto_v1_compatible": true}; @@ -26,7 +31,7 @@ var crypto = crypto || window.crypto; function notify(text, data) { - browser.runtime.sendMessage({type: "notification", options:{"message":text}}); + browser.runtime.sendMessage({type: "notification", options:{"message":text, "data":data}}); } function block_url(url) @@ -35,6 +40,12 @@ function 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)); @@ -124,7 +135,7 @@ function _encrypt(mkey, iv, data) { while ((data.length % 16)) data += "\0"; - + data = str2ab(data); promise = mkey.then(function(mkey){ @@ -173,8 +184,8 @@ async function _decrypt(mkey, iv, data) async function encrypt_ecb(mkey, data) { var result = ""; - - console.log("Encrypt ECB " + data); + + debug("Encrypt ECB " + data); nulliv = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); @@ -195,8 +206,8 @@ async function decrypt_ecb(mkey, data) { var result = ""; - console.log("Decrypt ECB " + data); - + 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) @@ -213,23 +224,23 @@ async function decrypt_ecb(mkey, data) } async function encrypt_cbc(mkey, iv, data) -{ - console.log("Encrypt CBC " + 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) { - console.log("Decrypt CBC " + data); + debug("Decrypt CBC " + data); var result = await _decrypt(mkey, str2ab(iv), data); // Remove PKCS7 padding - return result.slice(0, result.length-16); + return result.slice(0, result.length-16); } async function digest(data) @@ -238,3 +249,47 @@ async function digest(data) 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"], - "js": ["lib/parseuri.js", "lib/misc.js", "compat.js", "lib/main.js"], + "js": ["lib/misc.js", "lib/main.js"], "run_at" : "document_idle", "all_frames" : true } @@ -20,17 +20,26 @@ "background": { "persistent": true, - "scripts": ["background.js"] + "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": [ "", + "activeTab", "notifications", "webRequest", "webRequestBlocking", "tabs", - "storage" + "storage", + "clipboardWrite", + "contextMenus" ] } diff --git a/chrome_addon/popup/compat.js b/chrome_addon/popup/compat.js new file mode 120000 index 0000000..0429f6a --- /dev/null +++ b/chrome_addon/popup/compat.js @@ -0,0 +1 @@ +../compat.js \ No newline at end of file diff --git a/chrome_addon/popup/misc.js b/chrome_addon/popup/misc.js new file mode 120000 index 0000000..d955a61 --- /dev/null +++ b/chrome_addon/popup/misc.js @@ -0,0 +1 @@ +../lib/misc.js \ No newline at end of file diff --git a/chrome_addon/popup/popup.html b/chrome_addon/popup/popup.html new file mode 100644 index 0000000..8ce7697 --- /dev/null +++ b/chrome_addon/popup/popup.html @@ -0,0 +1,15 @@ + + + + + + +
+ Username
Master key
+ Your server +
+ + + + + diff --git a/chrome_addon/popup/popup.js b/chrome_addon/popup/popup.js new file mode 100644 index 0000000..ec09312 --- /dev/null +++ b/chrome_addon/popup/popup.js @@ -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 . +*/ + +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); diff --git a/firefox_webextension/background.js b/firefox_webextension/background.js index 1a027ec..2fa30c1 100644 --- a/firefox_webextension/background.js +++ b/firefox_webextension/background.js @@ -1,6 +1,261 @@ +/* + 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 . +*/ + +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 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 "": + break; + default: + debug("Unknown command " + params[0]); + + _notification("Error : It seems that it's not a gPass server", + this.responseText); + ret = SERVER.FAILED; + break; + } + } + + if (ret != SERVER.OK) + { + sendResponse({"value": ret, options:options}); + return; + } + + if (ciphered_password != "") + { + debug("Ciphered password : " + ciphered_password); + if (matched_key >= crypto_v2_logins_size) + // Crypto v1 + { + clear_password = await decrypt_ecb(mkey, hex2a(ciphered_password)); + // Remove trailing \0 and salt + clear_password = clear_password.replace(/\0*$/, ""); + clear_password = clear_password.substr(0, clear_password.length-3); + } + else + { + clear_password = await decrypt_cbc(mkey, global_iv, hex2a(ciphered_password)); + clear_password = clear_password.replace(/\0*$/, ""); + clear_password = clear_password.substr(3, clear_password.length); + } + debug("Clear password " + clear_password); + } + else + { + debug("No password found"); + + ret = SERVER.FAILED; + + _notification("No password found in database", "") + } + + sendResponse({"value": ret, "password":clear_password, "options":options}); + }, false); + gPassRequest.addEventListener("error", function(evt) { + debug("error"); + ret = false; + _notification("Error"); + }, false); + debug("connect to " + account_url); + gPassRequest.open("POST", account_url, true); + gPassRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'); + gPassRequest.send(keys); + + return true; +} + function url_block_callback(details) { - //console.log(details); + // debug(JSON.stringify(details)); if (details.requestBody) { if (details.requestBody.formData) @@ -21,57 +276,307 @@ function url_block_callback(details) // Analyse POST parameters if (details.method == "POST" && details.requestBody.raw) { - alert(details.requestBody.raw); - var postedString = decodeURIComponent(String.fromCharCode.apply(null, - new Uint8Array(details.requestBody.raw[0].bytes))); - if (postedString.indexOf("=@@") != -1 || - postedString.indexOf("=@_") != -1) - return {cancel: true}; + alert(details.requestBody.raw); + var postedString = decodeURIComponent(String.fromCharCode.apply(null, + new Uint8Array(details.requestBody.raw[0].bytes))); + if (postedString.indexOf("=@@") != -1 || + postedString.indexOf("=@_") != -1) + return {cancel: true}; } -*/ + */ } return {cancel: false}; } -browser.runtime.onMessage.addListener( - function(request) { +function url_unblock_callback(details) +{ + return {cancel: false}; +} - if (request.type == "notification") +function update_gpass_icon(iconId, tabId) +{ + debug("update_gpass_icon"); + + icon_infos = {"tabId":tabId}; + icon_name = ""; + + switch (iconId) + { + case GPASS_ICON.NORMAL: break; + case GPASS_ICON.DISABLED: + icon_name = "_disabled"; + break; + case GPASS_ICON.ACTIVATED: + icon_name = "_activated"; + break; + default: + } + + icon_infos["path"] = { + 16:"icons/gpass" + icon_name + "_icon_16.png", + 32:"icons/gpass" + icon_name + "_icon_32.png", + 64:"icons/gpass" + icon_name + "_icon_64.png", + 128:"icons/gpass" + icon_name + "_icon_128.png", + }; + + browser.browserAction.setIcon(icon_infos); +} + +function is_gpass_enabled(uri) +{ + var domain = parseURI.parseUri(uri); + domain = domain["host"]; + debug("Is gpass enabled for " + domain + " ?"); + return get_preference("disable-" + domain); +} + +function save_gpass_enable_config(uri, enable) +{ + var domain = parseURI.parseUri(uri); + domain = domain["host"]; + + key = "disable-" + domain; + if (enable) + { + debug("Enable gpass for " + domain); + delete_preference(key); + } + else + { + debug("Disable gpass for " + domain); + set_preference(key, true); + } +} + +function _block_url(tabs, callback) +{ + options = { + urls:[tabs[0].url], + "types":["main_frame"] + }; + + if (tabs.length) + { + options["tabId"] = tabs[0].id; + options["windowId"] = tabs[0].windowId; + } + + browser.webRequest.onBeforeRequest.addListener( + url_block_callback, + options, + ["blocking", "requestBody"]); + + return true; + +} + +function _query_tabs_block_url(tabs) +{ + return _block_url(tabs, url_block_callback); +} + +function _query_tabs_unblock_url(tabs) +{ + return _block_url(tabs, url_unblock_callback); +} + +function _query_tabs_is_gpass_enabled(tabs, sendResponse) +{ + if (tabs.length) + { + is_gpass_enabled(tabs[0].url).then( + function (key_present) { + enabled = (key_present == null); + update_gpass_icon((enabled)?GPASS_ICON.ENABLED:GPASS_ICON.DISABLED, tabs[0].id); + sendResponse({"enabled":enabled}); + } + ); + } + else + { + debug("No cur tab"); + sendResponse({"enabled":true}); + } + + return true; +} + +function _query_tabs_update_icon(tabs, iconId) +{ + if (tabs.length) + { + update_gpass_icon(iconId, tabs[0].id); + } +} + +function gpass_switch_enable(tab) +{ + is_gpass_enabled(tab.url).then( + function (key_present) { - options = { - type: "basic", - title : "gPass", - message : request.options.message, - iconUrl:browser.extension.getURL("icons/gpass_icon_64.png") - }; + enabled = (key_present == null); + // Do switch + enabled = !enabled; + if (enabled) + { + parameters = {type:"blockForms"}; + debug("Now enabled"); + } + else + { + parameters = {type:"unblockForms"}; + debug("Now disabled"); + } - browser.notifications.create("gPass", options); + save_gpass_enable_config(tab.url, enabled); + update_gpass_icon((enabled)?GPASS_ICON.ENABLED:GPASS_ICON.DISABLED, tab.id); + browser.tabs.sendMessage(tab.id, parameters); + }); +} - window.setTimeout(function() {browser.notifications.clear("gPass")}, 2000); - } - else if (request.type == "block_url") - { - browser.tabs.getCurrent().then( - function onGot(tab) { - if (tab) - { - browser.webRequest.onBeforeRequest.addListener( - url_block_callback, - {"urls":[request.options.url], - "types":["main_frame"], - "tabId":tab.id, - "windowId":tab.windowId - }, - ["blocking", "requestBody"]); - } - else - { - browser.webRequest.onBeforeRequest.addListener( - url_block_callback, - {"urls":[request.options.url], "types":["main_frame"]}, - ["blocking", "requestBody"]); - } - }); +function extension_load() +{ + browser.runtime.onMessage.addListener( + function(request, sender, sendResponse) { + if (request.type == "password") + { + var domain = parseURI.parseUri(request.domain); + domain = domain["host"]; + var wdomain = wildcard_domain(domain); + + ask_server(request.logins, domain, + wdomain, request.mkey, + sendResponse, request.options); + + return true; + } + else if (request.type == "notification") + { + _notification(request.options.message, request.options.data); + } + else if (request.type == "getServerAddress") + { + get_preference("account_url").then( + function (address) { + sendResponse({"value" : address}); + }); + return true; + } + else if (request.type == "block_url") + { + browser.tabs.query({active:true, currentWindow:true}, _query_tabs_block_url); + } + else if (request.type == "unblock_url") + { + browser.tabs.query({active:true, currentWindow:true}, _query_tabs_unblock_url); + } + else if (request.type == "is_gpass_enabled") + { + browser.tabs.query({active:true, currentWindow:true}, + function cb(tabs) { + _query_tabs_is_gpass_enabled(tabs, sendResponse); + }); + return true; + } + else if (request.type == "switch_enable") + { + debug("Switch enable"); + browser.tabs.query({active:true, currentWindow:true}, + function cb(tabs) { + _query_tabs_switch_enable(tabs, sendResponse) + }); + return true; + } + else if (request.type == "update_icon") + { + debug("update_icon"); + browser.tabs.query({active:true, currentWindow:true}, + function cb(tabs) { + _query_tabs_update_icon(tabs, request.icon_id); + }); + } + else + { + debug("Unknown message " + request.type); + } } + ); + + if (!browser.menus && browser.contextMenus) + browser.menus = browser.contextMenus; + + browser.menus.create({ + id: 'settings', + title: 'gPass Settings', + contexts: ['browser_action'] }); + + /* Not supported by Chrome */ + if (browser.menus.onShown) + title = 'disable gPass for this website'; + else + title = 'disable or enable gPass for this website'; + + browser.menus.create({ + id: 'switch_enable', + title: title, + contexts: ['browser_action'] + }); + + browser.menus.onClicked.addListener( + function(info, tab) { + switch (info.menuItemId) { + case 'settings': + browser.runtime.openOptionsPage(); + break; + + case 'switch_enable': + gpass_switch_enable(tab); + break; + } + } + ); + + if (browser.menus.onShown) + { + browser.menus.onShown.addListener( + function(info, tab) { + is_gpass_enabled(tab.url).then( + function (key_present) { + enabled = (key_present == null); + if (enabled) + title = 'disable gPass for this website'; + else + title = 'enable gPass for this website'; + browser.menus.update("switch_enable", + { + "title":title + } + ); + browser.menus.refresh(); + } + ); + } + ); + } +} + +async function self_test() +{ + mkey = crypto_pbkdf2("password", "salt", 4096); + res = await encrypt_ecb(mkey, "DDDDDDDDDDDDDDDD"); + + reference = new Uint8Array([0xc4, 0x76, 0x01, 0x07, 0xa1, 0xc0, 0x2f, 0x22, 0xee, 0xbe, 0x60, + 0xff, 0x65, 0x33, 0x5b, 0x9e]); + if (res != ab2str(reference)) + { + console.log("Self test ERROR !"); + } + else + console.log("Self test OK !"); +} + +//self_test(); + +extension_load(); diff --git a/firefox_webextension/compat.js b/firefox_webextension/compat.js index 49b9a6e..78264f6 100644 --- a/firefox_webextension/compat.js +++ b/firefox_webextension/compat.js @@ -1,29 +1,34 @@ /* - Copyright (C) 2013-2017 Grégory Soutadé - + Copyright (C) 2013-2020 Grégory Soutadé + This file is part of gPass. - + gPass is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - + gPass is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - + You should have received a copy of the GNU General Public License along with gPass. If not, see . */ -function getPref(key) +function get_preference(key) { return browser.storage.local.get(key) .then( function (pref) { if (!pref.hasOwnProperty(key)) - return default_preferences[key]; + { + if (default_preferences.hasOwnProperty(key)) + return default_preferences[key]; + else + return null; + } return pref[key]; } , @@ -33,7 +38,21 @@ function getPref(key) ); } -function setPref(key, value) +function set_preference(key, value) { - browser.storage.local.set({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); + } + ); } diff --git a/firefox_webextension/lib b/firefox_webextension/lib deleted file mode 120000 index 850519c..0000000 --- a/firefox_webextension/lib +++ /dev/null @@ -1 +0,0 @@ -../chrome_addon/lib/ \ No newline at end of file diff --git a/firefox_webextension/lib/main.js b/firefox_webextension/lib/main.js new file mode 100644 index 0000000..5da14e7 --- /dev/null +++ b/firefox_webextension/lib/main.js @@ -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 . +*/ + +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 && + 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 + for (var i=0; i"); +} + +function unblock_all_forms() +{ + debug("unblock all forms"); + + on_blur(null); + + for(var i=0; i"); + + 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 && + 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. +*/ + +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 +// 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; + } +}; diff --git a/firefox_webextension/manifest.json b/firefox_webextension/manifest.json index 5d9401d..20b1260 100644 --- a/firefox_webextension/manifest.json +++ b/firefox_webextension/manifest.json @@ -3,16 +3,16 @@ "name": "gPass", "short_name": "gPass", - "version": "0.8.1", + "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": [""], - "js": ["lib/parseuri.js", "lib/misc.js", "compat.js", "lib/main.js"], + "js": ["lib/misc.js", "lib/main.js"], "run_at" : "document_idle", "all_frames" : true } @@ -20,18 +20,30 @@ "background": { "persistent": true, - "scripts": ["background.js"] + "scripts": ["lib/parseuri.js", "lib/misc.js", "compat.js", "background.js"] }, - "options_ui": { "page":"options.html" }, + "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": [ "", + "activeTab", "notifications", "webRequest", "webRequestBlocking", "tabs", "storage", - "activeTab" + "clipboardWrite", + "menus" ] } diff --git a/firefox_webextension/options.html b/firefox_webextension/options.html deleted file mode 120000 index 1ea0970..0000000 --- a/firefox_webextension/options.html +++ /dev/null @@ -1 +0,0 @@ -../chrome_addon/options.html \ No newline at end of file diff --git a/firefox_webextension/options.html b/firefox_webextension/options.html new file mode 100644 index 0000000..07d56fd --- /dev/null +++ b/firefox_webextension/options.html @@ -0,0 +1,21 @@ + + + + gPass + + + + Account URL URL of your gPass account
+ WARNING 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.
+
+ PBKDF2 level Number of iterations used to derivate master key
+
+
+ Crypto v1 compatible Compatible with old crypto schema (AES ECB). Use it for encrypted passwords with server <= 0.7
+
+ + + + + diff --git a/firefox_webextension/popup/compat.js b/firefox_webextension/popup/compat.js new file mode 120000 index 0000000..0429f6a --- /dev/null +++ b/firefox_webextension/popup/compat.js @@ -0,0 +1 @@ +../compat.js \ No newline at end of file diff --git a/firefox_webextension/popup/misc.js b/firefox_webextension/popup/misc.js new file mode 120000 index 0000000..d955a61 --- /dev/null +++ b/firefox_webextension/popup/misc.js @@ -0,0 +1 @@ +../lib/misc.js \ No newline at end of file diff --git a/firefox_webextension/popup/popup.html b/firefox_webextension/popup/popup.html new file mode 100644 index 0000000..8ce7697 --- /dev/null +++ b/firefox_webextension/popup/popup.html @@ -0,0 +1,15 @@ + + + + + + +
+ Username
Master key
+ Your server +
+ + + + + diff --git a/firefox_webextension/popup/popup.js b/firefox_webextension/popup/popup.js new file mode 100644 index 0000000..ec09312 --- /dev/null +++ b/firefox_webextension/popup/popup.js @@ -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 . +*/ + +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);