Source: security/pib/pib.js

/**
 * Copyright (C) 2017-2018 Regents of the University of California.
 * @author: Jeff Thompson <[email protected]>
 * @author: From ndn-cxx security https://github.com/named-data/ndn-cxx/blob/master/src/security/pib/pib.cpp
 *
 * 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 ConfigFile = require('../../util/config-file.js').ConfigFile; /** @ignore */
var SyncPromise = require('../../util/sync-promise.js').SyncPromise;

/**
 * In general, a PIB (Public Information Base) stores the public portion of a
 * user's cryptography keys. The format and location of stored information is
 * indicated by the PIB locator. A PIB is designed to work with a TPM (Trusted
 * Platform Module) which stores private keys. There is a one-to-one association
 * between a PIB and a TPM, and therefore the TPM locator is recorded by the PIB
 * to enforce this association and prevent one from operating on mismatched PIB
 * and TPM.
 *
 * Information in the PIB is organized in a hierarchy of
 * Identity-Key-Certificate. At the top level, this Pib class provides access to
 * identities, and allows setting a default identity. Properties of an identity
 * (such as PibKey objects) can be accessed after obtaining a PibIdentity object.
 * (Likewise, CertificateV2 objects can be obtained from a PibKey object.)
 *
 * Note: A Pib instance is created and managed only by the KeyChain, and is
 * returned by the KeyChain getPib() method.
 *
 * Create a Pib instance. This constructor should only be called by KeyChain.
 *
 * @param {string} scheme The scheme for the PIB.
 * @param {string} location The location for the PIB.
 * @param {PibImpl} pibImpl The PIB backend implementation.
 * @constructor
 */
var Pib = function Pib(scheme, location, pibImpl)
{
  this.defaultIdentity_ = null;
  this.scheme_ = scheme;
  this.location_ = location;
  // Must call initializePromise_ before accessing this.
  this.identities_ = null;
  this.pibImpl_ = pibImpl;
  this.initializeTpm_ = null;
  this.initializePibLocator_ = null;
  this.initializeTpmLocator_ = null;
  this.initializeAllowReset_ = false;
  this.isInitialized_ = false;

  if (pibImpl == null)
    throw new Error("The pibImpl is null");
};

exports.Pib = Pib;

/**
 * Create a Pib.Error which represents a semantic error in PIB processing.
 * Call with: throw new Pib.Error(new Error("message")).
 * @constructor
 * @param {Error} error The exception created with new Error.
 */
Pib.Error = function PibError(error)
{
  if (error) {
    error.__proto__ = Pib.Error.prototype;
    return error;
  }
};

Pib.Error.prototype = new Error();
Pib.Error.prototype.name = "PibError";

/**
 * Get the scheme of the PIB locator.
 * @return {string} The scheme string.
 */
Pib.prototype.getScheme = function() { return this.scheme_; };

/**
 * Get the PIB locator.
 * @return {string} The PIB locator.
 */
Pib.prototype.getPibLocator = function()
{
  return this.scheme_ + ":" + this.location_;
};

/**
 * Set the corresponding TPM information to tpmLocator. If the tpmLocator is
 * different from the existing one, the PIB will be reset. Otherwise, nothing
 * will be changed.
 * @param {string} tpmLocator The TPM locator.
 * @param {boolean} useSync (optional) If true then return a SyncPromise which
 * is already fulfilled. If omitted or false, this may return a SyncPromise or
 * an async Promise.
 * @return {Promise|SyncPromise} A promise which fulfills when finished.
 */
Pib.prototype.setTpmLocatorPromise = function(tpmLocator, useSync)
{
  var thisPib = this;
  return this.initializePromise_(useSync)
  .then(function() {
    return thisPib.doSetTpmLocatorPromise_(tpmLocator, useSync);
  })
};

/**
 * Do the work of setTpmLocatorPromise without calling initializePromise_.
 * @param {string} tpmLocator The TPM locator.
 * @param {boolean} useSync (optional) If true then return a SyncPromise which
 * is already fulfilled. If omitted or false, this may return a SyncPromise or
 * an async Promise.
 * @return {Promise|SyncPromise} A promise which fulfills when finished.
 */
