enclose-io/compiler

View on GitHub
lts/lib/internal/crypto/keys.js

Summary

Maintainability
F
1 wk
Test Coverage
'use strict';

const {
  ObjectDefineProperty,
  Symbol,
} = primordials;

const {
  KeyObject: KeyObjectHandle,
  kKeyTypeSecret,
  kKeyTypePublic,
  kKeyTypePrivate,
  kKeyFormatPEM,
  kKeyFormatDER,
  kKeyEncodingPKCS1,
  kKeyEncodingPKCS8,
  kKeyEncodingSPKI,
  kKeyEncodingSEC1
} = internalBinding('crypto');
const {
  ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS,
  ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE,
  ERR_INVALID_ARG_TYPE,
  ERR_INVALID_ARG_VALUE,
  ERR_INVALID_OPT_VALUE,
  ERR_OUT_OF_RANGE
} = require('internal/errors').codes;
const { kHandle } = require('internal/crypto/util');

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

const kKeyType = Symbol('kKeyType');

// Key input contexts.
const kConsumePublic = 0;
const kConsumePrivate = 1;
const kCreatePublic = 2;
const kCreatePrivate = 3;

const encodingNames = [];
for (const m of [[kKeyEncodingPKCS1, 'pkcs1'], [kKeyEncodingPKCS8, 'pkcs8'],
                 [kKeyEncodingSPKI, 'spki'], [kKeyEncodingSEC1, 'sec1']])
  encodingNames[m[0]] = m[1];

class KeyObject {
  constructor(type, handle) {
    if (type !== 'secret' && type !== 'public' && type !== 'private')
      throw new ERR_INVALID_ARG_VALUE('type', type);
    if (typeof handle !== 'object')
      throw new ERR_INVALID_ARG_TYPE('handle', 'object', handle);

    this[kKeyType] = type;

    ObjectDefineProperty(this, kHandle, {
      value: handle,
      enumerable: false,
      configurable: false,
      writable: false
    });
  }

  get type() {
    return this[kKeyType];
  }
}

class SecretKeyObject extends KeyObject {
  constructor(handle) {
    super('secret', handle);
  }

  get symmetricKeySize() {
    return this[kHandle].getSymmetricKeySize();
  }

  export() {
    return this[kHandle].export();
  }
}

const kAsymmetricKeyType = Symbol('kAsymmetricKeyType');

class AsymmetricKeyObject extends KeyObject {
  get asymmetricKeyType() {
    return this[kAsymmetricKeyType] ||
           (this[kAsymmetricKeyType] = this[kHandle].getAsymmetricKeyType());
  }
}

class PublicKeyObject extends AsymmetricKeyObject {
  constructor(handle) {
    super('public', handle);
  }

  export(encoding) {
    const {
      format,
      type
    } = parsePublicKeyEncoding(encoding, this.asymmetricKeyType);
    return this[kHandle].export(format, type);
  }
}

class PrivateKeyObject extends AsymmetricKeyObject {
  constructor(handle) {
    super('private', handle);
  }

  export(encoding) {
    const {
      format,
      type,
      cipher,
      passphrase
    } = parsePrivateKeyEncoding(encoding, this.asymmetricKeyType);
    return this[kHandle].export(format, type, cipher, passphrase);
  }
}

function parseKeyFormat(formatStr, defaultFormat, optionName) {
  if (formatStr === undefined && defaultFormat !== undefined)
    return defaultFormat;
  else if (formatStr === 'pem')
    return kKeyFormatPEM;
  else if (formatStr === 'der')
    return kKeyFormatDER;
  throw new ERR_INVALID_OPT_VALUE(optionName, formatStr);
}

function parseKeyType(typeStr, required, keyType, isPublic, optionName) {
  if (typeStr === undefined && !required) {
    return undefined;
  } else if (typeStr === 'pkcs1') {
    if (keyType !== undefined && keyType !== 'rsa') {
      throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(
        typeStr, 'can only be used for RSA keys');
    }
    return kKeyEncodingPKCS1;
  } else if (typeStr === 'spki' && isPublic !== false) {
    return kKeyEncodingSPKI;
  } else if (typeStr === 'pkcs8' && isPublic !== true) {
    return kKeyEncodingPKCS8;
  } else if (typeStr === 'sec1' && isPublic !== true) {
    if (keyType !== undefined && keyType !== 'ec') {
      throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(
        typeStr, 'can only be used for EC keys');
    }
    return kKeyEncodingSEC1;
  }

  throw new ERR_INVALID_OPT_VALUE(optionName, typeStr);
}

function option(name, objName) {
  return objName === undefined ? name : `${objName}.${name}`;
}

function parseKeyFormatAndType(enc, keyType, isPublic, objName) {
  const { format: formatStr, type: typeStr } = enc;

  const isInput = keyType === undefined;
  const format = parseKeyFormat(formatStr,
                                isInput ? kKeyFormatPEM : undefined,
                                option('format', objName));

  const type = parseKeyType(typeStr,
                            !isInput || format === kKeyFormatDER,
                            keyType,
                            isPublic,
                            option('type', objName));

  return { format, type };
}

function isStringOrBuffer(val) {
  return typeof val === 'string' || isArrayBufferView(val);
}

