Source: encrypt/producer.js

/**
 * Copyright (C) 2015-2016 Regents of the University of California.
 * @author: Jeff Thompson <jefft0@remap.ucla.edu>
 * @author: From ndn-group-encrypt src/producer https://github.com/named-data/ndn-group-encrypt
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * A copy of the GNU Lesser General Public License is in the file COPYING.
 */

/** @ignore */
var Name = require('../name.js').Name; /** @ignore */
var Interest = require('../interest.js').Interest; /** @ignore */
var Data = require('../data.js').Data; /** @ignore */
var Exclude = require('../exclude.js').Exclude; /** @ignore */
var Encryptor = require('./algo/encryptor.js').Encryptor; /** @ignore */
var EncryptParams = require('./algo/encrypt-params.js').EncryptParams; /** @ignore */
var EncryptAlgorithmType = require('./algo/encrypt-params.js').EncryptAlgorithmType; /** @ignore */
var AesKeyParams = require('../security/key-params.js').AesKeyParams; /** @ignore */
var AesAlgorithm = require('./algo/aes-algorithm.js').AesAlgorithm; /** @ignore */
var Schedule = require('./schedule.js').Schedule; /** @ignore */
var EncryptError = require('./encrypt-error.js').EncryptError; /** @ignore */
var NdnCommon = require('../util/ndn-common.js').NdnCommon; /** @ignore */
var SyncPromise = require('../util/sync-promise.js').SyncPromise;

/**
 * A Producer manages content keys used to encrypt a data packet in the
 * group-based encryption protocol.
 * Create a Producer to use the given ProducerDb, Face and other values.
 *
 * A producer can produce data with a naming convention:
 *   /<prefix>/SAMPLE/<dataType>/[timestamp]
 *
 * The produced data packet is encrypted with a content key,
 * which is stored in the ProducerDb database.
 *
 * A producer also needs to produce data containing a content key
 * encrypted with E-KEYs. A producer can retrieve E-KEYs through the face,
 * and will re-try for at most repeatAttemps times when E-KEY retrieval fails.
 *
 * @param {Name} prefix The producer name prefix. This makes a copy of the Name.
 * @param {Name} dataType The dataType portion of the producer name. This makes
 * a copy of the Name.
 * @param {Face} face The face used to retrieve keys.
 * @param {KeyChain} keyChain The keyChain used to sign data packets.
 * @param {ProducerDb} database The ProducerDb database for storing keys.
 * @param {number} repeatAttempts (optional) The maximum retry for retrieving
 * keys. If omitted, use a default value of 3.
 * @note This class is an experimental feature. The API may change.
 * @constructor
 */
var Producer = function Producer
  (prefix, dataType, face, keyChain, database, repeatAttempts)
{
  this.face_ = face;
  this.keyChain_ = keyChain;
  this.database_ = database;
  this.maxRepeatAttempts_ = (repeatAttempts == undefined ? 3 : repeatAttempts);

  // The map key is the key name URI string. The value is an object with fields
  // "keyName" and "keyInfo" where "keyName" is the same Name used for the key
  // name URI string, and "keyInfo" is the Producer.KeyInfo_.
  // (Use a string because we can't use the Name object as the key in JavaScript.)
  // (Also put the original Name in the value because we need to iterate over
  // eKeyInfo_ and we don't want to rebuild the Name from the name URI string.)
  this.eKeyInfo_ = {};
  // The map key is the time stamp. The value is a Producer.KeyRequest_.
  this.keyRequests_ = {};

  var fixedPrefix = new Name(prefix);
  var fixedDataType = new Name(dataType);

  // Fill ekeyInfo_ with all permutations of dataType, including the 'E-KEY'
  // component of the name. This will be used in createContentKey to send
  // interests without reconstructing names every time.
  fixedPrefix.append(Encryptor.NAME_COMPONENT_READ);
  while (fixedDataType.size() > 0) {
    var nodeName = new Name(fixedPrefix);
    nodeName.append(fixedDataType);
    nodeName.append(Encryptor.NAME_COMPONENT_E_KEY);

    this.eKeyInfo_[nodeName.toUri()] =
      { keyName: nodeName, keyInfo: new Producer.KeyInfo_() };
    fixedDataType = fixedDataType.getPrefix(-1);
  }
  fixedPrefix.append(dataType);
  this.namespace_ = new Name(prefix);
  this.namespace_.append(Encryptor.NAME_COMPONENT_SAMPLE);
  this.namespace_.append(dataType);
};

