3
\$\begingroup\$

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 <jason.gerfen@gmail.com>
 * 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);
\$\endgroup\$
  • \$\begingroup\$ I think there's too much commenting, especially with the @ 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\$ – bjb568 Nov 15 '14 at 4:19
  • \$\begingroup\$ I can appreciate that. However for an automated method of creating documentation it comes in very handy. \$\endgroup\$ – jas- Nov 15 '14 at 16:12
2
\$\begingroup\$

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, usually uid 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
\$\endgroup\$
  • \$\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- Nov 15 '14 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 Nov 15 '14 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- Nov 15 '14 at 20:26
  • \$\begingroup\$ IANAl, but that is indeed my understanding \$\endgroup\$ – konijn Nov 16 '14 at 3:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.