current commit
260829.1 · crypto: add a generic MAC API
'use strict';

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

const {
  Mac: _Mac,
} = internalBinding('crypto');

const {
  getCachedMacId,
  getMacCache,
  kHandle,
} = require('internal/crypto/util');

const {
  getKeyObjectHandle,
  getKeyObjectType,
  isKeyObject,
} = require('internal/crypto/keys');

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

const {
  codes: {
    ERR_CRYPTO_MAC_FINALIZED,
    ERR_CRYPTO_MAC_UPDATE_FAILED,
    ERR_INVALID_ARG_TYPE,
    ERR_INVALID_ARG_VALUE,
  },
} = require('internal/errors');

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

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

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

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

function validateName(value, name) {
  validateString(value, name);
  if (value.length === 0 || StringPrototypeIncludes(value, '\0')) {
    throw new ERR_INVALID_ARG_VALUE(
      name, value, 'must be non-empty and contain no NUL bytes');
  }
  return value;
}

function normalizeBytes(value, name) {
  if (!isArrayBufferView(value) && !isAnyArrayBuffer(value)) {
    throw new ERR_INVALID_ARG_TYPE(
      name, ['ArrayBuffer', 'Buffer', 'TypedArray', 'DataView'], value);
  }
  return value;
}

function createHandle(algorithm, key, options) {
  const name = validateName(algorithm, 'algorithm');
  let digest;
  let cipher;
  let iv;
  let customization;
  let salt;
  let outputLength;
  let hasExtendedOptions = false;

  if (options !== undefined) {
    validateObject(options, 'options');

    const digestOption = options.digest;
    if (digestOption !== undefined) {
      digest = validateName(digestOption, 'options.digest');
    }
    const cipherOption = options.cipher;
    if (cipherOption !== undefined) {
      cipher = validateName(cipherOption, 'options.cipher');
      hasExtendedOptions = true;
    }
    const ivOption = options.iv;
    if (ivOption !== undefined) {
      iv = normalizeBytes(ivOption, 'options.iv');
      hasExtendedOptions = true;
    }
    const customizationOption = options.customization;
    if (customizationOption !== undefined) {
      customization = normalizeBytes(
        customizationOption, 'options.customization');
      hasExtendedOptions = true;
    }
    const saltOption = options.salt;
    if (saltOption !== undefined) {
      salt = normalizeBytes(saltOption, 'options.salt');
      hasExtendedOptions = true;
    }
    const outputLengthOption = options.outputLength;
    if (outputLengthOption !== undefined) {
      outputLength = outputLengthOption;
      validateUint32(outputLength, 'options.outputLength');
      outputLength += 0;
      hasExtendedOptions = true;
    }
  }

  key = normalizeKey(key);
  const id = getCachedMacId(name);
  const cache = getMacCache();
  if (!hasExtendedOptions) {
    return new _Mac(name, id, cache, key, digest);
  }
  return new _Mac(
    name,
    id,
    cache,
    key,
    digest,
    cipher,
    iv,
    customization,
    salt,
    outputLength,
  );
}

function normalizeKey(key) {
  if (isKeyObject(key)) {
    if (getKeyObjectType(key) !== 'secret') {
      throw new ERR_INVALID_ARG_TYPE(
        'key', ['ArrayBuffer', 'Buffer', 'TypedArray', 'DataView', 'KeyObject'], key);
    }
    return getKeyObjectHandle(key);
  }
  if (!isArrayBufferView(key) && !isAnyArrayBuffer(key)) {
    throw new ERR_INVALID_ARG_TYPE(
      'key', ['ArrayBuffer', 'Buffer', 'TypedArray', 'DataView', 'KeyObject'], key);
  }
  return key;
}

function normalizeOutputEncoding(outputEncoding) {
  if (outputEncoding === undefined) return 'buffer';
  validateString(outputEncoding, 'outputEncoding');
  if (StringPrototypeToLowerCase(outputEncoding) === 'buffer') return 'buffer';
  const normalized = normalizeEncoding(outputEncoding);
  if (normalized === undefined) {
    throw new ERR_INVALID_ARG_VALUE('outputEncoding', outputEncoding);
  }
  return normalized;
}

function normalizeInputEncoding(data, inputEncoding) {
  if (inputEncoding === undefined) return undefined;
  validateString(inputEncoding, 'inputEncoding');
  const normalized = normalizeEncoding(inputEncoding);
  if (normalized === undefined) {
    throw new ERR_INVALID_ARG_VALUE('inputEncoding', inputEncoding);
  }
  validateEncoding(data, normalized);
  return normalized;
}

function updateHandle(mac, data, encoding) {
  const state = mac[kState];
  let updated;
  try {
    updated = mac[kHandle].update(data, encoding);
  } catch (error) {
    state[kFinalized] = true;
    throw error;
  }
  if (!updated) {
    state[kFinalized] = true;
    throw new ERR_CRYPTO_MAC_UPDATE_FAILED();
  }
}

function finalizeHandle(mac, outputEncoding) {
  const state = mac[kState];
  state[kFinalized] = true;
  return mac[kHandle].final(outputEncoding);
}

function Mac(algorithm, key, options) {
  if (!new.target) return new Mac(algorithm, key, options);
  this[kHandle] = createHandle(algorithm, key, options);
  this[kState] = {
    [kFinalized]: false,
  };
  FunctionPrototypeCall(LazyTransform, this, options);
}

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

Mac.prototype._transform = function _transform(chunk, encoding, callback) {
  if (this[kState][kFinalized]) {
    callback(new ERR_CRYPTO_MAC_FINALIZED());
    return;
  }
  try {
    updateHandle(this, chunk, encoding);
  } catch (error) {
    callback(error);
    return;
  }
  callback();
};

Mac.prototype._flush = function _flush(callback) {
  if (this[kState][kFinalized]) {
    callback(new ERR_CRYPTO_MAC_FINALIZED());
    return;
  }
  try {
    const result = finalizeHandle(this);
    if (result.length !== 0) this.push(result);
  } catch (error) {
    callback(error);
    return;
  }
  callback();
};

Mac.prototype.update = function update(data, encoding) {
  if (this[kState][kFinalized]) throw new ERR_CRYPTO_MAC_FINALIZED();
  if (typeof data === 'string') {
    encoding = normalizeInputEncoding(data, encoding);
  } else if (!isArrayBufferView(data)) {
    throw new ERR_INVALID_ARG_TYPE(
      'data', ['string', 'Buffer', 'TypedArray', 'DataView'], data);
  }
  updateHandle(this, data, encoding);
  return this;
};

Mac.prototype.final = function final(outputEncoding) {
  if (this[kState][kFinalized]) throw new ERR_CRYPTO_MAC_FINALIZED();
  if (outputEncoding === undefined) return finalizeHandle(this);
  outputEncoding = normalizeOutputEncoding(outputEncoding);
  if (outputEncoding === 'buffer') return finalizeHandle(this);
  return finalizeHandle(this, outputEncoding);
};

module.exports = {
  Mac,
};