rittenhop-dev/versions/5.94.2/node_modules/node-jose/lib/jwe/encrypt.js
2024-09-23 19:40:12 -04:00

678 lines
20 KiB
JavaScript

/*!
* jwe/encrypt.js - Encrypt to a JWE
*
* Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file.
*/
"use strict";
var util = require("../util"),
generateCEK = require("./helpers").generateCEK,
JWK = require("../jwk"),
slice = require("./helpers").slice,
pako = require("pako"),
CONSTANTS = require("../algorithms/constants");
var assign = require("lodash/assign");
var clone = require("lodash/clone");
var DEFAULTS = require("./defaults");
/**
* @class JWE.Encrypter
* @classdesc
* Generator of encrypted data.
*
* @description
* **NOTE:** This class cannot be instantiated directly. Instead call {@link
* JWE.createEncrypt}.
*/
function JWEEncrypter(cfg, fields, recipients) {
var finalized = false,
format = cfg.format || "general",
protectAll = !!cfg.protectAll,
content = Buffer.alloc(0);
/**
* @member {String} JWE.Encrypter#zip
* @readonly
* @description
* Indicates the compression algorithm applied to the plaintext
* before it is encrypted. The possible values are:
*
* + **`"DEF"`**: Compress the plaintext using the DEFLATE algorithm.
* + **`""`**: Do not compress the plaintext.
*/
Object.defineProperty(this, "zip", {
get: function() {
return fields.zip || "";
},
enumerable: true
});
/**
* @member {Boolean} JWE.Encrypter#compact
* @readonly
* @description
* Indicates whether the output of this encryption generator is
* using the Compact serialization (`true`) or the JSON
* serialization (`false`).
*/
Object.defineProperty(this, "compact", {
get: function() { return "compact" === format; },
enumerable: true
});
/**
* @member {String} JWE.Encrypter#format
* @readonly
* @description
* Indicates the format the output of this encryption generator takes.
*/
Object.defineProperty(this, "format", {
get: function() { return format; },
enumerable: true
});
/**
* @member {String[]} JWE.Encrypter#protected
* @readonly
* @description
* The header parameter names that are protected. Protected header fields
* are first serialized to UTF-8 then encoded as util.base64url, then used as
* the additional authenticated data in the encryption operation.
*/
Object.defineProperty(this, "protected", {
get: function() {
return clone(cfg.protect);
},
enumerable: true
});
/**
* @member {Object} JWE.Encrypter#header
* @readonly
* @description
* The global header parameters, both protected and unprotected. Call
* {@link JWE.Encrypter#protected} to determine which parameters will
* be protected.
*/
Object.defineProperty(this, "header", {
get: function() {
return clone(fields);
},
enumerable: true
});
/**
* @method JWE.Encrypter#update
* @description
* Updates the plaintext data for the encryption generator. The plaintext
* is appended to the end of any other plaintext already applied.
*
* If {data} is a Buffer, {encoding} is ignored. Otherwise, {data} is
* converted to a Buffer internally to {encoding}.
*
* @param {Buffer|String} [data] The plaintext to apply.
* @param {String} [encoding] The encoding of the plaintext.
* @returns {JWE.Encrypter} This encryption generator.
* @throws {Error} If ciphertext has already been generated.
*/
Object.defineProperty(this, "update", {
value: function(data, encoding) {
if (finalized) {
throw new Error("already final");
}
if (data != null) {
data = util.asBuffer(data, encoding);
if (content.length) {
content = Buffer.concat([content, data],
content.length + data.length);
} else {
content = data;
}
}
return this;
}
});
/**
* @method JWE.Encrypter#final
* @description
* Finishes the encryption operation.
*
* The returned Promise, when fulfilled, is the JSON Web Encryption (JWE)
* object, either in the Compact (if {@link JWE.Encrypter#compact} is
* `true`) or the JSON serialization.
*
* @param {Buffer|String} [data] The final plaintext data to apply.
* @param {String} [encoding] The encoding of the final plaintext data
* (if any).
* @returns {Promise} A promise for the encryption operation.
* @throws {Error} If ciphertext has already been generated.
*/
Object.defineProperty(this, "final", {
value: function(data, encoding) {
if (finalized) {
return Promise.reject(new Error("already final"));
}
// last-minute data
this.update(data, encoding);
// mark as done...ish
finalized = true;
var promise = Promise.resolve({});
// determine CEK and IV
var encAlg = fields.enc;
var encKey;
promise = promise.then(function(jwe) {
if (cfg.cek) {
encKey = JWK.asKey(cfg.cek);
}
return jwe;
});
// process recipients
promise = promise.then(function(jwe) {
var procR = function(r, one) {
var props = {};
props = assign(props, fields);
props = assign(props, r.header);
var algKey = r.key,
algAlg = props.alg;
// generate Ephemeral EC Key
var tks,
rpromise;
if ((props.alg || "").indexOf("ECDH-ES") === 0) {
tks = algKey.keystore.temp();
if (r.epk) {
rpromise = Promise.resolve(r.epk).
then(function(epk) {
r.header.epk = epk.toJSON(false, ["kid"]);
props.epk = epk.toObject(true, ["kid"]);
});
} else {
rpromise = tks.generate("EC", algKey.get("crv")).
then(function(epk) {
r.header.epk = epk.toJSON(false, ["kid"]);
props.epk = epk.toObject(true, ["kid"]);
});
}
} else {
rpromise = Promise.resolve();
}
// encrypt the CEK
rpromise = rpromise.then(function() {
var cek,
p;
// special case 'alg=dir'
if ("dir" === algAlg && one) {
encKey = Promise.resolve(algKey);
p = encKey.then(function(jwk) {
// fixup encAlg
if (!encAlg) {
props.enc = fields.enc = encAlg = jwk.algorithms(JWK.MODE_ENCRYPT)[0];
}
return {
once: true,
direct: true
};
});
} else {
if (!encKey) {
if (!encAlg) {
props.enc = fields.enc = encAlg = cfg.contentAlg;
}
encKey = generateCEK(encAlg);
}
p = encKey.then(function(jwk) {
cek = jwk.get("k", true);
// algKey may or may not be a promise
return algKey;
});
p = p.then(function(algKey) {
return algKey.wrap(algAlg, cek, props);
});
}
return p;
});
rpromise = rpromise.then(function(wrapped) {
if (wrapped.once && !one) {
return Promise.reject(new Error("cannot use 'alg':'" + algAlg + "' with multiple recipients"));
}
var rjwe = {},
cek;
if (wrapped.data) {
cek = wrapped.data;
cek = util.base64url.encode(cek);
}
if (wrapped.direct && cek) {
// replace content key
encKey = JWK.asKey({
kty: "oct",
k: cek
});
} else if (cek) {
/* eslint camelcase: [0] */
rjwe.encrypted_key = cek;
}
if (r.header && Object.keys(r.header).length) {
rjwe.header = clone(r.header || {});
}
if (wrapped.header) {
rjwe.header = assign(rjwe.header || {},
wrapped.header);
}
return rjwe;
});
return rpromise;
};
var p = Promise.all(recipients);
p = p.then(function(rcpts) {
var single = (1 === rcpts.length);
rcpts = rcpts.map(function(r) {
return procR(r, single);
});
return Promise.all(rcpts);
});
p = p.then(function(rcpts) {
jwe.recipients = rcpts.filter(function(r) { return !!r; });
return jwe;
});
return p;
});
// normalize headers
var props = {};
promise = promise.then(function(jwe) {
var protect,
lenProtect,
unprotect,
lenUnprotect;
unprotect = clone(fields);
if ((protectAll && jwe.recipients.length === 1) || "compact" === format) {
// merge single recipient into fields
protect = {};
protect = assign({},
unprotect,
jwe.recipients[0].header);
lenProtect = Object.keys(protect).length;
unprotect = undefined;
lenUnprotect = 0;
delete jwe.recipients[0].header;
if (Object.keys(jwe.recipients[0]).length === 0) {
jwe.recipients.splice(0, 1);
}
} else {
protect = {};
lenProtect = 0;
lenUnprotect = Object.keys(unprotect).length;
cfg.protect.forEach(function(f) {
// remove protected header values from body unprotected header
if (!(f in unprotect)) {
return;
}
protect[f] = unprotect[f];
lenProtect++;
delete unprotect[f];
lenUnprotect--;
});
jwe.recipients = (jwe.recipients || []).map(function(rcpt) {
rcpt = rcpt || {};
var header = rcpt.header;
if (header) {
Object.keys(header).forEach(function (f) {
if (f in protect) { delete header[f]; }
});
if (!Object.keys(header).length) {
delete rcpt.header;
}
}
return rcpt;
});
}
if (!jwe.recipients || jwe.recipients.length === 0) {
delete jwe.recipients;
}
// "serialize" (and setup merged props)
if (unprotect && lenUnprotect > 0) {
props = assign(props, unprotect);
jwe.unprotected = unprotect;
}
if (protect && lenProtect > 0) {
props = assign(props, protect);
protect = JSON.stringify(protect);
jwe.protected = util.base64url.encode(protect, "utf8");
}
return jwe;
});
// (OPTIONAL) compress plaintext
promise = promise.then(function(jwe) {
var pdata = content;
if (!props.zip) {
jwe.plaintext = pdata;
return jwe;
} else if (props.zip === "DEF") {
return new Promise(function(resolve, reject) {
try {
var data = pako.deflateRaw(Buffer.from(pdata, "binary"));
jwe.plaintext = Buffer.from(data);
resolve(jwe);
} catch (error) {
reject(error);
}
});
}
return Promise.reject(new Error("unsupported 'zip' mode"));
});
// encrypt plaintext
promise = promise.then(function(jwe) {
props.adata = jwe.protected;
if ("aad" in cfg && cfg.aad != null) {
props.adata += "." + cfg.aad;
props.adata = Buffer.from(props.adata, "utf8");
}
// calculate IV
var iv = cfg.iv ||
util.randomBytes(CONSTANTS.NONCELENGTH[encAlg] / 8);
if ("string" === typeof iv) {
iv = util.base64url.decode(iv);
}
props.iv = iv;
if ("recipients" in jwe && jwe.recipients.length === 1) {
props.kdata = jwe.recipients[0].encrypted_key;
}
if ("epu" in cfg && cfg.epu != null) {
props.epu = cfg.epu;
}
if ("epv" in cfg && cfg.epv != null) {
props.epv = cfg.epv;
}
var pdata = jwe.plaintext;
delete jwe.plaintext;
return encKey.then(function(encKey) {
var p = encKey.encrypt(encAlg, pdata, props);
p = p.then(function(result) {
jwe.iv = util.base64url.encode(iv, "binary");
if ("aad" in cfg && cfg.aad != null) {
jwe.aad = cfg.aad;
}
jwe.ciphertext = util.base64url.encode(result.data, "binary");
jwe.tag = util.base64url.encode(result.tag, "binary");
return jwe;
});
return p;
});
});
// (OPTIONAL) compact/flattened results
switch (format) {
case "compact":
promise = promise.then(function(jwe) {
var compact = new Array(5);
compact[0] = jwe.protected;
if (jwe.recipients && jwe.recipients[0]) {
compact[1] = jwe.recipients[0].encrypted_key;
}
compact[2] = jwe.iv;
compact[3] = jwe.ciphertext;
compact[4] = jwe.tag;
compact = compact.join(".");
return compact;
});
break;
case "flattened":
promise = promise.then(function(jwe) {
var flattened = {},
rcpt = jwe.recipients && jwe.recipients[0];
if (jwe.protected) {
flattened.protected = jwe.protected;
}
if (jwe.unprotected) {
flattened.unprotected = jwe.unprotected;
}
["header", "encrypted_key"].forEach(function(f) {
if (!rcpt) { return; }
if (!(f in rcpt)) { return; }
if (!rcpt[f]) { return; }
if ("object" === typeof rcpt[f] && !Object.keys(rcpt[f]).length) { return; }
flattened[f] = rcpt[f];
});
if (jwe.aad) {
flattened.aad = jwe.aad;
}
flattened.iv = jwe.iv;
flattened.ciphertext = jwe.ciphertext;
flattened.tag = jwe.tag;
return flattened;
});
break;
case "general":
promise = promise.then(function(jwe) {
var recipients = jwe.recipients || [];
recipients = recipients.map(function (rcpt) {
if (!Object.keys(rcpt).length) { return undefined; }
return rcpt;
});
recipients = recipients.filter(function (rcpt) { return !!rcpt; });
if (recipients.length) {
jwe.recipients = recipients;
} else {
delete jwe.recipients;
}
return jwe;
});
}
return promise;
}
});
}
function createEncrypt(opts, rcpts) {
// fixup recipients
var options = opts,
rcptStart = 1,
rcptList = rcpts;
if (arguments.length === 0) {
throw new Error("at least one recipient must be provided");
}
if (arguments.length === 1) {
// assume opts is the recipient list
rcptList = opts;
rcptStart = 0;
options = {};
} else if (JWK.isKey(opts) ||
(opts && "kty" in opts) ||
(opts && "key" in opts &&
(JWK.isKey(opts.key) || "kty" in opts.key))) {
rcptList = opts;
rcptStart = 0;
options = {};
} else {
options = clone(opts);
}
if (!Array.isArray(rcptList)) {
rcptList = slice(arguments, rcptStart);
}
// fixup options
options = assign(clone(DEFAULTS), options);
// setup header fields
var fields = clone(options.fields || {});
if (options.zip) {
fields.zip = (typeof options.zip === "boolean") ?
(options.zip ? "DEF" : false) :
options.zip;
}
options.format = (options.compact ? "compact" : options.format) || "general";
switch (options.format) {
case "compact":
if ("aad" in opts) {
throw new Error("additional authenticated data cannot be used for compact serialization");
}
/* eslint no-fallthrough: [0] */
case "flattened":
if (rcptList.length > 1) {
throw new Error("too many recipients for compact serialization");
}
break;
}
// note protected fields (globally)
// protected fields are global only
var protectAll = false;
if ("compact" === options.format || "*" === options.protect) {
protectAll = true;
options.protect = Object.keys(fields).concat("enc");
} else if (typeof options.protect === "string") {
options.protect = [options.protect];
} else if (Array.isArray(options.protect)) {
options.protect = options.protect.concat();
} else if (!options.protect) {
options.protect = [];
} else {
throw new Error("protect must be a list of fields");
}
if (protectAll && 1 < rcptList.length) {
throw new Error("too many recipients to protect all header parameters");
}
rcptList = rcptList.map(function(r, idx) {
var p;
// resolve a key
if (r && "kty" in r) {
p = JWK.asKey(r);
p = p.then(function(k) {
return {
key: k
};
});
} else if (r) {
p = JWK.asKey(r.key);
p = p.then(function(k) {
return {
header: r.header,
reference: r.reference,
key: k
};
});
} else {
p = Promise.reject(new Error("missing key for recipient " + idx));
}
// convert ephemeral key (if present)
if (r.epk) {
p = p.then(function(recipient) {
return JWK.asKey(r.epk).
then(function(epk) {
recipient.epk = epk;
return recipient;
});
});
}
// resolve the complete recipient
p = p.then(function(recipient) {
var key = recipient.key;
// prepare the recipient header
var header = recipient.header || {};
recipient.header = header;
var props = {};
props = assign(props, fields);
props = assign(props, recipient.header);
// ensure key protection algorithm is set
if (!props.alg) {
props.alg = key.algorithms(JWK.MODE_WRAP)[0];
header.alg = props.alg;
}
if (!props.alg) {
return Promise.reject(new Error("key not valid for encrypting to recipient " + idx));
}
header.alg = props.alg;
// determine the key reference
var ref = recipient.reference;
delete recipient.reference;
if (undefined === ref) {
// header already contains the key reference
ref = ["kid", "jku", "x5c", "x5t", "x5u"].some(function(k) {
return (k in header);
});
ref = !ref ? "kid" : null;
} else if ("boolean" === typeof ref) {
// explicit (positive | negative) request for key reference
ref = ref ? "kid" : null;
}
var jwk;
if (ref) {
jwk = key.toJSON();
if ("jwk" === ref) {
if ("oct" === key.kty) {
return Promise.reject(new Error("cannot embed key"));
}
header.jwk = jwk;
} else if (ref in jwk) {
header[ref] = jwk[ref];
}
}
// freeze recipient
recipient = Object.freeze(recipient);
return recipient;
});
return p;
});
// create and configure encryption
var cfg = {
aad: ("aad" in options) ? util.base64url.encode(options.aad || "") : null,
contentAlg: options.contentAlg,
format: options.format,
protect: options.protect,
cek: options.cek,
iv: options.iv,
protectAll: protectAll
};
var enc = new JWEEncrypter(cfg, fields, rcptList);
return enc;
}
module.exports = {
encrypter: JWEEncrypter,
createEncrypt: createEncrypt
};