Pib.prototype.doSetTpmLocatorPromise_ = function(tpmLocator, useSync)
{
  var thisPib = this;

  return this.pibImpl_.getTpmLocatorPromise(useSync)
  .then(function(pibTpmLocator) {
    if (tpmLocator == pibTpmLocator)
      return SyncPromise.resolve();
    else {
      return thisPib.resetPromise_(useSync)
      .then(function() {
        return thisPib.pibImpl_.setTpmLocatorPromise(tpmLocator, useSync);
      });
    }
  });
};

/**
 * Get the TPM Locator.
 * @param {boolean} useSync (optional) If true then return a SyncPromise which
 * is already fulfilled. If omitted or false, this may return a SyncPromise or
 * an async Promise.
 * @return {Promise|SyncPromise} A promise which returns the TPM locator string,
 * or a promise rejected with Pib.Error if the TPM locator is empty.
 */
Pib.prototype.getTpmLocatorPromise = function(useSync)
{
  var thisPib = this;

  return this.initializePromise_(useSync)
  .then(function() {
    return thisPib.pibImpl_.getTpmLocatorPromise(useSync);
  })
  .then(function(tpmLocator) {
    if (tpmLocator == "")
      return SyncPromise.reject(new Pib.Error(new Error
        ("TPM info does not exist")));

    return SyncPromise.resolve(tpmLocator);
  });
};

/**
 * Get the identity with name identityName.
 * @param {Name} identityName The name of the identity.
 * @param {boolean} useSync (optional) If true then return a SyncPromise which
 * is already fulfilled. If omitted or false, this may return a SyncPromise or
 * an async Promise.
 * @return {Promise|SyncPromise} A promise which returns the PibIdentity object,
 * or a promise rejected with Pib.Error if the identity does not exist.
 */
Pib.prototype.getIdentityPromise = function(identityName, useSync)
{
  var thisPib = this;

  return this.initializePromise_(useSync)
  .then(function() {
    return thisPib.identities_.getPromise(identityName, useSync);
  });
};

/**
 * Get the identity with name identityName.
 * @param {Name} identityName The name of the identity.
 * @param {function} onComplete (optional) This calls
 * onComplete(identity) with the PibIdentity object. If omitted, the return
 * value is described below. (Some database libraries only use a callback, so
 * onComplete is required to use these.)
 * 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) If defined, then onComplete must be
 * defined and if there is an exception, then this calls onError(exception)
 * with the exception. If onComplete is defined but onError is undefined, then
 * this will log any thrown exception. (Some database libraries only use a
 * callback, so onError is required to be notified of an exception.)
 * 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.
 * @return {PibIdentity} If onComplete is omitted, return the PibIdentity object.
 * Otherwise, if onComplete is supplied then return undefined and use onComplete
 * as described above.
 * @throws Pib.Error if the identity does not exist. However, if onComplete and
 * onError are defined, then if there is an exception return undefined and call
 * onError(exception).
 */
Pib.prototype.getIdentity = function(identityName, onComplete, onError)
{
  return SyncPromise.complete(onComplete, onError,
    this.getIdentityPromise(identityName, !onComplete));
};

/**
 * Get the default identity.
 * @param {boolean} useSync (optional) If true then return a SyncPromise which
 * is already fulfilled. If omitted or false, this may return a SyncPromise or
 * an async Promise.
 * @return {Promise|SyncPromise} A promise which returns the PibIdentity object
 * of the default identity, or a promise rejected with Pib.Error for no default
 * identity.
 */
Pib.prototype.getDefaultIdentityPromise = function(useSync)
{
  if (this.defaultIdentity_ == null) {
    var thisPib = this;

    return this.initializePromise_(useSync)
    .then(function() {
      return thisPib.pibImpl_.getDefaultIdentityPromise(useSync);
    })
    .then(function(defaultIdentity) {
      return thisPib.identities_.getPromise(defaultIdentity, useSync);
    })
    .then(function(identity) {
      thisPib.defaultIdentity_ = identity;
      return SyncPromise.resolve(thisPib.defaultIdentity_);
    });
  }
  else
    return SyncPromise.resolve(this.defaultIdentity_);
};