exports.Producer = Producer;

/**
 * Create the content key corresponding to the timeSlot. This first checks if
 * the content key exists. For an existing content key, this returns the
 * content key name directly. If the key does not exist, this creates one and
 * encrypts it using the corresponding E-KEYs. The encrypted content keys are
 * passed to the onEncryptedKeys callback.
 * @param {number} timeSlot The time slot as milliseconds since Jan 1, 1970 UTC.
 * @param {function} onEncryptedKeys If this creates a content key, then this
 * calls onEncryptedKeys(keys) where keys is a list of encrypted content key
 * Data packets. If onEncryptedKeys is null, this does not use it.
 * NOTE: The library will log any exceptions thrown by this callback, but for
 * better error handling the callback should catch and properly handle any
 * exceptions.
 * @param {function} onContentKeyName This calls onContentKeyName(contentKeyName)
 * with the content key name for the time slot. If onContentKeyName is null,
 * this does not use it. (A callback is needed because of async database
 * operations.)
 * NOTE: The library will log any exceptions thrown by this callback, but for
 * better error handling the callback should catch and properly handle any
 * exceptions.
 * @param {function} onError (optional) This calls onError(errorCode, message)
 * for an error, where errorCode is from EncryptError.ErrorCode and message is a
 * string. If omitted, use a default callback which does nothing.
 * NOTE: The library will log any exceptions thrown by this callback, but for
 * better error handling the callback should catch and properly handle any
 * exceptions.
 */
Producer.prototype.createContentKey = function
  (timeSlot, onEncryptedKeys, onContentKeyName, onError)
{
  if (!onError)
    onError = Producer.defaultOnError;

  var hourSlot = Producer.getRoundedTimeSlot_(timeSlot);

  // Create the content key name.
  var contentKeyName = new Name(this.namespace_);
  contentKeyName.append(Encryptor.NAME_COMPONENT_C_KEY);
  contentKeyName.append(Schedule.toIsoString(hourSlot));

  var contentKeyBits;
  var thisProducer = this;

  // Check if we have created the content key before.
  this.database_.hasContentKeyPromise(timeSlot)
  .then(function(exists) {
    if (exists) {
      if (onContentKeyName != null)
        onContentKeyName(contentKeyName);
      return;
    }

    // We haven't created the content key. Create one and add it into the database.
    var aesParams = new AesKeyParams(128);
    contentKeyBits = AesAlgorithm.generateKey(aesParams).getKeyBits();
    thisProducer.database_.addContentKeyPromise(timeSlot, contentKeyBits)
    .then(function() {
      // Now we need to retrieve the E-KEYs for content key encryption.
      var timeCount = Math.round(timeSlot);
      thisProducer.keyRequests_[timeCount] =
        new Producer.KeyRequest_(thisProducer.getEKeyInfoSize_());
      var keyRequest = thisProducer.keyRequests_[timeCount];

      // Check if the current E-KEYs can cover the content key.
      var timeRange = new Exclude();
      Producer.excludeAfter
        (timeRange, new Name.Component(Schedule.toIsoString(timeSlot)));
      for (var keyNameUri in thisProducer.eKeyInfo_) {
         // For each current E-KEY.
        var entry = thisProducer.eKeyInfo_[keyNameUri];
        var keyInfo = entry.keyInfo;
        if (timeSlot < keyInfo.beginTimeSlot || timeSlot >= keyInfo.endTimeSlot) {
          // The current E-KEY cannot cover the content key, so retrieve one.
          keyRequest.repeatAttempts[keyNameUri] = 0;
          thisProducer.sendKeyInterest_
            (new Interest(entry.keyName).setExclude(timeRange).setChildSelector(1),
             timeSlot, onEncryptedKeys, onError);
        }
        else {
          // The current E-KEY can cover the content key.
          // Encrypt the content key directly.
          var eKeyName = new Name(entry.keyName);
          eKeyName.append(Schedule.toIsoString(keyInfo.beginTimeSlot));
          eKeyName.append(Schedule.toIsoString(keyInfo.endTimeSlot));
          thisProducer.encryptContentKeyPromise_
            (keyInfo.keyBits, eKeyName, timeSlot, onEncryptedKeys, onError);
        }
      }

      if (onContentKeyName != null)
        onContentKeyName(contentKeyName);
    });
  });
};

