From 84eaf0c6a1d0f44dd014ffc5e65b4de032426241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Soutad=C3=A9?= Date: Tue, 21 Jan 2014 19:00:26 +0100 Subject: [PATCH] New protocol v3 : include pkdbf2 level Remove hashtable from firefox addon Rework firefox addon Add pkdbf2_level as a preference (hidden) --- firefox_addon/README.md | 3 +- firefox_addon/lib/jshashtable-3.0.js | 404 --------------------------- firefox_addon/lib/main.js | 345 ++++++++++++++--------- firefox_addon/package.json | 16 +- server/_user | 8 +- server/conf.php | 41 +++ server/index.php | 11 +- server/ressources/gpass.js | 2 +- 8 files changed, 291 insertions(+), 539 deletions(-) delete mode 100644 firefox_addon/lib/jshashtable-3.0.js create mode 100644 server/conf.php diff --git a/firefox_addon/README.md b/firefox_addon/README.md index 60c05d1..033cdc5 100644 --- a/firefox_addon/README.md +++ b/firefox_addon/README.md @@ -42,10 +42,11 @@ 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. -You can activate/deactivate user creation by setting $ADMIN_MODE in index.php. +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 current server information. Currently there is no simple way to export/import a full gPass database. Client ------ diff --git a/firefox_addon/lib/jshashtable-3.0.js b/firefox_addon/lib/jshashtable-3.0.js deleted file mode 100644 index 080b89b..0000000 --- a/firefox_addon/lib/jshashtable-3.0.js +++ /dev/null @@ -1,404 +0,0 @@ -/** - * @license jahashtable, a JavaScript implementation of a hash table. It creates a single constructor function called - * Hashtable in the global scope. - * - * http://www.timdown.co.uk/jshashtable/ - * Copyright 2013 Tim Down. - * Version: 3.0 - * Build date: 17 July 2013 - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -var Hashtable = (function(UNDEFINED) { - var FUNCTION = "function", STRING = "string", UNDEF = "undefined"; - - // Require Array.prototype.splice, Object.prototype.hasOwnProperty and encodeURIComponent. In environments not - // having these (e.g. IE <= 5), we bail out now and leave Hashtable null. - if (typeof encodeURIComponent == UNDEF || - Array.prototype.splice === UNDEFINED || - Object.prototype.hasOwnProperty === UNDEFINED) { - return null; - } - - function toStr(obj) { - return (typeof obj == STRING) ? obj : "" + obj; - } - - function hashObject(obj) { - var hashCode; - if (typeof obj == STRING) { - return obj; - } else if (typeof obj.hashCode == FUNCTION) { - // Check the hashCode method really has returned a string - hashCode = obj.hashCode(); - return (typeof hashCode == STRING) ? hashCode : hashObject(hashCode); - } else { - return toStr(obj); - } - } - - function merge(o1, o2) { - for (var i in o2) { - if (o2.hasOwnProperty(i)) { - o1[i] = o2[i]; - } - } - } - - function equals_fixedValueHasEquals(fixedValue, variableValue) { - return fixedValue.equals(variableValue); - } - - function equals_fixedValueNoEquals(fixedValue, variableValue) { - return (typeof variableValue.equals == FUNCTION) ? - variableValue.equals(fixedValue) : (fixedValue === variableValue); - } - - function createKeyValCheck(kvStr) { - return function(kv) { - if (kv === null) { - throw new Error("null is not a valid " + kvStr); - } else if (kv === UNDEFINED) { - throw new Error(kvStr + " must not be undefined"); - } - }; - } - - var checkKey = createKeyValCheck("key"), checkValue = createKeyValCheck("value"); - - /*----------------------------------------------------------------------------------------------------------------*/ - - function Bucket(hash, firstKey, firstValue, equalityFunction) { - this[0] = hash; - this.entries = []; - this.addEntry(firstKey, firstValue); - - if (equalityFunction !== null) { - this.getEqualityFunction = function() { - return equalityFunction; - }; - } - } - - var EXISTENCE = 0, ENTRY = 1, ENTRY_INDEX_AND_VALUE = 2; - - function createBucketSearcher(mode) { - return function(key) { - var i = this.entries.length, entry, equals = this.getEqualityFunction(key); - while (i--) { - entry = this.entries[i]; - if ( equals(key, entry[0]) ) { - switch (mode) { - case EXISTENCE: - return true; - case ENTRY: - return entry; - case ENTRY_INDEX_AND_VALUE: - return [ i, entry[1] ]; - } - } - } - return false; - }; - } - - function createBucketLister(entryProperty) { - return function(aggregatedArr) { - var startIndex = aggregatedArr.length; - for (var i = 0, entries = this.entries, len = entries.length; i < len; ++i) { - aggregatedArr[startIndex + i] = entries[i][entryProperty]; - } - }; - } - - Bucket.prototype = { - getEqualityFunction: function(searchValue) { - return (typeof searchValue.equals == FUNCTION) ? equals_fixedValueHasEquals : equals_fixedValueNoEquals; - }, - - getEntryForKey: createBucketSearcher(ENTRY), - - getEntryAndIndexForKey: createBucketSearcher(ENTRY_INDEX_AND_VALUE), - - removeEntryForKey: function(key) { - var result = this.getEntryAndIndexForKey(key); - if (result) { - this.entries.splice(result[0], 1); - return result[1]; - } - return null; - }, - - addEntry: function(key, value) { - this.entries.push( [key, value] ); - }, - - keys: createBucketLister(0), - - values: createBucketLister(1), - - getEntries: function(destEntries) { - var startIndex = destEntries.length; - for (var i = 0, entries = this.entries, len = entries.length; i < len; ++i) { - // Clone the entry stored in the bucket before adding to array - destEntries[startIndex + i] = entries[i].slice(0); - } - }, - - containsKey: createBucketSearcher(EXISTENCE), - - containsValue: function(value) { - var entries = this.entries, i = entries.length; - while (i--) { - if ( value === entries[i][1] ) { - return true; - } - } - return false; - } - }; - - /*----------------------------------------------------------------------------------------------------------------*/ - - // Supporting functions for searching hashtable buckets - - function searchBuckets(buckets, hash) { - var i = buckets.length, bucket; - while (i--) { - bucket = buckets[i]; - if (hash === bucket[0]) { - return i; - } - } - return null; - } - - function getBucketForHash(bucketsByHash, hash) { - var bucket = bucketsByHash[hash]; - - // Check that this is a genuine bucket and not something inherited from the bucketsByHash's prototype - return ( bucket && (bucket instanceof Bucket) ) ? bucket : null; - } - - /*----------------------------------------------------------------------------------------------------------------*/ - - function Hashtable() { - var buckets = []; - var bucketsByHash = {}; - var properties = { - replaceDuplicateKey: true, - hashCode: hashObject, - equals: null - }; - - var arg0 = arguments[0], arg1 = arguments[1]; - if (arg1 !== UNDEFINED) { - properties.hashCode = arg0; - properties.equals = arg1; - } else if (arg0 !== UNDEFINED) { - merge(properties, arg0); - } - - var hashCode = properties.hashCode, equals = properties.equals; - - this.properties = properties; - - this.put = function(key, value) { - checkKey(key); - checkValue(value); - var hash = hashCode(key), bucket, bucketEntry, oldValue = null; - - // Check if a bucket exists for the bucket key - bucket = getBucketForHash(bucketsByHash, hash); - if (bucket) { - // Check this bucket to see if it already contains this key - bucketEntry = bucket.getEntryForKey(key); - if (bucketEntry) { - // This bucket entry is the current mapping of key to value, so replace the old value. - // Also, we optionally replace the key so that the latest key is stored. - if (properties.replaceDuplicateKey) { - bucketEntry[0] = key; - } - oldValue = bucketEntry[1]; - bucketEntry[1] = value; - } else { - // The bucket does not contain an entry for this key, so add one - bucket.addEntry(key, value); - } - } else { - // No bucket exists for the key, so create one and put our key/value mapping in - bucket = new Bucket(hash, key, value, equals); - buckets.push(bucket); - bucketsByHash[hash] = bucket; - } - return oldValue; - }; - - this.get = function(key) { - checkKey(key); - - var hash = hashCode(key); - - // Check if a bucket exists for the bucket key - var bucket = getBucketForHash(bucketsByHash, hash); - if (bucket) { - // Check this bucket to see if it contains this key - var bucketEntry = bucket.getEntryForKey(key); - if (bucketEntry) { - // This bucket entry is the current mapping of key to value, so return the value. - return bucketEntry[1]; - } - } - return null; - }; - - this.containsKey = function(key) { - checkKey(key); - var bucketKey = hashCode(key); - - // Check if a bucket exists for the bucket key - var bucket = getBucketForHash(bucketsByHash, bucketKey); - - return bucket ? bucket.containsKey(key) : false; - }; - - this.containsValue = function(value) { - checkValue(value); - var i = buckets.length; - while (i--) { - if (buckets[i].containsValue(value)) { - return true; - } - } - return false; - }; - - this.clear = function() { - buckets.length = 0; - bucketsByHash = {}; - }; - - this.isEmpty = function() { - return !buckets.length; - }; - - var createBucketAggregator = function(bucketFuncName) { - return function() { - var aggregated = [], i = buckets.length; - while (i--) { - buckets[i][bucketFuncName](aggregated); - } - return aggregated; - }; - }; - - this.keys = createBucketAggregator("keys"); - this.values = createBucketAggregator("values"); - this.entries = createBucketAggregator("getEntries"); - - this.remove = function(key) { - checkKey(key); - - var hash = hashCode(key), bucketIndex, oldValue = null; - - // Check if a bucket exists for the bucket key - var bucket = getBucketForHash(bucketsByHash, hash); - - if (bucket) { - // Remove entry from this bucket for this key - oldValue = bucket.removeEntryForKey(key); - if (oldValue !== null) { - // Entry was removed, so check if bucket is empty - if (bucket.entries.length == 0) { - // Bucket is empty, so remove it from the bucket collections - bucketIndex = searchBuckets(buckets, hash); - buckets.splice(bucketIndex, 1); - delete bucketsByHash[hash]; - } - } - } - return oldValue; - }; - - this.size = function() { - var total = 0, i = buckets.length; - while (i--) { - total += buckets[i].entries.length; - } - return total; - }; - } - - Hashtable.prototype = { - each: function(callback) { - var entries = this.entries(), i = entries.length, entry; - while (i--) { - entry = entries[i]; - callback(entry[0], entry[1]); - } - }, - - equals: function(hashtable) { - var keys, key, val, count = this.size(); - if (count == hashtable.size()) { - keys = this.keys(); - while (count--) { - key = keys[count]; - val = hashtable.get(key); - if (val === null || val !== this.get(key)) { - return false; - } - } - return true; - } - return false; - }, - - putAll: function(hashtable, conflictCallback) { - var entries = hashtable.entries(); - var entry, key, value, thisValue, i = entries.length; - var hasConflictCallback = (typeof conflictCallback == FUNCTION); - while (i--) { - entry = entries[i]; - key = entry[0]; - value = entry[1]; - - // Check for a conflict. The default behaviour is to overwrite the value for an existing key - if ( hasConflictCallback && (thisValue = this.get(key)) ) { - value = conflictCallback(key, thisValue, value); - } - this.put(key, value); - } - }, - - clone: function() { - var clone = new Hashtable(this.properties); - clone.putAll(this); - return clone; - } - }; - - Hashtable.prototype.toQueryString = function() { - var entries = this.entries(), i = entries.length, entry; - var parts = []; - while (i--) { - entry = entries[i]; - parts[i] = encodeURIComponent( toStr(entry[0]) ) + "=" + encodeURIComponent( toStr(entry[1]) ); - } - return parts.join("&"); - }; - - return Hashtable; -})(); - -exports.Hashtable = Hashtable; \ No newline at end of file diff --git a/firefox_addon/lib/main.js b/firefox_addon/lib/main.js index 4800972..e08b7a1 100644 --- a/firefox_addon/lib/main.js +++ b/firefox_addon/lib/main.js @@ -20,14 +20,14 @@ var {Cc, Ci} = require("chrome"); var notifications = require("sdk/notifications"); -// http://www.timdown.co.uk/jshashtable/ -var Hashtable = require("jshashtable-3.0").Hashtable; var pkdbf2 = require("pkdbf2").pkdbf2; var aes = require("jsaes").aes; var parseURI = require("parseuri").parseURI; -var prefSet = require("simple-prefs"); +var prefSet = require("sdk/simple-prefs"); var DEBUG = false; - +var pkdbf2_level = prefSet.prefs.pkdbf2_level; +var protocol_version = 3; +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) { @@ -54,6 +54,185 @@ function debug(s) console.log(s); } +function generate_request(domain, login, mkey) +{ + v = "@@" + domain + ";" + login; + debug("will encrypt " + v); + debug("with " + a2hex(mkey)); + enc = aes.encryptLongString(v, aes.init(mkey)); + aes.finish(); + debug("res " + enc); + + return enc; +} + +function ask_server(field, logins, domain, wdomain, mkey, salt) +{ + mkey = pkdbf2.pkdbf2(mkey, salt, pkdbf2_level, 256/8); + + keys = ""; + for(a=0, b=logins.length; a protocol_version) + { + notifications.notify({ + title: "gPasss", + text: "Protocol version not supported, please upgrade your addon", + data: "Protocol version not supported, please upgrade your addon", + }); + ret = SERVER.FAILED; + } + else + { + switch (server_protocol_version) + { + case 2: + server_pkdbf2_level = 1000; + break; + case 3: + // Version 3 : nothing special to do + break; + } + } + break; + case "pass": + ciphered_password = params[1]; + break; + case "pkdbf2_level": + server_pkdbf2_level = parseInt(params[1].match(/\d+/)[0], 10); + if (server_pkdbf2_level != NaN && server_pkdbf2_level != pkdbf2_level) + { + debug("New pkdbf2 level " + server_pkdbf2_level); + pkdbf2_level = server_pkdbf2_level; + prefSet.prefs.pkdbf2_level = server_pkdbf2_level; + ret = SERVER.RESTART_REQUEST; + } + break; + case "": + break; + default: + debug("Unknown command " + params[0]); + + notifications.notify({ + title: "gPasss", + text: "Error : It seems that it's not a gPass server", + data: this.responseText, + }); + ret = SERVER.FAILED; + break; + } + } + + if (ret != SERVER.OK) + return; + + if (ciphered_password != "") + { + debug("Ciphered password : " + ciphered_password); + clear_password = aes.decryptLongString(hex2a(ciphered_password), aes.init(mkey)); + aes.finish(); + // Remove trailing \0 and salt + clear_password = clear_password.replace(/\0*$/, ""); + clear_password = clear_password.substr(0, clear_password.length-3); + debug("Clear password " + clear_password); + field.value = clear_password; + } + else + { + debug("No password found"); + + ret = SERVER.FAILED; + + notifications.notify({ + title: "gPasss", + text: "No password found in database", + data: "No password found in database", + }); + } + }, false); + gPassRequest.addEventListener("error", function(evt) { + debug("error"); + ret = false; + notifications.notify({ + title: "gPasss", + text: "Error", + data: "Error", + }); + + }, false); + debug("connect to " + prefSet.prefs.account_url); + gPassRequest.open("POST", prefSet.prefs.account_url, false); + gPassRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'); + gPassRequest.send(keys); + + return ret; +} + function wildcard_domain(domain) { parts = domain.split("."); @@ -78,17 +257,19 @@ function on_sumbit(e) { var form = this; var fields = form.getElementsByTagName("input"); - var my_map = new Hashtable(); domain = parseURI.parseUri(form.ownerDocument.baseURI); domain = domain["host"]; wdomain = wildcard_domain(domain); - salt = parseURI.parseUri(prefSet.prefs["account_url"]); + salt = parseURI.parseUri(prefSet.prefs.account_url); salt = salt["host"] + salt["path"]; debug("salt " + salt); + user = null; + all_logins = new Array; + // Get all && for (i=0; i for (i=0; i. */ +include("conf.php"); + function load_database() { try { @@ -31,7 +33,7 @@ function load_database() return $db; } -$PROTOCOL_VERSION = 2; +$PROTOCOL_VERSION = 3; $db = load_database(); @@ -40,6 +42,8 @@ $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"; for ($i=0; isset($_POST["k$i"]); $i++) { diff --git a/server/conf.php b/server/conf.php new file mode 100644 index 0000000..c1ade4a --- /dev/null +++ b/server/conf.php @@ -0,0 +1,41 @@ +/* + Copyright (C) 2013-2014 Grégory Soutadé + + This file is part of gPass. + + gPass is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + gPass is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with gPass. If not, see . +*/ + + \ No newline at end of file diff --git a/server/index.php b/server/index.php index 260ccf5..b527bed 100644 --- a/server/index.php +++ b/server/index.php @@ -1,6 +1,6 @@ + diff --git a/server/ressources/gpass.js b/server/ressources/gpass.js index 94409a0..88e363b 100755 --- a/server/ressources/gpass.js +++ b/server/ressources/gpass.js @@ -103,7 +103,7 @@ function a2hex(str) { function derive_mkey(user, mkey) { url = url_domain(document.URL) + "/" + user; - mkey = a2hex(pkdbf2(mkey, url, 1000, 256/8)); + mkey = a2hex(pkdbf2(mkey, url, pkdbf2_level, 256/8)); return mkey; }