/**
 * Get the default identity.
 * @param {function} onComplete (optional) This calls
 * onComplete(identity) with the PibIdentity object. If omitted, the return
 * value is described below. (Some database libraries only use a callback, so
 * onComplete is required to use these.)
 * 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) If defined, then onComplete must be
 * defined and if there is an exception, then this calls onError(exception)
 * with the exception. If onComplete is defined but onError is undefined, then
 * this will log any thrown exception. (Some database libraries only use a
 * callback, so onError is required to be notified of an exception.)
 * 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.
 * @return {PibIdentity} If onComplete is omitted, return the PibIdentity object.
 * Otherwise, if onComplete is supplied then return undefined and use onComplete
 * as described above.
 * @throws Pib.Error for no default identity. However, if onComplete and onError
 * are defined, then if there is an exception return undefined and call
 * onError(exception).
 */
Pib.prototype.getDefaultIdentity = function(onComplete, onError)
{
  return SyncPromise.complete(onComplete, onError,
    this.getDefaultIdentityPromise(!onComplete));
};

/**
 * Reset the content in the PIB, including a reset of the TPM locator. This
 * should only be called by initializeFromLocatorsPromise_.
 * @param {boolean} useSync (optional) If true then return a SyncPromise which
 * is already fulfilled. If omitted or false, this may return a SyncPromise or
 * an async Promise.
 * @return {Promise|SyncPromise} A promise which fulfills when finished.
 */
Pib.prototype.resetPromise_ = function(useSync)
{
  var thisPib = this;

  // Don't call initializePromise_ since this is already being called by it.
  return this.pibImpl_.clearIdentitiesPromise(useSync)
  .then(function() {
    return thisPib.pibImpl_.setTpmLocatorPromise("", useSync);
  })
  .then(function() {
    thisPib.defaultIdentity_ = null;

    // Call PibIdentityContainer.makePromise the same as initializePromise_ .
    return PibIdentityContainer.makePromise(thisPib.pibImpl_, useSync);
  })
  .then(function(container) {
    thisPib.identities_ = container;
    return thisPib.identities_.resetPromise(useSync);
  });
};

/**
 * Add an identity with name identityName. Create the identity if it does not
 * exist. This should only be called by KeyChain.
 * @param {Name} identityName The name of the identity, which is copied.
 * @param {boolean} useSync (optional) If true then return a SyncPromise which
 * is already fulfilled. If omitted or false, this may return a SyncPromise or
 * an async Promise.
 * @return {Promise|SyncPromise} A promise which returns the PibIdentity object
 * of the added identity.
 */
Pib.prototype.addIdentityPromise_ = function(identityName, useSync)
{
  var thisPib = this;
  return this.initializePromise_(useSync)
  .then(function() {
    return thisPib.identities_.addPromise(identityName, useSync);
  });
};

/**
 * Remove the identity with name identityName, and its related keys and
 * certificates. If the default identity is being removed, no default identity
 * will be selected.  If the identity does not exist, do nothing. This should
 * only be called by KeyChain.
 * @param {Name} identityName The name of the identity.
 * @param {boolean} useSync (optional) If true then return a SyncPromise which
 * is already fulfilled. If omitted or false, this may return a SyncPromise or
 * an async Promise.
 * @return {Promise|SyncPromise} A promise which fulfills when finished.
 */
Pib.prototype.removeIdentityPromise_ = function(identityName, useSync)
{
  if (this.defaultIdentity_ != null &&
      this.defaultIdentity_.getName().equals(identityName))
    this.defaultIdentity_ = null;

  var thisPib = this;
  return this.initializePromise_(useSync)
  .then(function() {
    return thisPib.identities_.removePromise(identityName, useSync);
  });
};

/**
 * Set the identity with name identityName as the default identity. Create the
 * identity if it does not exist. This should only be called by KeyChain.
 * @param {Name} identityName The name of the identity.
 * @param {boolean} useSync (optional) If true then return a SyncPromise which
 * is already fulfilled. If omitted or false, this may return a SyncPromise or
 * an async Promise.
 * @return {Promise|SyncPromise} A promise which returns the PibIdentity object
 * of the default identity.
 */
Pib.prototype.setDefaultIdentityPromise_ = function(identityName, useSync)
{
  var thisPib = this;

  return this.initializePromise_(useSync)
  .then(function() {
    return thisPib.identities_.addPromise(identityName, useSync);
  })
  .then(function(identity) {
    thisPib.defaultIdentity_ = identity;

    return thisPib.pibImpl_.setDefaultIdentityPromise(identityName);
  })
  .then(function() {
    return SyncPromise.resolve(thisPib.defaultIdentity_);
  });
};