/**
 * Encrypt the given content with the content key that covers timeSlot, and
 * update the data packet with the encrypted content and an appropriate data
 * name.
 * @param {Data} data An empty Data object which is updated.
 * @param {number} timeSlot The time slot as milliseconds since Jan 1, 1970 UTC.
 * @param {Blob} content The content to encrypt.
 * @param {function} onComplete This calls onComplete() when the data packet has
 * been updated.
 * NOTE: The library will log any exceptions thrown by this callback, but for
 * better error handling the callback should catch and properly handle any
 * exceptions.
 * @param {function} onError (optional) This calls onError(errorCode, message)
 * for an error, where errorCode is from EncryptError.ErrorCode and message is a
 * string. If omitted, use a default callback which does nothing.
 * NOTE: The library will log any exceptions thrown by this callback, but for
 * better error handling the callback should catch and properly handle any
 * exceptions.
 */
Producer.prototype.produce = function
  (data, timeSlot, content, onComplete, onError)
{
  if (!onError)
    onError = Producer.defaultOnError;

  var thisProducer = this;

  // Get a content key.
  this.createContentKey(timeSlot, null, function(contentKeyName) {
    thisProducer.database_.getContentKeyPromise(timeSlot)
    .then(function(contentKey) {
      // Produce data.
      var dataName = new Name(thisProducer.namespace_);
      dataName.append(Schedule.toIsoString(timeSlot));

      data.setName(dataName);
      var params = new EncryptParams(EncryptAlgorithmType.AesCbc, 16);
      return Encryptor.encryptData
        (data, content, contentKeyName, contentKey, params);
    })
    .then(function() {
      return thisProducer.keyChain_.signPromise(data);
    })
    .then(function() {
      try {
        onComplete();
      } catch (ex) {
        console.log("Error in onComplete: " + NdnCommon.getErrorWithStackTrace(ex));
      }
    }, function(error) {
      try {
        onError(EncryptError.ErrorCode.General, "" + error);
      } catch (ex) {
        console.log("Error in onError: " + NdnCommon.getErrorWithStackTrace(ex));
      }
    });
  }, onError);
};

/**
 * The default onError callback which does nothing.
 */
Producer.defaultOnError = function(errorCode, message)
{
  // Do nothing.
};

Producer.KeyInfo_ = function ProducerKeyInfo()
{
  this.beginTimeSlot = 0.0;
  this.endTimeSlot = 0.0;
  this.keyBits = null; // Blob
};

Producer.KeyRequest_ = function ProducerKeyRequest(interests)
{
  this.interestCount = interests; // number
  // The map key is the name URI string. The value is an int count.
  // (Use a string because we can't use the Name object as the key in JavaScript.)
  this.repeatAttempts = {};
  this.encryptedKeys = []; // of Data
};

/**
 * Round timeSlot to the nearest whole hour, so that we can store content keys
 * uniformly (by start of the hour).
 * @param {number} timeSlot The time slot as milliseconds since Jan 1, 1970 UTC.
 * @return {number} The start of the hour as milliseconds since Jan 1, 1970 UTC.
 */
Producer.getRoundedTimeSlot_ = function(timeSlot)
{
  return Math.round(Math.floor(Math.round(timeSlot) / 3600000.0) * 3600000.0);
}

