/*! * jwe/decrypt.js - Decrypt from a JWE * * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. */ "use strict"; var base64url = require("../util/base64url"), AlgConfig = require("../util/algconfig"), JWK = require("../jwk"), merge = require("../util/merge"), pako = require("pako"); var DEFAULT_OPTIONS = { algorithms: "*" }; /** * @class JWE.Decrypter * @classdesc Processor of encrypted data. * * @description * **NOTE:** This class cannot be instantiated directly. Instead * call {@link JWE.createDecrypt}. */ function JWEDecrypter(ks, globalOpts) { var assumedKey, keystore; if (JWK.isKey(ks)) { assumedKey = ks; keystore = assumedKey.keystore; } else if (JWK.isKeyStore(ks)) { keystore = ks; } else { throw new TypeError("Keystore must be provided"); } globalOpts = merge({}, DEFAULT_OPTIONS, globalOpts); /** * Decrypts the given input. * * {opts}, if provided, is used to customize this specific decrypt operation. * This argument has the same semantics as {JWE.createDecrypt}, and takes * precedence over those options. * * The returned PRomise, when fulfilled, returns an object with the * following members: * * - `header` - The JOSE Header, combined from the relevant "header" and * "protected" fields from the original JWE object. * - `protected` - An array containing the names of the protected fields * - `key` - The used to decrypt the content * - `payload` - The decrypted content (as a Buffer) * - `plaintext` - An alias for `payload` * * @param {Object|String} input The encrypted content * @param {Object} [opts] The options for this decryption operation. * @returns {Promise} A promise for the decyprted plaintext */ Object.defineProperty(this, "decrypt", { value: function(input, opts) { opts = merge({}, globalOpts, opts || {}); var extraHandlers = opts.handlers || {}; var handlerKeys = Object.keys(extraHandlers); var algSpec = new AlgConfig(opts.algorithms); /* eslint camelcase: [0] */ if (typeof input === "string") { input = input.split("."); input = { protected: input[0], recipients: [ { encrypted_key: input[1] } ], iv: input[2], ciphertext: input[3], tag: input[4] }; } else if (!input || typeof input !== "object") { throw new Error("invalid input"); } if ("encrypted_key" in input) { input.recipients = [ { encrypted_key: input.encrypted_key } ]; } var promise; // ensure recipients exists var rcptList = input.recipients || [{}]; promise = Promise.resolve(rcptList); //combine fields var fields, protect; promise = promise.then(function(rcptList) { if (input.protected) { protect = base64url.decode(input.protected).toString("utf8"); protect = JSON.parse(protect); // verify "crit" field first var crit = protect.crit; if (crit) { if (!Array.isArray(crit)) { return Promise.reject(new Error("Invalid 'crit' header")); } for (var idx = 0; crit.length > idx; idx++) { if (-1 === handlerKeys.indexOf(crit[idx])) { return Promise.reject(new Error( "Critical extension is not supported: " + crit[idx] )); } } } fields = protect; protect = Object.keys(protect); } else { fields = {}; protect = []; } fields = merge(input.unprotected || {}, fields); rcptList = rcptList.map(function(r) { var promise = Promise.resolve(); var header = r.header || {}; header = merge(header, fields); r.header = header; r.protected = protect; // check on allowed algorithms if (!algSpec.match(header.alg)) { promise = promise.then(function() { return Promise.reject(new Error("Algorithm not allowed: " + header.alg)); }); } if (!algSpec.match(header.enc)) { promise = promise.then(function () { return Promise.reject(new Error("Algorithm not allowed: " + header.enc)); }); } if (header.epk) { promise = promise.then(function() { return JWK.asKey(header.epk); }); promise = promise.then(function(epk) { header.epk = epk.toObject(false); }); } return promise.then(function() { return r; }); }); return Promise.all(rcptList); }); // decrypt with first key found var algKey, encKey, kdata; promise = promise.then(function(rcptList) { var jwe = {}; return new Promise(function(resolve, reject) { var processKey = function() { var rcpt = rcptList.shift(); if (!rcpt) { reject(new Error("no key found")); return; } var algPromise = Promise.resolve(rcpt); algPromise = algPromise.then(function(rcpt) { // try to unwrap encrypted key var prekey = kdata = rcpt.encrypted_key || ""; prekey = base64url.decode(prekey); algKey = assumedKey || keystore.get({ use: "enc", alg: rcpt.header.alg, kid: rcpt.header.kid }); if (algKey) { return algKey.unwrap(rcpt.header.alg, prekey, rcpt.header); } else { return Promise.reject(); } }); algPromise = algPromise.then(function(key) { encKey = { "kty": "oct", "k": base64url.encode(key) }; encKey = JWK.asKey(encKey); jwe.key = algKey; jwe.header = rcpt.header; jwe.protected = rcpt.protected; resolve(jwe); }); algPromise.catch(processKey); }; processKey(); }); }); // assign decipher inputs promise = promise.then(function(jwe) { jwe.iv = input.iv; jwe.tag = input.tag; jwe.ciphertext = input.ciphertext; return jwe; }); // process any prepare-decrypt handlers promise = promise.then(function(jwe) { var processing = []; handlerKeys.forEach(function(h) { h = extraHandlers[h]; var p; if ("function" === typeof h) { p = h(jwe); } else if ("object" === typeof h && "function" === typeof h.prepare) { p = h.prepare(jwe); } if (p) { processing.push(Promise.resolve(p)); } }); return Promise.all(processing).then(function() { // don't actually care about individual handler results // assume {jwe} is updated return jwe; }); }); // prepare decrypt inputs promise = promise.then(function(jwe) { if (!Buffer.isBuffer(jwe.ciphertext)) { jwe.ciphertext = base64url.decode(jwe.ciphertext); } return jwe; }); // decrypt it! promise = promise.then(function(jwe) { var adata = input.protected; if ("aad" in input && null != input.aad) { adata += "." + input.aad; } var params = { iv: jwe.iv, adata: adata, tag: jwe.tag, kdata: kdata, epu: jwe.epu, epv: jwe.epv }; var cdata = jwe.ciphertext; delete jwe.iv; delete jwe.tag; delete jwe.ciphertext; return encKey. then(function(enkKey) { return enkKey.decrypt(jwe.header.enc, cdata, params). then(function(pdata) { jwe.payload = jwe.plaintext = pdata; return jwe; }); }); }); // (OPTIONAL) decompress plaintext promise = promise.then(function(jwe) { if ("DEF" === jwe.header.zip) { return new Promise(function(resolve, reject) { try { var data = pako.inflateRaw(Buffer.from(jwe.plaintext)) jwe.payload = jwe.plaintext = Buffer.from(data); resolve(jwe); } catch (err) { reject(err); } }); } return jwe; }); // process any post-decrypt handlers promise = promise.then(function(jwe) { var processing = []; handlerKeys.forEach(function(h) { h = extraHandlers[h]; var p; if ("object" === typeof h && "function" === typeof h.complete) { p = h.complete(jwe); } if (p) { processing.push(Promise.resolve(p)); } }); return Promise.all(processing).then(function() { // don't actually care about individual handler results // assume {jwe} is updated return jwe; }); }); return promise; } }); } /** * @description * Creates a new Decrypter for the given Key or KeyStore. * * {opts}, when provided, is used to customize decryption processes. The * following options are currently supported: * * - `handlers` - An object where each name is a JOSE header member name and * the value can be a boolean, function, or an object. * * Handlers are intended to support 'crit' extensions. When a boolean value, * the member is expected to be processed once decryption is fully complete. * When a function, it is called just before the ciphertext is decrypted * (processed as if it were a `prepare` handler, as decribed below). When an * object, it can contain any of the following members: * * - `recipient` - A function called after a valid key is determined; it takes * an object describing the recipient, and returns a Promise that is * fulfilled once the handler's processing is complete. * - `prepare` - A function called just prior to decrypting the ciphertext; * it takes an object describing the decryption result (but containing * `ciphertext` and `tag' instead of `payload` and `plaintext`), and * returns a Promise that is fulfilled once the handler's processing is * complete. * - `complete` - A function called once decryption is complete, just prior * to fulfilling the Promise returned by `decrypt()`; it takes the object * that will be returned by `decrypt()`'s fulfilled Promise, and returns * a Promise that is fulfilled once the handler's processing is complete. * * Note that normal processing of `decrypt()` does not continue until all * relevant handlers have completed. Any changes handlers make to the * provided objects affects `decrypt()`'s processing. * * @param {JWK.Key|JWK.KeyStore} ks The Key or KeyStore to use for decryption. * @param {Object} [opts] The options for this Decrypter. * @returns {JWE.Decrypter} The new Decrypter. */ function createDecrypt(ks, opts) { var dec = new JWEDecrypter(ks, opts); return dec; } module.exports = { decrypter: JWEDecrypter, createDecrypt: createDecrypt };