Source: workhorsefunctions.js

/**
 * @module      workhorsefunctions
 * @overview    A suite of functions to implement the logic of interacting
 *              with the UI/DOM as well as the mechanism of interpreting
 *              and saving user preferences. This is the main "workhorse"
 *              responsible for implementing the algorithm.
 *
 * @author      Manjul Apratim (manjul.apratim@gmail.com)
 * @date        Nov 19, 2014
 *
 * @license     GNU General Public License v3 or Later
 * @copyright   Manjul Apratim, 2014, 2015
 */

"use strict";

// ========================================================================
// GLOBAL CONSTANTS

/**
 * @namespace
 * @summary A global namespace for miscellaneous "environment" variables.
 */
var AUX_ENV                         = {

    /**
     * @summary The default number of iterations of PBKDF2 (when unset).
     */
    defaultIterations               : 10000,

    /**
     * @summary The actionable events that could be triggered when
     *          interacting with the UI.
     * @enum    {string}
     */
    events                          : {
        CHANGE                      : "change",
        CLICK                       : "click",
        DONE                        : "done",
        ERROR                       : "error",
        GENERATE                    : "generate"
    },

    /**
     * @summary The indentation for "pretty-printing" JSON objects.
     */
    indentation                     : 4,

    /**
     * @summary The text field input type;
     *          when "text", the text field shows its contents,
     *          when "password", the text field contents are not visible.
     * @enum    {string}
     */
    inputType                       : {
        PASSWORD                    : "password",
        TEXT                        : "text"
    },

    /**
     * @summary A "category" to log with, to identify which component
     *          the log is coming from; for instance,
     *          if a log is coming from within the main thread or
     *          a worker thread.
     */
    logCategory                     : "WORKHORSE: ",

    /**
     * @summary The UTF-8 code used to identify "success" or "failure"
     *          when communicating back to the user (in this case when
     *          saving site attributes).
     *          SUCCESS would show up as a "check mark", and
     *          FAILURE would show up as a "cross".
     * @enum    {string}
     */
    successString                   : {
        SUCCESS                     : "\u2713",
        FAILURE                     : "\u2717"
    },

    /**
     * @summary Types referenced for the "typeof" command.
     * @enum    {string}
     */
    types                           : {
        FUNCTION                    : "function",
        STRING                      : "string",
        UNDEFINED                   : "undefined"
    }

};  // end namepace AUX_ENV

// ========================================================================
// CLASS DEFINITIONS

// ------------------------------------------------------------------------
// The Attributes class

/**
 * @class
 * @summary A class to describe the user-level "attributes" of a website.
 * @param   {string} domain - The domain (default null).
 * @param   {int} iterations - The number of PBKDF2 iterations
 *          (default null).
 * @param   {int} truncation - The number of characters to truncate to
 *          (default Attributes.NO_TRUNCATION).
 */
var Attributes = function(domain, iterations, truncation) {
    this.domain     = (AUX_ENV.types.UNDEFINED === typeof(domain) ?
                       null : domain);
    this.iterations = (AUX_ENV.types.UNDEFINED === typeof(iterations) ?
                       null : iterations);
    this.truncation = (AUX_ENV.types.UNDEFINED === typeof(truncation) ?
                       Attributes.NO_TRUNCATION : truncation);
};

/**
 * @summary A static value which indicates that no truncation
 *          will be performed on the result (since the number of
 *          characters to truncate to cannot be negative).
 */
Attributes.NO_TRUNCATION = -1;

/**
 * @summary A function to check if an object of the Attributes class
 * has been "set", or has the default values.
 * @return  {boolean} "true" or "false"
 */
Attributes.prototype.attributesExist = function() {
    return (null !== this.domain &&
            null !== this.iterations &&
            Attributes.NO_TRUNCATION !== this.truncation);
};

// ------------------------------------------------------------------------
// The AttributesCodec class