/**
 * Send an interest with the given name through the face with callbacks to
 * handleCoveringKey_ and handleTimeout_.
 * @param {Interest} interest The interest to send.
 * @param {number} timeSlot The time slot, passed to handleCoveringKey_ and
 * handleTimeout_.
 * @param {function} onEncryptedKeys The OnEncryptedKeys callback, passed to
 * handleCoveringKey_ and handleTimeout_.
 * @param {function} onError This calls onError(errorCode, message) for an error.
 */
Producer.prototype.sendKeyInterest_ = function
  (interest, timeSlot, onEncryptedKeys, onError)
{
  var thisProducer = this;

  function onKey(interest, data) {
    thisProducer.handleCoveringKey_
      (interest, data, timeSlot, onEncryptedKeys, onError);
  }

  function onTimeout(interest) {
    thisProducer.handleTimeout_(interest, timeSlot, onEncryptedKeys, onError);
  }

  this.face_.expressInterest(interest, onKey, onTimeout);
};

/**
 * This is called from an expressInterest timeout to update the state of
 * keyRequest. Re-express the interest if the number of retrials is less than
 * the max limit.
 * @param {Interest} interest The timed-out interest.
 * @param {number} timeSlot The time slot as milliseconds since Jan 1, 1970 UTC.
 * @param {function} onEncryptedKeys When there are no more interests to process,
 * this calls onEncryptedKeys(keys) where keys is a list of encrypted content
 * key Data packets. If onEncryptedKeys is null, this does not use it.
 * @param {function} onError This calls onError(errorCode, message) for an error.
 */
Producer.prototype.handleTimeout_ = function
  (interest, timeSlot, onEncryptedKeys, onError)
{
  var timeCount = Math.round(timeSlot);
  var keyRequest = this.keyRequests_[timeCount];

  var interestName = interest.getName();
  var interestNameUri = interestName.toUri();

  if (keyRequest.repeatAttempts[interestNameUri] < this.maxRepeatAttempts_) {
    // Increase the retrial count.
    ++keyRequest.repeatAttempts[interestNameUri];
    this.sendKeyInterest_(interest, timeSlot, onEncryptedKeys, onError);
  }
  else
    // No more retrials.
    this.updateKeyRequest_(keyRequest, timeCount, onEncryptedKeys);
};

/**
 * Decrease the count of outstanding E-KEY interests for the C-KEY for
 * timeCount. If the count decreases to 0, invoke onEncryptedKeys.
 * @param {Producer.KeyRequest_} keyRequest The KeyRequest with the
 * interestCount to update.
 * @param {number} timeCount The time count for indexing keyRequests_.
 * @param {function} onEncryptedKeys When there are no more interests to
 * process, this calls onEncryptedKeys(keys) where keys is a list of encrypted
 * content key Data packets. If onEncryptedKeys is null, this does not use it.
 */
Producer.prototype.updateKeyRequest_ = function
  (keyRequest, timeCount, onEncryptedKeys)
{
  --keyRequest.interestCount;
  if (keyRequest.interestCount == 0 && onEncryptedKeys != null) {
    try {
      onEncryptedKeys(keyRequest.encryptedKeys);
    } catch (ex) {
      console.log("Error in onEncryptedKeys: " + NdnCommon.getErrorWithStackTrace(ex));
    }
    delete this.keyRequests_[timeCount];
  }
};

/**
 * This is called from an expressInterest OnData to check that the encryption
 * key contained in data fits the timeSlot. This sends a refined interest if
 * required.
 * @param {Interest} interest The interest given to expressInterest.
 * @param {Data} data The fetched Data packet.
 * @param {number} timeSlot The time slot as milliseconds since Jan 1, 1970 UTC.
 * @param {function} onEncryptedKeys When there are no more interests to process,
 * this calls onEncryptedKeys(keys) where keys is a list of encrypted content
 * key Data packets. If onEncryptedKeys is null, this does not use it.
 * @param {function} onError This calls onError(errorCode, message) for an error.
 */
