types/ed25519-sha256.js

'use strict'

/**
 * @module types
 */

const nacl = require('tweetnacl')
const BaseSha256 = require('./base-sha256')
const MissingDataError = require('../errors/missing-data-error')
const ValidationError = require('../errors/validation-error')
const Asn1Ed25519FingerprintContents = require('../schemas/fingerprint').Ed25519FingerprintContents

let ed25519
try {
  ed25519 = require('ed25519')
} catch (err) { }

/**
 * ED25519: Ed25519 signature condition.
 *
 * This condition implements Ed25519 signatures.
 *
 * ED25519 is assigned the type ID 4. It relies only on the ED25519 feature
 * suite which corresponds to a bitmask of 0x20.
 */
class Ed25519Sha256 extends BaseSha256 {
  constructor () {
    super()
    this.publicKey = null
    this.signature = null
  }

  /**
   * Set the public publicKey.
   *
   * This is the Ed25519 public key. It has to be provided as a buffer.
   *
   * @param {Buffer} publicKey Public Ed25519 publicKey
   */
  setPublicKey (publicKey) {
    if (!Buffer.isBuffer(publicKey)) {
      throw new TypeError('Public key must be a Buffer, was: ' + publicKey)
    }

    if (publicKey.length !== 32) {
      throw new Error('Public key must be 32 bytes, was: ' + publicKey.length)
    }

    // TODO Validate public key

    this.publicKey = publicKey
  }

  /**
   * Set the signature.
   *
   * Instead of using the private key to sign using the sign() method, we can
   * also generate the signature elsewhere and pass it in.
   *
   * @param {Buffer} signature 64-byte signature.
   */
  setSignature (signature) {
    if (!Buffer.isBuffer(signature)) {
      throw new TypeError('Signature must be a Buffer, was: ' + signature)
    }

    if (signature.length !== 64) {
      throw new Error('Signature must be 64 bytes, was: ' + signature.length)
    }

    this.signature = signature
  }

  /**
   * Sign a message.
   *
   * This method will take a message and an Ed25519 private key and store a
   * corresponding signature in this fulfillment.
   *
   * @param {Buffer} message Message to sign.
   * @param {String} privateKey Ed25519 private key.
   */
  sign (message, privateKey) {
    if (!Buffer.isBuffer(message)) {
      throw new MissingDataError('Message must be a Buffer')
    }
    if (!Buffer.isBuffer(privateKey)) {
      throw new TypeError('Private key must be a Buffer, was: ' + privateKey)
    }
    if (privateKey.length !== 32) {
      throw new Error('Private key must be 32 bytes, was: ' + privateKey.length)
    }

    // This would be the Ed25519ph version:
    // message = crypto.createHash('sha512')
    //   .update(message)
    //   .digest()

    // Use native library if available (~65x faster)
    if (ed25519) {
      const keyPair = ed25519.MakeKeypair(privateKey)
      this.setPublicKey(keyPair.publicKey)
      this.signature = ed25519.Sign(message, keyPair)
    } else {
      const keyPair = nacl.sign.keyPair.fromSeed(privateKey)
      this.setPublicKey(Buffer.from(keyPair.publicKey))
      this.signature = Buffer.from(nacl.sign.detached(message, keyPair.secretKey))
    }
  }

  parseJson (json) {
    this.setPublicKey(Buffer.from(json.publicKey, 'base64'))
    this.setSignature(Buffer.from(json.signature, 'base64'))
  }

  /**
   * 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.publicKey) {
      throw new MissingDataError('Requires public key')
    }

    return Asn1Ed25519FingerprintContents.encode({
      publicKey: this.publicKey
    })
  }

  getAsn1JsonPayload () {
    return {
      publicKey: this.publicKey,
      signature: this.signature
    }
  }

  /**
   * Calculate the cost of fulfilling this condition.
   *
   * The cost of the Ed25519 condition is 2^17 = 131072.
   *
   * @return {Number} Expected maximum cost to fulfill this condition
   * @private
   */
  calculateCost () {
    return Ed25519Sha256.CONSTANT_COST
  }

  /**
   * Verify the signature of this Ed25519 fulfillment.
   *
   * The signature of this Ed25519 fulfillment is verified against the provided
   * message and public key.
   *
   * @param {Buffer} message Message to validate against.
   * @return {Boolean} Whether this fulfillment is valid.
   */
  validate (message) {
    if (!Buffer.isBuffer(message)) {
      throw new TypeError('Message must be a Buffer')
    }

    // Use native library if available (~60x faster)
    let result
    if (ed25519) {
      result = ed25519.Verify(message, this.signature, this.publicKey)
    } else {
      result = nacl.sign.detached.verify(message, this.signature, this.publicKey)
    }

    if (result !== true) {
      throw new ValidationError('Invalid ed25519 signature')
    }

    return true
  }
}

Ed25519Sha256.TYPE_ID = 4
Ed25519Sha256.TYPE_NAME = 'ed25519-sha-256'
Ed25519Sha256.TYPE_ASN1_CONDITION = 'ed25519Sha256Condition'
Ed25519Sha256.TYPE_ASN1_FULFILLMENT = 'ed25519Sha256Fulfillment'
Ed25519Sha256.TYPE_CATEGORY = 'simple'

Ed25519Sha256.CONSTANT_COST = 131072

module.exports = Ed25519Sha256