/**
 * @namespace AttributesCodec
 * @summary A class to code/decode Attributes objects/lists to/from
 *          the format they are stored in the browser preferences system.
 */
var AttributesCodec = {

    delimiter : "|",

    /**
     * @summary A function to parse the string representing
     *          the list of site attributes stored in the browser
     *          preferences system.
     * @param   {string} encodedAttributesList - The encoded string
     *          containing a stringified version
     *          of the JSON object representing
     *          the (domain, encoded Attributes) pair.
     * @return  {object} A JSON of key-value pairs with site domain as key
     *          and site attributes (in compact string form) as value.
     *          Values are of the form <salt|iterations|truncation>,
     *          where at most two of those may be empty.
     */
    getDecodedAttributesList : function(encodedAttributesList) {
        var siteAttributesList = {};

        // JSON.parse will fail if the input string is empty;
        // catch any exceptions and leave it to the user to proceed or not
        // (Empty attributes will be returned in this case).
        try {
            siteAttributesList = JSON.parse(encodedAttributesList);
        } catch (e) {
            console.info(AUX_ENV.logCategory +
                         "WARN Failed to parse siteAttributesList" +
                         ", errorMsg=\"" + e + "\"");
            return {};
        }

        console.debug(AUX_ENV.logCategory + "siteAttributesList=" +
                      JSON.stringify(siteAttributesList,
                                     null,
                                     AUX_ENV.indentation));
        return siteAttributesList;
    },

    /**
     * @summary A function to parse the attributes string for a domain
     *          into an object of the Attributes class.
     * @param   {string} encodedAttributes - The encoded attribute string,
     *          of the form <salt|iterations|truncation>,
     *          where any number of the three may be empty.
     *          This format is chosen to eliminate storing redundant
     *          information for a domain when a cetain attribute is
     *          identical to the one produced by the default algorithm.
     * @return  {Attributes} The decoded object.
     *          If decoding fails,
     *          a default initialized object is returned.
     */
    getDecodedAttributes : function(encodedAttributes) {
        // Create a "default-initialized" object of type "Attributes"
        var attributes = new Attributes();

        if (AUX_ENV.types.UNDEFINED !== typeof(encodedAttributes)) {
            var siteAttributesArray =
                encodedAttributes.split(AttributesCodec.delimiter);
            // Sanity check
            if (3 !== siteAttributesArray.length) {
                console.error(AUX_ENV.logCategory +
                              "ERROR: Malformed Attributes! " +
                              "(Expected <salt|iterations|truncation>)");
                return attributes;
            }

            // Check if the salt is non-empty
            if (siteAttributesArray[0] !== "") {
                attributes.domain = siteAttributesArray[0];
            }

            // Check if the number of iterations is non-empty
            if (siteAttributesArray[1] !== "") {
                attributes.iterations = parseInt(siteAttributesArray[1]);
            }

            // Check if the truncation parameter is non-empty
            if (siteAttributesArray[2] !== "") {
                attributes.truncation = parseInt(siteAttributesArray[2]);
            }
        }

        return attributes;
    },

    /**
     * @summary A function to obtain the saved "Attributes" object
     *          corresonding to a site domain.
     * @param   {string} domain - The site domain to obtain
     *          the saved attributes for.
     * @param   {object} savedAttributesList - The JSON object containing
     *          (domain, encoded Attributes) pairs.
     * @return  {Attributes} The saved attributes. If no saved attributes
     *          exist, a default initialized object is returned.
     */
    getSavedAttributes : function(domain, savedAttributesList) {
        var savedAttributes =
            (savedAttributesList.hasOwnProperty(domain) ?
             savedAttributesList[domain] : undefined);
        return this.getDecodedAttributes(savedAttributes);
    },

    /**
     * @summary A function to encode an "Attributes" object to the form
     *          <domain|iterations|truncation> for storage in the browser
     *          preferences system. Attributes that are reproducible by the
     *          default algorithm will be skipped for efficient storage
     *          (since sync space is precious). However, if a previously
     *          overridden attribute is reset to the default value, it
     *          will still be saved, since this module can only compare
     *          differences and does not know about defaults.
     * @param   {Attributes} savedAttributes - The existing
     *          saved attributes for a domain (default if none existed).
     * @param   {Attributes} proposedAttributes - The proposed attributes
     *          following the application of the algorithm
     *          on the saved/default attributes.
     * @param   {Attributes} attributes - The actual attributes
     *          that were captured from the UI
     *          when the user hit "Generate",
     *          which will encompass any custom changes the user made.
     * @return  {string} The encoded attributes to save, if asked so.
     */
    getEncodedAttributes : function(savedAttributes,
                                    proposedAttributes,
                                    attributes) {
        var encodedAttributes = "";

        var domainToSave = "";
        var iterationsToSave = "";
        var truncationToSave = "";
        if (savedAttributes.attributesExist()) {
            domainToSave =
                (proposedAttributes.domain !== attributes.domain) ?
                attributes.domain :
                (null !== savedAttributes.domain ?
                 savedAttributes.domain : "");
            iterationsToSave =
                (proposedAttributes.iterations !== attributes.iterations) ?
                attributes.iterations :
                (null !== savedAttributes.iterations ?
                 ("" + savedAttributes.iterations) : "");
            truncationToSave =
                (proposedAttributes.truncation !== attributes.truncation) ?
                attributes.truncation :
                (Attributes.NO_TRUNCATION !== savedAttributes.truncation ?
                 ("" + savedAttributes.truncation) : "");
        } else {
            domainToSave =
                (proposedAttributes.domain !== attributes.domain) ?
                attributes.domain : "";
            iterationsToSave =
                (proposedAttributes.iterations !== attributes.iterations) ?
                ("" + attributes.iterations) : "";
            truncationToSave =
                (proposedAttributes.truncation !== attributes.truncation) ?
                ("" + attributes.truncation) : "";
        }

        // A saved attributes entry is created IFF
        // at least one of the three elements is not empty.
        if (!(("" === domainToSave) &&
              ("" === iterationsToSave) &&
              ("" === truncationToSave))) {
            encodedAttributes =
                domainToSave + AttributesCodec.delimiter +
                iterationsToSave + AttributesCodec.delimiter +
                truncationToSave;
        }

        return encodedAttributes;
    },

    /**
     * @summary A function to encode the JSON (domain, encodedAttributes)
     *          object to the string used for storage in the browser
     *          preference system. The format is just the
     *          JSON stringification of the object.
     * @param   {string} domain - The site domain.
     * @param   {object} siteAttributesList - A JSON
     *          (domain, encodedAttributes)
     *          representing existing saved attributes
     *          in the browser's preference system.
     * @return  {string} The encoded list of attributes.
     */
    getEncodedAttributesList : function(domain,
                                        siteAttributesList,
                                        savedAttributes,
                                        proposedAttributes,
                                        attributes) {
        var encodedAttributes =
            this.getEncodedAttributes(savedAttributes,
                                      proposedAttributes,
                                      attributes);
        if ("" === encodedAttributes) {
            console.info(AUX_ENV.logCategory + "No custom changes to save");
            return "";
        } else {
            console.info(AUX_ENV.logCategory +
                         "customAttributes=" + encodedAttributes);
        }

        // Create new, or update existing
        siteAttributesList[domain] = encodedAttributes;
        console.debug(AUX_ENV.logCategory + "Saving siteAttributesList=" +
                      JSON.stringify(siteAttributesList,
                                     null,
                                     AUX_ENV.indentation));

        var encodedAttributesList = JSON.stringify(siteAttributesList);
        console.debug(AUX_ENV.logCategory +
                      "encodedAttributesList=" + encodedAttributesList);
        return encodedAttributesList;
    }

};

