current commit
260930.5 · crypto: validate digest output encodings
'use strict';

const {
  FunctionPrototypeCall,
  ObjectSetPrototypeOf,
  StringPrototypeToLowerCase,
  Symbol,
  TypedArrayPrototypeIncludes,
} = primordials;

const {
  Hash: _Hash,
  HashJob,
  Hmac: _Hmac,
  kCryptoJobWebCrypto,
  oneShotDigest,
  TurboShakeJob,
  KangarooTwelveJob,
} = internalBinding('crypto');

const {
  getStringOption,
  jobPromise,
  normalizeHashName,
  validateAlgorithm,
  validateMaxBufferLength,
  kHandle,
  getCachedHashId,
  getHashCache,
  getArrayBufferOrView,
  getBufferSourceBytes,
  getOptionalByteLength,
} = require('internal/crypto/util');

const {
  prepareSecretKey,
} = require('internal/crypto/keys');

const {
  normalizeEncoding,
  encodingsMap,
  getDeprecationWarningEmitter,
} = require('internal/util');

const {
  Buffer,
} = require('buffer');

const {
  codes: {
    ERR_CRYPTO_HASH_FINALIZED,
    ERR_CRYPTO_HASH_UPDATE_FAILED,
    ERR_INVALID_ARG_TYPE,
    ERR_INVALID_ARG_VALUE,
    ERR_UNKNOWN_ENCODING,
  },
} = require('internal/errors');

const {
  validateEncoding,
  validateString,
  validateObject,
  validateUint32,
} = require('internal/validators');

const {
  isArrayBufferView,
} = require('internal/util/types');

const LazyTransform = require('internal/streams/lazy_transform');

const kState = Symbol('kState');
const kFinalized = Symbol('kFinalized');

const emitHmacDigestDeprecation = getDeprecationWarningEmitter(
  'DEP0206',
  'Calling Hmac.digest() more than once is deprecated.',
);

function normalizeCShakeParameter(value, name) {
  if (value === undefined) return undefined;

  value = getArrayBufferOrView(value, name);
  if (TypedArrayPrototypeIncludes(getBufferSourceBytes(value), 0)) {
    throw new ERR_INVALID_ARG_VALUE(
      name, value, 'must not contain NUL bytes');
  }
  return value;
}

function Hash(algorithm, options) {
  if (!new.target)
    return new Hash(algorithm, options);
  const isCopy = algorithm instanceof _Hash;
  if (!isCopy)
    validateString(algorithm, 'algorithm');
  let xofLen = typeof options === 'object' && options !== null ?
    options.outputLength : undefined;
  if (xofLen !== undefined) {
    validateUint32(xofLen, 'options.outputLength');
    // Coerce -0 to +0.
    xofLen += 0;
  }
  let functionName;
  let customization;
  if (!isCopy && options !== undefined && options !== null) {
    const functionNameOption = options.functionName;
    if (functionNameOption !== undefined) {
      functionName = normalizeCShakeParameter(
        functionNameOption, 'options.functionName');
    }
    const customizationOption = options.customization;
    if (customizationOption !== undefined) {
      customization = normalizeCShakeParameter(
        customizationOption, 'options.customization');
    }
  }
  // Lookup the cached ID from JS land because it's faster than decoding
  // the string in C++ land.
  const algorithmId = isCopy ? -1 : getCachedHashId(algorithm);
  if (functionName === undefined && customization === undefined) {
    this[kHandle] = new _Hash(
      algorithm, xofLen, algorithmId, getHashCache());
  } else {
    this[kHandle] = new _Hash(
      algorithm,
      xofLen,
      algorithmId,
      getHashCache(),
      functionName,
      customization,
    );
  }
  this[kState] = {
    [kFinalized]: false,
  };
  FunctionPrototypeCall(LazyTransform, this, options);
}

ObjectSetPrototypeOf(Hash.prototype, LazyTransform.prototype);
ObjectSetPrototypeOf(Hash, LazyTransform);

