signature.js

const crypto = require('crypto');

/**
 * The signature module contains a small set of basic utilities for generating
 * and verifying signatures.
 * @module easy-crypto/signature
 */

/**
 * Generate signatures for a message using a private key.
 * @since 0.1.0
 * @function sign
 * @param {Object|string|Buffer|KeyObject} privateKey The private key to be used for signing the message.
 * @param {string} algorithm The hashing algorithm to be used.
 * @param {Data} message The message to be signed.
 * @param {string} inputEncoding The encoding of the `message`. If `message` is a string and no value is provided, an encoding of `'utf8'` will be enforced. Ignored if message is a `Buffer`, `TypedArray` or `DataView`.
 * @param {string} outputEncoding The encoding of the output signature. If provided a `string` is returned, otherwise a `Buffer` is returned.
 * @returns {string|Buffer} The generated signature for the provided message.
 * @static
 */
function sign(privateKey, algorithm, message, inputEncoding, outputEncoding) {
  const signFunc = crypto.createSign(algorithm);
  signFunc.update(message, inputEncoding);
  signFunc.end();
  return signFunc.sign(privateKey, outputEncoding);
}

/**
 * Generate signatures for a message encapsulated in a stream using a private key.
 * @since 0.1.0
 * @async
 * @function signStream
 * @param {Object|string|Buffer|KeyObject} privateKey The private key to be used for signing the message.
 * @param {string} algorithm The hashing algorithm to be used.
 * @param {ReadableStream<Data>} input The input stream containing the message to be signed.
 * @param {string} inputEncoding The encoding of the `message`. If `message` is a string and no value is provided, an encoding of `'utf8'` will be enforced. Ignored if message is a `Buffer`, `TypedArray` or `DataView`.
 * @param {string} outputEncoding The encoding of the output signature. If provided a `string` is returned, otherwise a `Buffer` is returned.
 * @returns {Promise<string|Buffer>} The generated signature for the provided message.
 * @static
 */
function signStream(
  privateKey,
  algorithm,
  input,
  inputEncoding,
  outputEncoding
) {
  return new Promise((resolve, reject) => {
    let data;
    let wasBuffer;

    const dataHandler = chunk => {
      const isBuffer = Buffer.isBuffer(chunk);
      if ((!isBuffer && wasBuffer) || (isBuffer && wasBuffer === false)) {
        reject(new Error('Inconsistent data.'));
        input.removeListener('data', dataHandler);
        return;
      }

      if (isBuffer) {
        wasBuffer = true;
        chunk = chunk.toString('utf8');
      } else {
        wasBuffer = false;
      }

      if (data === undefined) {
        data = chunk;
      } else {
        data += chunk;
      }
    };
    input.on('data', dataHandler);

    input.on('end', () => {
      if (data === undefined) {
        reject(new Error('No data to sign.'));
      } else {
        resolve(
          sign(privateKey, algorithm, data, inputEncoding, outputEncoding)
        );
      }
    });
  });
}

/**
 * Verify if a signature is valid for a given message using the corresponding public key.
 * @since 0.1.0
 * @function verify
 * @param {Object|string|Buffer|KeyObject} publicKey The public key to be used for verifying the signature.
 * @param {string} algorithm The hashing algorithm to be used.
 * @param {Data} message The message for which the signature has been generated.
 * @param {string|Buffer} signature The signature to be verified.
 * @param {string} inputEncoding The encoding of the `message`. If `message` is a string and no value is provided, an encoding of `'utf8'` will be enforced. Ignored if message is a `Buffer`, `TypedArray` or `DataView`.
 * @param {string} signatureEncoding The encoding of the provided `signature`. If a signatureEncoding is specified, the signature is expected to be a string; otherwise signature is expected to be a Buffer, TypedArray, or DataView.
 * @returns {boolean} Wether the signature was valid or not.
 * @static
 */
function verify(
  publicKey,
  algorithm,
  message,
  signature,
  inputEncoding,
  signatureEncoding
) {
  const verifyFunc = crypto.createVerify(algorithm);
  verifyFunc.update(message, inputEncoding);
  verifyFunc.end();
  return verifyFunc.verify(publicKey, signature, signatureEncoding);
}

/**
 * Verify if a signature is valid for a given message encapsulated in a stream using the corresponding public key.
 * @since 0.1.0
 * @async
 * @function verifyStream
 * @param {Object|string|Buffer|KeyObject} publicKey The public key to be used for verifying the signature.
 * @param {string} algorithm The hashing algorithm to be used.
 * @param {ReadableStream<Data>} input The stream containing the message for which the signature has been generated.
 * @param {string|Buffer} signature The signature to be verified.
 * @param {string} inputEncoding The encoding of the `message`. If `message` is a string and no value is provided, an encoding of `'utf8'` will be enforced. Ignored if message is a `Buffer`, `TypedArray` or `DataView`.
 * @param {string} signatureEncoding The encoding of the provided `signature`. If a signatureEncoding is specified, the signature is expected to be a string; otherwise signature is expected to be a Buffer, TypedArray, or DataView.
 * @returns {Promise<boolean>} Wether the signature was valid or not.
 * @static
 */
function verifyStream(
  publicKey,
  algorithm,
  input,
  signature,
  inputEncoding,
  signatureEncoding
) {
  return new Promise((resolve, reject) => {
    let data;
    let wasBuffer;

    const dataHandler = chunk => {
      const isBuffer = Buffer.isBuffer(chunk);
      if ((!isBuffer && wasBuffer) || (isBuffer && wasBuffer === false)) {
        reject(new Error('Inconsistent data.'));
        input.removeListener('data', dataHandler);
        return;
      }

      if (isBuffer) {
        wasBuffer = true;
        chunk = chunk.toString('utf8');
      } else {
        wasBuffer = false;
      }

      if (data === undefined) {
        data = chunk;
      } else {
        data += chunk;
      }
    };
    input.on('data', dataHandler);

    input.on('end', () => {
      if (data === undefined) {
        reject(new Error('No data to sign.'));
      } else {
        resolve(
          verify(
            publicKey,
            algorithm,
            data,
            signature,
            inputEncoding,
            signatureEncoding
          )
        );
      }
    });
  });
}

module.exports = { sign, signStream, verify, verifyStream };