mirror of
https://github.com/Ylianst/MeshCommander
synced 2025-12-06 06:03:20 +00:00
1730 lines
54 KiB
JavaScript
1730 lines
54 KiB
JavaScript
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 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";
|
|
//**************************************************************************************
|
|
/**
|
|
* 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 Initial variables
|
|
const encryptionParameters = 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 Initialize encryption parameters
|
|
if(("oaepHashAlgorithm" in encryptionParameters) === false)
|
|
encryptionParameters.oaepHashAlgorithm = "SHA-512";
|
|
|
|
if(("kdfAlgorithm" in encryptionParameters) === false)
|
|
encryptionParameters.kdfAlgorithm = "SHA-512";
|
|
|
|
if(("kekEncryptionLength" in encryptionParameters) === false)
|
|
encryptionParameters.kekEncryptionLength = 256;
|
|
//endregion
|
|
|
|
//region Add new "recipient" depends on "variant" and certificate type
|
|
switch(variant)
|
|
{
|
|
case 1: // Key transport scheme
|
|
{
|
|
//region keyEncryptionAlgorithm
|
|
const oaepOID = getOIDByAlgorithm({
|
|
name: "RSA-OAEP"
|
|
});
|
|
if(oaepOID === "")
|
|
throw new Error("Can not find OID for 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()
|
|
})
|
|
});
|
|
//endregion
|
|
|
|
//region KeyTransRecipientInfo
|
|
const keyInfo = new KeyTransRecipientInfo({
|
|
version: 0,
|
|
rid: new IssuerAndSerialNumber({
|
|
issuer: certificate.issuer,
|
|
serialNumber: certificate.serialNumber
|
|
}),
|
|
keyEncryptionAlgorithm: new AlgorithmIdentifier({
|
|
algorithmId: oaepOID,
|
|
algorithmParams: rsaOAEPParams.toSchema()
|
|
}),
|
|
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
|
|
{
|
|
//region RecipientEncryptedKey
|
|
const encryptedKey = new RecipientEncryptedKey({
|
|
rid: new KeyAgreeRecipientIdentifier({
|
|
variant: 1,
|
|
value: new IssuerAndSerialNumber({
|
|
issuer: certificate.issuer,
|
|
serialNumber: certificate.serialNumber
|
|
})
|
|
})
|
|
// "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,
|
|
algorithmParams: new asn1js.Null()
|
|
});
|
|
//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 keyInfo = new KeyAgreeRecipientInfo({
|
|
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]
|
|
}),
|
|
recipientCertificate: certificate
|
|
});
|
|
//endregion
|
|
|
|
//region Final values for "CMS_ENVELOPED_DATA"
|
|
this.recipientInfos.push(new RecipientInfo({
|
|
variant: 2,
|
|
value: keyInfo
|
|
}));
|
|
//endregion
|
|
}
|
|
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
|
|
}
|
|
//**********************************************************************************
|
|
/**
|
|
* 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();
|
|
|
|
let ecdhPublicKey;
|
|
let ecdhPrivateKey;
|
|
|
|
let recipientCurve;
|
|
let recipientCurveLength;
|
|
|
|
let exportedECDHPublicKey;
|
|
//endregion
|
|
|
|
//region Get "namedCurve" parameter from recipient's certificate
|
|
currentSequence = currentSequence.then(() =>
|
|
{
|
|
const curveObject = _this.recipientInfos[index].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";
|
|
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 recipientCurve;
|
|
}, error =>
|
|
Promise.reject(error));
|
|
//endregion
|
|
|
|
//region Generate ephemeral ECDH key
|
|
currentSequence = currentSequence.then(result =>
|
|
crypto.generateKey({
|
|
name: "ECDH",
|
|
namedCurve: result
|
|
},
|
|
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 Import recipient's public key
|
|
currentSequence = currentSequence.then(result =>
|
|
{
|
|
exportedECDHPublicKey = result;
|
|
|
|
return _this.recipientInfos[index].value.recipientCertificate.getPublicKey({
|
|
algorithm: {
|
|
algorithm: {
|
|
name: "ECDH",
|
|
namedCurve: recipientCurve
|
|
},
|
|
usages: []
|
|
}
|
|
});
|
|
}, 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
|
|
currentSequence = currentSequence.then(
|
|
/**
|
|
* @param {ArrayBuffer} result
|
|
*/
|
|
result =>
|
|
{
|
|
//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 eccInfo = new ECCCMSSharedInfo({
|
|
keyInfo: new AlgorithmIdentifier({
|
|
algorithmId: aesKWAlgorithm.algorithmId,
|
|
/*
|
|
Initially RFC5753 says that AES algorithms have absent parameters.
|
|
But since early implementations all put NULL here. Thus, in order to be
|
|
"backward compatible", index also put NULL here.
|
|
*/
|
|
algorithmParams: new asn1js.Null()
|
|
}),
|
|
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, 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 });
|
|
// There is option when we can stay with ECParameters, but here index prefer to avoid the params
|
|
if("algorithmParams" in originator.value.algorithm)
|
|
delete originator.value.algorithm.algorithmParams;
|
|
|
|
_this.recipientInfos[index].value.originator = originator;
|
|
//endregion
|
|
|
|
//region RecipientEncryptedKey
|
|
/*
|
|
We will not support using of same ephemeral key for many recipients
|
|
*/
|
|
_this.recipientInfos[index].value.recipientEncryptedKeys.encryptedKeys[0].encryptedKey = new asn1js.OctetString({ valueHex: result });
|
|
//endregion
|
|
|
|
return {ecdhPrivateKey};
|
|
}, error =>
|
|
Promise.reject(error)
|
|
);
|
|
//endregion
|
|
|
|
return currentSequence;
|
|
}
|
|
|
|
function SubKeyTransRecipientInfo(index)
|
|
{
|
|
//region Initial variables
|
|
let currentSequence = Promise.resolve();
|
|
//endregion
|
|
|
|
//region Get recipient's public key
|
|
currentSequence = currentSequence.then(() =>
|
|
{
|
|
//region Check we have a correct algorithm here
|
|
const oaepOID = getOIDByAlgorithm({
|
|
name: "RSA-OAEP"
|
|
});
|
|
if(oaepOID === "")
|
|
throw new Error("Can not find OID for OAEP");
|
|
|
|
if(_this.recipientInfos[index].value.keyEncryptionAlgorithm.algorithmId !== oaepOID)
|
|
throw new Error("Not supported encryption scheme, only RSA-OAEP is supported for key transport encryption scheme");
|
|
//endregion
|
|
|
|
//region Get current used SHA algorithm
|
|
const schema = _this.recipientInfos[index].value.keyEncryptionAlgorithm.algorithmParams;
|
|
const rsaOAEPParams = new RSAESOAEPParams({ schema });
|
|
|
|
const hashAlgorithm = getAlgorithmByOID(rsaOAEPParams.hashAlgorithm.algorithmId);
|
|
if(("name" in hashAlgorithm) === false)
|
|
return Promise.reject(`Incorrect OID for hash algorithm: ${rsaOAEPParams.hashAlgorithm.algorithmId}`);
|
|
//endregion
|
|
|
|
return _this.recipientInfos[index].value.recipientCertificate.getPublicKey({
|
|
algorithm: {
|
|
algorithm: {
|
|
name: "RSA-OAEP",
|
|
hash: {
|
|
name: hashAlgorithm.name
|
|
}
|
|
},
|
|
usages: ["encrypt", "wrapKey"]
|
|
}
|
|
});
|
|
}, error =>
|
|
Promise.reject(error));
|
|
//endregion
|
|
//region Encrypt early exported session key on recipient's public key
|
|
currentSequence = currentSequence.then(result =>
|
|
crypto.encrypt(result.algorithm, result, exportedSessionKey),
|
|
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 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
|
|
|
|
//region Get "namedCurve" parameter from recipient's certificate
|
|
currentSequence = currentSequence.then(() =>
|
|
{
|
|
if(("recipientCertificate" in decryptionParameters) === false)
|
|
return Promise.reject("Parameter \"recipientCertificate\" is mandatory for \"KeyAgreeRecipientInfo\"");
|
|
|
|
if(("recipientPrivateKey" in decryptionParameters) === false)
|
|
return Promise.reject("Parameter \"recipientPrivateKey\" is mandatory for \"KeyAgreeRecipientInfo\"");
|
|
|
|
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();
|
|
|
|
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 _this.recipientInfos[index].value.originator.value.algorithm) === false)
|
|
_this.recipientInfos[index].value.originator.value.algorithm.algorithmParams = new asn1js.ObjectIdentifier({ value: curveOID });
|
|
//endregion
|
|
|
|
//region Create ArrayBuffer with sender's public key
|
|
const buffer = _this.recipientInfos[index].value.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
|
|
currentSequence = currentSequence.then(
|
|
/**
|
|
* @param {ArrayBuffer} result
|
|
*/
|
|
result =>
|
|
{
|
|
//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 eccInfo = new ECCCMSSharedInfo({
|
|
keyInfo: new AlgorithmIdentifier({
|
|
algorithmId: aesKWAlgorithm.algorithmId,
|
|
/*
|
|
Initially RFC5753 says that AES algorithms have absent parameters.
|
|
But since early implementations all put NULL here. Thus, in order to be
|
|
"backward compatible", index also put NULL here.
|
|
*/
|
|
algorithmParams: new asn1js.Null()
|
|
}),
|
|
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, 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,
|
|
["unwrapKey"]),
|
|
error => Promise.reject(error)
|
|
);
|
|
//endregion
|
|
//region Finally unwrap 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.recipientEncryptedKeys.encryptedKeys[0].encryptedKey.valueBlock.valueHex,
|
|
result,
|
|
{ name: "AES-KW" },
|
|
contentEncryptionAlgorithm,
|
|
true,
|
|
["decrypt"]);
|
|
}, error =>
|
|
Promise.reject(error)
|
|
);
|
|
//endregion
|
|
|
|
return currentSequence;
|
|
}
|
|
|
|
function SubKeyTransRecipientInfo(index)
|
|
{
|
|
//region Initial variables
|
|
let currentSequence = Promise.resolve();
|
|
//endregion
|
|
|
|
//region Import recipient's private key
|
|
currentSequence = currentSequence.then(() =>
|
|
{
|
|
if(("recipientPrivateKey" in decryptionParameters) === false)
|
|
return Promise.reject("Parameter \"recipientPrivateKey\" is mandatory for \"KeyTransRecipientInfo\"");
|
|
|
|
//region Get current used SHA algorithm
|
|
const schema = _this.recipientInfos[index].value.keyEncryptionAlgorithm.algorithmParams;
|
|
const rsaOAEPParams = new RSAESOAEPParams({ schema });
|
|
|
|
const hashAlgorithm = getAlgorithmByOID(rsaOAEPParams.hashAlgorithm.algorithmId);
|
|
if(("name" in hashAlgorithm) === false)
|
|
return Promise.reject(`Incorrect OID for hash algorithm: ${rsaOAEPParams.hashAlgorithm.algorithmId}`);
|
|
//endregion
|
|
|
|
return crypto.importKey("pkcs8",
|
|
decryptionParameters.recipientPrivateKey,
|
|
{
|
|
name: "RSA-OAEP",
|
|
hash: {
|
|
name: hashAlgorithm.name
|
|
}
|
|
},
|
|
true,
|
|
["decrypt"]);
|
|
}, error =>
|
|
Promise.reject(error)
|
|
);
|
|
//endregion
|
|
//region Decrypt encrypted session key
|
|
currentSequence = currentSequence.then(result =>
|
|
crypto.decrypt(result.algorithm,
|
|
result,
|
|
_this.recipientInfos[index].value.encryptedKey.valueBlock.valueHex
|
|
), error =>
|
|
Promise.reject(error)
|
|
);
|
|
//endregion
|
|
//region Import decrypted 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.importKey("raw",
|
|
result,
|
|
contentEncryptionAlgorithm,
|
|
true,
|
|
["decrypt"]
|
|
);
|
|
}, error =>
|
|
Promise.reject(error)
|
|
);
|
|
//endregion
|
|
|
|
return currentSequence;
|
|
}
|
|
|
|
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;
|
|
}
|
|
//**********************************************************************************
|
|
}
|
|
//**************************************************************************************
|