/*! * jwk/basekey.js - JWK Key Base Class Implementation * * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. */ "use strict"; var merge = require("../util/merge"); const { v4: uuidv4 } = require("uuid"); var assign = require("lodash/assign"); var clone = require("lodash/clone"); var flatten = require("lodash/flatten"); var intersection = require("lodash/intersection"); var omit = require("lodash/omit"); var pick = require("lodash/pick"); var uniq = require("lodash/uniq"); var ALGORITHMS = require("../algorithms"), CONSTANTS = require("./constants.js"), HELPERS = require("./helpers.js"), UTIL = require("../util"); /** * @class JWK.Key * @classdesc * Represents a JSON Web Key instance. * * @description * **NOTE:** This class cannot be instantiated directly. Instead call * {@link JWK.asKey}, {@link JWK.KeyStore#add}, or * {@link JWK.KeyStore#generate}. */ var JWKBaseKeyObject = function(kty, ks, props, cfg) { // ### validate/coerce arguments ### if (!kty) { throw new Error("kty cannot be null"); } if (!ks) { throw new Error("keystore cannot be null"); } if (!props) { throw new Error("props cannot be null"); } else if ("string" === typeof props) { props = JSON.parse(props); } if (!cfg) { throw new Error("cfg cannot be null"); } var excluded = []; var keys = {}, json = {}, prints, kid; props = clone(props); // strip thumbprints if present prints = assign({}, props[HELPERS.INTERNALS.THUMBPRINT_KEY] || {}); delete props[HELPERS.INTERNALS.THUMBPRINT_KEY]; Object.keys(prints).forEach(function(a) { var h = prints[a]; if (!kid) { kid = h; if (Buffer.isBuffer(kid)) { kid = UTIL.base64url.encode(kid); } } if (!Buffer.isBuffer(h)) { h = UTIL.base64url.decode(h); prints[a] = h; } }); // force certain values props.kty = kty; props.kid = props.kid || kid || uuidv4(); // setup base info var included = Object.keys(HELPERS.COMMON_PROPS).map(function(p) { return HELPERS.COMMON_PROPS[p].name; }); json.base = pick(props, included); excluded = excluded.concat(Object.keys(json.base)); // setup public information json.public = clone(props); keys.public = cfg.publicKey(json.public); if (keys.public) { // exclude public values from extra excluded = excluded.concat(Object.keys(json.public)); } // setup private information json.private = clone(props); keys.private = cfg.privateKey(json.private); if (keys.private) { // exclude private values from extra excluded = excluded.concat(Object.keys(json.private)); } // setup extra information json.extra = omit(props, excluded); // TODO: validate 'alg' against supported algorithms // setup calculated values var keyLen; if (keys.public && ("length" in keys.public)) { keyLen = keys.public.length; } else if (keys.private && ("length" in keys.private)) { keyLen = keys.private.length; } else { keyLen = NaN; } // ### Public Properties ### /** * @member {JWK.KeyStore} JWK.Key#keystore * @description * The owning keystore. */ Object.defineProperty(this, "keystore", { value: ks, enumerable: true }); /** * @member {Number} JWK.Key#length * @description * The size of this Key, in bits. */ Object.defineProperty(this, "length", { value: keyLen, enumerable: true }); /** * @member {String} JWK.Key#kty * @description * The type of Key. */ Object.defineProperty(this, "kty", { value: kty, enumerable: true }); /** * @member {String} JWK.Key#kid * @description * The identifier for this Key. */ Object.defineProperty(this, "kid", { value: json.base.kid, enumerable: true }); /** * @member {String} JWK.Key#use * @description * The usage for this Key. */ Object.defineProperty(this, "use", { value: json.base.use || "", enumerable: true }); /** * @member {String} JWK.Key#alg * @description * The sole algorithm this key can be used for. */ Object.defineProperty(this, "alg", { value: json.base.alg || "", enumerable: true }); // ### Public Methods ### /** * Generates the thumbprint of this Key. * * @param {String} [] The hash algorithm to use * @returns {Promise} The promise for the thumbprint generation. */ Object.defineProperty(this, "thumbprint", { value: function(hash) { hash = (hash || HELPERS.INTERNALS.THUMBPRINT_HASH).toUpperCase(); if (prints[hash]) { // return cached value return Promise.resolve(prints[hash]); } var p = HELPERS.thumbprint(cfg, json, hash); p = p.then(function(result) { if (result) { prints[hash] = result; } return result; }); return p; } }); /** * @method JWK.Key#algorithms * @description * The possible algorithms this Key can be used for. The returned * list is not any particular order, but is filtered based on the * Key's intended usage. * * @param {String} mode The operation mode * @returns {String[]} The list of supported algorithms * @see JWK.Key#supports */ Object.defineProperty(this, "algorithms", { value: function(mode) { var modes = []; if (!this.use || this.use === "sig") { if (!mode || CONSTANTS.MODE_SIGN === mode) { modes.push(CONSTANTS.MODE_SIGN); } if (!mode || CONSTANTS.MODE_VERIFY === mode) { modes.push(CONSTANTS.MODE_VERIFY); } } if (!this.use || this.use === "enc") { if (!mode || CONSTANTS.MODE_ENCRYPT === mode) { modes.push(CONSTANTS.MODE_ENCRYPT); } if (!mode || CONSTANTS.MODE_DECRYPT === mode) { modes.push(CONSTANTS.MODE_DECRYPT); } if (!mode || CONSTANTS.MODE_WRAP === mode) { modes.push(CONSTANTS.MODE_WRAP); } if (!mode || CONSTANTS.MODE_UNWRAP === mode) { modes.push(CONSTANTS.MODE_UNWRAP); } } var self = this; var algs = modes.map(function(m) { return cfg.algorithms.call(self, keys, m); }); algs = flatten(algs); algs = uniq(algs); if (this.alg) { // TODO: fix this correctly var valid; if ("oct" === kty) { valid = [this.alg, "dir"]; } else { valid = [this.alg]; } algs = intersection(algs, valid); } return algs; } }); /** * @method JWK.Key#supports * @description * Determines if the given algorithm is supported. * * @param {String} alg The algorithm in question * @param {String} [mode] The operation mode * @returns {Boolean} `true` if {alg} is supported, and `false` otherwise. * @see JWK.Key#algorithms */ Object.defineProperty(this, "supports", { value: function(alg, mode) { return (this.algorithms(mode).indexOf(alg) !== -1); } }); /** * @method JWK.Key#has * @description * Determines if this Key contains the given parameter. * * @param {String} name The name of the parameter * @param {Boolean} [isPrivate=false] `true` if private parameters should be * checked. * @returns {Boolean} `true` if the given parameter is present; `false` * otherwise. */ Object.defineProperty(this, "has", { value: function(name, isPrivate) { var contains = false; contains = contains || !!(json.base && (name in json.base)); contains = contains || !!(keys.public && (name in keys.public)); contains = contains || !!(json.extra && (name in json.extra)); contains = contains || !!(isPrivate && keys.private && (name in keys.private)); // TODO: check for export restrictions return contains; } }); /** * @method JWK.Key#get * @description * Retrieves the value of the given parameter. The value returned by this * method is in its natural format, which might not exactly match its * JSON encoding (e.g., a binary string rather than a base64url-encoded * string). * * **NOTE:** This method can return `false`. Call * {@link JWK.Key#has} to determine if the parameter is present. * * @param {String} name The name of the parameter * @param {Boolean} [isPrivate=false] `true` if private parameters should * be checked. * @returns {any} The value of the named parameter, or undefined if * it is not present. */ Object.defineProperty(this, "get", { value: function(name, isPrivate) { var src; if (json.base && (name in json.base)) { src = json.base; } else if (keys.public && (name in keys.public)) { src = keys.public; } else if (json.extra && (name in json.extra)) { src = json.extra; } else if (isPrivate && keys.private && (name in keys.private)) { // TODO: check for export restrictions src = keys.private; } return src && src[name] || null; } }); /** * @method JWK.Key#toJSON * @description * Returns the JSON representation of this Key. All properties of the * returned JSON object are properly encoded (e.g., base64url encoding for * any binary strings). * * @param {Boolean} [isPrivate=false] `true` if private parameters should be * included. * @param {String[]} [excluded] The list of parameters to exclude from * the returned JSON. * @returns {Object} The plain JSON object */ Object.defineProperty(this, "toJSON", { value: function(isPrivate, excluded) { // coerce arguments if (Array.isArray(isPrivate)) { excluded = isPrivate; isPrivate = false; } var result = {}; // TODO: check for export restrictions result = merge(result, json.base, json.public, ("boolean" === typeof isPrivate && isPrivate) ? json.private : {}, json.extra); result = omit(result, excluded || []); return result; } }); /** * @method JWK.Key#toPEM * @description * Returns the PEM representation of this Key as a string. * * @param {Boolean} [isPrivate=false] `true` if private parameters should be * included. * @returns {string} The PEM-encoded string */ Object.defineProperty(this, "toPEM", { value: function(isPrivate) { if (isPrivate === null) { isPrivate = false; } if (!cfg.convertToPEM) { throw new Error("Unsupported key type for PEM encoding"); } var k = (isPrivate) ? keys.private : keys.public; if (!k) { throw new Error("Invalid key"); } return cfg.convertToPEM.call(this, k, isPrivate); } }); /** * @method JWK.Key#toObject * @description * Returns the plain object representing this Key. All properties of the * returned object are in their natural encoding (e.g., binary strings * instead of base64url encoded). * * @param {Boolean} [isPrivate=false] `true` if private parameters should be * included. * @param {String[]} [excluded] The list of parameters to exclude from * the returned object. * @returns {Object} The plain Object. */ Object.defineProperty(this, "toObject", { value: function(isPrivate, excluded) { // coerce arguments if (Array.isArray(isPrivate)) { excluded = isPrivate; isPrivate = false; } var result = {}; // TODO: check for export restrictions result = merge(result, json.base, keys.public, ("boolean" === typeof isPrivate && isPrivate) ? keys.private : {}, json.extra); result = omit(result, (excluded || []).concat("length")); return result; } }); /** * @method JWK.Key#sign * @description * Sign the given data using the specified algorithm. * * **NOTE:** This is the primitive signing operation; the output is * _**NOT**_ a JSON Web Signature (JWS) object. * * The Promise, when fulfilled, returns an Object with the following * properties: * * + **data**: The data that was signed (and should be equal to {data}). * + **mac**: The signature or message authentication code (MAC). * * @param {String} alg The signing algorithm * @param {String|Buffer} data The data to sign * @param {Object} [props] Additional properties for the signing * algorithm. * @returns {Promise} The promise for the signing operation. * @throws {Error} If {alg} is not appropriate for this Key; or if * this Key does not contain the appropriate parameters. */ Object.defineProperty(this, "sign", { value: function(alg, data, props) { // validate appropriateness if (this.algorithms("sign").indexOf(alg) === -1) { return Promise.reject(new Error("unsupported algorithm")); } var k = cfg.signKey.call(this, alg, keys); if (!k) { return Promise.reject(new Error("improper key")); } // prepare properties (if any) props = (props) ? clone(props) : {}; if (cfg.signProps) { props = merge(props, cfg.signProps.call(this, alg, props)); } return ALGORITHMS.sign(alg, k, data, props); } }); /** * @method JWK.Key#verify * @description * Verify the given data and signature using the specified algorithm. * * **NOTE:** This is the primitive verification operation; the input is * _**NOT**_ a JSON Web Signature.

* * The Promise, when fulfilled, returns an Object with the following * properties: * * + **data**: The data that was verified (and should be equal to * {data}). * + **mac**: The signature or MAC that was verified (and should be equal * to {mac}). * + **valid**: `true` if {mac} is valid for {data}. * * @param {String} alg The verification algorithm * @param {String|Buffer} data The data to verify * @param {String|Buffer} mac The signature or MAC to verify * @param {Object} [props] Additional properties for the verification * algorithm. * @returns {Promise} The promise for the verification operation. * @throws {Error} If {alg} is not appropriate for this Key; or if * the Key does not contain the appropriate properties. */ Object.defineProperty(this, "verify", { value: function(alg, data, mac, props) { // validate appropriateness if (this.algorithms("verify").indexOf(alg) === -1) { return Promise.reject(new Error("unsupported algorithm")); } var k = cfg.verifyKey.call(this, alg, keys); if (!k) { return Promise.reject(new Error("improper key")); } // prepare properties (if any) props = (props) ? clone(props) : {}; if (cfg.verifyProps) { props = merge(props, cfg.verifyProps.call(this, alg, props)); } return ALGORITHMS.verify(alg, k, data, mac, props); } }); /** * @method JWK.Key#encrypt * @description * Encrypts the given data using the specified algorithm. * * **NOTE:** This is the primitive encryption operation; the output is * _**NOT**_ a JSON Web Encryption (JWE) object. * * **NOTE:** This operation is treated as distinct from {@link * JWK.Key#wrap}, as different algorithms and properties are often * used for wrapping a key versues encrypting arbitrary data. * * The Promise, when fulfilled, returns an object with the following * properties: * * + **data**: The ciphertext data * + **mac**: The associated message authentication code (MAC). * * @param {String} alg The encryption algorithm * @param {Buffer|String} data The data to encrypt * @param {Object} [props] Additional properties for the encryption * algorithm. * @returns {Promise} The promise for the encryption operation. * @throws {Error} If {alg} is not appropriate for this Key; or if * this Key does not contain the appropriate parameters. */ Object.defineProperty(this, "encrypt", { value: function(alg, data, props) { // validate appropriateness if (this.algorithms("encrypt").indexOf(alg) === -1) { return Promise.reject(new Error("unsupported algorithm")); } var k = cfg.encryptKey.call(this, alg, keys); if (!k) { return Promise.reject(new Error("improper key")); } // prepare properties (if any) props = (props) ? clone(props) : {}; if (cfg.encryptProps) { props = merge(props, cfg.encryptProps.call(this, alg, props)); } return ALGORITHMS.encrypt(alg, k, data, props); } }); /** * @method JWK.Key#decrypt * @description * Decrypts the given data using the specified algorithm. * * **NOTE:** This is the primitive decryption operation; the input is * _**NOT**_ a JSON Web Encryption (JWE) object. * * **NOTE:** This operation is treated as distinct from {@link * JWK.Key#unwrap}, as different algorithms and properties are often used * for unwrapping a key versues decrypting arbitrary data. * * The Promise, when fulfilled, returns the plaintext data. * * @param {String} alg The decryption algorithm. * @param {Buffer|String} data The data to decypt. * @param {Object} [props] Additional data for the decryption operation. * @returns {Promise} The promise for the decryption operation. * @throws {Error} If {alg} is not appropriate for this Key; or if * the Key does not contain the appropriate properties. */ Object.defineProperty(this, "decrypt", { value: function(alg, data, props) { // validate appropriateness if (this.algorithms("decrypt").indexOf(alg) === -1) { return Promise.reject(new Error("unsupported algorithm")); } var k = cfg.decryptKey.call(this, alg, keys); if (!k) { return Promise.reject(new Error("improper key")); } // prepare properties (if any) props = (props) ? clone(props) : {}; if (cfg.decryptProps) { props = merge(props, cfg.decryptProps.call(this, alg, props)); } return ALGORITHMS.decrypt(alg, k, data, props); } }); /** * @method JWK.Key#wrap * @description * Wraps the given key using the specified algorithm. * * **NOTE:** This is the primitive encryption operation; the output is * _**NOT**_ a JSON Web Encryption (JWE) object. * * **NOTE:** This operation is treated as distinct from {@link * JWK.Key#encrypt}, as different algorithms and properties are * often used for wrapping a key versues encrypting arbitrary data. * * The Promise, when fulfilled, returns an object with the following * properties: * * + **data**: The ciphertext data * + **headers**: The additional header parameters to apply to a JWE. * * @param {String} alg The encryption algorithm * @param {Buffer|String} data The data to encrypt * @param {Object} [props] Additional properties for the encryption * algorithm. * @returns {Promise} The promise for the encryption operation. * @throws {Error} If {alg} is not appropriate for this Key; or if * this Key does not contain the appropriate parameters. */ Object.defineProperty(this, "wrap", { value: function(alg, data, props) { // validate appropriateness if (this.algorithms("wrap").indexOf(alg) === -1) { return Promise.reject(new Error("unsupported algorithm")); } var k = cfg.wrapKey.call(this, alg, keys); if (!k) { return Promise.reject(new Error("improper key")); } // prepare properties (if any) props = (props) ? clone(props) : {}; if (cfg.wrapProps) { props = merge(props, cfg.wrapProps.call(this, alg, props)); } return ALGORITHMS.encrypt(alg, k, data, props); } }); /** * @method JWK.Key#unwrap * @description * Unwraps the given key using the specified algorithm. * * **NOTE:** This is the primitive unwrap operation; the input is * _**NOT**_ a JSON Web Encryption (JWE) object. * * **NOTE:** This operation is treated as distinct from {@link * JWK.Key#decrypt}, as different algorithms and properties are often used * for unwrapping a key versues decrypting arbitrary data. * * The Promise, when fulfilled, returns the unwrapped key. * * @param {String} alg The unwrap algorithm. * @param {Buffer|String} data The data to unwrap. * @param {Object} [props] Additional data for the unwrap operation. * @returns {Promise} The promise for the unwrap operation. * @throws {Error} If {alg} is not appropriate for this Key; or if * the Key does not contain the appropriate properties. */ Object.defineProperty(this, "unwrap", { value: function(alg, data, props) { // validate appropriateness if (this.algorithms("unwrap").indexOf(alg) === -1) { return Promise.reject(new Error("unsupported algorithm")); } var k = cfg.unwrapKey.call(this, alg, keys); if (!k) { return Promise.reject(new Error("improper key")); } // prepare properties (if any) props = (props) ? clone(props) : {}; if (cfg.unwrapProps) { props = merge(props, cfg.unwrapProps.call(this, alg, props)); } return ALGORITHMS.decrypt(alg, k, data, props); } }); }; module.exports = JWKBaseKeyObject;