Source: security/v2/certificate-cache-v2.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/v2/certificate-cache.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 Name = require('../../name.js').Name; /** @ignore */
var Schedule = require('../../encrypt/schedule.js').Schedule; /** @ignore */
var CertificateV2 = require('./certificate-v2.js').CertificateV2; /** @ignore */
var LOG = require('../../log.js').Log.LOG;

/**
 * A CertificateCacheV2 holds other user's verified certificates in security v2
 * format CertificateV2. A certificate is removed no later than its NotAfter
 * time, or maxLifetime after it has been added to the cache.
 *
 * Create a CertificateCacheV2.
 * @param {number} maxLifetimeMilliseconds (optional) The maximum time that
 * certificates can live inside the cache, in milliseconds. If omitted, use
 * getDefaultLifetime().
 * @constructor
 */
var CertificateCacheV2 = function CertificateCacheV2(maxLifetimeMilliseconds)
{
  // Array of objects with fields "name" of type Name, "certificate" of type
  // CertificateV2 and "removalTime" as milliseconds since Jan 1, 1970 UTC. We
  // can't use an {} object since the Name key itself is an object, and also it
  // needs to be sorted by Name.
  this.certificatesByName_ = [];
  this.nextRefreshTime_ = Number.MAX_VALUE;
  this.maxLifetimeMilliseconds_ = (maxLifetimeMilliseconds == undefined ?
    CertificateCacheV2.getDefaultLifetime() : maxLifetimeMilliseconds);
  this.nowOffsetMilliseconds_ = 0;
};

exports.CertificateCacheV2 = CertificateCacheV2;

/**
 * Insert the certificate into the cache. The inserted certificate will be
 * removed no later than its NotAfter time, or maxLifetimeMilliseconds given to
 * the constructor.
 * @param {CertificateV2} certificate The certificate object, which is copied.
 */
CertificateCacheV2.prototype.insert = function(certificate)
{
  var notAfterTime = certificate.getValidityPeriod().getNotAfter();
  // nowOffsetMilliseconds_ is only used for testing.
  var now = new Date().getTime() + this.nowOffsetMilliseconds_;
  if (notAfterTime < now) {
    if (LOG > 3) console.log("Not adding " + certificate.getName().toUri() +
      ": already expired at " + Schedule.toIsoString(notAfterTime));
    return;
  }

  var removalTime =
    Math.min(notAfterTime, now + this.maxLifetimeMilliseconds_);
  if (removalTime < this.nextRefreshTime_)
    // We need to run refresh() sooner.)
    this.nextRefreshTime_ = removalTime;

  if (LOG > 3) console.log("Adding " + certificate.getName().toUri() +
    ", will remove in " + (removalTime - now) / (3600 * 1000.0) + " hours");

  var certificateCopy = new CertificateV2(certificate);

  var name = certificateCopy.getName();
  var i = this.findFirstByName_(name);
  if (i < 0)
    // Not found, so set to insert at the end of the list.
    i = this.certificatesByName_.length;
  else {
    if (this.certificatesByName_[i].name.equals(name)) {
      // Just replace the existing entry value.
      this.certificatesByName_[i].certificate = certificateCopy;
      this.certificatesByName_[i].removalTime = removalTime;
      return;
    }
  }

  this.certificatesByName_.splice
    (i, 0, {name: name, certificate: certificateCopy, removalTime: removalTime});
};

/**
 * Find the certificate by the given prefix or interest.
 * @param {Name|Interest} prefixOrInterest If a Name, return the first
 * certificate (ordered by name) where the Name is a prefix of the certificate
 * name. If an Interest, return the first certificate (ordered by Name) where
 * interest.matchesData(certificate) .
 * @return {CertificateV2}  The found certificate, or null if not found. You
 * must not modify the returned object. If you need to modify it, then make a
 * copy.
 * @note ChildSelector is not supported.
 */