function parseKeyEncoding(enc, keyType, isPublic, objName) {
  if (enc === null || typeof enc !== 'object')
    throw new ERR_INVALID_ARG_TYPE('options', 'object', enc);

  const isInput = keyType === undefined;

  const {
    format,
    type
  } = parseKeyFormatAndType(enc, keyType, isPublic, objName);

  let cipher, passphrase;
  if (isPublic !== true) {
    ({ cipher, passphrase } = enc);

    if (!isInput) {
      if (cipher != null) {
        if (typeof cipher !== 'string')
          throw new ERR_INVALID_OPT_VALUE(option('cipher', objName), cipher);
        if (format === kKeyFormatDER &&
            (type === kKeyEncodingPKCS1 ||
             type === kKeyEncodingSEC1)) {
          throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(
            encodingNames[type], 'does not support encryption');
        }
      } else if (passphrase !== undefined) {
        throw new ERR_INVALID_OPT_VALUE(option('cipher', objName), cipher);
      }
    }

    if ((isInput && passphrase !== undefined &&
         !isStringOrBuffer(passphrase)) ||
        (!isInput && cipher != null && !isStringOrBuffer(passphrase))) {
      throw new ERR_INVALID_OPT_VALUE(option('passphrase', objName),
                                      passphrase);
    }
  }

  return { format, type, cipher, passphrase };
}

// Parses the public key encoding based on an object. keyType must be undefined
// when this is used to parse an input encoding and must be a valid key type if
// used to parse an output encoding.
function parsePublicKeyEncoding(enc, keyType, objName) {
  return parseKeyEncoding(enc, keyType, keyType ? true : undefined, objName);
}

// Parses the private key encoding based on an object. keyType must be undefined
// when this is used to parse an input encoding and must be a valid key type if
// used to parse an output encoding.
function parsePrivateKeyEncoding(enc, keyType, objName) {
  return parseKeyEncoding(enc, keyType, false, objName);
}

function getKeyObjectHandle(key, ctx) {
  if (ctx === kCreatePrivate) {
    throw new ERR_INVALID_ARG_TYPE(
      'key',
      ['string', 'Buffer', 'TypedArray', 'DataView'],
      key
    );
  }

  if (key.type !== 'private') {
    if (ctx === kConsumePrivate || ctx === kCreatePublic)
      throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, 'private');
    if (key.type !== 'public') {
      throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type,
                                                   'private or public');
    }
  }

  return key[kHandle];
}

function prepareAsymmetricKey(key, ctx) {
  if (isKeyObject(key)) {
    // Best case: A key object, as simple as that.
    return { data: getKeyObjectHandle(key, ctx) };
  } else if (typeof key === 'string' || isArrayBufferView(key)) {
    // Expect PEM by default, mostly for backward compatibility.
    return { format: kKeyFormatPEM, data: key };
  } else if (typeof key === 'object') {
    const data = key.key;
    // The 'key' property can be a KeyObject as well to allow specifying
    // additional options such as padding along with the key.
    if (isKeyObject(data))
      return { data: getKeyObjectHandle(data, ctx) };
    // Either PEM or DER using PKCS#1 or SPKI.
    if (!isStringOrBuffer(data)) {
      throw new ERR_INVALID_ARG_TYPE(
        'key.key',
        ['string', 'Buffer', 'TypedArray', 'DataView',
         ...(ctx !== kCreatePrivate ? ['KeyObject'] : [])],
        data);
    }

    const isPublic =
      (ctx === kConsumePrivate || ctx === kCreatePrivate) ? false : undefined;
    return { data, ...parseKeyEncoding(key, undefined, isPublic) };
  }
  throw new ERR_INVALID_ARG_TYPE(
    'key',
    ['string', 'Buffer', 'TypedArray', 'DataView',
     ...(ctx !== kCreatePrivate ? ['KeyObject'] : [])],
    key
  );
}

function preparePrivateKey(key) {
  return prepareAsymmetricKey(key, kConsumePrivate);
}

function preparePublicOrPrivateKey(key) {
  return prepareAsymmetricKey(key, kConsumePublic);
}

function prepareSecretKey(key, bufferOnly = false) {
  if (!isArrayBufferView(key) && (bufferOnly || typeof key !== 'string')) {
    if (isKeyObject(key) && !bufferOnly) {
      if (key.type !== 'secret')
        throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, 'secret');
      return key[kHandle];
    }
    throw new ERR_INVALID_ARG_TYPE(
      'key',
      ['Buffer', 'TypedArray', 'DataView',
       ...(bufferOnly ? [] : ['string', 'KeyObject'])],
      key);
  }
  return key;
}

function createSecretKey(key) {
  key = prepareSecretKey(key, true);
  if (key.byteLength === 0)
    throw new ERR_OUT_OF_RANGE('key.byteLength', '> 0', key.byteLength);
  const handle = new KeyObjectHandle(kKeyTypeSecret);
  handle.init(key);
  return new SecretKeyObject(handle);
}

function createPublicKey(key) {
  const { format, type, data } = prepareAsymmetricKey(key, kCreatePublic);
  const handle = new KeyObjectHandle(kKeyTypePublic);
  handle.init(data, format, type);
  return new PublicKeyObject(handle);
}

function createPrivateKey(key) {
  const { format, type, data, passphrase } =
    prepareAsymmetricKey(key, kCreatePrivate);
  const handle = new KeyObjectHandle(kKeyTypePrivate);
  handle.init(data, format, type, passphrase);
  return new PrivateKeyObject(handle);
}

function isKeyObject(key) {
  return key instanceof KeyObject;
}

module.exports = {
  // Public API.
  createSecretKey,
  createPublicKey,
  createPrivateKey,
  KeyObject,

  // These are designed for internal use only and should not be exposed.
  parsePublicKeyEncoding,
  parsePrivateKeyEncoding,
  preparePrivateKey,
  preparePublicOrPrivateKey,
  prepareSecretKey,
  SecretKeyObject,
  PublicKeyObject,
  PrivateKeyObject,
  isKeyObject
};