Hash.prototype.copy = function copy(options) {
  const state = this[kState];
  if (state[kFinalized])
    throw new ERR_CRYPTO_HASH_FINALIZED();

  return new Hash(this[kHandle], options);
};

Hash.prototype._transform = function _transform(chunk, encoding, callback) {
  if (!this[kHandle].update(chunk, encoding))
    return callback(new ERR_CRYPTO_HASH_UPDATE_FAILED());
  callback();
};

Hash.prototype._flush = function _flush(callback) {
  this.push(this[kHandle].digest());
  callback();
};

Hash.prototype.update = function update(data, encoding) {
  const state = this[kState];
  if (state[kFinalized])
    throw new ERR_CRYPTO_HASH_FINALIZED();

  if (typeof data === 'string') {
    validateEncoding(data, encoding);
  } else if (!isArrayBufferView(data)) {
    throw new ERR_INVALID_ARG_TYPE(
      'data', ['string', 'Buffer', 'TypedArray', 'DataView'], data);
  }

  if (!this[kHandle].update(data, encoding))
    throw new ERR_CRYPTO_HASH_UPDATE_FAILED();
  return this;
};


function validateDigestEncoding(outputEncoding) {
  // Explicit conversion of truthy values for backward compatibility.
  outputEncoding &&= `${outputEncoding}`;
  if (outputEncoding &&
      normalizeEncoding(outputEncoding) === undefined &&
      StringPrototypeToLowerCase(outputEncoding) !== 'buffer') {
    throw new ERR_UNKNOWN_ENCODING(outputEncoding);
  }
  return outputEncoding;
}

Hash.prototype.digest = function digest(outputEncoding) {
  const state = this[kState];
  if (state[kFinalized])
    throw new ERR_CRYPTO_HASH_FINALIZED();

  const ret = this[kHandle].digest(validateDigestEncoding(outputEncoding));
  state[kFinalized] = true;
  return ret;
};

function Hmac(hmac, key, options) {
  if (!(this instanceof Hmac))
    return new Hmac(hmac, key, options);
  validateString(hmac, 'hmac');
  const encoding = getStringOption(options, 'encoding');
  key = prepareSecretKey(key, encoding);
  this[kHandle] = new _Hmac();
  this[kHandle].init(hmac, key);
  this[kState] = {
    [kFinalized]: false,
  };
  FunctionPrototypeCall(LazyTransform, this, options);
}

ObjectSetPrototypeOf(Hmac.prototype, LazyTransform.prototype);
ObjectSetPrototypeOf(Hmac, LazyTransform);

Hmac.prototype.update = Hash.prototype.update;

Hmac.prototype.digest = function digest(outputEncoding) {
  const state = this[kState];

  if (state[kFinalized]) {
    emitHmacDigestDeprecation();
    const buf = Buffer.from('');
    if (outputEncoding && outputEncoding !== 'buffer')
      return buf.toString(outputEncoding);
    return buf;
  }

  const ret = this[kHandle].digest(validateDigestEncoding(outputEncoding));
  state[kFinalized] = true;
  return ret;
};

Hmac.prototype._flush = function _flush(callback) {
  this.push(this[kHandle].digest());
  this[kState][kFinalized] = true; // This diverges from Hash.prototype._flush:
  // Hash instances are still usable after a flush, Hmac are not, see DEP0206.
  callback();
};
Hmac.prototype._transform = Hash.prototype._transform;

// Implementation for WebCrypto subtle.digest()

