import * as asn1js from "asn1js";
import { getParametersValue, utilConcatBuf, clearProps } from "pvutils";
import { getOIDByAlgorithm, getRandomValues, getCrypto, getAlgorithmByOID, kdf } from "./common.js";
import OriginatorInfo from "./OriginatorInfo.js";
import RecipientInfo from "./RecipientInfo.js";
import EncryptedContentInfo from "./EncryptedContentInfo.js";
import Attribute from "./Attribute.js";
import AlgorithmIdentifier from "./AlgorithmIdentifier.js";
import RSAESOAEPParams from "./RSAESOAEPParams.js";
import KeyTransRecipientInfo from "./KeyTransRecipientInfo.js";
import IssuerAndSerialNumber from "./IssuerAndSerialNumber.js";
import RecipientKeyIdentifier from "./RecipientKeyIdentifier.js";
import RecipientEncryptedKey from "./RecipientEncryptedKey.js";
import KeyAgreeRecipientIdentifier from "./KeyAgreeRecipientIdentifier.js";
import KeyAgreeRecipientInfo from "./KeyAgreeRecipientInfo.js";
import RecipientEncryptedKeys from "./RecipientEncryptedKeys.js";
import KEKRecipientInfo from "./KEKRecipientInfo.js";
import KEKIdentifier from "./KEKIdentifier.js";
import PBKDF2Params from "./PBKDF2Params.js";
import PasswordRecipientinfo from "./PasswordRecipientinfo.js";
import ECCCMSSharedInfo from "./ECCCMSSharedInfo.js";
import OriginatorIdentifierOrKey from "./OriginatorIdentifierOrKey.js";
import OriginatorPublicKey from "./OriginatorPublicKey.js";
//**************************************************************************************
const defaultEncryptionParams = {
	kdfAlgorithm: "SHA-512",
	kekEncryptionLength: 256
};
const curveLengthByName = {
	"P-256": 256,
	"P-384": 384,
	"P-521": 528
};
//**************************************************************************************
/**
 * Class from RFC5652
 */