// ------------------------------------------------------------------------
// The DOM class

/**
 * @namespace
 * @summary A namespace for the elements of the DOM of the UI.
 */
var DOM                             = {

    elements                        : {
        domain                      : "domain",
        password                    : "password",
        hash                        : "hash",
        showHash                    : "showHash",
        iterations                  : "iterations",
        truncate                    : "truncate",
        truncation                  : "truncation",
        saveAttributes              : "saveAttributes",
        attributesSaveSuccessLabel  : "attributesSaveSuccessLabel",
        generateButton              : "generateButton",
    },

    /**
     * @namespace
     * @summary A namespace for event listeners for DOM elements.
     */
    listeners                       : {

        /**
         * @summary Event listener for the "show" event for
         *          the proxy password, which will toggle the visibility
         *          of the generated password by flipping the type
         *          of the "hash" field to
         *          "text" (show) or to "password" (don't show).
         * @return  {undefined}
         */
        showListener : function() {
            var checkBox = document.getElementById(DOM.elements.showHash);
            var hashField = document.getElementById(DOM.elements.hash);
            if (checkBox.checked) {
                hashField.type = AUX_ENV.inputType.TEXT;
            } else {
                hashField.type = AUX_ENV.inputType.PASSWORD;
            }
        },


        /**
         * @summary Event listener for the truncate checkbox.
         *          When checked, the number of characters to truncate to
         *          can be changed;
         *          when unchecked, the number of characters to truncate
         *          is reset to Attributes.NO_TRUNCATION and disabled.
         * @return  {undefined}
         */
        truncateListener : function() {
            var checkBox = document.getElementById(DOM.elements.truncate);
            var truncationBox =
                document.getElementById(DOM.elements.truncation);
            if (checkBox.checked) {
                truncationBox.disabled = false;
            } else {
                truncationBox.value = Attributes.NO_TRUNCATION;
                truncationBox.disabled = true;
            }
        },


        /**
         * @summary Event listener for the "Generate" button.
         * @return  {undefined}
         */
        generateListener : function() {
            var button =
                document.getElementById(DOM.elements.generateButton);

            // Obtain the "domain", the "iterations" and the "truncation"
            // from the input boxes
            // even if they were restored from saved attributes.
            // This in case the user made any custom changes.
            var attributes =
                new Attributes(document.getElementById(
                                    DOM.elements.domain).value,
                               parseInt(document.getElementById(
                                    DOM.elements.iterations).value),
                               parseInt(document.getElementById(
                                    DOM.elements.truncation).value));
            // Note that the script never sees
            // the input password itself at all -
            // it is hashed through SHA256
            // at the time of extraction itself.
            // This ensures the freedom to subsequently log
            // all kinds of data to the console
            // without compromising security.
            Workhorse.generate({
                domain              : button.domain,
                saltKey             : button.saltKey,
                seedSHA             : sjcl.codec.hex.fromBits(
                                        sjcl.hash.sha256.hash(
                                            document.getElementById(
                                            DOM.elements.password).value)),
                attributes          : attributes,
                proposedAttributes  : button.proposedAttributes,
                savedAttributes     : button.savedAttributes,
                saveAttributes      : document.getElementById(
                                      DOM.elements.saveAttributes).checked,
                savedAttributesList : button.savedAttributesList
            });
        }

    },  // end namespace "listeners"

    /**
     * @namespace
     * @summary A namespace for functions to "toggle" certain UI elements.
     */
    togglers                        : {

        /**
         * @summary A function to toggle the type and text of the
         *          proxy password field
         *          based on the kind of event encountered.
         * @param   {string} eventType - An enum indicating
         *          the type of event -
         *          "click" (show password),
         *          "done" (normal, don't show password), or
         *          "error" (show error message),
         *          "generate" (disable the field)
         * @param   {string} message - The value of the text to set.
         * @return  {undefined}
         */
        toggleHashField : function(eventType, message) {
            var hashField = document.getElementById(DOM.elements.hash);
            if (eventType === AUX_ENV.events.CLICK) {
                hashField.disabled = false;
                hashField.type = AUX_ENV.inputType.TEXT;
            } else if (eventType === AUX_ENV.events.DONE) {
                hashField.disabled = false;
                hashField.type = AUX_ENV.inputType.PASSWORD;
            } else if (eventType === AUX_ENV.events.ERROR) {
                hashField.disabled = false;
                hashField.type = AUX_ENV.inputType.TEXT;
            } else if (eventType === AUX_ENV.events.GENERATE) {
                hashField.disabled = true;
            }

            if (AUX_ENV.types.STRING === typeof(message)) {
                hashField.value = message;
            }
        },

        /**
         * @summary A function to toggle the "attributesSaveSuccessLabel"
         *          of the DOM based on whether the operation of
         *          saving site attributes, when asked, was a success.
         *          A "true" input shows a check mark,
         *          while a "false" input shows a cross.
         * @return  {undefined}
         */
        toggleAttributesSaveSuccess : function(success) {
            document.getElementById(
                DOM.elements.attributesSaveSuccessLabel).textContent =
                (success ?  AUX_ENV.successString.SUCCESS :
                            AUX_ENV.successString.FAILURE);
        }

    },  // end namespace "togglers"

    /**
     * @summary Function to configure all the elements of the DOM for
     *          the url in question. This is the "entry" point to the
     *          algorithm, which sets the stage for generating
     *          the "pseudo" password, as well as for returning
     *          the payload to the main routine.
     * @param   {object} options - General options to the algorithm,
     *          independent of the url.
     * @param   {object} params - Parameters specific to the url.
     *          It is expected to have the following properties:
     *          @prop   {string} params.url -  The site url,
     *          @prop   {int} params.defaultIterations - The default number
     *                  of PBKDF2 iterations,
     *          @prop   {string} params.saltKey - The saved
     *                  key for generating the salt from the domain name,
     *          @prop   {string} params.encodedAttributesList - The saved
     *                  attributes list for all sites.
     * @return  {undefined}
     */
    configure : function(options, params) {
        console.info(AUX_ENV.logCategory +
                     "workhorseOptions=" +
                     JSON.stringify(options, null, AUX_ENV.indentation) +
                     ",\nworkhorseParams=" +
                     JSON.stringify(params, null, AUX_ENV.indentation));

        // ----------------------------------------------------------------
        // Private Methods

        /**
         * @brief   A function to obtain the domain name of the url
         *          (more correctly, the domain-subdomain-subsub...).
         *          This will be used as the "key" to identify this
         *          website, e.g.,
         *          https://foo.bar.baz.lol/firstLevel/secondLevelPage.html
         *          would be reduced to foo.bar.baz.lol for translation.
         *          This ensures a unique input for that website even if
         *          the website is reorganized internally.
         *          Of course, if multiple pages for the same
         *          domain-subdomain allow different logins,
         *          one will be using the same password per above,
         *          which is probably not all that bad.
         *          The author is not really aware of such a case and
         *          no attempt shall be made to address them differently.
         * @param   {string} url - The website url.
         * @return  {string} The extracted domain name.
         */
        var extractDomain = function(url) {
            // Chop off the beginning "http://", "https://",
            // or "ftp://" or whatever, if it exists.
            // This so that different modes of accessing the same domain
            // do not necessitate different passwords.
            var domain = (url.indexOf("://") !== -1 ?
                          url.split("://")[1] : url);

            // Obtain the "root" of the url - the domain-subdomain
            domain = domain.split("/")[0];
            console.info(AUX_ENV.logCategory + "domain=" + domain);

            return domain;
        };

        /**
         * @brief   A function to configure
         *          the "domain" element of the DOM.
         * @param   {string} domain - The website domain.
         * @param   {string} savedDomain - The saved domain,
         *          (if overridden, and if exists).
         * @return  {string} The extracted domain name.
         */
        var configureDomain = function(domain,
                                       savedDomain) {
            var domainBox = document.getElementById(DOM.elements.domain);
            domainBox.value = (savedDomain ? savedDomain : domain);
            return domainBox.value;
        }

        /**
         * @summary A function to configure the "iterations" element
         *          of the DOM.
         * @param   {int} defaultIterations - The default number of
         *          PBKDF2 iterations.
         * @param   {int} savedIterations - An overridden number
         *          of PBKDF2 iterations stored for this domain (if exist).
         * @return  {int} The number of PBKDF2 iterations.
         */
        var configureIterations = function(defaultIterations,
                                           savedIterations) {
            var iterationBox =
                document.getElementById(DOM.elements.iterations);
            var iterations = savedIterations;
            if (!iterations) {
                iterations = defaultIterations;
            }
            if (!iterations) {
                iterations = AUX_ENV.defaultIterations;
            }
            console.info(AUX_ENV.logCategory + "iterations=" + iterations);
            iterationBox.value = iterations;
            return iterations;
        };

        /**
         * @summary A function to configure the "hash" element of the DOM,
         *          where the proxy password will be output.
         *          The field is disabled, until the algorithm is run.
         * @return  {undefined}
         */
        var configureHash = function() {
            // The proxy password output field
            var hashField = document.getElementById(DOM.elements.hash);
            hashField.value = "";
            hashField.disabled = true;

            // The "Show" checkbox for the proxy password
            var checkBox = document.getElementById(DOM.elements.showHash);

            // Add an event listener to the "click" action,
            // which toggles the visibility of the generated hash.
            checkBox.addEventListener(AUX_ENV.events.CHANGE,
                                      DOM.listeners.showListener);
        };

        /**
         * @summary A function to configure the "truncation" element
         *          of the DOM.
         * @param   {int} truncation - The number of truncation characters.
         *          If this number is equal to Attributes.NO_TRUNCATION,
         *          the input box will be disabled.
         * @return  {int} The number of characters to truncate 
         *          the proxy password to.
         */
        var configureTruncation = function(truncation) {
            var truncationBox =
                document.getElementById(DOM.elements.truncation);
            truncationBox.disabled =
                (Attributes.NO_TRUNCATION === truncation) ? true : false;
            truncationBox.value = truncation;

            // Configure the "truncate" checkbox of the DOM
            var checkBox = document.getElementById(DOM.elements.truncate);
            checkBox.checked =
                (Attributes.NO_TRUNCATION === truncation) ? false : true;

            // Add an event listener to the "click" action,
            // which toggles the editability of the truncation parameter.
            checkBox.addEventListener(AUX_ENV.events.CHANGE,
                                      DOM.listeners.truncateListener);

            return truncation;
        };

        /**
         * @summary A function to configure the "saveAttributes" checkbox
         *          of the DOM (unchecked by default), and the associated
         *          "attributesSaveSuccessLabel".
         *          By default, the label is empty.
         * @return  {undefined}
         */
        var configureSaveAttributes = function() {
            document.getElementById(
                DOM.elements.saveAttributes).checked = false;
            document.getElementById(
                DOM.elements.attributesSaveSuccessLabel).textContent = "";
        };

        /**
         * @summary A function to configure the "Generate" button
         *          of the DOM.
         * @param   {string} domain - The site domain.
         * @param   {string} saltKey - The key used for generating
         *          the salt from the domain name.
         * @param   {Attributes} savedAttributes - The saved attributes
         *          of the domain (default if none).
         * @param   {Attributes} proposedAttributes - The proposed
         *          attributes that would be applicable
         *          (after `337-translation etc),
         *          if the user did not make any custom changes.
         * @param   {string} savedAttributesList - The saved
         *          encoded attributes list for all domains.
         * @return  {undefined}
         */
        var configureGenerateButton = function(domain,
                                               saltKey,
                                               savedAttributes,
                                               proposedAttributes,
                                               savedAttributesList) {
            var button =
                document.getElementById(DOM.elements.generateButton);

            // Attach the other input parameters to the "button",
            // so that they may be accessed in the attached event listener
            // (cf. below).
            button.domain = domain;
            button.saltKey = saltKey;
            button.savedAttributesList = savedAttributesList;
            button.savedAttributes = savedAttributes;
            button.proposedAttributes = proposedAttributes;

            // Add an event listener to the "click" action,
            // whose purpose is to gather the values of the elements
            // when "Generate" is hit (the input to the algorithm).
            button.addEventListener(AUX_ENV.events.CLICK,
                                    DOM.listeners.generateListener);
        };

        // ----------------------------------------------------------------
        // Check if the saltKey was generated, i.e., it is not empty.
        if (!params.saltKey) {
            DOM.togglers.toggleHashField(AUX_ENV.events.ERROR,
                                         "ERROR: " +
                                         "saltKey === EMPTY");
            return;
        }

        // ----------------------------------------------------------------
        // Configure all the elements.

        console.info(AUX_ENV.logCategory + "Configuring elements...");

        // Check and extract saved site attributes, if any.
        var domain = extractDomain(params.url);

        var savedAttributesList =
            AttributesCodec.getDecodedAttributesList(
                                    params.encodedAttributesList);
        var savedAttributes =
            AttributesCodec.getSavedAttributes(domain,
                                               savedAttributesList);
        console.info(AUX_ENV.logCategory + "savedAttributes=" +
                     JSON.stringify(savedAttributes,
                                    null,
                                    AUX_ENV.indentation));

        var proposedAttributes =
            new Attributes(configureDomain(domain,
                                           savedAttributes.domain),
                           configureIterations(params.defaultIterations,
                                               savedAttributes.iterations),
                           configureTruncation(
                                         savedAttributes.truncation));

        configureHash();
        configureSaveAttributes();
        configureGenerateButton(domain,
                                params.saltKey,
                                savedAttributes,
                                proposedAttributes,
                                savedAttributesList);
    },

    /**
     * @summary Function to "deconfigure" the DOM so that it is ready for
     *          a new set of inputs.
     * @return  {undefined}
     */
    deconfigure : function() {
        console.info(AUX_ENV.logCategory + "Cleaning up resources...");
        // Remove all the event listeners
        var generateButton =
            document.getElementById(DOM.elements.generateButton);
        generateButton.removeEventListener(AUX_ENV.events.CLICK,
                                           DOM.listeners.generateListener);
        var truncateBox =
            document.getElementById(DOM.elements.truncate);
        truncateBox.removeEventListener(AUX_ENV.events.CHANGE,
                                        DOM.listeners.truncateListener);
        var showBox = document.getElementById(DOM.elements.showHash);
        showBox.removeEventListener(AUX_ENV.events.CHANGE,
                                    DOM.listeners.showListener);
    }

};  // end namespace "DOM"