Producer.prototype.handleCoveringKey_ = function
  (interest, data, timeSlot, onEncryptedKeys, onError)
{
  var timeCount = Math.round(timeSlot);
  var keyRequest = this.keyRequests_[timeCount];

  var interestName = interest.getName();
  var interestNameUrl = interestName.toUri();
  var keyName = data.getName();

  var begin = Schedule.fromIsoString
    (keyName.get(Producer.START_TIME_STAMP_INDEX).getValue().toString());
  var end = Schedule.fromIsoString
    (keyName.get(Producer.END_TIME_STAMP_INDEX).getValue().toString());

  if (timeSlot >= end) {
    // If the received E-KEY covers some earlier period, try to retrieve an
    // E-KEY covering a later one.
    var timeRange = new Exclude(interest.getExclude());
    Producer.excludeBefore(timeRange, keyName.get(Producer.START_TIME_STAMP_INDEX));
    keyRequest.repeatAttempts[interestNameUrl] = 0;
    this.sendKeyInterest_
      (new Interest(interestName).setExclude(timeRange).setChildSelector(1),
       timeSlot, onEncryptedKeys, onError);
  }
  else {
    // If the received E-KEY covers the content key, encrypt the content.
    var encryptionKey = data.getContent();
    var thisProducer = this;
    this.encryptContentKeyPromise_
      (encryptionKey, keyName, timeSlot, onEncryptedKeys, onError)
    .then(function(success) {
      if (success) {
        var keyInfo = thisProducer.eKeyInfo_[interestNameUrl].keyInfo;
        keyInfo.beginTimeSlot = begin;
        keyInfo.endTimeSlot = end;
        keyInfo.keyBits = encryptionKey;
      }
    });
  }
};

/**
 * Get the content key from the database_ and encrypt it for the timeSlot
 * using encryptionKey.
 * @param {Blob} encryptionKey The encryption key value.
 * @param {Name} eKeyName The key name for the EncryptedContent.
 * @param {number} timeSlot The time slot as milliseconds since Jan 1, 1970 UTC.
 * @param {function} onEncryptedKeys When there are no more interests to process,
 * this calls onEncryptedKeys(keys) where keys is a list of encrypted content
 * key Data packets. If onEncryptedKeys is null, this does not use it.
 * @param {function} onError This calls onError(errorCode, message) for an error.
 * @return {Promise} A promise that returns true if encryption succeeds,
 * otherwise false.
 */
Producer.prototype.encryptContentKeyPromise_ = function
  (encryptionKey, eKeyName, timeSlot, onEncryptedKeys, onError)
{
  var timeCount = Math.round(timeSlot);
  var keyRequest = this.keyRequests_[timeCount];

  var keyName = new Name(this.namespace_);
  keyName.append(Encryptor.NAME_COMPONENT_C_KEY);
  keyName.append(Schedule.toIsoString(Producer.getRoundedTimeSlot_(timeSlot)));

  var cKeyData;
  var thisProducer = this;

  return this.database_.getContentKeyPromise(timeSlot)
  .then(function(contentKey) {
    cKeyData = new Data();
    cKeyData.setName(keyName);
    var params = new EncryptParams(EncryptAlgorithmType.RsaOaep);
    return Encryptor.encryptDataPromise
      (cKeyData, contentKey, eKeyName, encryptionKey, params);
  })
  .then(function() {
    return SyncPromise.resolve(true);
  }, function(error) {
    try {
      onError(EncryptError.ErrorCode.EncryptionFailure,
              "encryptData failed: " + error);
    } catch (ex) {
      console.log("Error in onError: " + NdnCommon.getErrorWithStackTrace(ex));
    }
    return SyncPromise.resolve(false);
  })
  .then(function(success) {
    if (success) {
      return thisProducer.keyChain_.signPromise(cKeyData)
      .then(function() {
        keyRequest.encryptedKeys.push(cKeyData);
        thisProducer.updateKeyRequest_(keyRequest, timeCount, onEncryptedKeys);
        return SyncPromise.resolve(true);
      });
    }
    else
      return SyncPromise.resolve(false);
  });
};

