types/prefix-sha256.js

'use strict'

/**
 * @module types
 */

const Condition = require('../lib/condition')
const Fulfillment = require('../lib/fulfillment')
const BaseSha256 = require('./base-sha256')
const MissingDataError = require('../errors/missing-data-error')
const isInteger = require('../util/is-integer')
const Asn1PrefixFingerprintContents = require('../schemas/fingerprint').PrefixFingerprintContents

/**
 * PREFIX-SHA-256: Prefix condition using SHA-256.
 *
 * A prefix condition will prepend a static prefix to the message before passing
 * the prefixed message on to a single subcondition.
 *
 * You can use prefix conditions to effectively narrow the scope of a public key
 * or set of public keys. Simply take the condition representing the public key
 * and place it as a subcondition in a prefix condition. Now any message passed
 * to the subcondition will be prepended with a prefix.
 *
 * Prefix conditions are especially useful in conjunction with threshold
 * conditions. You could have a group of signers, each using a different prefix
 * to sign a common message.
 *
 * PREFIX-SHA-256 is assigned the type ID 1. It relies on the SHA-256 and PREFIX
 * feature suites which corresponds to a feature bitmask of 0x05.
 */
class PrefixSha256 extends BaseSha256 {
  constructor () {
    super()

    this.prefix = Buffer.alloc(0)
    this.subcondition = null
    this.maxMessageLength = 16384
  }

  /**
   * Set the (unfulfilled) subcondition.
   *
   * Each prefix condition builds on an existing condition which is provided via
   * this method.
   *
   * @param {Condition|String} subcondition Condition object or URI string
   *   representing the condition that will receive the prefixed message.
   */
  setSubcondition (subcondition) {
    if (typeof subcondition === 'string') {
      subcondition = Condition.fromUri(subcondition)
    } else if (!(subcondition instanceof Condition)) {
      throw new Error('Subconditions must be URIs or objects of type Condition')
    }

    this.subcondition = subcondition
  }

  /**
   * Set the (fulfilled) subcondition.
   *
   * When constructing a prefix fulfillment, this method allows you to pass in
   * a fulfillment for the condition that will receive the prefixed message.
   *
   * Note that you only have to add either the subcondition or a subfulfillment,
   * but not both.
   *
   * @param {Fulfillment|String} fulfillment Fulfillment object or URI string
   *   representing the fulfillment to use as the subcondition.
   */
  setSubfulfillment (subfulfillment) {
    if (typeof subfulfillment === 'string') {
      subfulfillment = Fulfillment.fromUri(subfulfillment)
    } else if (!(subfulfillment instanceof Fulfillment)) {
      throw new Error('Subfulfillments must be objects of type Fulfillment')
    }

    this.subcondition = subfulfillment
  }

  /**
   * Set the prefix.
   *
   * The prefix will be prepended to the message during validation before the
   * message is passed on to the subcondition.
   *
   * @param {Buffer} prefix Prefix to apply to the message.
   */
  setPrefix (prefix) {
    if (!Buffer.isBuffer(prefix)) {
      throw new TypeError('Prefix must be a Buffer, was: ' + prefix)
    }

    this.prefix = prefix
  }

  /**
   * Set the threshold.
   *
   * Determines the threshold that is used to consider this condition fulfilled.
   * If the number of valid subfulfillments is greater or equal to this number,
   * the threshold condition is considered to be fulfilled.
   *
   * @param {Number} maxMessageLength Integer threshold
   */
  setMaxMessageLength (maxMessageLength) {
    if (!isInteger(maxMessageLength) || maxMessageLength < 0) {
      throw new TypeError('Max message length must be an integer greater than or equal to zero, was: ' +
        maxMessageLength)
    }

    this.maxMessageLength = maxMessageLength
  }

