Source code

Revision control

Copy as Markdown

Other Tools

import * as asn1js from "asn1js";
import * as pvutils from "pvutils";
import * as common from "./common";
import { OriginatorInfo, OriginatorInfoJson } from "./OriginatorInfo";
import { RecipientInfo, RecipientInfoJson } from "./RecipientInfo";
import { EncryptedContentInfo, EncryptedContentInfoJson, EncryptedContentInfoSchema } from "./EncryptedContentInfo";
import { Attribute, AttributeJson } from "./Attribute";
import { AlgorithmIdentifier, AlgorithmIdentifierParameters } from "./AlgorithmIdentifier";
import { RSAESOAEPParams } from "./RSAESOAEPParams";
import { KeyTransRecipientInfo } from "./KeyTransRecipientInfo";
import { IssuerAndSerialNumber } from "./IssuerAndSerialNumber";
import { RecipientKeyIdentifier } from "./RecipientKeyIdentifier";
import { RecipientEncryptedKey } from "./RecipientEncryptedKey";
import { KeyAgreeRecipientIdentifier } from "./KeyAgreeRecipientIdentifier";
import { KeyAgreeRecipientInfo, KeyAgreeRecipientInfoParameters } from "./KeyAgreeRecipientInfo";
import { RecipientEncryptedKeys } from "./RecipientEncryptedKeys";
import { KEKRecipientInfo } from "./KEKRecipientInfo";
import { KEKIdentifier } from "./KEKIdentifier";
import { PBKDF2Params } from "./PBKDF2Params";
import { PasswordRecipientinfo } from "./PasswordRecipientinfo";
import { ECCCMSSharedInfo } from "./ECCCMSSharedInfo";
import { OriginatorIdentifierOrKey } from "./OriginatorIdentifierOrKey";
import { OriginatorPublicKey } from "./OriginatorPublicKey";
import * as Schema from "./Schema";
import { Certificate } from "./Certificate";
import { ArgumentError, AsnError } from "./errors";
import { PkiObject, PkiObjectParameters } from "./PkiObject";
import { EMPTY_STRING } from "./constants";
const VERSION = "version";
const ORIGINATOR_INFO = "originatorInfo";
const RECIPIENT_INFOS = "recipientInfos";
const ENCRYPTED_CONTENT_INFO = "encryptedContentInfo";
const UNPROTECTED_ATTRS = "unprotectedAttrs";
const CLEAR_PROPS = [
VERSION,
ORIGINATOR_INFO,
RECIPIENT_INFOS,
ENCRYPTED_CONTENT_INFO,
UNPROTECTED_ATTRS
];
const defaultEncryptionParams = {
kdfAlgorithm: "SHA-512",
kekEncryptionLength: 256
};
const curveLengthByName: Record<string, number> = {
"P-256": 256,
"P-384": 384,
"P-521": 528
};
export interface IEnvelopedData {
/**
* Version number.
*
* The appropriate value depends on `originatorInfo`, `RecipientInfo`, and `unprotectedAttrs`.
*
* The version MUST be assigned as follows:
* ```
* IF (originatorInfo is present) AND
* ((any certificates with a type of other are present) OR
* (any crls with a type of other are present))
* THEN version is 4
* ELSE
* IF ((originatorInfo is present) AND
* (any version 2 attribute certificates are present)) OR
* (any RecipientInfo structures include pwri) OR
* (any RecipientInfo structures include ori)
* THEN version is 3
* ELSE
* IF (originatorInfo is absent) AND
* (unprotectedAttrs is absent) AND
* (all RecipientInfo structures are version 0)
* THEN version is 0
* ELSE version is 2
* ```
*/
version: number;
/**
* Optionally provides information about the originator. It is present only if required by the key management algorithm.
* It may contain certificates and CRLs.
*/
originatorInfo?: OriginatorInfo;
/**
* Collection of per-recipient information. There MUST be at least one element in the collection.
*/
recipientInfos: RecipientInfo[];
/**
* Encrypted content information
*/
encryptedContentInfo: EncryptedContentInfo;
/**
* Collection of attributes that are not encrypted
*/
unprotectedAttrs?: Attribute[];
}
/**
* JSON representation of {@link EnvelopedData}
*/
export interface EnvelopedDataJson {
version: number;
originatorInfo?: OriginatorInfoJson;
recipientInfos: RecipientInfoJson[];
encryptedContentInfo: EncryptedContentInfoJson;
unprotectedAttrs?: AttributeJson[];
}
export type EnvelopedDataParameters = PkiObjectParameters & Partial<IEnvelopedData>;
export interface EnvelopedDataEncryptionParams {
kekEncryptionLength: number;
kdfAlgorithm: string;
}
/**
* Represents the EnvelopedData structure described in [RFC5652](https://datatracker.ietf.org/doc/html/rfc5652)
*
* @example The following example demonstrates how to create and encrypt CMS Enveloped Data
* ```js
* const cmsEnveloped = new pkijs.EnvelopedData();
*
* // Add recipient
* cmsEnveloped.addRecipientByCertificate(cert, { oaepHashAlgorithm: "SHA-256" });
*
* // Secret key algorithm
* const alg = {
* name: "AES-GCM",
* length: 256,
* }
* await cmsEnveloped.encrypt(alg, dataToEncrypt);
*
* // Add Enveloped Data into CMS Content Info
* const cmsContent = new pkijs.ContentInfo();
* cmsContent.contentType = pkijs.ContentInfo.ENVELOPED_DATA;
* cmsContent.content = cmsEnveloped.toSchema();
*
* const cmsContentRaw = cmsContent.toSchema().toBER();
* ```
*
* @example The following example demonstrates how to decrypt CMS Enveloped Data
* ```js
* // Get a "crypto" extension
* const crypto = pkijs.getCrypto();
*
* // Parse CMS Content Info
* const cmsContent = pkijs.ContentInfo.fromBER(cmsContentRaw);
* if (cmsContent.contentType !== pkijs.ContentInfo.ENVELOPED_DATA) {
* throw new Error("CMS is not Enveloped Data");
* }
* // Parse CMS Enveloped Data
* const cmsEnveloped = new pkijs.EnvelopedData({ schema: cmsContent.content });
*
* // Export private key to PKCS#8
* const pkcs8 = await crypto.exportKey("pkcs8", keys.privateKey);
*
* // Decrypt data
* const decryptedData = await cmsEnveloped.decrypt(0, {
* recipientCertificate: cert,
* recipientPrivateKey: pkcs8,
* });
* ```
*/
export class EnvelopedData extends PkiObject implements IEnvelopedData {
public static override CLASS_NAME = "EnvelopedData";
public version!: number;
public originatorInfo?: OriginatorInfo;
public recipientInfos!: RecipientInfo[];
public encryptedContentInfo!: EncryptedContentInfo;
public unprotectedAttrs?: Attribute[];
/**
* Initializes a new instance of the {@link EnvelopedData} class
* @param parameters Initialization parameters
*/
constructor(parameters: EnvelopedDataParameters = {}) {
super();
this.version = pvutils.getParametersValue(parameters, VERSION, EnvelopedData.defaultValues(VERSION));
if (ORIGINATOR_INFO in parameters) {
this.originatorInfo = pvutils.getParametersValue(parameters, ORIGINATOR_INFO, EnvelopedData.defaultValues(ORIGINATOR_INFO));
}
this.recipientInfos = pvutils.getParametersValue(parameters, RECIPIENT_INFOS, EnvelopedData.defaultValues(RECIPIENT_INFOS));
this.encryptedContentInfo = pvutils.getParametersValue(parameters, ENCRYPTED_CONTENT_INFO, EnvelopedData.defaultValues(ENCRYPTED_CONTENT_INFO));
if (UNPROTECTED_ATTRS in parameters) {
this.unprotectedAttrs = pvutils.getParametersValue(parameters, UNPROTECTED_ATTRS, EnvelopedData.defaultValues(UNPROTECTED_ATTRS));
}
if (parameters.schema) {
this.fromSchema(parameters.schema);
}
}
/**
* Returns default values for all class members
* @param memberName String name for a class member
* @returns Default value
*/
public static override defaultValues(memberName: typeof VERSION): number;
public static override defaultValues(memberName: typeof ORIGINATOR_INFO): OriginatorInfo;
public static override defaultValues(memberName: typeof RECIPIENT_INFOS): RecipientInfo[];
public static override defaultValues(memberName: typeof ENCRYPTED_CONTENT_INFO): EncryptedContentInfo;
public static override defaultValues(memberName: typeof UNPROTECTED_ATTRS): Attribute[];
public static override defaultValues(memberName: string): any {
switch (memberName) {
case VERSION:
return 0;
case ORIGINATOR_INFO:
return new OriginatorInfo();
case RECIPIENT_INFOS:
return [];
case ENCRYPTED_CONTENT_INFO:
return new EncryptedContentInfo();
case UNPROTECTED_ATTRS:
return [];
default:
return super.defaultValues(memberName);
}
}
/**
* Compare values with default values for all class members
* @param memberName String name for a class member
* @param memberValue Value to compare with default value
*/
public static compareWithDefault(memberName: string, memberValue: any): boolean {
switch (memberName) {
case VERSION:
return (memberValue === EnvelopedData.defaultValues(memberName));
case ORIGINATOR_INFO:
return ((memberValue.certs.certificates.length === 0) && (memberValue.crls.crls.length === 0));
case RECIPIENT_INFOS:
case UNPROTECTED_ATTRS:
return (memberValue.length === 0);
case ENCRYPTED_CONTENT_INFO:
return ((EncryptedContentInfo.compareWithDefault("contentType", memberValue.contentType)) &&
(EncryptedContentInfo.compareWithDefault("contentEncryptionAlgorithm", memberValue.contentEncryptionAlgorithm) &&
(EncryptedContentInfo.compareWithDefault("encryptedContent", memberValue.encryptedContent))));
default:
return super.defaultValues(memberName);
}
}
/**
* @inheritdoc
* @asn ASN.1 schema
* ```asn
* EnvelopedData ::= SEQUENCE {
* version CMSVersion,
* originatorInfo [0] IMPLICIT OriginatorInfo OPTIONAL,
* recipientInfos RecipientInfos,
* encryptedContentInfo EncryptedContentInfo,
* unprotectedAttrs [1] IMPLICIT UnprotectedAttributes OPTIONAL }
*```
*/
public static override schema(parameters: Schema.SchemaParameters<{
version?: string;
originatorInfo?: string;
recipientInfos?: string;
encryptedContentInfo?: EncryptedContentInfoSchema;
unprotectedAttrs?: string;
}> = {}): Schema.SchemaType {
const names = pvutils.getParametersValue<NonNullable<typeof parameters.names>>(parameters, "names", {});
return (new asn1js.Sequence({
name: (names.blockName || EMPTY_STRING),
value: [
new asn1js.Integer({ name: (names.version || EMPTY_STRING) }),
new asn1js.Constructed({
name: (names.originatorInfo || EMPTY_STRING),
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 || EMPTY_STRING),
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 || EMPTY_STRING),
value: Attribute.schema()
})
]
})
]
}));
}
public fromSchema(schema: Schema.SchemaType): void {
// Clear input data first
pvutils.clearProps(schema, CLEAR_PROPS);
// Check the schema is valid
const asn1 = asn1js.compareSchema(schema,
schema,
EnvelopedData.schema({
names: {
version: VERSION,
originatorInfo: ORIGINATOR_INFO,
recipientInfos: RECIPIENT_INFOS,
encryptedContentInfo: {
names: {
blockName: ENCRYPTED_CONTENT_INFO
}
},
unprotectedAttrs: UNPROTECTED_ATTRS
}
})
);
AsnError.assertSchema(asn1, this.className);
// Get internal properties from parsed schema
this.version = asn1.result.version.valueBlock.valueDec;
if (ORIGINATOR_INFO in asn1.result) {
this.originatorInfo = new OriginatorInfo({
schema: new asn1js.Sequence({
value: asn1.result.originatorInfo.valueBlock.value
})
});
}
this.recipientInfos = Array.from(asn1.result.recipientInfos, o => new RecipientInfo({ schema: o }));
this.encryptedContentInfo = new EncryptedContentInfo({ schema: asn1.result.encryptedContentInfo });
if (UNPROTECTED_ATTRS in asn1.result)
this.unprotectedAttrs = Array.from(asn1.result.unprotectedAttrs, o => new Attribute({ schema: o }));
}
public toSchema(): asn1js.Sequence {
//#region Create array for output sequence
const outputArray = [];
outputArray.push(new asn1js.Integer({ value: this.version }));
if (this.originatorInfo) {
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, o => o.toSchema())
}));
outputArray.push(this.encryptedContentInfo.toSchema());
if (this.unprotectedAttrs) {
outputArray.push(new asn1js.Constructed({
optional: true,
idBlock: {
tagClass: 3, // CONTEXT-SPECIFIC
tagNumber: 1 // [1]
},
value: Array.from(this.unprotectedAttrs, o => o.toSchema())
}));
}
//#endregion
//#region Construct and return new ASN.1 schema for this object
return (new asn1js.Sequence({
value: outputArray
}));
//#endregion
}
public toJSON(): EnvelopedDataJson {
const res: EnvelopedDataJson = {
version: this.version,
recipientInfos: Array.from(this.recipientInfos, o => o.toJSON()),
encryptedContentInfo: this.encryptedContentInfo.toJSON(),
};
if (this.originatorInfo)
res.originatorInfo = this.originatorInfo.toJSON();
if (this.unprotectedAttrs)
res.unprotectedAttrs = Array.from(this.unprotectedAttrs, o => o.toJSON());
return res;
}
/**
* 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 Recipient's certificate
* @param parameters Additional parameters necessary for "fine tunning" of encryption process
* @param variant Variant = 1 is for "key transport", variant = 2 is for "key agreement". In fact the "variant" is unnecessary 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.
* @param crypto Crypto engine
*/
public addRecipientByCertificate(certificate: Certificate, parameters?: {
// empty
}, variant?: number, crypto = common.getCrypto(true)): boolean {
//#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) {
// keyEncryptionAlgorithm
algorithmId = crypto.getOIDByAlgorithm({
name: "RSA-OAEP"
}, true, "keyEncryptionAlgorithm");
//#region RSAES-OAEP-params
const hashOID = crypto.getOIDByAlgorithm({
name: encryptionParameters.oaepHashAlgorithm
}, true, "RSAES-OAEP-params");
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 = crypto.getOIDByAlgorithm({
name: "RSAES-PKCS1-v1_5"
});
if (algorithmId === EMPTY_STRING)
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 },
crypto,
);
}
break;
default:
throw new Error(`Unknown "variant" value: ${variant}`);
}
//#endregion
return true;
}
/**
* Add recipient based on pre-defined data like password or KEK
* @param preDefinedData ArrayBuffer with pre-defined data
* @param parameters Additional parameters necessary for "fine tunning" of encryption process
* @param variant Variant = 1 for pre-defined "key encryption key" (KEK). Variant = 2 for password-based encryption.
* @param crypto Crypto engine
*/
public addRecipientByPreDefinedData(preDefinedData: ArrayBuffer, parameters: {
keyIdentifier?: ArrayBuffer;
hmacHashAlgorithm?: string;
iterationCount?: number;
keyEncryptionAlgorithm?: AesKeyGenParams;
keyEncryptionAlgorithmParams?: any;
} = {}, variant: number, crypto = common.getCrypto(true)) {
//#region Check initial parameters
ArgumentError.assert(preDefinedData, "preDefinedData", "ArrayBuffer");
if (!preDefinedData.byteLength) {
throw new Error("Pre-defined data could have zero length");
}
//#endregion
//#region Initialize encryption parameters
if (!parameters.keyIdentifier) {
const keyIdentifierBuffer = new ArrayBuffer(16);
const keyIdentifierView = new Uint8Array(keyIdentifierBuffer);
crypto.getRandomValues(keyIdentifierView);
parameters.keyIdentifier = keyIdentifierBuffer;
}
if (!parameters.hmacHashAlgorithm)
parameters.hmacHashAlgorithm = "SHA-512";
if (parameters.iterationCount === undefined) {
parameters.iterationCount = 2048;
}
if (!parameters.keyEncryptionAlgorithm) {
parameters.keyEncryptionAlgorithm = {
name: "AES-KW",
length: 256
};
}
if (!parameters.keyEncryptionAlgorithmParams)
parameters.keyEncryptionAlgorithmParams = new asn1js.Null();
//#endregion
//#region Add new recipient based on passed variant
switch (variant) {
case 1: // KEKRecipientInfo
{
// keyEncryptionAlgorithm
const kekOID = crypto.getOIDByAlgorithm(parameters.keyEncryptionAlgorithm, true, "keyEncryptionAlgorithm");
//#region KEKRecipientInfo
const keyInfo = new KEKRecipientInfo({
version: 4,
kekid: new KEKIdentifier({
keyIdentifier: new asn1js.OctetString({ valueHex: parameters.keyIdentifier })
}),
keyEncryptionAlgorithm: new AlgorithmIdentifier({
algorithmId: kekOID,
/*
For AES-KW params are NULL, but for other algorithm could another situation.
*/
algorithmParams: parameters.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
{
// keyDerivationAlgorithm
const pbkdf2OID = crypto.getOIDByAlgorithm({ name: "PBKDF2" }, true, "keyDerivationAlgorithm");
//#region Salt
const saltBuffer = new ArrayBuffer(64);
const saltView = new Uint8Array(saltBuffer);
crypto.getRandomValues(saltView);
//#endregion
//#region HMAC-based algorithm
const hmacOID = crypto.getOIDByAlgorithm({
name: "HMAC",
hash: {
name: parameters.hmacHashAlgorithm
}
} as Algorithm, true, "hmacHashAlgorithm");
//#endregion
//#region PBKDF2-params
const pbkdf2Params = new PBKDF2Params({
salt: new asn1js.OctetString({ valueHex: saltBuffer }),
iterationCount: parameters.iterationCount,
prf: new AlgorithmIdentifier({
algorithmId: hmacOID,
algorithmParams: new asn1js.Null()
})
});
//#endregion
// keyEncryptionAlgorithm
const kekOID = crypto.getOIDByAlgorithm(parameters.keyEncryptionAlgorithm, true, "keyEncryptionAlgorithm");
//#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: parameters.keyEncryptionAlgorithmParams
}),
password: preDefinedData
// "encryptedKey" would be set in "encrypt" 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 key Recipient's public key
* @param keyId The id for the recipient's public key
* @param parameters Additional parameters for "fine tuning" the encryption process
* @param crypto Crypto engine
*/
addRecipientByKeyIdentifier(key?: CryptoKey, keyId?: ArrayBuffer, parameters?: any, crypto = common.getCrypto(true)) {
//#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 },
crypto,
);
}
/**
* Add a "RecipientInfo" using a KeyAgreeRecipientInfo of type RecipientKeyIdentifier.
* @param recipientIdentifier Recipient identifier
* @param encryptionParameters Additional parameters for "fine tuning" the encryption process
* @param extraRecipientInfoParams Additional params for KeyAgreeRecipientInfo
* @param crypto Crypto engine
*/
private _addKeyAgreeRecipientInfo(recipientIdentifier: KeyAgreeRecipientIdentifier, encryptionParameters: EnvelopedDataEncryptionParams, extraRecipientInfoParams: KeyAgreeRecipientInfoParameters, crypto = common.getCrypto(true)) {
//#region RecipientEncryptedKey
const encryptedKey = new RecipientEncryptedKey({
rid: recipientIdentifier
// "encryptedKey" will be calculated in "encrypt" function
});
//#endregion
//#region keyEncryptionAlgorithm
const aesKWoid = crypto.getOIDByAlgorithm({
name: "AES-KW",
length: encryptionParameters.kekEncryptionLength
} as Algorithm, true, "keyEncryptionAlgorithm");
const aesKW = new AlgorithmIdentifier({
algorithmId: aesKWoid,
});
//#endregion
//#region KeyAgreeRecipientInfo
const ecdhOID = crypto.getOIDByAlgorithm({
name: "ECDH",
kdf: encryptionParameters.kdfAlgorithm
} as Algorithm, true, "KeyAgreeRecipientInfo");
// 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);
crypto.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
}
/**
* Creates a new CMS Enveloped Data content with encrypted data
* @param contentEncryptionAlgorithm WebCrypto algorithm. For the moment here could be only "AES-CBC" or "AES-GCM" algorithms.
* @param contentToEncrypt Content to encrypt
* @param crypto Crypto engine
*/
public async encrypt(contentEncryptionAlgorithm: Algorithm, contentToEncrypt: ArrayBuffer, crypto = common.getCrypto(true)): Promise<(void | { ecdhPrivateKey: CryptoKey; })[]> {
//#region Initial variables
const ivBuffer = new ArrayBuffer(16); // For AES we need IV 16 bytes long
const ivView = new Uint8Array(ivBuffer);
crypto.getRandomValues(ivView);
const contentView = new Uint8Array(contentToEncrypt);
//#endregion
// Check for input parameters
const contentEncryptionOID = crypto.getOIDByAlgorithm(contentEncryptionAlgorithm, true, "contentEncryptionAlgorithm");
//#region Generate new content encryption key
const sessionKey = await crypto.generateKey(contentEncryptionAlgorithm as AesKeyAlgorithm, true, ["encrypt"]);
//#endregion
//#region Encrypt content
const encryptedContent = await crypto.encrypt({
name: contentEncryptionAlgorithm.name,
iv: ivView
},
sessionKey,
contentView);
//#endregion
//#region Export raw content of content encryption key
const exportedSessionKey = await crypto.exportKey("raw", sessionKey);
//#endregion
//#region Append common information to CMS_ENVELOPED_DATA
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 })
});
//#endregion
//#region Special sub-functions to work with each recipient's type
const SubKeyAgreeRecipientInfo = async (index: number) => {
//#region Initial variables
const recipientInfo = this.recipientInfos[index].value as KeyAgreeRecipientInfo;
let recipientCurve: string;
//#endregion
//#region Get public key and named curve from recipient's certificate or public key
let recipientPublicKey: CryptoKey;
if (recipientInfo.recipientPublicKey) {
recipientCurve = (recipientInfo.recipientPublicKey.algorithm as EcKeyAlgorithm).namedCurve;
recipientPublicKey = recipientInfo.recipientPublicKey;
} else if (recipientInfo.recipientCertificate) {
const curveObject = recipientInfo.recipientCertificate.subjectPublicKeyInfo.algorithm.algorithmParams;
if (curveObject.constructor.blockName() !== asn1js.ObjectIdentifier.blockName())
throw new Error(`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:
throw new Error(`Incorrect curve OID for index ${index}`);
}
recipientPublicKey = await recipientInfo.recipientCertificate.getPublicKey({
algorithm: {
algorithm: {
name: "ECDH",
namedCurve: recipientCurve
} as EcKeyAlgorithm,
usages: []
}
}, crypto);
} else {
throw new Error("Unsupported RecipientInfo");
}
//#endregion
//#region Generate ephemeral ECDH key
const recipientCurveLength = curveLengthByName[recipientCurve];
const ecdhKeys = await crypto.generateKey(
{ name: "ECDH", namedCurve: recipientCurve } as EcKeyGenParams,
true,
["deriveBits"]
);
//#endregion
//#region Export public key of ephemeral ECDH key pair
const exportedECDHPublicKey = await crypto.exportKey("spki", ecdhKeys.publicKey);
//#endregion
//#region Create shared secret
const derivedBits = await crypto.deriveBits({
name: "ECDH",
public: recipientPublicKey
},
ecdhKeys.privateKey,
recipientCurveLength);
//#endregion
//#region Apply KDF function to shared secret
//#region Get length of used AES-KW algorithm
const aesKWAlgorithm = new AlgorithmIdentifier({ schema: recipientInfo.keyEncryptionAlgorithm.algorithmParams });
const kwAlgorithm = crypto.getAlgorithmByOID<AesKeyAlgorithm>(aesKWAlgorithm.algorithmId, true, "aesKWAlgorithm");
//#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 as KeyAgreeRecipientInfo).ukm, // TODO remove `as KeyAgreeRecipientInfo`
suppPubInfo: new asn1js.OctetString({ valueHex: kwLengthBuffer })
});
const encodedInfo = eccInfo.toSchema().toBER(false);
//#endregion
//#region Get SHA algorithm used together with ECDH
const ecdhAlgorithm = crypto.getAlgorithmByOID<any>(recipientInfo.keyEncryptionAlgorithm.algorithmId, true, "ecdhAlgorithm");
//#endregion
const derivedKeyRaw = await common.kdf(ecdhAlgorithm.kdf, derivedBits, kwAlgorithm.length, encodedInfo, crypto);
//#endregion
//#region Import AES-KW key from result of KDF function
const awsKW = await crypto.importKey("raw", derivedKeyRaw, { name: "AES-KW" }, true, ["wrapKey"]);
//#endregion
//#region Finally wrap session key by using AES-KW algorithm
const wrappedKey = await crypto.wrapKey("raw", sessionKey, awsKW, { name: "AES-KW" });
//#endregion
//#region Append all necessary data to current CMS_RECIPIENT_INFO object
//#region OriginatorIdentifierOrKey
const originator = new OriginatorIdentifierOrKey();
originator.variant = 3;
originator.value = OriginatorPublicKey.fromBER(exportedECDHPublicKey);
recipientInfo.originator = originator;
//#endregion
//#region RecipientEncryptedKey
/*
We will not support using of same ephemeral key for many recipients
*/
recipientInfo.recipientEncryptedKeys.encryptedKeys[0].encryptedKey = new asn1js.OctetString({ valueHex: wrappedKey });
//#endregion
return { ecdhPrivateKey: ecdhKeys.privateKey };
//#endregion
};
const SubKeyTransRecipientInfo = async (index: number) => {
const recipientInfo = this.recipientInfos[index].value as KeyTransRecipientInfo; // TODO Remove `as KeyTransRecipientInfo`
const algorithmParameters = crypto.getAlgorithmByOID<any>(recipientInfo.keyEncryptionAlgorithm.algorithmId, true, "keyEncryptionAlgorithm");
//#region RSA-OAEP case
if (algorithmParameters.name === "RSA-OAEP") {
const schema = recipientInfo.keyEncryptionAlgorithm.algorithmParams;
const rsaOAEPParams = new RSAESOAEPParams({ schema });
algorithmParameters.hash = crypto.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 recipientInfo.recipientCertificate.getPublicKey({
algorithm: {
algorithm: algorithmParameters,
usages: ["encrypt", "wrapKey"]
}
}, crypto);
const encryptedKey = await crypto.encrypt(publicKey.algorithm, publicKey, exportedSessionKey);
//#region RecipientEncryptedKey
recipientInfo.encryptedKey = new asn1js.OctetString({ valueHex: encryptedKey });
//#endregion
}
catch {
// nothing
}
};
const SubKEKRecipientInfo = async (index: number) => {
//#region Initial variables
const recipientInfo = this.recipientInfos[index].value as KEKRecipientInfo; // TODO Remove `as KEKRecipientInfo`
//#endregion
//#region Import KEK from pre-defined data
//#region Get WebCrypto form of "keyEncryptionAlgorithm"
const kekAlgorithm = crypto.getAlgorithmByOID(recipientInfo.keyEncryptionAlgorithm.algorithmId, true, "kekAlgorithm");
//#endregion
const kekKey = await crypto.importKey("raw",
new Uint8Array(recipientInfo.preDefinedKEK),
kekAlgorithm,
true,
["wrapKey"]); // Too specific for AES-KW
//#endregion
//#region Wrap previously exported session key
const wrappedKey = await crypto.wrapKey("raw", sessionKey, kekKey, kekAlgorithm);
//#endregion
//#region Append all necessary data to current CMS_RECIPIENT_INFO object
//#region RecipientEncryptedKey
recipientInfo.encryptedKey = new asn1js.OctetString({ valueHex: wrappedKey });
//#endregion
//#endregion
};
const SubPasswordRecipientinfo = async (index: number) => {
//#region Initial variables
const recipientInfo = this.recipientInfos[index].value as PasswordRecipientinfo; // TODO Remove `as PasswordRecipientinfo`
let pbkdf2Params: PBKDF2Params;
//#endregion
//#region Check that we have encoded "keyDerivationAlgorithm" plus "PBKDF2_params" in there
if (!recipientInfo.keyDerivationAlgorithm)
throw new Error("Please append encoded \"keyDerivationAlgorithm\"");
if (!recipientInfo.keyDerivationAlgorithm.algorithmParams)
throw new Error("Incorrectly encoded \"keyDerivationAlgorithm\"");
try {
pbkdf2Params = new PBKDF2Params({ schema: recipientInfo.keyDerivationAlgorithm.algorithmParams });
}
catch (ex) {
throw new Error("Incorrectly encoded \"keyDerivationAlgorithm\"");
}
//#endregion
//#region Derive PBKDF2 key from "password" buffer
const passwordView = new Uint8Array(recipientInfo.password);
const derivationKey = await crypto.importKey("raw",
passwordView,
"PBKDF2",
false,
["deriveKey"]);
//#endregion
//#region Derive key for "keyEncryptionAlgorithm"
//#region Get WebCrypto form of "keyEncryptionAlgorithm"
const kekAlgorithm = crypto.getAlgorithmByOID<any>(recipientInfo.keyEncryptionAlgorithm.algorithmId, true, "kekAlgorithm");
//#endregion
//#region Get HMAC hash algorithm
let hmacHashAlgorithm = "SHA-1";
if (pbkdf2Params.prf) {
const prfAlgorithm = crypto.getAlgorithmByOID<any>(pbkdf2Params.prf.algorithmId, true, "prfAlgorithm");
hmacHashAlgorithm = prfAlgorithm.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
const derivedKey = await crypto.deriveKey({
name: "PBKDF2",
hash: {
name: hmacHashAlgorithm
},
salt: saltView,
iterations
},
derivationKey,
kekAlgorithm,
true,
["wrapKey"]); // Usages are too specific for KEK algorithm
//#endregion
//#region Wrap previously exported session key (Also too specific for KEK algorithm)
const wrappedKey = await crypto.wrapKey("raw", sessionKey, derivedKey, kekAlgorithm);
//#endregion
//#region Append all necessary data to current CMS_RECIPIENT_INFO object
//#region RecipientEncryptedKey
recipientInfo.encryptedKey = new asn1js.OctetString({ valueHex: wrappedKey });
//#endregion
//#endregion
};
//#endregion
const res = [];
//#region Create special routines for each "recipient"
for (let i = 0; i < this.recipientInfos.length; i++) {
switch (this.recipientInfos[i].variant) {
case 1: // KeyTransRecipientInfo
res.push(await SubKeyTransRecipientInfo(i));
break;
case 2: // KeyAgreeRecipientInfo
res.push(await SubKeyAgreeRecipientInfo(i));
break;
case 3: // KEKRecipientInfo
res.push(await SubKEKRecipientInfo(i));
break;
case 4: // PasswordRecipientinfo
res.push(await SubPasswordRecipientinfo(i));
break;
default:
throw new Error(`Unknown recipient type in array with index ${i}`);
}
}
//#endregion
return res;
}
/**
* Decrypts existing CMS Enveloped Data content
* @param recipientIndex Index of recipient
* @param parameters Additional parameters
* @param crypto Crypto engine
*/
async decrypt(recipientIndex: number, parameters: {
recipientCertificate?: Certificate;
recipientPrivateKey?: BufferSource;
preDefinedData?: BufferSource;
}, crypto = common.getCrypto(true)) {
//#region Initial variables
const decryptionParameters = parameters || {};
//#endregion
//#region Check for input parameters
if ((recipientIndex + 1) > this.recipientInfos.length) {
throw new Error(`Maximum value for "index" is: ${this.recipientInfos.length - 1}`);
}
//#endregion
//#region Special sub-functions to work with each recipient's type
const SubKeyAgreeRecipientInfo = async (index: number) => {
//#region Initial variables
const recipientInfo = this.recipientInfos[index].value as KeyAgreeRecipientInfo; // TODO Remove `as KeyAgreeRecipientInfo`
//#endregion
let curveOID: string;
let recipientCurve: string;
let recipientCurveLength: number;
const originator = recipientInfo.originator;
//#region Get "namedCurve" parameter from recipient's certificate
if (decryptionParameters.recipientCertificate) {
const curveObject = decryptionParameters.recipientCertificate.subjectPublicKeyInfo.algorithm.algorithmParams;
if (curveObject.constructor.blockName() !== asn1js.ObjectIdentifier.blockName()) {
throw new Error(`Incorrect "recipientCertificate" for index ${index}`);
}
curveOID = curveObject.valueBlock.toString();
} else if (originator.value.algorithm.algorithmParams) {
const curveObject = originator.value.algorithm.algorithmParams;
if (curveObject.constructor.blockName() !== asn1js.ObjectIdentifier.blockName()) {
throw new Error(`Incorrect originator for index ${index}`);
}
curveOID = curveObject.valueBlock.toString();
} else {
throw new Error("Parameter \"recipientCertificate\" is mandatory for \"KeyAgreeRecipientInfo\" if algorithm params are missing from originator");
}
if (!decryptionParameters.recipientPrivateKey)
throw new Error("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:
throw new Error(`Incorrect curve OID for index ${index}`);
}
const ecdhPrivateKey = await crypto.importKey("pkcs8",
decryptionParameters.recipientPrivateKey,
{
name: "ECDH",
namedCurve: recipientCurve
} as EcKeyImportParams,
true,
["deriveBits"]
);
//#endregion
//#region Import sender's ephemeral public key
//#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
const ecdhPublicKey = await crypto.importKey("spki",
buffer,
{
name: "ECDH",
namedCurve: recipientCurve
} as EcKeyImportParams,
true,
[]);
//#endregion
//#region Create shared secret
const sharedSecret = await crypto.deriveBits({
name: "ECDH",
public: ecdhPublicKey
},
ecdhPrivateKey,
recipientCurveLength);
//#endregion
//#region Apply KDF function to shared secret
async function applyKDF(includeAlgorithmParams?: boolean) {
includeAlgorithmParams = includeAlgorithmParams || false;
//#region Get length of used AES-KW algorithm
const aesKWAlgorithm = new AlgorithmIdentifier({ schema: recipientInfo.keyEncryptionAlgorithm.algorithmParams });
const kwAlgorithm = crypto.getAlgorithmByOID<any>(aesKWAlgorithm.algorithmId, true, "kwAlgorithm");
//#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: AlgorithmIdentifierParameters = {
algorithmId: aesKWAlgorithm.algorithmId
};
if (includeAlgorithmParams) {
keyInfoAlgorithm.algorithmParams = new asn1js.Null();
}
const eccInfo = new ECCCMSSharedInfo({
keyInfo: new AlgorithmIdentifier(keyInfoAlgorithm),
entityUInfo: recipientInfo.ukm,
suppPubInfo: new asn1js.OctetString({ valueHex: kwLengthBuffer })
});
const encodedInfo = eccInfo.toSchema().toBER(false);
//#endregion
//#region Get SHA algorithm used together with ECDH
const ecdhAlgorithm = crypto.getAlgorithmByOID<any>(recipientInfo.keyEncryptionAlgorithm.algorithmId, true, "ecdhAlgorithm");
if (!ecdhAlgorithm.name) {
throw new Error(`Incorrect OID for key encryption algorithm: ${recipientInfo.keyEncryptionAlgorithm.algorithmId}`);
}
//#endregion
return common.kdf(ecdhAlgorithm.kdf, sharedSecret, kwAlgorithm.length, encodedInfo, crypto);
}
const kdfResult = await applyKDF();
//#endregion
//#region Import AES-KW key from result of KDF function
const importAesKwKey = async (kdfResult: ArrayBuffer) => {
return crypto.importKey("raw",
kdfResult,
{ name: "AES-KW" },
true,
["unwrapKey"]
);
};
const aesKwKey = await importAesKwKey(kdfResult);
//#endregion
//#region Finally unwrap session key
const unwrapSessionKey = async (aesKwKey: CryptoKey) => {
//#region Get WebCrypto form of content encryption algorithm
const algorithmId = this.encryptedContentInfo.contentEncryptionAlgorithm.algorithmId;
const contentEncryptionAlgorithm = crypto.getAlgorithmByOID<any>(algorithmId, true, "contentEncryptionAlgorithm");
//#endregion
return crypto.unwrapKey("raw",
recipientInfo.recipientEncryptedKeys.encryptedKeys[0].encryptedKey.valueBlock.valueHexView,
aesKwKey,
{ name: "AES-KW" },
contentEncryptionAlgorithm,
true,
["decrypt"]);
};
try {
return await unwrapSessionKey(aesKwKey);
} catch {
const kdfResult = await applyKDF(true);
const aesKwKey = await importAesKwKey(kdfResult);
return unwrapSessionKey(aesKwKey);
}
};
//#endregion
const SubKeyTransRecipientInfo = async (index: number) => {
const recipientInfo = this.recipientInfos[index].value as KeyTransRecipientInfo; // TODO Remove `as KeyTransRecipientInfo`
if (!decryptionParameters.recipientPrivateKey) {
throw new Error("Parameter \"recipientPrivateKey\" is mandatory for \"KeyTransRecipientInfo\"");
}
const algorithmParameters = crypto.getAlgorithmByOID<any>(recipientInfo.keyEncryptionAlgorithm.algorithmId, true, "keyEncryptionAlgorithm");
//#region RSA-OAEP case
if (algorithmParameters.name === "RSA-OAEP") {
const schema = recipientInfo.keyEncryptionAlgorithm.algorithmParams;
const rsaOAEPParams = new RSAESOAEPParams({ schema });
algorithmParameters.hash = crypto.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,
recipientInfo.encryptedKey.valueBlock.valueHexView
);
//#region Get WebCrypto form of content encryption algorithm
const algorithmId = this.encryptedContentInfo.contentEncryptionAlgorithm.algorithmId;
const contentEncryptionAlgorithm = crypto.getAlgorithmByOID(algorithmId, true, "contentEncryptionAlgorithm");
if (("name" in contentEncryptionAlgorithm) === false)
throw new Error(`Incorrect "contentEncryptionAlgorithm": ${algorithmId}`);
//#endregion
return crypto.importKey("raw",
sessionKey,
contentEncryptionAlgorithm,
true,
["decrypt"]
);
};
const SubKEKRecipientInfo = async (index: number) => {
//#region Initial variables
const recipientInfo = this.recipientInfos[index].value as KEKRecipientInfo; // TODO Remove `as KEKRecipientInfo`
//#endregion
//#region Import KEK from pre-defined data
if (!decryptionParameters.preDefinedData)
throw new Error("Parameter \"preDefinedData\" is mandatory for \"KEKRecipientInfo\"");
//#region Get WebCrypto form of "keyEncryptionAlgorithm"
const kekAlgorithm = crypto.getAlgorithmByOID<any>(recipientInfo.keyEncryptionAlgorithm.algorithmId, true, "kekAlgorithm");
//#endregion
const importedKey = await crypto.importKey("raw",
decryptionParameters.preDefinedData,
kekAlgorithm,
true,
["unwrapKey"]); // Too specific for AES-KW
//#endregion
//#region Unwrap previously exported session key
//#region Get WebCrypto form of content encryption algorithm
const algorithmId = this.encryptedContentInfo.contentEncryptionAlgorithm.algorithmId;
const contentEncryptionAlgorithm = crypto.getAlgorithmByOID<any>(algorithmId, true, "contentEncryptionAlgorithm");
if (!contentEncryptionAlgorithm.name) {
throw new Error(`Incorrect "contentEncryptionAlgorithm": ${algorithmId}`);
}
//#endregion
return crypto.unwrapKey("raw",
recipientInfo.encryptedKey.valueBlock.valueHexView,
importedKey,
kekAlgorithm,
contentEncryptionAlgorithm,
true,
["decrypt"]);
//#endregion
};
const SubPasswordRecipientinfo = async (index: number) => {
//#region Initial variables
const recipientInfo = this.recipientInfos[index].value as PasswordRecipientinfo; // TODO Remove `as PasswordRecipientinfo`
let pbkdf2Params: PBKDF2Params;
//#endregion
//#region Derive PBKDF2 key from "password" buffer
if (!decryptionParameters.preDefinedData) {
throw new Error("Parameter \"preDefinedData\" is mandatory for \"KEKRecipientInfo\"");
}
if (!recipientInfo.keyDerivationAlgorithm) {
throw new Error("Please append encoded \"keyDerivationAlgorithm\"");
}
if (!recipientInfo.keyDerivationAlgorithm.algorithmParams) {
throw new Error("Incorrectly encoded \"keyDerivationAlgorithm\"");
}
try {
pbkdf2Params = new PBKDF2Params({ schema: recipientInfo.keyDerivationAlgorithm.algorithmParams });
}
catch (ex) {
throw new Error("Incorrectly encoded \"keyDerivationAlgorithm\"");
}
const pbkdf2Key = await crypto.importKey("raw",
decryptionParameters.preDefinedData,
"PBKDF2",
false,
["deriveKey"]);
//#endregion
//#region Derive key for "keyEncryptionAlgorithm"
//#region Get WebCrypto form of "keyEncryptionAlgorithm"
const kekAlgorithm = crypto.getAlgorithmByOID<any>(recipientInfo.keyEncryptionAlgorithm.algorithmId, true, "keyEncryptionAlgorithm");
//#endregion
// Get HMAC hash algorithm
const hmacHashAlgorithm = pbkdf2Params.prf
? crypto.getAlgorithmByOID<any>(pbkdf2Params.prf.algorithmId, true, "prfAlgorithm").hash.name
: "SHA-1";
//#region Get PBKDF2 "salt" value
const saltView = new Uint8Array(pbkdf2Params.salt.valueBlock.valueHex);
//#endregion
//#region Get PBKDF2 iterations count
const iterations = pbkdf2Params.iterationCount;
//#endregion
const kekKey = await crypto.deriveKey({
name: "PBKDF2",
hash: {
name: hmacHashAlgorithm
},
salt: saltView,
iterations
},
pbkdf2Key,
kekAlgorithm,
true,
["unwrapKey"]); // Usages are too specific for KEK algorithm
//#endregion
//#region Unwrap previously exported session key
//#region Get WebCrypto form of content encryption algorithm
const algorithmId = this.encryptedContentInfo.contentEncryptionAlgorithm.algorithmId;
const contentEncryptionAlgorithm = crypto.getAlgorithmByOID<any>(algorithmId, true, "contentEncryptionAlgorithm");
//#endregion
return crypto.unwrapKey("raw",
recipientInfo.encryptedKey.valueBlock.valueHexView,
kekKey,
kekAlgorithm,
contentEncryptionAlgorithm,
true,
["decrypt"]);
//#endregion
};
//#endregion
//#region Perform steps, specific to each type of session key encryption
let unwrappedKey: CryptoKey;
switch (this.recipientInfos[recipientIndex].variant) {
case 1: // KeyTransRecipientInfo
unwrappedKey = await SubKeyTransRecipientInfo(recipientIndex);
break;
case 2: // KeyAgreeRecipientInfo
unwrappedKey = await SubKeyAgreeRecipientInfo(recipientIndex);
break;
case 3: // KEKRecipientInfo
unwrappedKey = await SubKEKRecipientInfo(recipientIndex);
break;
case 4: // PasswordRecipientinfo
unwrappedKey = await SubPasswordRecipientinfo(recipientIndex);
break;
default:
throw new Error(`Unknown recipient type in array with index ${recipientIndex}`);
}
//#endregion
//#region Finally decrypt data by session key
//#region Get WebCrypto form of content encryption algorithm
const algorithmId = this.encryptedContentInfo.contentEncryptionAlgorithm.algorithmId;
const contentEncryptionAlgorithm = crypto.getAlgorithmByOID(algorithmId, true, "contentEncryptionAlgorithm");
//#endregion
//#region Get "initialization 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
if (!this.encryptedContentInfo.encryptedContent) {
throw new Error("Required property `encryptedContent` is empty");
}
const dataBuffer = this.encryptedContentInfo.getEncryptedContent();
//#endregion
return crypto.decrypt(
{
name: (contentEncryptionAlgorithm as any).name,
iv: ivView
},
unwrappedKey,
dataBuffer);
//#endregion
}
}