Producer.prototype.getEKeyInfoSize_ = function()
{
  // Note: This is really a method to find the key count in any object, but we
  // don't want to claim that it is a tested and general utility method.
  var size = 0;
  for (key in this.eKeyInfo_) {
    if (this.eKeyInfo_.hasOwnProperty(key))
      ++size;
  }

  return size;
};

// TODO: Move this to be the main representation inside the Exclude object.
/**
 * Create a new ExcludeEntry.
 * @param {Name.Component} component
 * @param {boolean} anyFollowsComponent
 */
Producer.ExcludeEntry = function ExcludeEntry(component, anyFollowsComponent)
{
  this.component_ = component;
  this.anyFollowsComponent_ = anyFollowsComponent;
};

/**
 * Create a list of ExcludeEntry from the Exclude object.
 * @param {Exclude} exclude The Exclude object to read.
 * @return {Array<ExcludeEntry>} A new array of ExcludeEntry.
 */
Producer.getExcludeEntries = function(exclude)
{
  var entries = [];

  for (var i = 0; i < exclude.size(); ++i) {
    if (exclude.get(i) == Exclude.ANY) {
      if (entries.length == 0)
        // Add a "beginning ANY".
        entries.push(new Producer.ExcludeEntry(new Name.Component(), true));
      else
        // Set anyFollowsComponent of the final component.
        entries[entries.length - 1].anyFollowsComponent_ = true;
    }
    else
      entries.push(new Producer.ExcludeEntry(exclude.get(i), false));
  }

  return entries;
};

/**
 * Set the Exclude object from the array of ExcludeEntry.
 * @param {Exclude} exclude The Exclude object to update.
 * @param {Array<ExcludeEntry>} entries The array of ExcludeEntry.
 */
Producer.setExcludeEntries = function(exclude, entries)
{
  exclude.clear();

  for (var i = 0; i < entries.length; ++i) {
    var entry = entries[i];

    if (i == 0 && entry.component_.getValue().size() == 0 &&
        entry.anyFollowsComponent_)
      // This is a "beginning ANY".
      exclude.appendAny();
    else {
      exclude.appendComponent(entry.component_);
      if (entry.anyFollowsComponent_)
        exclude.appendAny();
    }
  }
};

/**
 * Get the latest entry in the array whose component_ is less than or equal to
 * component.
 * @param {Array<ExcludeEntry>} entries The array of ExcludeEntry.
 * @param {Name.Component} component The component to compare.
 * @return {number} The index of the found entry, or -1 if not found.
 */
Producer.findEntryBeforeOrAt = function(entries, component)
{
  var i = entries.length - 1;
  while (i >= 0) {
    if (entries[i].component_.compare(component) <= 0)
      break;
    --i;
  }

  return i;
};

/**
 * Exclude all components in the range beginning at "from".
 * @param {Exclude} exclude The Exclude object to update.
 * @param {Name.Component} from The first component in the exclude range.
 */
Producer.excludeAfter = function(exclude, from)
{
  var entries = Producer.getExcludeEntries(exclude);

  var iNewFrom;
  var iFoundFrom = Producer.findEntryBeforeOrAt(entries, from);
  if (iFoundFrom < 0) {
    // There is no entry before "from" so insert at the beginning.
    entries.splice(0, 0, new Producer.ExcludeEntry(from, true));
    iNewFrom = 0;
  }
  else {
    var foundFrom = entries[iFoundFrom];

    if (!foundFrom.anyFollowsComponent_) {
      if (foundFrom.component_.equals(from)) {
        // There is already an entry with "from", so just set the "ANY" flag.
        foundFrom.anyFollowsComponent_ = true;
        iNewFrom = iFoundFrom;
      }
      else {
        // Insert following the entry before "from".
        entries.splice(iFoundFrom + 1, 0, new Producer.ExcludeEntry(from, true));
        iNewFrom = iFoundFrom + 1;
      }
    }
    else
      // The entry before "from" already has an "ANY" flag, so do nothing.
      iNewFrom = iFoundFrom;
  }

  // Remove intermediate entries since they are inside the range.
  var iRemoveBegin = iNewFrom + 1;
  var nRemoveNeeded = entries.length - iRemoveBegin;
  entries.splice(iRemoveBegin, nRemoveNeeded);

  Producer.setExcludeEntries(exclude, entries);
};