export default class EnvelopedData
{
	//**********************************************************************************
	/**
	 * Constructor for EnvelopedData class
	 * @param {Object} [parameters={}]
	 * @param {Object} [parameters.schema] asn1js parsed value to initialize the class from
	 */
	constructor(parameters = {})
	{
		//region Internal properties of the object
		/**
		 * @type {number}
		 * @desc version
		 */
		this.version = getParametersValue(parameters, "version", EnvelopedData.defaultValues("version"));

		if("originatorInfo" in parameters)
			/**
			 * @type {OriginatorInfo}
			 * @desc originatorInfo
			 */
			this.originatorInfo = getParametersValue(parameters, "originatorInfo", EnvelopedData.defaultValues("originatorInfo"));

		/**
		 * @type {Array.<RecipientInfo>}
		 * @desc recipientInfos
		 */
		this.recipientInfos = getParametersValue(parameters, "recipientInfos", EnvelopedData.defaultValues("recipientInfos"));
		/**
		 * @type {EncryptedContentInfo}
		 * @desc encryptedContentInfo
		 */
		this.encryptedContentInfo = getParametersValue(parameters, "encryptedContentInfo", EnvelopedData.defaultValues("encryptedContentInfo"));

		if("unprotectedAttrs" in parameters)
			/**
			 * @type {Array.<Attribute>}
			 * @desc unprotectedAttrs
			 */
			this.unprotectedAttrs = getParametersValue(parameters, "unprotectedAttrs", EnvelopedData.defaultValues("unprotectedAttrs"));
		//endregion

		//region If input argument array contains "schema" for this object
		if("schema" in parameters)
			this.fromSchema(parameters.schema);
		//endregion
	}
	//**********************************************************************************
	/**
	 * Return default values for all class members
	 * @param {string} memberName String name for a class member
	 */
	static defaultValues(memberName)
	{
		switch(memberName)
		{
			case "version":
				return 0;
			case "originatorInfo":
				return new OriginatorInfo();
			case "recipientInfos":
				return [];
			case "encryptedContentInfo":
				return new EncryptedContentInfo();
			case "unprotectedAttrs":
				return [];
			default:
				throw new Error(`Invalid member name for EnvelopedData class: ${memberName}`);
		}
	}
	//**********************************************************************************
	/**
	 * Compare values with default values for all class members
	 * @param {string} memberName String name for a class member
	 * @param {*} memberValue Value to compare with default value
	 */
	static compareWithDefault(memberName, memberValue)
	{
		switch(memberName)
		{
			case "version":
				return (memberValue === EnvelopedData.defaultValues(memberName));
			case "originatorInfo":
				return ((memberValue.certs.certificates.length === 0) && (memberValue.crls.crls.length === 0));
			case "recipientInfos":
			case "unprotectedAttrs":
				return (memberValue.length === 0);
			case "encryptedContentInfo":
				return ((EncryptedContentInfo.compareWithDefault("contentType", memberValue.contentType)) &&
				(EncryptedContentInfo.compareWithDefault("contentEncryptionAlgorithm", memberValue.contentEncryptionAlgorithm) &&
				(EncryptedContentInfo.compareWithDefault("encryptedContent", memberValue.encryptedContent))));
			default:
				throw new Error(`Invalid member name for EnvelopedData class: ${memberName}`);
		}
	}
	//**********************************************************************************
	/**
	 * Return value of pre-defined ASN.1 schema for current class
	 *
	 * ASN.1 schema:
	 * ```asn1
	 * EnvelopedData ::= SEQUENCE {
	 *    version CMSVersion,
	 *    originatorInfo [0] IMPLICIT OriginatorInfo OPTIONAL,
	 *    recipientInfos RecipientInfos,
	 *    encryptedContentInfo EncryptedContentInfo,
	 *    unprotectedAttrs [1] IMPLICIT UnprotectedAttributes OPTIONAL }
	 * ```
	 *
	 * @param {Object} parameters Input parameters for the schema
	 * @returns {Object} asn1js schema object
	 */
	static schema(parameters = {})
	{
		/**
		 * @type {Object}
		 * @property {string} [blockName]
		 * @property {string} [version]
		 * @property {string} [originatorInfo]
		 * @property {string} [recipientInfos]
		 * @property {string} [encryptedContentInfo]
		 * @property {string} [unprotectedAttrs]
		 */
		const names = getParametersValue(parameters, "names", {});

		return (new asn1js.Sequence({
			name: (names.blockName || ""),
			value: [
				new asn1js.Integer({ name: (names.version || "") }),
				new asn1js.Constructed({
					name: (names.originatorInfo || ""),
					optional: true,
					idBlock: {
						tagClass: 3, // CONTEXT-SPECIFIC
						tagNumber: 0 // [0]
					},
					value: OriginatorInfo.schema().valueBlock.value
				}),
				new asn1js.Set({
					value: [
						new asn1js.Repeated({
							name: (names.recipientInfos || ""),
							value: RecipientInfo.schema()
						})
					]
				}),
				EncryptedContentInfo.schema(names.encryptedContentInfo || {}),
				new asn1js.Constructed({
					optional: true,
					idBlock: {
						tagClass: 3, // CONTEXT-SPECIFIC
						tagNumber: 1 // [1]
					},
					value: [
						new asn1js.Repeated({
							name: (names.unprotectedAttrs || ""),
							value: Attribute.schema()
						})
					]
				})
			]
		}));
	}
	//**********************************************************************************
	/**
	 * Convert parsed asn1js object into current class
	 * @param {!Object} schema
	 */
	fromSchema(schema)
	{
		//region Clear input data first
		clearProps(schema, [
			"version",
			"originatorInfo",
			"recipientInfos",
			"encryptedContentInfo",
			"unprotectedAttrs"
		]);
		//endregion

		//region Check the schema is valid
		const asn1 = asn1js.compareSchema(schema,
			schema,
			EnvelopedData.schema({
				names: {
					version: "version",
					originatorInfo: "originatorInfo",
					recipientInfos: "recipientInfos",
					encryptedContentInfo: {
						names: {
							blockName: "encryptedContentInfo"
						}
					},
					unprotectedAttrs: "unprotectedAttrs"
				}
			})
		);

		if(asn1.verified === false)
			throw new Error("Object's schema was not verified against input data for EnvelopedData");
		//endregion

		//region Get internal properties from parsed schema
		this.version = asn1.result.version.valueBlock.valueDec;

		if("originatorInfo" in asn1.result)
		{
			this.originatorInfo = new OriginatorInfo({
				schema: new asn1js.Sequence({
					value: asn1.result.originatorInfo.valueBlock.value
				})
			});
		}

		this.recipientInfos = Array.from(asn1.result.recipientInfos, element => new RecipientInfo({ schema: element }));
		this.encryptedContentInfo = new EncryptedContentInfo({ schema: asn1.result.encryptedContentInfo });

		if("unprotectedAttrs" in asn1.result)
			this.unprotectedAttrs = Array.from(asn1.result.unprotectedAttrs, element => new Attribute({ schema: element }));
		//endregion
	}
	//**********************************************************************************
	/**
	 * Convert current object to asn1js object and set correct values
	 * @returns {Object} asn1js object
	 */
	toSchema()
	{
		//region Create array for output sequence
		const outputArray = [];

		outputArray.push(new asn1js.Integer({ value: this.version }));

		if("originatorInfo" in this)
		{
			outputArray.push(new asn1js.Constructed({
				optional: true,
				idBlock: {
					tagClass: 3, // CONTEXT-SPECIFIC
					tagNumber: 0 // [0]
				},
				value: this.originatorInfo.toSchema().valueBlock.value
			}));
		}

		outputArray.push(new asn1js.Set({
			value: Array.from(this.recipientInfos, element => element.toSchema())
		}));

		outputArray.push(this.encryptedContentInfo.toSchema());

		if("unprotectedAttrs" in this)
		{
			outputArray.push(new asn1js.Constructed({
				optional: true,
				idBlock: {
					tagClass: 3, // CONTEXT-SPECIFIC
					tagNumber: 1 // [1]
				},
				value: Array.from(this.unprotectedAttrs, element => element.toSchema())
			}));
		}
		//endregion

		//region Construct and return new ASN.1 schema for this object
		return (new asn1js.Sequence({
			value: outputArray
		}));
		//endregion
	}
	//**********************************************************************************
	/**
	 * Convertion for the class to JSON object
	 * @returns {Object}
	 */
	toJSON()
	{
		const _object = {
			version: this.version
		};

		if("originatorInfo" in this)
			_object.originatorInfo = this.originatorInfo.toJSON();

		_object.recipientInfos = Array.from(this.recipientInfos, element => element.toJSON());
		_object.encryptedContentInfo = this.encryptedContentInfo.toJSON();

		if("unprotectedAttrs" in this)
			_object.unprotectedAttrs = Array.from(this.unprotectedAttrs, element => element.toJSON());

		return _object;
	}
	//**********************************************************************************
	/**
	 * Helpers function for filling "RecipientInfo" based on recipient's certificate.
	 * Problem with WebCrypto is that for RSA certificates we have only one option - "key transport" and
	 * for ECC certificates we also have one option - "key agreement". As soon as Google will implement
	 * DH algorithm it would be possible to use "key agreement" also for RSA certificates.
	 * @param {Certificate} [certificate] Recipient's certificate
	 * @param {Object} [parameters] Additional parameters neccessary for "fine tunning" of encryption process
	 * @param {number} [variant] Variant = 1 is for "key transport", variant = 2 is for "key agreement". In fact the "variant" is unneccessary now because Google has no DH algorithm implementation. Thus key encryption scheme would be choosen by certificate type only: "key transport" for RSA and "key agreement" for ECC certificates.
	 */
	addRecipientByCertificate(certificate, parameters, variant)
	{
		//region Initialize encryption parameters
		const encryptionParameters = Object.assign(
			{ useOAEP: true, oaepHashAlgorithm: "SHA-512" },
			defaultEncryptionParams,
			parameters || {}
		);
		//endregion

		//region Check type of certificate
		if(certificate.subjectPublicKeyInfo.algorithm.algorithmId.indexOf("1.2.840.113549") !== (-1))
			variant = 1; // For the moment it is the only variant for RSA-based certificates
		else
		{
			if(certificate.subjectPublicKeyInfo.algorithm.algorithmId.indexOf("1.2.840.10045") !== (-1))
				variant = 2; // For the moment it is the only variant for ECC-based certificates
			else
				throw new Error(`Unknown type of certificate's public key: ${certificate.subjectPublicKeyInfo.algorithm.algorithmId}`);
		}
		//endregion

		//region Add new "recipient" depends on "variant" and certificate type
		switch(variant)
		{
			case 1: // Key transport scheme
				{
					let algorithmId;
					let algorithmParams;

					if(encryptionParameters.useOAEP === true)
					{
						//region keyEncryptionAlgorithm
						algorithmId = getOIDByAlgorithm({
							name: "RSA-OAEP"
						});
						if(algorithmId === "")
							throw new Error("Can not find OID for RSA-OAEP");
						//endregion

						//region RSAES-OAEP-params
						const hashOID = getOIDByAlgorithm({
							name: encryptionParameters.oaepHashAlgorithm
						});
						if(hashOID === "")
							throw new Error(`Unknown OAEP hash algorithm: ${encryptionParameters.oaepHashAlgorithm}`);

						const hashAlgorithm = new AlgorithmIdentifier({
							algorithmId: hashOID,
							algorithmParams: new asn1js.Null()
						});

						const rsaOAEPParams = new RSAESOAEPParams({
							hashAlgorithm,
							maskGenAlgorithm: new AlgorithmIdentifier({
								algorithmId: "1.2.840.113549.1.1.8", // id-mgf1
								algorithmParams: hashAlgorithm.toSchema()
							})
						});

						algorithmParams = rsaOAEPParams.toSchema();
						//endregion
					}
					else // Use old RSAES-PKCS1-v1_5 schema instead
					{
						//region keyEncryptionAlgorithm
						algorithmId = getOIDByAlgorithm({
							name: "RSAES-PKCS1-v1_5"
						});
						if(algorithmId === "")
							throw new Error("Can not find OID for RSAES-PKCS1-v1_5");
						//endregion

						algorithmParams = new asn1js.Null();
					}

					//region KeyTransRecipientInfo
					const keyInfo = new KeyTransRecipientInfo({
						version: 0,
						rid: new IssuerAndSerialNumber({
							issuer: certificate.issuer,
							serialNumber: certificate.serialNumber
						}),
						keyEncryptionAlgorithm: new AlgorithmIdentifier({
							algorithmId,
							algorithmParams
						}),
						recipientCertificate: certificate
						// "encryptedKey" will be calculated in "encrypt" function
					});
					//endregion

					//region Final values for "CMS_ENVELOPED_DATA"
					this.recipientInfos.push(new RecipientInfo({
						variant: 1,
						value: keyInfo
					}));
					//endregion
				}
				break;
			case 2: // Key agreement scheme
				{
					const recipientIdentifier = new KeyAgreeRecipientIdentifier({
						variant: 1,
						value: new IssuerAndSerialNumber({
							issuer: certificate.issuer,
							serialNumber: certificate.serialNumber
						})
					});
					this._addKeyAgreeRecipientInfo(
						recipientIdentifier,
						encryptionParameters,
						{recipientCertificate: certificate}
					);
				}
				break;
			default:
				throw new Error(`Unknown "variant" value: ${variant}`);
		}
		//endregion

		return true;
	}
	//**********************************************************************************
	/**
	 * Add recipient based on pre-defined data like password or KEK
	 * @param {ArrayBuffer} preDefinedData ArrayBuffer with pre-defined data
	 * @param {Object} parameters Additional parameters neccessary for "fine tunning" of encryption process
	 * @param {number} variant Variant = 1 for pre-defined "key encryption key" (KEK). Variant = 2 for password-based encryption.
	 */
	addRecipientByPreDefinedData(preDefinedData, parameters, variant)
	{
		//region Initial variables
		const encryptionParameters = parameters || {};
		//endregion

		//region Check initial parameters
		if((preDefinedData instanceof ArrayBuffer) === false)
			throw new Error("Please pass \"preDefinedData\" in ArrayBuffer type");

		if(preDefinedData.byteLength === 0)
			throw new Error("Pre-defined data could have zero length");
		//endregion

		//region Initialize encryption parameters
		if(("keyIdentifier" in encryptionParameters) === false)
		{
			const keyIdentifierBuffer = new ArrayBuffer(16);
			const keyIdentifierView = new Uint8Array(keyIdentifierBuffer);
			getRandomValues(keyIdentifierView);

			encryptionParameters.keyIdentifier = keyIdentifierBuffer;
		}

		if(("hmacHashAlgorithm" in encryptionParameters) === false)
			encryptionParameters.hmacHashAlgorithm = "SHA-512";

		if(("iterationCount" in encryptionParameters) === false)
			encryptionParameters.iterationCount = 2048;

		if(("keyEncryptionAlgorithm" in encryptionParameters) === false)
		{
			encryptionParameters.keyEncryptionAlgorithm = {
				name: "AES-KW",
				length: 256
			};
		}

		if(("keyEncryptionAlgorithmParams" in encryptionParameters) === false)
			encryptionParameters.keyEncryptionAlgorithmParams = new asn1js.Null();
		//endregion

		//region Add new recipient based on passed variant
		switch(variant)
		{
			case 1: // KEKRecipientInfo
				{
					//region keyEncryptionAlgorithm
					const kekOID = getOIDByAlgorithm(encryptionParameters.keyEncryptionAlgorithm);
					if(kekOID === "")
						throw new Error("Incorrect value for \"keyEncryptionAlgorithm\"");
					//endregion

					//region KEKRecipientInfo
					const keyInfo = new KEKRecipientInfo({
						version: 4,
						kekid: new KEKIdentifier({
							keyIdentifier: new asn1js.OctetString({ valueHex: encryptionParameters.keyIdentifier })
						}),
						keyEncryptionAlgorithm: new AlgorithmIdentifier({
							algorithmId: kekOID,
							/*
							 For AES-KW params are NULL, but for other algorithm could another situation.
							 */
							algorithmParams: encryptionParameters.keyEncryptionAlgorithmParams
						}),
						preDefinedKEK: preDefinedData
					// "encryptedKey" would be set in "ecrypt" function
					});
					//endregion

					//region Final values for "CMS_ENVELOPED_DATA"
					this.recipientInfos.push(new RecipientInfo({
						variant: 3,
						value: keyInfo
					}));
					//endregion
				}
				break;
			case 2: // PasswordRecipientinfo
				{
					//region keyDerivationAlgorithm
					const pbkdf2OID = getOIDByAlgorithm({
						name: "PBKDF2"
					});
					if(pbkdf2OID === "")
						throw new Error("Can not find OID for PBKDF2");
					//endregion

					//region Salt
					const saltBuffer = new ArrayBuffer(64);
					const saltView = new Uint8Array(saltBuffer);
					getRandomValues(saltView);
					//endregion

					//region HMAC-based algorithm
					const hmacOID = getOIDByAlgorithm({
						name: "HMAC",
						hash: {
							name: encryptionParameters.hmacHashAlgorithm
						}
					});
					if(hmacOID === "")
						throw new Error(`Incorrect value for "hmacHashAlgorithm": ${encryptionParameters.hmacHashAlgorithm}`);
					//endregion

					//region PBKDF2-params
					const pbkdf2Params = new PBKDF2Params({
						salt: new asn1js.OctetString({ valueHex: saltBuffer }),
						iterationCount: encryptionParameters.iterationCount,
						prf: new AlgorithmIdentifier({
							algorithmId: hmacOID,
							algorithmParams: new asn1js.Null()
						})
					});
					//endregion

					//region keyEncryptionAlgorithm
					const kekOID = getOIDByAlgorithm(encryptionParameters.keyEncryptionAlgorithm);
					if(kekOID === "")
						throw new Error("Incorrect value for \"keyEncryptionAlgorithm\"");
					//endregion

					//region PasswordRecipientinfo
					const keyInfo = new PasswordRecipientinfo({
						version: 0,
						keyDerivationAlgorithm: new AlgorithmIdentifier({
							algorithmId: pbkdf2OID,
							algorithmParams: pbkdf2Params.toSchema()
						}),
						keyEncryptionAlgorithm: new AlgorithmIdentifier({
							algorithmId: kekOID,
							/*
							 For AES-KW params are NULL, but for other algorithm could be another situation.
							 */
							algorithmParams: encryptionParameters.keyEncryptionAlgorithmParams
						}),
						password: preDefinedData
					// "encryptedKey" would be set in "ecrypt" function
					});
					//endregion

					//region Final values for "CMS_ENVELOPED_DATA"
					this.recipientInfos.push(new RecipientInfo({
						variant: 4,
						value: keyInfo
					}));
					//endregion
				}
				break;
			default:
				throw new Error(`Unknown value for "variant": ${variant}`);
		}
		//endregion
	}
	//**********************************************************************************
	/**
	 * Add a "RecipientInfo" using a KeyAgreeRecipientInfo of type RecipientKeyIdentifier.
	 * @param {CryptoKey} [key] Recipient's public key
	 * @param {ArrayBuffer} [keyId] The id for the recipient's public key
	 * @param {Object} [parameters] Additional parameters for "fine tuning" the encryption process
	 */
	addRecipientByKeyIdentifier(key, keyId, parameters)
	{
		//region Initialize encryption parameters
		const encryptionParameters = Object.assign({}, defaultEncryptionParams, parameters || {});
		//endregion

		const recipientIdentifier = new KeyAgreeRecipientIdentifier({
			variant: 2,
			value: new RecipientKeyIdentifier({
				subjectKeyIdentifier: new asn1js.OctetString({valueHex: keyId}),
			})
		});
		this._addKeyAgreeRecipientInfo(
			recipientIdentifier,
			encryptionParameters,
			{recipientPublicKey: key}
		);
	}
	//**********************************************************************************
	/**
	 * Add a "RecipientInfo" using a KeyAgreeRecipientInfo of type RecipientKeyIdentifier.
	 * @param {KeyAgreeRecipientIdentifier} [recipientIdentifier] Recipient identifier
	 * @param {Object} [encryptionParameters] Additional parameters for "fine tuning" the encryption process
	 * @param {Object} [extraRecipientInfoParams] Additional params for KeyAgreeRecipientInfo
	 */
	_addKeyAgreeRecipientInfo(recipientIdentifier, encryptionParameters, extraRecipientInfoParams)
	{
		//region RecipientEncryptedKey
		const encryptedKey = new RecipientEncryptedKey({
			rid: recipientIdentifier
			// "encryptedKey" will be calculated in "encrypt" function
		});
		//endregion

		//region keyEncryptionAlgorithm
		const aesKWoid = getOIDByAlgorithm({
			name: "AES-KW",
			length: encryptionParameters.kekEncryptionLength
		});
		if (aesKWoid === "")
			throw new Error(`Unknown length for key encryption algorithm: ${encryptionParameters.kekEncryptionLength}`);

		const aesKW = new AlgorithmIdentifier({
			algorithmId: aesKWoid,
		});
		//endregion

		//region KeyAgreeRecipientInfo
		const ecdhOID = getOIDByAlgorithm({
			name: "ECDH",
			kdf: encryptionParameters.kdfAlgorithm
		});
		if (ecdhOID === "")
			throw new Error(`Unknown KDF algorithm: ${encryptionParameters.kdfAlgorithm}`);

		// In fact there is no need in so long UKM, but RFC2631
		// has requirement that "UserKeyMaterial" must be 512 bits long
		const ukmBuffer = new ArrayBuffer(64);
		const ukmView = new Uint8Array(ukmBuffer);
		getRandomValues(ukmView); // Generate random values in 64 bytes long buffer

		const recipientInfoParams = {
			version: 3,
			// "originator" will be calculated in "encrypt" function because ephemeral key would be generated there
			ukm: new asn1js.OctetString({valueHex: ukmBuffer}),
			keyEncryptionAlgorithm: new AlgorithmIdentifier({
				algorithmId: ecdhOID,
				algorithmParams: aesKW.toSchema()
			}),
			recipientEncryptedKeys: new RecipientEncryptedKeys({
				encryptedKeys: [encryptedKey]
			})
		};
		const keyInfo = new KeyAgreeRecipientInfo(Object.assign(recipientInfoParams, extraRecipientInfoParams));
		//endregion

		//region Final values for "CMS_ENVELOPED_DATA"
		this.recipientInfos.push(new RecipientInfo({
			variant: 2,
			value: keyInfo
		}));
		//endregion
	}
	//**********************************************************************************
	/**
	 * Create a new CMS Enveloped Data content with encrypted data
	 * @param {Object} contentEncryptionAlgorithm WebCrypto algorithm. For the moment here could be only "AES-CBC" or "AES-GCM" algorithms.
	 * @param {ArrayBuffer} contentToEncrypt Content to encrypt
	 * @returns {Promise}
	 */
	encrypt(contentEncryptionAlgorithm, contentToEncrypt)
	{
		//region Initial variables
		let sequence = Promise.resolve();

		const ivBuffer = new ArrayBuffer(16); // For AES we need IV 16 bytes long
		const ivView = new Uint8Array(ivBuffer);
		getRandomValues(ivView);

		const contentView = new Uint8Array(contentToEncrypt);

		let sessionKey;
		let encryptedContent;
		let exportedSessionKey;

		const recipientsPromises = [];

		const _this = this;
		//endregion

		//region Check for input parameters
		const contentEncryptionOID = getOIDByAlgorithm(contentEncryptionAlgorithm);
		if(contentEncryptionOID === "")
			return Promise.reject("Wrong \"contentEncryptionAlgorithm\" value");
		//endregion

		//region Get a "crypto" extension
		const crypto = getCrypto();
		if(typeof crypto === "undefined")
			return Promise.reject("Unable to create WebCrypto object");
		//endregion

		//region Generate new content encryption key
		sequence = sequence.then(() =>
			crypto.generateKey(contentEncryptionAlgorithm, true, ["encrypt"]));
		//endregion
		//region Encrypt content
		sequence = sequence.then(result =>
		{
			sessionKey = result;

			return crypto.encrypt({
				name: contentEncryptionAlgorithm.name,
				iv: ivView
			},
			sessionKey,
			contentView);
		}, error =>
			Promise.reject(error));
		//endregion
		//region Export raw content of content encryption key
		sequence = sequence.then(result =>
		{
			//region Create output OCTETSTRING with encrypted content
			encryptedContent = result;
			//endregion

			return crypto.exportKey("raw", sessionKey);
		}, error =>
			Promise.reject(error)
		).then(result =>
		{
			exportedSessionKey = result;

			return true;
		}, error =>
			Promise.reject(error));
		//endregion
		//region Append common information to CMS_ENVELOPED_DATA
		sequence = sequence.then(() =>
		{
			this.version = 2;
			this.encryptedContentInfo = new EncryptedContentInfo({
				contentType: "1.2.840.113549.1.7.1", // "data"
				contentEncryptionAlgorithm: new AlgorithmIdentifier({
					algorithmId: contentEncryptionOID,
					algorithmParams: new asn1js.OctetString({ valueHex: ivBuffer })
				}),
				encryptedContent: new asn1js.OctetString({ valueHex: encryptedContent })
			});
		}, error =>
			Promise.reject(error));
		//endregion

		//region Special sub-functions to work with each recipient's type
		function SubKeyAgreeRecipientInfo(index)
		{
			//region Initial variables
			let currentSequence = Promise.resolve();

			const recipientInfo = _this.recipientInfos[index];

			let ecdhPublicKey;
			let ecdhPrivateKey;

			let recipientPublicKey;
			let recipientCurve;
			let recipientCurveLength;

			let exportedECDHPublicKey;
			//endregion

			//region Get public key and named curve from recipient's certificate or public key
			currentSequence = currentSequence.then(() =>
			{
				if (recipientInfo.value.recipientPublicKey) {
					recipientCurve = recipientInfo.value.recipientPublicKey.algorithm.namedCurve;
					return recipientInfo.value.recipientPublicKey;

				} else {
					const curveObject = recipientInfo.value.recipientCertificate.subjectPublicKeyInfo.algorithm.algorithmParams;

					if (curveObject.constructor.blockName() !== asn1js.ObjectIdentifier.blockName())
						return Promise.reject(`Incorrect "recipientCertificate" for index ${index}`);

					const curveOID = curveObject.valueBlock.toString();

					switch (curveOID) {
						case "1.2.840.10045.3.1.7":
							recipientCurve = "P-256";
							break;
						case "1.3.132.0.34":
							recipientCurve = "P-384";
							break;
						case "1.3.132.0.35":
							recipientCurve = "P-521";
							break;
						default:
							return Promise.reject(`Incorrect curve OID for index ${index}`);
					}

					return recipientInfo.value.recipientCertificate.getPublicKey({
						algorithm: {
							algorithm: {
								name: "ECDH",
								namedCurve: recipientCurve
							},
							usages: []
						}
					});
				}
			}, error =>
				Promise.reject(error));
			//endregion

			//region Generate ephemeral ECDH key
			currentSequence = currentSequence.then(result => {
				recipientPublicKey = result;
				recipientCurveLength = curveLengthByName[recipientCurve];

				return crypto.generateKey(
					{name: "ECDH", namedCurve: recipientCurve},
					true,
					["deriveBits"]
				);
			},
			error =>
				Promise.reject(error)
			);
			//endregion
			//region Export public key of ephemeral ECDH key pair
			currentSequence = currentSequence.then(result =>
			{
				ecdhPublicKey = result.publicKey;
				ecdhPrivateKey = result.privateKey;

				return crypto.exportKey("spki", ecdhPublicKey);
			},
			error =>
				Promise.reject(error));
			//endregion

			//region Save public key of ephemeral ECDH key pair
			currentSequence = currentSequence.then(result =>
			{
				exportedECDHPublicKey = result;
			}, error =>
				Promise.reject(error));
			//endregion
			//region Create shared secret
			currentSequence = currentSequence.then(() => crypto.deriveBits({
				name: "ECDH",
				public: recipientPublicKey
			},
			ecdhPrivateKey,
			recipientCurveLength),
			error =>
				Promise.reject(error));
			//endregion

			//region Apply KDF function to shared secret
			currentSequence = currentSequence.then(
				/**
				 * @param {ArrayBuffer} result
				 */
				result =>
				{
					//region Get length of used AES-KW algorithm
					const aesKWAlgorithm = new AlgorithmIdentifier({ schema: recipientInfo.value.keyEncryptionAlgorithm.algorithmParams });

					const KWalgorithm = getAlgorithmByOID(aesKWAlgorithm.algorithmId);
					if(("name" in KWalgorithm) === false)
						return Promise.reject(`Incorrect OID for key encryption algorithm: ${aesKWAlgorithm.algorithmId}`);
					//endregion

					//region Translate AES-KW length to ArrayBuffer
					let kwLength = KWalgorithm.length;

					const kwLengthBuffer = new ArrayBuffer(4);
					const kwLengthView = new Uint8Array(kwLengthBuffer);

					for(let j = 3; j >= 0; j--)
					{
						kwLengthView[j] = kwLength;
						kwLength >>= 8;
					}
					//endregion

					//region Create and encode "ECC-CMS-SharedInfo" structure
					const eccInfo = new ECCCMSSharedInfo({
						keyInfo: new AlgorithmIdentifier({
							algorithmId: aesKWAlgorithm.algorithmId
						}),
						entityUInfo: recipientInfo.value.ukm,
						suppPubInfo: new asn1js.OctetString({ valueHex: kwLengthBuffer })
					});

					const encodedInfo = eccInfo.toSchema().toBER(false);
					//endregion

					//region Get SHA algorithm used together with ECDH
					const ecdhAlgorithm = getAlgorithmByOID(recipientInfo.value.keyEncryptionAlgorithm.algorithmId);
					if(("name" in ecdhAlgorithm) === false)
						return Promise.reject(`Incorrect OID for key encryption algorithm: ${recipientInfo.value.keyEncryptionAlgorithm.algorithmId}`);
					//endregion

					return kdf(ecdhAlgorithm.kdf, result, KWalgorithm.length, encodedInfo);
				},
				error =>
					Promise.reject(error));
			//endregion
			//region Import AES-KW key from result of KDF function
			currentSequence = currentSequence.then(result =>
				crypto.importKey("raw", result, { name: "AES-KW" }, true, ["wrapKey"]),
			error =>
				Promise.reject(error)
			);
			//endregion
			//region Finally wrap session key by using AES-KW algorithm
			currentSequence = currentSequence.then(result => crypto.wrapKey("raw", sessionKey, result, { name: "AES-KW" }),
				error =>
					Promise.reject(error)
			);
			//endregion
			//region Append all neccessary data to current CMS_RECIPIENT_INFO object
			currentSequence = currentSequence.then(result =>
			{
				//region OriginatorIdentifierOrKey
				const asn1 = asn1js.fromBER(exportedECDHPublicKey);

				const originator = new OriginatorIdentifierOrKey();
				originator.variant = 3;
				originator.value = new OriginatorPublicKey({ schema: asn1.result });

				recipientInfo.value.originator = originator;
				//endregion

				//region RecipientEncryptedKey
				/*
				 We will not support using of same ephemeral key for many recipients
				 */
				recipientInfo.value.recipientEncryptedKeys.encryptedKeys[0].encryptedKey = new asn1js.OctetString({ valueHex: result });
				//endregion

				return {ecdhPrivateKey};
			}, error =>
				Promise.reject(error)
			);
			//endregion

			return currentSequence;
		}

		async function SubKeyTransRecipientInfo(index)
		{
			const algorithmParameters = getAlgorithmByOID(_this.recipientInfos[index].value.keyEncryptionAlgorithm.algorithmId);
			if("name" in algorithmParameters === false)
				throw new Error(`Unknown keyEncryptionAlgorithm: ${_this.recipientInfos[index].value.keyEncryptionAlgorithm.algorithmId}`);

			//region RSA-OAEP case
			if(algorithmParameters.name === "RSA-OAEP")
			{
				const schema = _this.recipientInfos[index].value.keyEncryptionAlgorithm.algorithmParams;
				const rsaOAEPParams = new RSAESOAEPParams({ schema });

				algorithmParameters.hash = getAlgorithmByOID(rsaOAEPParams.hashAlgorithm.algorithmId);
				if(("name" in algorithmParameters.hash) === false)
					throw new Error(`Incorrect OID for hash algorithm: ${rsaOAEPParams.hashAlgorithm.algorithmId}`);
			}
			//endregion

			try
			{
				const publicKey = await _this.recipientInfos[index].value.recipientCertificate.getPublicKey({
					algorithm: {
						algorithm: algorithmParameters,
						usages: ["encrypt", "wrapKey"]
					}
				});

				const encryptedKey = await crypto.encrypt(publicKey.algorithm, publicKey, exportedSessionKey);

				//region RecipientEncryptedKey
				_this.recipientInfos[index].value.encryptedKey = new asn1js.OctetString({valueHex: encryptedKey});
				//endregion
			}
			catch(ex)
			{
				const jjj = 0;
			}
		}

		function SubKEKRecipientInfo(index)
		{
			//region Initial variables
			let currentSequence = Promise.resolve();
			let kekAlgorithm;
			//endregion

			//region Import KEK from pre-defined data
			currentSequence = currentSequence.then(() =>
			{
				//region Get WebCrypto form of "keyEncryptionAlgorithm"
				kekAlgorithm = getAlgorithmByOID(_this.recipientInfos[index].value.keyEncryptionAlgorithm.algorithmId);
				if(("name" in kekAlgorithm) === false)
					return Promise.reject(`Incorrect OID for "keyEncryptionAlgorithm": ${_this.recipientInfos[index].value.keyEncryptionAlgorithm.algorithmId}`);
					//endregion

				return crypto.importKey("raw",
					new Uint8Array(_this.recipientInfos[index].value.preDefinedKEK),
					kekAlgorithm,
					true,
					["wrapKey"]); // Too specific for AES-KW
			}, error =>
				Promise.reject(error)
			);
			//endregion

			//region Wrap previously exported session key
			currentSequence = currentSequence.then(result =>
				crypto.wrapKey("raw", sessionKey, result, kekAlgorithm),
			error =>
				Promise.reject(error)
			);
			//endregion
			//region Append all neccessary data to current CMS_RECIPIENT_INFO object
			currentSequence = currentSequence.then(result =>
			{
				//region RecipientEncryptedKey
				_this.recipientInfos[index].value.encryptedKey = new asn1js.OctetString({ valueHex: result });
				//endregion
			}, error =>
				Promise.reject(error)
			);
			//endregion

			return currentSequence;
		}

		function SubPasswordRecipientinfo(index)
		{
			//region Initial variables
			let currentSequence = Promise.resolve();
			let pbkdf2Params;
			let kekAlgorithm;
			//endregion

			//region Check that we have encoded "keyDerivationAlgorithm" plus "PBKDF2_params" in there
			currentSequence = currentSequence.then(() =>
			{
				if(("keyDerivationAlgorithm" in _this.recipientInfos[index].value) === false)
					return Promise.reject("Please append encoded \"keyDerivationAlgorithm\"");

				if(("algorithmParams" in _this.recipientInfos[index].value.keyDerivationAlgorithm) === false)
					return Promise.reject("Incorrectly encoded \"keyDerivationAlgorithm\"");

				try
				{
					pbkdf2Params = new PBKDF2Params({ schema: _this.recipientInfos[index].value.keyDerivationAlgorithm.algorithmParams });
				}
				catch(ex)
				{
					return Promise.reject("Incorrectly encoded \"keyDerivationAlgorithm\"");
				}

				return Promise.resolve();
			}, error =>
				Promise.reject(error)
			);
			//endregion
			//region Derive PBKDF2 key from "password" buffer
			currentSequence = currentSequence.then(() =>
			{
				const passwordView = new Uint8Array(_this.recipientInfos[index].value.password);

				return crypto.importKey("raw",
					passwordView,
					"PBKDF2",
					false,
					["deriveKey"]);
			}, error =>
				Promise.reject(error)
			);
			//endregion
			//region Derive key for "keyEncryptionAlgorithm"
			currentSequence = currentSequence.then(result =>
			{
				//region Get WebCrypto form of "keyEncryptionAlgorithm"
				kekAlgorithm = getAlgorithmByOID(_this.recipientInfos[index].value.keyEncryptionAlgorithm.algorithmId);
				if(("name" in kekAlgorithm) === false)
					return Promise.reject(`Incorrect OID for "keyEncryptionAlgorithm": ${_this.recipientInfos[index].value.keyEncryptionAlgorithm.algorithmId}`);
				//endregion

				//region Get HMAC hash algorithm
				let hmacHashAlgorithm = "SHA-1";

				if("prf" in pbkdf2Params)
				{
					const algorithm = getAlgorithmByOID(pbkdf2Params.prf.algorithmId);
					if(("name" in algorithm) === false)
						return Promise.reject("Incorrect OID for HMAC hash algorithm");

					hmacHashAlgorithm = algorithm.hash.name;
				}
				//endregion

				//region Get PBKDF2 "salt" value
				const saltView = new Uint8Array(pbkdf2Params.salt.valueBlock.valueHex);
				//endregion

				//region Get PBKDF2 iterations count
				const iterations = pbkdf2Params.iterationCount;
				//endregion

				return crypto.deriveKey({
					name: "PBKDF2",
					hash: {
						name: hmacHashAlgorithm
					},
					salt: saltView,
					iterations
				},
				result,
				kekAlgorithm,
				true,
				["wrapKey"]); // Usages are too specific for KEK algorithm
			}, error =>
				Promise.reject(error)
			);
			//endregion
			//region Wrap previously exported session key (Also too specific for KEK algorithm)
			currentSequence = currentSequence.then(result =>
				crypto.wrapKey("raw", sessionKey, result, kekAlgorithm),
			error =>
				Promise.reject(error)
			);
			//endregion
			//region Append all neccessary data to current CMS_RECIPIENT_INFO object
			currentSequence = currentSequence.then(result =>
			{
				//region RecipientEncryptedKey
				_this.recipientInfos[index].value.encryptedKey = new asn1js.OctetString({ valueHex: result });
				//endregion
			}, error =>
				Promise.reject(error)
			);
			//endregion

			return currentSequence;
		}

		//endregion

		//region Create special routines for each "recipient"
		sequence = sequence.then(() =>
		{
			for(let i = 0; i < this.recipientInfos.length; i++)
			{
				//region Initial variables
				let currentSequence = Promise.resolve();
				//endregion

				switch(this.recipientInfos[i].variant)
				{
					case 1: // KeyTransRecipientInfo
						currentSequence = SubKeyTransRecipientInfo(i);
						break;
					case 2: // KeyAgreeRecipientInfo
						currentSequence = SubKeyAgreeRecipientInfo(i);
						break;
					case 3: // KEKRecipientInfo
						currentSequence = SubKEKRecipientInfo(i);
						break;
					case 4: // PasswordRecipientinfo
						currentSequence = SubPasswordRecipientinfo(i);
						break;
					default:
						return Promise.reject(`Uknown recipient type in array with index ${i}`);
				}

				recipientsPromises.push(currentSequence);
			}

			return Promise.all(recipientsPromises);
		}, error =>
			Promise.reject(error)
		);
		//endregion

		return sequence;
	}
	//**********************************************************************************
	/**
	 * Decrypt existing CMS Enveloped Data content
	 * @param {number} recipientIndex Index of recipient
	 * @param {Object} parameters Additional parameters
	 * @returns {Promise}
	 */
	decrypt(recipientIndex, parameters)
	{
		//region Initial variables
		let sequence = Promise.resolve();

		const decryptionParameters = parameters || {};

		const _this = this;
		//endregion

		//region Check for input parameters
		if((recipientIndex + 1) > this.recipientInfos.length)
			return Promise.reject(`Maximum value for "index" is: ${this.recipientInfos.length - 1}`);
		//endregion

		//region Get a "crypto" extension
		const crypto = getCrypto();
		if(typeof crypto === "undefined")
			return Promise.reject("Unable to create WebCrypto object");
		//endregion

		//region Special sub-functions to work with each recipient's type
		function SubKeyAgreeRecipientInfo(index)
		{
			//region Initial variables
			let currentSequence = Promise.resolve();

			let recipientCurve;
			let recipientCurveLength;

			let curveOID;

			let ecdhPrivateKey;
			//endregion

			const originator = _this.recipientInfos[index].value.originator;

			//region Get "namedCurve" parameter from recipient's certificate
			currentSequence = currentSequence.then(() =>
			{
				if ("recipientCertificate" in decryptionParameters) {
					const curveObject = decryptionParameters.recipientCertificate.subjectPublicKeyInfo.algorithm.algorithmParams;
					if(curveObject.constructor.blockName() !== asn1js.ObjectIdentifier.blockName()) {
						return Promise.reject(`Incorrect "recipientCertificate" for index ${index}`);
					}
					curveOID = curveObject.valueBlock.toString();
				} else if ("algorithmParams" in originator.value.algorithm) {
					const curveObject = originator.value.algorithm.algorithmParams;
					if(curveObject.constructor.blockName() !== asn1js.ObjectIdentifier.blockName()) {
						return Promise.reject(`Incorrect originator for index ${index}`);
					}
					curveOID = curveObject.valueBlock.toString();
				} else {
					return Promise.reject("Parameter \"recipientCertificate\" is mandatory for \"KeyAgreeRecipientInfo\" if algorithm params are missing from originator");
				}

				if(("recipientPrivateKey" in decryptionParameters) === false)
					return Promise.reject("Parameter \"recipientPrivateKey\" is mandatory for \"KeyAgreeRecipientInfo\"");

				switch(curveOID)
				{
					case "1.2.840.10045.3.1.7":
						recipientCurve = "P-256";
						recipientCurveLength = 256;
						break;
					case "1.3.132.0.34":
						recipientCurve = "P-384";
						recipientCurveLength = 384;
						break;
					case "1.3.132.0.35":
						recipientCurve = "P-521";
						recipientCurveLength = 528;
						break;
					default:
						return Promise.reject(`Incorrect curve OID for index ${index}`);
				}

				return crypto.importKey("pkcs8",
					decryptionParameters.recipientPrivateKey,
					{
						name: "ECDH",
						namedCurve: recipientCurve
					},
					true,
					["deriveBits"]
				);
			}, error =>
				Promise.reject(error)
			);
			//endregion
			//region Import sender's ephemeral public key
			currentSequence = currentSequence.then(result =>
			{
				ecdhPrivateKey = result;

				//region Change "OriginatorPublicKey" if "curve" parameter absent
				if(("algorithmParams" in originator.value.algorithm) === false)
					originator.value.algorithm.algorithmParams = new asn1js.ObjectIdentifier({ value: curveOID });
				//endregion

				//region Create ArrayBuffer with sender's public key
				const buffer = originator.value.toSchema().toBER(false);
				//endregion

				return crypto.importKey("spki",
					buffer,
					{
						name: "ECDH",
						namedCurve: recipientCurve
					},
					true,
					[]);
			}, error =>
				Promise.reject(error)
			);
			//endregion
			//region Create shared secret
			currentSequence = currentSequence.then(result =>
				crypto.deriveBits({
					name: "ECDH",
					public: result
				},
				ecdhPrivateKey,
				recipientCurveLength),
			error =>
				Promise.reject(error)
			);
			//endregion
			//region Apply KDF function to shared secret
			function applyKDF(includeAlgorithmParams) {
				includeAlgorithmParams = includeAlgorithmParams || false;

				//region Get length of used AES-KW algorithm
				const aesKWAlgorithm = new AlgorithmIdentifier({ schema: _this.recipientInfos[index].value.keyEncryptionAlgorithm.algorithmParams });

				const KWalgorithm = getAlgorithmByOID(aesKWAlgorithm.algorithmId);
				if(("name" in KWalgorithm) === false)
					return Promise.reject(`Incorrect OID for key encryption algorithm: ${aesKWAlgorithm.algorithmId}`);
				//endregion

				//region Translate AES-KW length to ArrayBuffer
				let kwLength = KWalgorithm.length;

				const kwLengthBuffer = new ArrayBuffer(4);
				const kwLengthView = new Uint8Array(kwLengthBuffer);

				for(let j = 3; j >= 0; j--)
				{
					kwLengthView[j] = kwLength;
					kwLength >>= 8;
				}
				//endregion

				//region Create and encode "ECC-CMS-SharedInfo" structure
				const keyInfoAlgorithm = {
					algorithmId: aesKWAlgorithm.algorithmId
				};
				if (includeAlgorithmParams) {
					keyInfoAlgorithm.algorithmParams = new asn1js.Null();
				}
				const eccInfo = new ECCCMSSharedInfo({
					keyInfo: new AlgorithmIdentifier(keyInfoAlgorithm),
					entityUInfo: _this.recipientInfos[index].value.ukm,
					suppPubInfo: new asn1js.OctetString({ valueHex: kwLengthBuffer })
				});

				const encodedInfo = eccInfo.toSchema().toBER(false);
				//endregion

				//region Get SHA algorithm used together with ECDH
				const ecdhAlgorithm = getAlgorithmByOID(_this.recipientInfos[index].value.keyEncryptionAlgorithm.algorithmId);
				if(("name" in ecdhAlgorithm) === false)
					return Promise.reject(`Incorrect OID for key encryption algorithm: ${_this.recipientInfos[index].value.keyEncryptionAlgorithm.algorithmId}`);
				//endregion

				return kdf(ecdhAlgorithm.kdf, sharedSecret, KWalgorithm.length, encodedInfo);
			}
			let sharedSecret;
			currentSequence = currentSequence.then(
				/**
				 * @param {ArrayBuffer} result
				 */
				result =>
				{
					sharedSecret = result;
					return applyKDF();
				},
				error =>
					Promise.reject(error)
			);
			//endregion
			//region Import AES-KW key from result of KDF function
			function importAesKwKey(kdfResult) {
				return crypto.importKey("raw",
					kdfResult,
					{ name: "AES-KW" },
					true,
					["unwrapKey"]
				);
			}
			currentSequence = currentSequence.then(
				importAesKwKey,
				error => Promise.reject(error)
			);
			//endregion
			//region Finally unwrap session key
			function unwrapSessionKey(aesKwKey) {
				//region Get WebCrypto form of content encryption algorithm
				const contentEncryptionAlgorithm = getAlgorithmByOID(_this.encryptedContentInfo.contentEncryptionAlgorithm.algorithmId);
				if(("name" in contentEncryptionAlgorithm) === false)
					return Promise.reject(`Incorrect "contentEncryptionAlgorithm": ${_this.encryptedContentInfo.contentEncryptionAlgorithm.algorithmId}`);
					//endregion

				return crypto.unwrapKey("raw",
					_this.recipientInfos[index].value.recipientEncryptedKeys.encryptedKeys[0].encryptedKey.valueBlock.valueHex,
					aesKwKey,
					{ name: "AES-KW" },
					contentEncryptionAlgorithm,
					true,
					["decrypt"]);
			}
			currentSequence = currentSequence.then(
				result => unwrapSessionKey(result).catch(() => applyKDF(true).then(importAesKwKey).then(unwrapSessionKey)),
				error => Promise.reject(error)
			);
			//endregion

			return currentSequence;
		}

		async function SubKeyTransRecipientInfo(index)
		{
			if(("recipientPrivateKey" in decryptionParameters) === false)
				throw new Error("Parameter \"recipientPrivateKey\" is mandatory for \"KeyTransRecipientInfo\"");

			const algorithmParameters = getAlgorithmByOID(_this.recipientInfos[index].value.keyEncryptionAlgorithm.algorithmId);
			if("name" in algorithmParameters === false)
				throw new Error(`Unknown keyEncryptionAlgorithm: ${_this.recipientInfos[index].value.keyEncryptionAlgorithm.algorithmId}`);

			//region RSA-OAEP case
			if(algorithmParameters.name === "RSA-OAEP")
			{
				const schema = _this.recipientInfos[index].value.keyEncryptionAlgorithm.algorithmParams;
				const rsaOAEPParams = new RSAESOAEPParams({ schema });

				algorithmParameters.hash = getAlgorithmByOID(rsaOAEPParams.hashAlgorithm.algorithmId);
				if(("name" in algorithmParameters.hash) === false)
					throw new Error(`Incorrect OID for hash algorithm: ${rsaOAEPParams.hashAlgorithm.algorithmId}`);
			}
			//endregion

			const privateKey = await crypto.importKey(
				"pkcs8",
				decryptionParameters.recipientPrivateKey,
				algorithmParameters,
				true,
				["decrypt"]
			);

			const sessionKey = await crypto.decrypt(
				privateKey.algorithm,
				privateKey,
				_this.recipientInfos[index].value.encryptedKey.valueBlock.valueHex
			);

			//region Get WebCrypto form of content encryption algorithm
			const contentEncryptionAlgorithm = getAlgorithmByOID(_this.encryptedContentInfo.contentEncryptionAlgorithm.algorithmId);
			if(("name" in contentEncryptionAlgorithm) === false)
				throw new Error(`Incorrect "contentEncryptionAlgorithm": ${_this.encryptedContentInfo.contentEncryptionAlgorithm.algorithmId}`);
			//endregion

			return crypto.importKey("raw",
				sessionKey,
				contentEncryptionAlgorithm,
				true,
				["decrypt"]
			);
		}

		function SubKEKRecipientInfo(index)
		{
			//region Initial variables
			let currentSequence = Promise.resolve();
			let kekAlgorithm;
			//endregion

			//region Import KEK from pre-defined data
			currentSequence = currentSequence.then(() =>
			{
				if(("preDefinedData" in decryptionParameters) === false)
					return Promise.reject("Parameter \"preDefinedData\" is mandatory for \"KEKRecipientInfo\"");

				//region Get WebCrypto form of "keyEncryptionAlgorithm"
				kekAlgorithm = getAlgorithmByOID(_this.recipientInfos[index].value.keyEncryptionAlgorithm.algorithmId);
				if(("name" in kekAlgorithm) === false)
					return Promise.reject(`Incorrect OID for "keyEncryptionAlgorithm": ${_this.recipientInfos[index].value.keyEncryptionAlgorithm.algorithmId}`);
				//endregion

				return crypto.importKey("raw",
					decryptionParameters.preDefinedData,
					kekAlgorithm,
					true,
					["unwrapKey"]); // Too specific for AES-KW
			}, error =>
				Promise.reject(error)
			);
			//endregion
			//region Unwrap previously exported session key
			currentSequence = currentSequence.then(result =>
			{
				//region Get WebCrypto form of content encryption algorithm
				const contentEncryptionAlgorithm = getAlgorithmByOID(_this.encryptedContentInfo.contentEncryptionAlgorithm.algorithmId);
				if(("name" in contentEncryptionAlgorithm) === false)
					return Promise.reject(`Incorrect "contentEncryptionAlgorithm": ${_this.encryptedContentInfo.contentEncryptionAlgorithm.algorithmId}`);
				//endregion

				return crypto.unwrapKey("raw",
					_this.recipientInfos[index].value.encryptedKey.valueBlock.valueHex,
					result,
					kekAlgorithm,
					contentEncryptionAlgorithm,
					true,
					["decrypt"]);
			}, error =>
				Promise.reject(error)
			);
			//endregion

			return currentSequence;
		}

		function SubPasswordRecipientinfo(index)
		{
			//region Initial variables
			let currentSequence = Promise.resolve();
			let pbkdf2Params;
			let kekAlgorithm;
			//endregion

			//region Derive PBKDF2 key from "password" buffer
			currentSequence = currentSequence.then(() =>
			{
				if(("preDefinedData" in decryptionParameters) === false)
					return Promise.reject("Parameter \"preDefinedData\" is mandatory for \"KEKRecipientInfo\"");

				if(("keyDerivationAlgorithm" in _this.recipientInfos[index].value) === false)
					return Promise.reject("Please append encoded \"keyDerivationAlgorithm\"");

				if(("algorithmParams" in _this.recipientInfos[index].value.keyDerivationAlgorithm) === false)
					return Promise.reject("Incorrectly encoded \"keyDerivationAlgorithm\"");

				try
				{
					pbkdf2Params = new PBKDF2Params({ schema: _this.recipientInfos[index].value.keyDerivationAlgorithm.algorithmParams });
				}
				catch(ex)
				{
					return Promise.reject("Incorrectly encoded \"keyDerivationAlgorithm\"");
				}

				return crypto.importKey("raw",
					decryptionParameters.preDefinedData,
					"PBKDF2",
					false,
					["deriveKey"]);
			}, error =>
				Promise.reject(error)
			);
			//endregion
			//region Derive key for "keyEncryptionAlgorithm"
			currentSequence = currentSequence.then(result =>
			{
				//region Get WebCrypto form of "keyEncryptionAlgorithm"
				kekAlgorithm = getAlgorithmByOID(_this.recipientInfos[index].value.keyEncryptionAlgorithm.algorithmId);
				if(("name" in kekAlgorithm) === false)
					return Promise.reject(`Incorrect OID for "keyEncryptionAlgorithm": ${_this.recipientInfos[index].value.keyEncryptionAlgorithm.algorithmId}`);
				//endregion

				//region Get HMAC hash algorithm
				let hmacHashAlgorithm = "SHA-1";

				if("prf" in pbkdf2Params)
				{
					const algorithm = getAlgorithmByOID(pbkdf2Params.prf.algorithmId);
					if(("name" in algorithm) === false)
						return Promise.reject("Incorrect OID for HMAC hash algorithm");

					hmacHashAlgorithm = algorithm.hash.name;
				}
				//endregion

				//region Get PBKDF2 "salt" value
				const saltView = new Uint8Array(pbkdf2Params.salt.valueBlock.valueHex);
				//endregion

				//region Get PBKDF2 iterations count
				const iterations = pbkdf2Params.iterationCount;
				//endregion

				return crypto.deriveKey({
					name: "PBKDF2",
					hash: {
						name: hmacHashAlgorithm
					},
					salt: saltView,
					iterations
				},
				result,
				kekAlgorithm,
				true,
				["unwrapKey"]); // Usages are too specific for KEK algorithm
			}, error =>
				Promise.reject(error)
			);
			//endregion
			//region Unwrap previously exported session key
			currentSequence = currentSequence.then(result =>
			{
				//region Get WebCrypto form of content encryption algorithm
				const contentEncryptionAlgorithm = getAlgorithmByOID(_this.encryptedContentInfo.contentEncryptionAlgorithm.algorithmId);
				if(("name" in contentEncryptionAlgorithm) === false)
					return Promise.reject(`Incorrect "contentEncryptionAlgorithm": ${_this.encryptedContentInfo.contentEncryptionAlgorithm.algorithmId}`);
					//endregion

				return crypto.unwrapKey("raw",
					_this.recipientInfos[index].value.encryptedKey.valueBlock.valueHex,
					result,
					kekAlgorithm,
					contentEncryptionAlgorithm,
					true,
					["decrypt"]);
			}, error =>
				Promise.reject(error)
			);
			//endregion

			return currentSequence;
		}

		//endregion

		//region Perform steps, specific to each type of session key encryption
		sequence = sequence.then(() =>
		{
			//region Initial variables
			let currentSequence = Promise.resolve();
			//endregion

			switch(this.recipientInfos[recipientIndex].variant)
			{
				case 1: // KeyTransRecipientInfo
					currentSequence = SubKeyTransRecipientInfo(recipientIndex);
					break;
				case 2: // KeyAgreeRecipientInfo
					currentSequence = SubKeyAgreeRecipientInfo(recipientIndex);
					break;
				case 3: // KEKRecipientInfo
					currentSequence = SubKEKRecipientInfo(recipientIndex);
					break;
				case 4: // PasswordRecipientinfo
					currentSequence = SubPasswordRecipientinfo(recipientIndex);
					break;
				default:
					return Promise.reject(`Uknown recipient type in array with index ${recipientIndex}`);
			}

			return currentSequence;
		}, error =>
			Promise.reject(error)
		);
		//endregion

		//region Finally decrypt data by session key
		sequence = sequence.then(result =>
		{
			//region Get WebCrypto form of content encryption algorithm
			const contentEncryptionAlgorithm = getAlgorithmByOID(this.encryptedContentInfo.contentEncryptionAlgorithm.algorithmId);
			if(("name" in contentEncryptionAlgorithm) === false)
				return Promise.reject(`Incorrect "contentEncryptionAlgorithm": ${this.encryptedContentInfo.contentEncryptionAlgorithm.algorithmId}`);
			//endregion

			//region Get "intialization vector" for content encryption algorithm
			const ivBuffer = this.encryptedContentInfo.contentEncryptionAlgorithm.algorithmParams.valueBlock.valueHex;
			const ivView = new Uint8Array(ivBuffer);
			//endregion

			//region Create correct data block for decryption
			let dataBuffer = new ArrayBuffer(0);

			if(this.encryptedContentInfo.encryptedContent.idBlock.isConstructed === false)
				dataBuffer = this.encryptedContentInfo.encryptedContent.valueBlock.valueHex;
			else
			{
				for(const content of this.encryptedContentInfo.encryptedContent.valueBlock.value)
					dataBuffer = utilConcatBuf(dataBuffer, content.valueBlock.valueHex);
			}
			//endregion

			return crypto.decrypt({
				name: contentEncryptionAlgorithm.name,
				iv: ivView
			},
			result,
			dataBuffer);
		}, error =>
			Promise.reject(error)
		);
		//endregion

		return sequence;
	}
	//**********************************************************************************
}
//**************************************************************************************
