kripod/hmac-rng.js

View on GitHub
lib/index.js

Summary

Maintainability
A
35 mins
Test Coverage
var crypto = require('crypto');

/**
 * @module HmacRng
 */
module.exports = HmacRng;

/**
 * Creates a new HMAC-RNG instance.
 * @constructor
 * @alias module:HmacRng
 * @param {string} seed Seed used for randomization.
 * @param {string} [algorithm] Cryptographic algorithm to use HMAC with.
 */
function HmacRng(seed, algorithm) {
  this.seed = seed;
  this.algorithm = algorithm || HmacRng.defaultAlgorithm;

  // Initialize the first hash to generate outputs from
  this._changeHash(seed);
}

/**
 * Determines the default cryptographic algorithm to use HMAC with.
 * Useful for altering the output of static methods.
 */
HmacRng.defaultAlgorithm = 'sha512';

/**
 * Gets the next random integer in the current sequence.
 * The maximum range of 'min' and 'max' is 2^28 (268435456).
 * @param {number} min Inclusive lower bound of the random integer returned.
 * @param {number} max Inclusive upper bound of the random integer returned.
 * This must be greater than 'min'.
 * @returns {number} The generated random integer.
 */
HmacRng.prototype.nextInt = function (min, max) {
  var outputRangeMinus1 = max - min;
  var outputRange = outputRangeMinus1 + 1;

  // Calculate the amount of HEX digits to read from the hash
  var hexDigitCount;
  for (var i = 1; i < 8; i++) {
    if (outputRangeMinus1 >> (4 * i) === 0) {
      // Read one more digit than needed to improve efficiency
      hexDigitCount = Math.min(i + 1, 7);
      break;
    }
  }

  // Calculate the range of the HEX substring's possible values
  var hexRange = 1 << (4 * hexDigitCount);

  // Get the lowest integer which is not part of the equal distribution range
  var firstTooHighValue = Math.floor(hexRange / outputRange) * outputRange;

  // Iterate through the HEX substrings of the hash.
  // Equal distribution is ensured by continuing with the next substring if the
  // current substring is not qualifiable for the equal distribution range.
  while (true) { // eslint-disable-line no-constant-condition
    var value = parseInt(this._readHash(hexDigitCount), 16);
    if (firstTooHighValue === 0 || value < firstTooHighValue) {
      // Return as soon as a valid output can be generated
      return (value % outputRange) + min;
    }
  }
};

/**
 * Gets the next random integers in the current sequence.
 * The maximum range of 'min' and 'max' is 2^28 (268435456).
 * @param {number} min Inclusive lower bound of the random integers returned.
 * @param {number} max Inclusive upper bound of the random integers returned.
 * This must be greater than 'min'.
 * @param {number} amount Amount of integers to be generated.
 * @returns {number[]} The generated array of random integers.
 */
HmacRng.prototype.nextInts = function (min, max, amount) {
  var output = [];
  for (var i = amount; i > 0; i--) {
    output.push(this.nextInt(min, max));
  }
  return output;
};

/**
 * Generates a random integer using the default algorithm.
 * The maximum range of 'min' and 'max' is 2^28 (268435456).
 * @param {string} seed Seed used for randomization.
 * @param {number} min Inclusive lower bound of the random integer returned.
 * @param {number} max Inclusive upper bound of the random integer returned.
 * This must be greater than 'min'.
 * @returns {number} The generated random integer.
 * @since 1.1.0
 */
HmacRng.getRandomInt = function (seed, min, max) {
  return new HmacRng(seed).nextInt(min, max);
};

/**
 * Generates random integers using the default algorithm.
 * The maximum range of 'min' and 'max' is 2^28 (268435456).
 * @param {string} seed Seed used for randomization.
 * @param {number} min Inclusive lower bound of the random integers returned.
 * @param {number} max Inclusive upper bound of the random integers returned.
 * This must be greater than 'min'.
 * @param {number} amount Amount of integers to be generated.
 * @returns {number[]} The generated array of random integers.
 * @since 1.1.0
 */
HmacRng.getRandomInts = function (seed, min, max, amount) {
  return new HmacRng(seed).nextInts(min, max, amount);
};

/**
 * Shuffles the given array using the default algorithm.
 * @param {string} seed Seed used for randomization.
 * @param {Object[]} array Array to be shuffled.
 * @returns {Object[]} The array which has been shuffled.
 * @since 1.1.0
 */
HmacRng.shuffleArray = function (seed, array) {
  var instance = new HmacRng(seed);
  var output = array.slice(0);

  // Iterating backwards is extremely important
  for (var i = output.length - 1; i > 0; i--) {
    // Generate a random integer between 0 and i
    var j = instance.nextInt(0, i);

    // Exchange output[i] and output[j]
    var temp = output[i];
    output[i] = output[j];
    output[j] = temp;
  }

  return output;
};

HmacRng.prototype._readHash = function (amount) {
  var pointer = this._pointer;
  var hash = this._hash;

  // Change the hash if necassary
  if (pointer + amount > hash.length) {
    hash = this._changeHash(this._hash);
  }

  // Read a specific amount of chars from the hash, increase the pointer's
  // value, and then return the output
  var output = hash.substr(pointer, amount);
  this._pointer += amount;
  return output;
};

HmacRng.prototype._changeHash = function (key) {
  // Change the hash based on the given key. Update with the original seed to
  // avoid potential security issues when a hash collision happens.
  this._pointer = 0;
  return this._hash = crypto.createHmac(this.algorithm, key)
    .update(this.seed)
    .digest('hex');
};