// ------------------------------------------------------------------------
// The Workhorse class

/**
 * @summary A namespace for "workhorse" function, which implement the
 *          algorithm.
 */
var Workhorse           = {

    /**
     * @summary Function to generate the proxy password from all the input
     *          parameters.
     * @param   {object} params - The input parameters, which are expected
     *          to have the following properties:
     *          @prop {string} params.domain - The site domain,
     *          @prop {string} params.seedSHA - The SHA256 encoded
     *                  actual password,
     *          @prop {Attributes} params.savedAttributes The saved
     *                  attributes for the domain (default if none exist),
     *          @prop {Attributes} params.proposedAttributes - The proposed
     *                  attributes that would be applicable
     *                  if the user did not make any custom changes,
     *          @prop {Attributes} params.attributes - The current
     *                  attributes for the domain captured from the DOM
     *                  when "Generate" was hit.
     *          @prop {boolean} params.saveAttributes - A flag to determine
     *                  if the user asked to save the current attributes
     *                  for this domain,
     *          @prop {string} params.savedAttributesList - The encoded
     *                  list of
     *                  (domain, encoded Attributes) for all domains,
     *                  which will be mutated with the current
     *                  attributes if "saveAttributes" is true.
     * @return  {undefined}
     */
    generate : function(params) {
        // Clear the password field immediately
        document.getElementById(DOM.elements.password).value = "";
        // Clear the attributesSaveSuccessLabel (in case set)
        document.getElementById(
                DOM.elements.attributesSaveSuccessLabel).textContent = "";
        // Toggle the "show" checkbox
        document.getElementById(DOM.elements.showHash).checked = false;
        DOM.togglers.toggleHashField(AUX_ENV.events.GENERATE, "");

        console.debug(AUX_ENV.logCategory + "generateParams=" +
                      JSON.stringify(params, null, AUX_ENV.indentation));

        // Check if Web Workers are supported
        if (AUX_ENV.types.UNDEFINED === typeof(Worker)) {
            // No support for web workers;
            // do nothing,
            // since doing intensive operations here will
            // block the UI thread.
            // Modern browsers support the API, so no reason to be burdened
            // by chains of the past.
            console.error(AUX_ENV.logCategory +
                          "ERROR: No web worker support!");
            DOM.togglers.toggleHashField(AUX_ENV.events.ERROR,
                                         "ERROR: " +
                                         "browser.Type === ANCIENT");
            return;
        }

        console.info(AUX_ENV.logCategory + "Firing web worker HASHER...");
        var hasher = new Worker("hasher.js");

        hasher.onmessage = function(oEvent) {
            var logCategory = "HASHER: ";
            console.debug(logCategory +
                          JSON.stringify(oEvent.data,
                                         null,
                                         AUX_ENV.indentation));
            var eventData = oEvent.data;

            // Finalize the worker, if this is the "done" signal
            if (eventData.hasOwnProperty(AUX_ENV.events.DONE)) {
                console.info(AUX_ENV.logCategory + "Finalizing worker...");
                DOM.togglers.toggleHashField(AUX_ENV.events.DONE,
                                             eventData.password);

                var encodedAttributesList = "";
                if (params.saveAttributes) {
                    console.info(AUX_ENV.logCategory +
                                 "Generating attributes list string...");
                    encodedAttributesList =
                        AttributesCodec.getEncodedAttributesList(
                                            params.domain,
                                            params.savedAttributesList,
                                            params.savedAttributes,
                                            params.proposedAttributes,
                                            params.attributes);
                }

                // Check if a function "finalize" is defined,
                // and if so, call it.
                if (AUX_ENV.types.FUNCTION === typeof(finalize)) {
                    finalize({
                        domain                  : params.domain,
                        password                : eventData.password,
                        attributesListString    : encodedAttributesList
                    });
                }

                // Post a message to the worker to close itself.
                // DO NOT CALL "hasher.terminate()",
                // which rudely terminates the web worker
                // without a chance to clean up.
                hasher.postMessage({ done : AUX_ENV.events.DONE });
            };
        };

        hasher.onerror = function(oEvent) {
            throw new Error(oEvent.message +
                            " (" + oEvent.filename +
                            ":" + oEvent.lineno + ")");
        };

        DOM.togglers.toggleHashField(AUX_ENV.events.CLICK,
                                     "<Generating...>");
        var hasherParams = {
            saltKey         : params.saltKey,
            seedSHA         : params.seedSHA,
            attributes      : params.attributes,
        };
        hasher.postMessage(hasherParams);
    }

};  // end namespace Workhorse