CertificateCacheV2.prototype.find = function(prefixOrInterest)
{
  if (prefixOrInterest instanceof Name) {
    var certificatePrefix = prefixOrInterest;

    if (certificatePrefix.size() > 0 &&
        certificatePrefix.get(-1).isImplicitSha256Digest())
      console.log
        ("Certificate search using a name with an implicit digest is not yet supported");

    this.refresh_();

    var i = this.findFirstByName_(certificatePrefix);
    if (i < 0)
      return null;

    var entry = this.certificatesByName_[i];
    if (!certificatePrefix.isPrefixOf(entry.certificate.getName()))
      return null;
    return entry.certificate;
  }
  else {
    var interest = prefixOrInterest;

    if (interest.getChildSelector() != null)
      console.log
        ("Certificate search using a ChildSelector is not supported. Searching as if this selector not specified");

    if (interest.getName().size() > 0 &&
        interest.getName().get(-1).isImplicitSha256Digest())
      console.log
        ("Certificate search using a name with an implicit digest is not yet supported");

    this.refresh_();

    var i = this.findFirstByName_(interest.getName());
    if (i < 0)
      return null;

    // Search the remaining entries.
    for (; i < this.certificatesByName_.length; ++i) {
      var certificate = this.certificatesByName_[i].certificate;
      if (!interest.getName().isPrefixOf(certificate.getName()))
        break;

      if (interest.matchesData(certificate))
        return certificate;
    }

    return null;
  }
};

/**
 * Remove the certificate whose name equals the given name. If no such
 * certificate is in the cache, do nothing.
 * @param {Name} certificateName The name of the certificate.
 */
CertificateCacheV2.prototype.deleteCertificate = function(certificateName)
{
  for (var i = 0; i < this.certificatesByName_.length; ++i) {
    if (this.certificatesByName_[i].name.equals(certificateName)) {
      this.certificatesByName_.splice(i, 1);
      return;
    }
  }

  // This may be the certificate to be removed at nextRefreshTime_ by refresh(),
  // but just allow refresh() to run instead of update nextRefreshTime_ now.
};

/**
 * Clear all certificates from the cache.
 */
CertificateCacheV2.prototype.clear = function()
{
  this.certificatesByName_ = [];
  this.nextRefreshTime_ = Number.MAX_VALUE;
};

/**
 * Get the default maximum lifetime (1 hour).
 * @return {number} The lifetime in milliseconds.
 */
CertificateCacheV2.getDefaultLifetime = function() { return 3600.0 * 1000; };

/**
 * Set the offset when insert() and refresh_() get the current time, which
 * should only be used for testing.
 * @param {number} nowOffsetMilliseconds The offset in milliseconds.
 */
CertificateCacheV2.prototype.setNowOffsetMilliseconds_ = function
  (nowOffsetMilliseconds)
{
  this.nowOffsetMilliseconds_ = nowOffsetMilliseconds;
};

/**
 * A private helper method to get the first entry in certificatesByName_ whose
 * name is greater than or equal to the given name.
 * @param {Name} name The name to search for.
 * @return {number} The index of the found certificatesByName_ entry, or -1 if
 * not found.
 */
CertificateCacheV2.prototype.findFirstByName_ = function(name)
{
  for (var i = 0; i < this.certificatesByName_.length; ++i) {
    if (this.certificatesByName_[i].name.compare(name) >= 0)
      return i;
  }

  return -1;
};

/**
 * Remove all outdated certificate entries.
 */
CertificateCacheV2.prototype.refresh_ = function()
{
  // nowOffsetMilliseconds_ is only used for testing.
  var now = new Date().getTime() + this.nowOffsetMilliseconds_;
  if (now < this.nextRefreshTime_)
    return;

  // We recompute nextRefreshTime_.
  var nextRefreshTime = Number.MAX_VALUE;
  // Go backwards through the list so we can erase entries.
  for (var i = this.certificatesByName_.length - 1; i >= 0; --i) {
    var entry = this.certificatesByName_[i];

    if (entry.removalTime <= now)
      this.certificatesByName_.splice(i, 1);
    else
      nextRefreshTime = Math.min(nextRefreshTime, entry.removalTime);
  }

  this.nextRefreshTime_ = nextRefreshTime;
};