I am looking for feedback and possible suggestions regarding a piece of JS code that performs transparent symmetric encryption/decryption of user specified data using the HTML5 localStorage
, sessionStorage
or depreciated cookie options.
The project can be viewed in its entirety at here.
/**
* secStore.js - Encryption enabled browser storage
*
* https://www.github.com/jas-/secStore.js
*
* Author: Jason Gerfen <[email protected]>
* License: GPL (see LICENSE)
*/
(function(window, undefined) {
'use strict';
/**
* @function secStore
* @abstract Namespace for saving/retrieving encrypted HTML5 storage engine
* data
*/
var secStore = secStore || function() {
/**
* @var {Object} defaults
* @abstract Default set of options for plug-in
*
* @param {Boolean} encrypt Optionally encrypt stored data
* @param {Object} data Data to be setd (JSON objects)
* @param {String} passphrase Passphrase to use (optional)
* @param {String} storage Storage mechanism (local, session or cookies)
*/
var defaults = {
encrypt: false,
data: {},
key: 'secStore.js',
passphrase: '',
storage: 'local'
};
/**
* @method setup
* @scope private
* @abstract Initial setup routines
*/
var setup = setup || {
/**
* @function set
* @scope private
* @abstract Initialization
*
* @param {Object} opts Plug-in option object
*/
init: function(opts) {
opts.passphrase = (opts.encrypt && opts.passphrase) ?
opts.passphrase : (opts.encrypt && !opts.passphrase) ?
crypto.key(opts) : false;
}
};
/**
* @method storage
* @scope private
* @abstract Interface to handle storage options
*/
var storage = storage || {
/**
* @function quota
* @scope private
* @abstract Tests specified storage option for current amount of space available.
* - Cookies: 4K
* - localStorage: 5MB
* - sessionStorage: 5MB
* - default: 5MB
*
* @param {String} t Type of storage specified
*
* @returns {Boolean}
*/
quota: function(storage) {
var max = /local|session/.test(storage) ? 1024 * 1025 * 5 :
1024 * 4,
cur = libs.total(storage),
total = max - cur;
if (total <= 0) {
return false;
}
return true;
},
/**
* @function set
* @scope private
* @abstract Interface for saving to available storage mechanisms
*
* @param {Object} opts Default options
* @param {Function} cb Callback function
*
* @returns {Boolean}
*/
set: function(opts, cb) {
var ret = false;
if (!storage.quota(opts.storage))
cb('Browser storage quota has been exceeded.');
opts.data = (opts.encrypt) ?
sjcl.encrypt(opts.passphrase, storage.fromJSON(opts.data)) :
storage.fromJSON(opts.data);
switch (opts.storage) {
case 'cookie':
ret = this.cookie.set(opts);
break;
case 'local':
ret = this.local.set(opts);
break;
case 'session':
ret = this.session.set(opts);
break;
default:
ret = this.local.set(opts);
break;
}
if (!ret) {
cb('Error occured saving data');
} else {
cb(null, 'Successfully set data');
}
},
/**
* @function get
* @scope private
* @abstract Interface for retrieving from available storage mechanisms
*
* @param {Object} opts Default options
* @param {Function} cb Callback function
*
* @returns {Object}
*/
get: function(opts, cb) {
var ret = {};
switch (opts.storage) {
case 'cookie':
ret = this.cookie.get(opts);
break;
case 'local':
ret = this.local.get(opts);
break;
case 'session':
ret = this.session.get(opts);
break;
default:
ret = this.local.get(opts);
break;
}
ret = sjcl.decrypt(opts.passphrase, ret);
ret = storage.toJSON(ret);
if (/object/.test(ret)) {
cb(null, ret);
} else {
cb('Error occured retrieving storage data');
}
},
/**
* @function fromJSON
* @scope private
* @abstract Convert to JSON object to string
*
* @param {Object|Array|String} obj Object, Array or String to convert to JSON object
*
* @returns {String}
*/
fromJSON: function(obj) {
return (/object/.test(typeof(obj))) ? JSON.stringify(obj) : obj;
},
/**
* @function toJSON
* @scope private
* @abstract Creates JSON object from formatted string
*
* @param {String} obj Object to convert to JSON object
*
* @returns {Object}
*/
toJSON: function(obj) {
return (/string/.test(typeof(obj))) ? JSON.parse(obj) : obj;
},
/**
* @method cookie
* @scope private
* @abstract Method for handling setting & retrieving of cookie objects
*/
cookie: {
/**
* @function set
* @scope private
* @abstract Handle setting of cookie objects
*
* @param {String} key Key to use for cookies
* @param {String|Object} value String or object to place in cookie
*
* @returns {Boolean}
*/
set: function(key, value) {
var d = new Date();
d.setTime(d.getTime() + (30 * 24 * 60 * 60 * 1000));
document.cookie = key + '=' + value + ';expires=' + d.toGMTString() +
';path=/;domain=' + this.domain();
return true;
},
/**
* @function get
* @scope private
* @abstract Handle retrieval of cookie objects
*
* @param {String} key cookie key
*
* @returns {String|False}
*/
get: function(key) {
var i, x, y, z = document.cookie.split(";");
for (i = 0; i < z.length; i++) {
x = z[i].substr(0, z[i].indexOf('='));
y = z[i].substr(z[i].indexOf('=') + 1);
x = x.replace(/^\s+|\s+$/g, '');
if (x == key) {
return unescape(y);
}
}
return false;
},
/**
* @function domain
* @scope private
* @abstract Provides current domain of client for cookie realm
*
* @returns {String}
*/
domain: function() {
return location.hostname;
}
},
/**
* @method local
* @scope private
* @abstract Method for handling setting & retrieving of localStorage objects
*/
local: {
/**
* @function set
* @scope private
* @abstract Handle setting & retrieving of localStorage objects
*
* @param {Object} opts Application defaults
*
* @returns {Boolean}
*/
set: function(opts) {
try {
window.localStorage.setItem(opts.key, opts.data);
return true;
} catch (e) {
return false;
}
},
/**
* @function get
* @scope private
* @abstract Handle retrieval of localStorage objects
*
* @param {Object} o Application defaults
*
* @returns {Object|String|Boolean}
*/
get: function(opts) {
return window.localStorage.getItem(opts.key);
}
},
/**
* @method session
* @scope private
* @abstract Method for handling setting & retrieving of sessionStorage objects
*/
session: {
/**
* @function set
* @scope private
* @abstract Set session storage objects
*
* @param {Object} o Application defaults
*
* @returns {Boolean}
*/
set: function(opts) {
try {
window.sessionStorage.setItem(opts.key, opts.data);
return true;
} catch (e) {
return false;
}
},
/**
* @function get
* @scope private
* @abstract Retrieves sessionStorage objects
*
* @param {Object} opts Application defaults
*
* @returns {Object|String|Boolean}
*/
get: function(opts) {
return window.sessionStorage.getItem(opts.key);
}
}
};
/**
* @method crypto
* @scope private
* @abstract Interface to handle encryption option
*/
var crypto = crypto || {
/**
* @function key
* @scope private
* @abstract Prepares key for encryption/decryption routines
*
* @returns {String}
*/
key: function() {
var pass = crypto.uid(),
salt = crypto.salt(pass);
return sjcl.codec.hex.fromBits(sjcl.misc.pbkdf2(pass, salt,
10000, 256));
},
/**
* @function uid
* @scope private
* @abstract Generates a machine identifier
*
* @returns {String}
*/
uid: function() {
var ret = window.navigator.appName +
window.navigator.appCodeName +
window.navigator.product +
window.navigator.productSub +
window.navigator.appVersion +
window.navigator.buildID +
window.navigator.userAgent +
window.navigator.language +
window.navigator.platform +
window.navigator.oscpu;
return ret.replace(/\s/, '');
},
/**
* @function salt
* @scope private
* @abstract Creates salt from string & iv
*
* @param {String} str Machine identification used as salt
*
* @returns {String}
*/
salt: function(str) {
var rec, ret, hash = [],
slt = crypto.iv(str);
hash[0] = sjcl.hash.sha256.hash(str), rec = [], rec = hash[0],
ret;
for (var i = 1; i < 3; i++) {
hash[i] = sjcl.hash.sha256.hash(hash[i - 1].concat(slt));
ret = rec.concat(hash[i]);
}
return JSON.stringify(sjcl.codec.hex.fromBits(ret));
},
/**
* @function iv
* @scope private
* @abstract Creates IV based on UID
*
* @param {String} str Supplied string
*
* @returns {String}
*/
iv: function(str) {
return encodeURI(str.replace(/-/gi, '').substring(16, Math.ceil(
16 * str.length) % str.length));
}
};
/**
* @method libs
* @scope private
* @abstract Miscellaneous helper libraries
*/
var libs = libs || {
/**
* @function total
* @scope private
* @abstract Returns size of specified storage
*
* @param {String} engine Storage mechanism
*
* @returns {Insteger}
*/
total: function(storage) {
var current = '',
engine = window.storage + 'Storage';
for (var key in engine) {
if (engine.hasOwnProperty(key)) {
current += engine[key];
}
}
return current ? 3 + ((current.length * 16) / (8 * 1024)) : 0;
},
/**
* @function size
* @scope private
* @abstract Perform calculation on objects
*
* @param {Object|Array} obj The object/array to calculate
*
* @returns {Integer}
*/
size: function(obj) {
var n = 0;
if (/object/.test(typeof(obj))) {
for (var i in obj) {
if (obj.hasOwnProperty(obj[i])) n++;
}
} else if (/array/.test(typeof(obj))) {
n = obj.length;
}
return n;
},
/**
* @function merge
* @scope private
* @abstract Perform preliminary option/default object merge
*
* @param {Object} defaults Application defaults
* @param {Object} obj User supplied object
*
* @returns {Object}
*/
merge: function(defaults, obj) {
defaults = defaults || {};
for (var item in defaults) {
if (defaults.hasOwnProperty(item)) {
obj[item] = (/object/.test(typeof(defaults[item]))) ?
this.merge(obj[item], defaults[item]) : defaults[item];
}
obj[item] = defaults[item];
}
return obj;
}
};
/**
* @function get
* @scope public
* @abstract Retrieves storage engine data
*
* @param {Object} obj User supplied options
* @param {Function} cb User supplied callback function
*/
secStore.prototype.get = function(obj, cb) {
cb = cb || obj;
var opts = libs.merge(obj, defaults);
setup.init(opts);
storage.get(opts, cb);
};
/**
* @function set
* @scope public
* @abstract Saves data to specified storage engine
*
* @param {Object} obj User supplied options
* @param {Function} cb User supplied callback function
*/
secStore.prototype.set = function(obj, cb) {
cb = cb || obj;
var opts = libs.merge(obj, defaults);
setup.init(opts);
storage.set(opts, cb);
};
};
/* secStore.js, do work */
window.secStore = secStore;
})(window);
1 Answer 1
From a once over:
- Yay, GPL! I love GPL, note that by posting your code here anybody can use this now as not-GPL
This is the most readable nested ternary I ever saw
init: function(opts) { opts.passphrase = (opts.encrypt && opts.passphrase) ? opts.passphrase : (opts.encrypt && !opts.passphrase) ? crypto.key(opts) : false; }
You could consider this
init: function(opts) { opts.passphrase = opts.encrypt ? (opts.passphrase || crypto.key(opts)) : false; }
Considering the craftyness of your other code I was surprised to find this:
if (total <= 0) { return false; } return true;
consider
return !(total <= 0); //Or.. return total > 0;
If your switch equals the exact function name like here:
switch (opts.storage) { case 'cookie': ret = this.cookie.set(opts); break; case 'local': ret = this.local.set(opts); break; case 'session': ret = this.session.set(opts); break; default: ret = this.local.set(opts); break; }
You can just simply access the function dynamically
ret = this[opt.storage] ? this[opt.storage].set(opts) : this.local.set(opts);
var i, x, y, z = document.cookie.split(";");
<- x,y,z are unfortunate variable names, I am sure they got teased a lot in school- To name a a machine identifier
uid
is not ideal, usuallyuid
is reserved for unique record id's - I did not review any of the cryptographic code, but at least you depend on a third party library, that is 90% of the work
-
\$\begingroup\$ Thanks. I always forget about simply calling the variable as a function. Can you point out the sentence within the GPL that would allow that? Is it GPLv3? I am not a legal jargon master so maybe I need to change the license. \$\endgroup\$jas-– jas-2014年11月15日 15:45:27 +00:00Commented Nov 15, 2014 at 15:45
-
\$\begingroup\$ It's in the usage terms of stackexchange.com, as I understand it, all your posted code becomes creativecommons.org/licenses/by-sa/2.5 \$\endgroup\$konijn– konijn2014年11月15日 18:43:21 +00:00Commented Nov 15, 2014 at 18:43
-
\$\begingroup\$ No kidding, well didn't know that prior to posting here. I suppose the version that is here is the licensed as such but those getting it from my project page (and updated versions) are then under the GPL license correct? \$\endgroup\$jas-– jas-2014年11月15日 20:26:43 +00:00Commented Nov 15, 2014 at 20:26
-
\$\begingroup\$ IANAl, but that is indeed my understanding \$\endgroup\$konijn– konijn2014年11月16日 03:09:05 +00:00Commented Nov 16, 2014 at 3:09
@
things. A short description for when the function name and arguments aren't obviously shouting the purpose of the function is fine, but echoing the function name, scope, arguments, and return value is a bit OTT in my view. I'd write most of them as a single line comment with just the abstract. \$\endgroup\$