/**
 * Exclude all components in the range ending at "to".
 * @param {Exclude} exclude The Exclude object to update.
 * @param {Name.Component} to The last component in the exclude range.
 */
Producer.excludeBefore = function(exclude, to)
{
  Producer.excludeRange(exclude, new Name.Component(), to);
};

/**
 * Exclude all components in the range beginning at "from" and ending at "to".
 * @param {Exclude} exclude The Exclude object to update.
 * @param {Name.Component} from The first component in the exclude range.
 * @param {Name.Component} to The last component in the exclude range.
 */
Producer.excludeRange = function(exclude, from, to)
{
  if (from.compare(to) >= 0) {
    if (from.compare(to) == 0)
      throw new Error
        ("excludeRange: from == to. To exclude a single component, sue excludeOne.");
    else
      throw new Error
        ("excludeRange: from must be less than to. Invalid range: [" +
         from.toEscapedString() + ", " + to.toEscapedString() + "]");
  }

  var entries = Producer.getExcludeEntries(exclude);

  var iNewFrom;
  var iFoundFrom = Producer.findEntryBeforeOrAt(entries, from);
  if (iFoundFrom < 0) {
    // There is no entry before "from" so insert at the beginning.
    entries.splice(0, 0, new Producer.ExcludeEntry(from, true));
    iNewFrom = 0;
  }
  else {
    var foundFrom = entries[iFoundFrom];

    if (!foundFrom.anyFollowsComponent_) {
      if (foundFrom.component_.equals(from)) {
        // There is already an entry with "from", so just set the "ANY" flag.
        foundFrom.anyFollowsComponent_ = true;
        iNewFrom = iFoundFrom;
      }
      else {
        // Insert following the entry before "from".
        entries.splice(iFoundFrom + 1, 0, new Producer.ExcludeEntry(from, true));
        iNewFrom = iFoundFrom + 1;
      }
    }
    else
      // The entry before "from" already has an "ANY" flag, so do nothing.
      iNewFrom = iFoundFrom;
  }

  // We have at least one "from" before "to", so we know this will find an entry.
  var iFoundTo = Producer.findEntryBeforeOrAt(entries, to);
  var foundTo = entries[iFoundTo];
  if (iFoundTo == iNewFrom)
    // Insert the "to" immediately after the "from".
    entries.splice(iNewFrom + 1, 0, new Producer.ExcludeEntry(to, false));
  else {
    var iRemoveEnd;
    if (!foundTo.anyFollowsComponent_) {
      if (foundTo.component_.equals(to))
        // The "to" entry already exists. Remove up to it.
        iRemoveEnd = iFoundTo;
      else {
        // Insert following the previous entry, which will be removed.
        entries.splice(iFoundTo + 1, 0, new Producer.ExcludeEntry(to, false));
        iRemoveEnd = iFoundTo + 1;
      }
    }
    else
      // "to" follows a component which is already followed by "ANY", meaning
      // the new range now encompasses it, so remove the component.
      iRemoveEnd = iFoundTo + 1;

    // Remove intermediate entries since they are inside the range.
    var iRemoveBegin = iNewFrom + 1;
    var nRemoveNeeded = iRemoveEnd - iRemoveBegin;
    entries.splice(iRemoveBegin, nRemoveNeeded);
  }

  Producer.setExcludeEntries(exclude, entries);
};

Producer.START_TIME_STAMP_INDEX = -2;
Producer.END_TIME_STAMP_INDEX = -1;