/**
 * If isInitialized_ is false, initialize identities_ using
 * PibIdentityContainer.makePromise and set isInitialized_. However, if
 * isInitialized_ is already true, do nothing. This must be called by each
 * method before using this object. This is necessary because the constructor
 * cannot perform async operations.
 * @param {boolean} useSync (optional) If true then return a SyncPromise which
 * is already fulfilled. If omitted or false, this may return a SyncPromise or
 * an async Promise.
 * @return {Promise|SyncPromise} A promise which fulfills when finished.
 */
Pib.prototype.initializePromise_ = function(useSync)
{
  if (this.isInitialized_)
    return SyncPromise.resolve();

  var thisPib = this;
  return PibIdentityContainer.makePromise(this.pibImpl_, useSync)
  .then(function(container) {
    thisPib.identities_ = container;

    if (thisPib.initializeTpm_ != null)
      return thisPib.initializeFromLocatorsPromise_(useSync);
    else
      return SyncPromise.resolve();
  })
  .then(function() {
    thisPib.isInitialized_ = true;
    return SyncPromise.resolve();
  });
};

/**
 * Initialize from initializePibLocator_ and initializeTpmLocator_ in the same
 * way that the KeyChain constructor would if it could do async operations. Set
 * up initializeTpm_ and set its isInitialized_ true.
 * @param {boolean} useSync (optional) If true then return a SyncPromise which
 * is already fulfilled. If omitted or false, this may return a SyncPromise or
 * an async Promise.
 * @return {Promise|SyncPromise} A promise which fulfills when finished.
 */
Pib.prototype.initializeFromLocatorsPromise_ = function(useSync)
{
  // Repeat this from the KeyChain constructor.
  var pibScheme = [null];
  var pibLocation = [null];
  KeyChain.parseAndCheckPibLocator_
    (this.initializePibLocator_, pibScheme, pibLocation);
  var canonicalPibLocator = pibScheme[0] + ":" + pibLocation[0];

  var canonicalTpmLocator;
  var thisPib = this;
  return this.pibImpl_.getTpmLocatorPromise(useSync)
  .then(function(oldTpmLocator) {
    // TPM locator.
    var tpmScheme = [null];
    var tpmLocation = [null];
    KeyChain.parseAndCheckTpmLocator_
      (thisPib.initializeTpmLocator_, tpmScheme, tpmLocation);
    canonicalTpmLocator = tpmScheme[0] + ":" + tpmLocation[0];

    var resetPib = false;
    var config;
    if (ConfigFile)
      // Assume we are not in the browser.
      config = new ConfigFile();
    if (ConfigFile && canonicalPibLocator == KeyChain.getDefaultPibLocator_(config)) {
      // The default PIB must use the default TPM.
      if (oldTpmLocator != "" &&
          oldTpmLocator != KeyChain.getDefaultTpmLocator_(config)) {
        resetPib = true;
        canonicalTpmLocator = KeyChain.getDefaultTpmLocator_(config);
      }
    }
    else {
      // Check the consistency of the non-default PIB.
      if (oldTpmLocator != "" && oldTpmLocator != canonicalTpmLocator) {
        if (thisPib.initializeAllowReset_)
          resetPib = true;
        else
          return SyncPromise.reject(new LocatorMismatchError(new Error
            ("The supplied TPM locator does not match the TPM locator in the PIB: " +
             oldTpmLocator + " != " + canonicalTpmLocator)));
      }
    }

    if (resetPib)
      return thisPib.resetPromise_(useSync);
    else
      return SyncPromise.resolve();
  })
  .then(function() {
    // Note that a key mismatch may still happen if the TPM locator is initially
    // set to a wrong one or if the PIB was shared by more than one TPM before.
    // This is due to the old PIB not having TPM info. The new PIB should not
    // have this problem.
    KeyChain.setUpTpm_(thisPib.initializeTpm_, canonicalTpmLocator);
    thisPib.initializeTpm_.isInitialized_ = true;
    return thisPib.doSetTpmLocatorPromise_(canonicalTpmLocator, useSync);
  });
};

// Put these last to avoid a require loop.
/** @ignore */
var KeyChain = require('../key-chain.js').KeyChain; /** @ignore */
var LocatorMismatchError = require('../key-chain.js').LocatorMismatchError; /** @ignore */
var PibIdentityContainer = require('./pib-identity-container.js').PibIdentityContainer;