function asyncDigest(algorithm, data) {
  validateMaxBufferLength(data, 'data');
  validateAlgorithm(algorithm, 'digest');

  switch (algorithm.name) {
    case 'SHA-1':
      // Fall through
    case 'SHA-256':
      // Fall through
    case 'SHA-384':
      // Fall through
    case 'SHA-512':
      // Fall through
    case 'SHA3-256':
      // Fall through
    case 'SHA3-384':
      // Fall through
    case 'SHA3-512':
      return jobPromise(() => new HashJob(
        kCryptoJobWebCrypto,
        normalizeHashName(algorithm.name),
        data));
    case 'cSHAKE128':
      // Fall through
    case 'cSHAKE256': {
      const outputLength = algorithm.outputLength;
      if (getOptionalByteLength(algorithm.functionName) ||
          getOptionalByteLength(algorithm.customization)) {
        return jobPromise(() => new HashJob(
          kCryptoJobWebCrypto,
          StringPrototypeToLowerCase(algorithm.name),
          data,
          outputLength,
          algorithm.functionName,
          algorithm.customization));
      }

      return jobPromise(() => new HashJob(
        kCryptoJobWebCrypto,
        normalizeHashName(algorithm.name),
        data,
        outputLength));
    }
    case 'TurboSHAKE128':
      // Fall through
    case 'TurboSHAKE256':
      return jobPromise(() => new TurboShakeJob(
        kCryptoJobWebCrypto,
        algorithm.name,
        algorithm.domainSeparation ?? 0x1f,
        algorithm.outputLength / 8,
        data));
    case 'KT128':
      // Fall through
    case 'KT256':
      return jobPromise(() => new KangarooTwelveJob(
        kCryptoJobWebCrypto,
        algorithm.name,
        algorithm.customization,
        algorithm.outputLength / 8,
        data));
    /* c8 ignore start */
    default: {
      const assert = require('internal/assert');
      assert.fail('Unreachable code');
    }
    /* c8 ignore stop */
  }
}

function hash(algorithm, input, options) {
  validateString(algorithm, 'algorithm');
  if (typeof input !== 'string' && !isArrayBufferView(input)) {
    throw new ERR_INVALID_ARG_TYPE('input', ['Buffer', 'TypedArray', 'DataView', 'string'], input);
  }
  if (options === undefined) {
    return oneShotDigest(algorithm, getCachedHashId(algorithm), getHashCache(),
                         input, 'hex', encodingsMap.hex, undefined);
  }
  let outputEncoding;
  let outputLength;
  let functionName;
  let customization;

  if (typeof options === 'string') {
    outputEncoding = options;
  } else if (options !== undefined) {
    validateObject(options, 'options');
    outputLength = options.outputLength;
    outputEncoding = options.outputEncoding;
    const functionNameOption = options.functionName;
    if (functionNameOption !== undefined) {
      functionName = normalizeCShakeParameter(
        functionNameOption, 'options.functionName');
    }
    const customizationOption = options.customization;
    if (customizationOption !== undefined) {
      customization = normalizeCShakeParameter(
        customizationOption, 'options.customization');
    }
  }

  outputEncoding ??= 'hex';

  let normalized = outputEncoding;
  // Fast case: if it's 'hex', we don't need to validate it further.
  if (normalized !== 'hex') {
    validateString(outputEncoding, 'outputEncoding');
    normalized = normalizeEncoding(outputEncoding);
    // If the encoding is invalid, normalizeEncoding() returns undefined.
    if (normalized === undefined) {
      // normalizeEncoding() doesn't handle 'buffer'.
      if (StringPrototypeToLowerCase(outputEncoding) === 'buffer') {
        normalized = 'buffer';
      } else {
        throw new ERR_INVALID_ARG_VALUE('outputEncoding', outputEncoding);
      }
    }
  }

  if (outputLength !== undefined) {
    validateUint32(outputLength, 'outputLength');
    // Coerce -0 to +0.
    outputLength += 0;
  }

  if (functionName === undefined && customization === undefined) {
    return oneShotDigest(algorithm, getCachedHashId(algorithm), getHashCache(),
                         input, normalized, encodingsMap[normalized], outputLength);
  }
  return oneShotDigest(algorithm, getCachedHashId(algorithm), getHashCache(),
                       input, normalized, encodingsMap[normalized], outputLength,
                       functionName, customization);
}

module.exports = {
  Hash,
  Hmac,
  asyncDigest,
  hash,
};