  /**
   * Get types used in this condition.
   *
   * This is a type of condition that contains a subcondition. A complete
   * set of subtypes must contain the set of types that must be supported in
   * order to validate this fulfillment. Therefore, we need to join the type of
   * this condition to the types used in the subcondition.
   *
   * @return {Set<String>} Complete type names for this fulfillment.
   */
  getSubtypes () {
    const subtypes = new Set([...this.subcondition.getSubtypes(), this.subcondition.getTypeName()])

    // Never include our own type as a subtype. The reason is that we already
    // know that the validating implementation knows how to interpret this type,
    // otherwise it wouldn't be able to verify this fulfillment to begin with.
    subtypes.delete(this.constructor.TYPE_NAME)

    return subtypes
  }

  /**
   * Produce the contents of the condition hash.
   *
   * This function is called internally by the `getCondition` method.
   *
   * @return {Buffer} Encoded contents of fingerprint hash.
   *
   * @private
   */
  getFingerprintContents () {
    if (!this.subcondition) {
      throw new MissingDataError('Requires subcondition')
    }

    return Asn1PrefixFingerprintContents.encode({
      prefix: this.prefix,
      maxMessageLength: this.maxMessageLength,
      subcondition: this.subcondition instanceof Condition
        ? this.subcondition.getAsn1Json()
        : this.subcondition.getCondition().getAsn1Json()
    })
  }

  getAsn1JsonPayload () {
    return {
      prefix: this.prefix,
      maxMessageLength: this.maxMessageLength,
      subfulfillment: this.subcondition.getAsn1Json()
    }
  }

  parseJson (json) {
    this.setPrefix(Buffer.from(json.prefix, 'base64'))
    this.setMaxMessageLength(json.maxMessageLength)
    this.setSubfulfillment(Fulfillment.fromJson(json.subfulfillment))
  }

  parseAsn1JsonPayload (json) {
    this.setPrefix(Buffer.from(json.prefix, 'base64'))
    this.setMaxMessageLength(json.maxMessageLength.toNumber())
    this.setSubfulfillment(Fulfillment.fromAsn1Json(json.subfulfillment))
  }

  /**
   * Calculate the cost of fulfilling this condition.
   *
   * The cost of the prefix condition equals (1 + l/256) * (16384 + s) where l
   * is the prefix length in bytes and s is the subcondition cost.
   *
   * @return {Number} Expected maximum cost to fulfill this condition
   * @private
   */
  calculateCost () {
    if (!this.prefix) {
      throw new MissingDataError('Prefix must be specified')
    }

    if (!this.subcondition) {
      throw new MissingDataError('Subcondition must be specified')
    }

    const subconditionCost = this.subcondition instanceof Condition
      ? this.subcondition.getCost()
      : this.subcondition.getCondition().getCost()

    return Number(this.prefix.length) + this.maxMessageLength + subconditionCost + 1024
  }

  /**
   * Check whether this fulfillment meets all validation criteria.
   *
   * This will validate the subfulfillment. The message will be prepended with
   * the prefix before being passed to the subfulfillment's validation routine.
   *
   * @param {Buffer} message Message to validate against.
   * @return {Boolean} Whether this fulfillment is valid.
   */
  validate (message) {
    if (!(this.subcondition instanceof Fulfillment)) {
      throw new Error('Subcondition is not a fulfillment')
    }
    if (!Buffer.isBuffer(message)) {
      throw new Error('Message must be provided as a Buffer, was: ' + message)
    }

    // Ensure the subfulfillment is valid
    return this.subcondition.validate(Buffer.concat([this.prefix, message]))
  }
}

PrefixSha256.TYPE_ID = 1
PrefixSha256.TYPE_NAME = 'prefix-sha-256'
PrefixSha256.TYPE_ASN1_CONDITION = 'prefixSha256Condition'
PrefixSha256.TYPE_ASN1_FULFILLMENT = 'prefixSha256Fulfillment'
PrefixSha256.TYPE_CATEGORY = 'compound'

PrefixSha256.CONSTANT_BASE_COST = 16384
PrefixSha256.CONSTANT_COST_DIVISOR = 256

// DEPRECATED
PrefixSha256.prototype.setSubconditionUri =
  PrefixSha256.prototype.setSubcondition
PrefixSha256.prototype.setSubfulfillmentUri =
  PrefixSha256.prototype.setSubfulfillment

module.exports = PrefixSha256