import * as asn1js from "asn1js"; import * as pvutils from "pvutils"; import * as common from "./common"; import { ContentInfo, ContentInfoJson, ContentInfoSchema } from "./ContentInfo"; import { MacData, MacDataJson, MacDataSchema } from "./MacData"; import { DigestInfo } from "./DigestInfo"; import { AlgorithmIdentifier } from "./AlgorithmIdentifier"; import { SignedData } from "./SignedData"; import { EncapsulatedContentInfo } from "./EncapsulatedContentInfo"; import { Attribute } from "./Attribute"; import { SignerInfo } from "./SignerInfo"; import { IssuerAndSerialNumber } from "./IssuerAndSerialNumber"; import { SignedAndUnsignedAttributes } from "./SignedAndUnsignedAttributes"; import { AuthenticatedSafe } from "./AuthenticatedSafe"; import * as Schema from "./Schema"; import { Certificate } from "./Certificate"; import { ArgumentError, AsnError, ParameterError } from "./errors"; import { PkiObject, PkiObjectParameters } from "./PkiObject"; import { BufferSourceConverter } from "pvtsutils"; import { EMPTY_STRING } from "./constants"; const VERSION = "version"; const AUTH_SAFE = "authSafe"; const MAC_DATA = "macData"; const PARSED_VALUE = "parsedValue"; const CLERA_PROPS = [ VERSION, AUTH_SAFE, MAC_DATA ]; export interface IPFX { version: number; authSafe: ContentInfo; macData?: MacData; parsedValue?: PFXParsedValue; } export interface PFXJson { version: number; authSafe: ContentInfoJson; macData?: MacDataJson; } export type PFXParameters = PkiObjectParameters & Partial; export interface PFXParsedValue { authenticatedSafe?: AuthenticatedSafe; integrityMode?: number; } export type MakeInternalValuesParams = { // empty } | { iterations: number; pbkdf2HashAlgorithm: Algorithm; hmacHashAlgorithm: string; password: ArrayBuffer; } | { signingCertificate: Certificate; privateKey: CryptoKey; hashAlgorithm: string; }; /** * Represents the PFX structure described in [RFC7292](https://datatracker.ietf.org/doc/html/rfc7292) */ export class PFX extends PkiObject implements IPFX { public static override CLASS_NAME = "PFX"; public version!: number; public authSafe!: ContentInfo; public macData?: MacData; public parsedValue?: PFXParsedValue; /** * Initializes a new instance of the {@link PFX} class * @param parameters Initialization parameters */ constructor(parameters: PFXParameters = {}) { super(); this.version = pvutils.getParametersValue(parameters, VERSION, PFX.defaultValues(VERSION)); this.authSafe = pvutils.getParametersValue(parameters, AUTH_SAFE, PFX.defaultValues(AUTH_SAFE)); if (MAC_DATA in parameters) { this.macData = pvutils.getParametersValue(parameters, MAC_DATA, PFX.defaultValues(MAC_DATA)); } if (PARSED_VALUE in parameters) { this.parsedValue = pvutils.getParametersValue(parameters, PARSED_VALUE, PFX.defaultValues(PARSED_VALUE)); } 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 AUTH_SAFE): ContentInfo; public static override defaultValues(memberName: typeof MAC_DATA): MacData; public static override defaultValues(memberName: typeof PARSED_VALUE): PFXParsedValue; public static override defaultValues(memberName: string): any { switch (memberName) { case VERSION: return 3; case AUTH_SAFE: return (new ContentInfo()); case MAC_DATA: return (new MacData()); case PARSED_VALUE: 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 === PFX.defaultValues(memberName)); case AUTH_SAFE: return ((ContentInfo.compareWithDefault("contentType", memberValue.contentType)) && (ContentInfo.compareWithDefault("content", memberValue.content))); case MAC_DATA: return ((MacData.compareWithDefault("mac", memberValue.mac)) && (MacData.compareWithDefault("macSalt", memberValue.macSalt)) && (MacData.compareWithDefault("iterations", memberValue.iterations))); case PARSED_VALUE: return ((memberValue instanceof Object) && (Object.keys(memberValue).length === 0)); default: return super.defaultValues(memberName); } } /** * @inheritdoc * @asn ASN.1 schema * ```asn * PFX ::= SEQUENCE { * version INTEGER {v3(3)}(v3,...), * authSafe ContentInfo, * macData MacData OPTIONAL * } *``` */ public static override schema(parameters: Schema.SchemaParameters<{ version?: string; authSafe?: ContentInfoSchema; macData?: MacDataSchema; }> = {}): Schema.SchemaType { const names = pvutils.getParametersValue>(parameters, "names", {}); return (new asn1js.Sequence({ name: (names.blockName || EMPTY_STRING), value: [ new asn1js.Integer({ name: (names.version || VERSION) }), ContentInfo.schema(names.authSafe || { names: { blockName: AUTH_SAFE } }), MacData.schema(names.macData || { names: { blockName: MAC_DATA, optional: true } }) ] })); } public fromSchema(schema: Schema.SchemaType): void { // Clear input data first pvutils.clearProps(schema, CLERA_PROPS); // Check the schema is valid const asn1 = asn1js.compareSchema(schema, schema, PFX.schema({ names: { version: VERSION, authSafe: { names: { blockName: AUTH_SAFE } }, macData: { names: { blockName: MAC_DATA } } } }) ); AsnError.assertSchema(asn1, this.className); // Get internal properties from parsed schema this.version = asn1.result.version.valueBlock.valueDec; this.authSafe = new ContentInfo({ schema: asn1.result.authSafe }); if (MAC_DATA in asn1.result) this.macData = new MacData({ schema: asn1.result.macData }); } public toSchema(): asn1js.Sequence { //#region Construct and return new ASN.1 schema for this object const outputArray = [ new asn1js.Integer({ value: this.version }), this.authSafe.toSchema() ]; if (this.macData) { outputArray.push(this.macData.toSchema()); } return (new asn1js.Sequence({ value: outputArray })); //#endregion } public toJSON(): PFXJson { const output: PFXJson = { version: this.version, authSafe: this.authSafe.toJSON() }; if (this.macData) { output.macData = this.macData.toJSON(); } return output; } /** * Making ContentInfo from PARSED_VALUE object * @param parameters Parameters, specific to each "integrity mode" * @param crypto Crypto engine */ public async makeInternalValues(parameters: MakeInternalValuesParams = {}, crypto = common.getCrypto(true)) { //#region Check mandatory parameter ArgumentError.assert(parameters, "parameters", "object"); if (!this.parsedValue) { throw new Error("Please call \"parseValues\" function first in order to make \"parsedValue\" data"); } ParameterError.assertEmpty(this.parsedValue.integrityMode, "integrityMode", "parsedValue"); ParameterError.assertEmpty(this.parsedValue.authenticatedSafe, "authenticatedSafe", "parsedValue"); //#endregion //#region Makes values for each particular integrity mode switch (this.parsedValue.integrityMode) { //#region HMAC-based integrity case 0: { //#region Check additional mandatory parameters if (!("iterations" in parameters)) throw new ParameterError("iterations"); ParameterError.assertEmpty(parameters.pbkdf2HashAlgorithm, "pbkdf2HashAlgorithm"); ParameterError.assertEmpty(parameters.hmacHashAlgorithm, "hmacHashAlgorithm"); ParameterError.assertEmpty(parameters.password, "password"); //#endregion //#region Initial variables const saltBuffer = new ArrayBuffer(64); const saltView = new Uint8Array(saltBuffer); crypto.getRandomValues(saltView); const data = this.parsedValue.authenticatedSafe.toSchema().toBER(false); this.authSafe = new ContentInfo({ contentType: ContentInfo.DATA, content: new asn1js.OctetString({ valueHex: data }) }); //#endregion //#region Call current crypto engine for making HMAC-based data stamp const result = await crypto.stampDataWithPassword({ password: parameters.password, hashAlgorithm: parameters.hmacHashAlgorithm, salt: saltBuffer, iterationCount: parameters.iterations, contentToStamp: data }); //#endregion //#region Make MAC_DATA values this.macData = new MacData({ mac: new DigestInfo({ digestAlgorithm: new AlgorithmIdentifier({ algorithmId: crypto.getOIDByAlgorithm({ name: parameters.hmacHashAlgorithm }, true, "hmacHashAlgorithm"), }), digest: new asn1js.OctetString({ valueHex: result }) }), macSalt: new asn1js.OctetString({ valueHex: saltBuffer }), iterations: parameters.iterations }); //#endregion //#endregion } break; //#endregion //#region publicKey-based integrity case 1: { //#region Check additional mandatory parameters if (!("signingCertificate" in parameters)) { throw new ParameterError("signingCertificate"); } ParameterError.assertEmpty(parameters.privateKey, "privateKey"); ParameterError.assertEmpty(parameters.hashAlgorithm, "hashAlgorithm"); //#endregion //#region Making data to be signed // NOTE: all internal data for "authenticatedSafe" must be already prepared. // Thus user must call "makeValues" for all internal "SafeContent" value with appropriate parameters. // Or user can choose to use values from initial parsing of existing PKCS#12 data. const toBeSigned = this.parsedValue.authenticatedSafe.toSchema().toBER(false); //#endregion //#region Initial variables const cmsSigned = new SignedData({ version: 1, encapContentInfo: new EncapsulatedContentInfo({ eContentType: "1.2.840.113549.1.7.1", // "data" content type eContent: new asn1js.OctetString({ valueHex: toBeSigned }) }), certificates: [parameters.signingCertificate] }); //#endregion //#region Making additional attributes for CMS Signed Data //#region Create a message digest const result = await crypto.digest({ name: parameters.hashAlgorithm }, new Uint8Array(toBeSigned)); //#endregion //#region Combine all signed extensions //#region Initial variables const signedAttr: Attribute[] = []; //#endregion //#region contentType signedAttr.push(new Attribute({ type: "1.2.840.113549.1.9.3", values: [ new asn1js.ObjectIdentifier({ value: "1.2.840.113549.1.7.1" }) ] })); //#endregion //#region signingTime signedAttr.push(new Attribute({ type: "1.2.840.113549.1.9.5", values: [ new asn1js.UTCTime({ valueDate: new Date() }) ] })); //#endregion //#region messageDigest signedAttr.push(new Attribute({ type: "1.2.840.113549.1.9.4", values: [ new asn1js.OctetString({ valueHex: result }) ] })); //#endregion //#region Making final value for "SignerInfo" type cmsSigned.signerInfos.push(new SignerInfo({ version: 1, sid: new IssuerAndSerialNumber({ issuer: parameters.signingCertificate.issuer, serialNumber: parameters.signingCertificate.serialNumber }), signedAttrs: new SignedAndUnsignedAttributes({ type: 0, attributes: signedAttr }) })); //#endregion //#endregion //#endregion //#region Signing CMS Signed Data await cmsSigned.sign(parameters.privateKey, 0, parameters.hashAlgorithm, undefined, crypto); //#endregion //#region Making final CMS_CONTENT_INFO type this.authSafe = new ContentInfo({ contentType: "1.2.840.113549.1.7.2", content: cmsSigned.toSchema(true) }); //#endregion } break; //#endregion //#region default default: throw new Error(`Parameter "integrityMode" has unknown value: ${this.parsedValue.integrityMode}`); //#endregion } //#endregion } public async parseInternalValues(parameters: { checkIntegrity?: boolean; password?: ArrayBuffer; }, crypto = common.getCrypto(true)) { //#region Check input data from "parameters" ArgumentError.assert(parameters, "parameters", "object"); if (parameters.checkIntegrity === undefined) { parameters.checkIntegrity = true; } //#endregion //#region Create value for "this.parsedValue.authenticatedSafe" and check integrity this.parsedValue = {}; switch (this.authSafe.contentType) { //#region data case ContentInfo.DATA: { //#region Check additional mandatory parameters ParameterError.assertEmpty(parameters.password, "password"); //#endregion //#region Integrity based on HMAC this.parsedValue.integrityMode = 0; //#endregion //#region Check that we do have OCTETSTRING as "content" ArgumentError.assert(this.authSafe.content, "authSafe.content", asn1js.OctetString); //#endregion //#region Check we have "constructive encoding" for AuthSafe content const authSafeContent = this.authSafe.content.getValue(); //#endregion //#region Set "authenticatedSafe" value this.parsedValue.authenticatedSafe = AuthenticatedSafe.fromBER(authSafeContent); //#endregion //#region Check integrity if (parameters.checkIntegrity) { //#region Check that MAC_DATA exists if (!this.macData) { throw new Error("Absent \"macData\" value, can not check PKCS#12 data integrity"); } //#endregion //#region Initial variables const hashAlgorithm = crypto.getAlgorithmByOID(this.macData.mac.digestAlgorithm.algorithmId, true, "digestAlgorithm"); //#endregion //#region Call current crypto engine for verifying HMAC-based data stamp const result = await crypto.verifyDataStampedWithPassword({ password: parameters.password, hashAlgorithm: hashAlgorithm.name, salt: BufferSourceConverter.toArrayBuffer(this.macData.macSalt.valueBlock.valueHexView), iterationCount: this.macData.iterations || 1, contentToVerify: authSafeContent, signatureToVerify: BufferSourceConverter.toArrayBuffer(this.macData.mac.digest.valueBlock.valueHexView), }); //#endregion //#region Verify HMAC signature if (!result) { throw new Error("Integrity for the PKCS#12 data is broken!"); } //#endregion } //#endregion } break; //#endregion //#region signedData case ContentInfo.SIGNED_DATA: { //#region Integrity based on signature using public key this.parsedValue.integrityMode = 1; //#endregion //#region Parse CMS Signed Data const cmsSigned = new SignedData({ schema: this.authSafe.content }); //#endregion //#region Check that we do have OCTET STRING as "content" const eContent = cmsSigned.encapContentInfo.eContent; ParameterError.assert(eContent, "eContent", "cmsSigned.encapContentInfo"); ArgumentError.assert(eContent, "eContent", asn1js.OctetString); //#endregion //#region Create correct data block for verification const data = eContent.getValue(); //#endregion //#region Set "authenticatedSafe" value this.parsedValue.authenticatedSafe = AuthenticatedSafe.fromBER(data); //#endregion //#region Check integrity const ok = await cmsSigned.verify({ signer: 0, checkChain: false }, crypto); if (!ok) { throw new Error("Integrity for the PKCS#12 data is broken!"); } //#endregion } break; //#endregion //#region default default: throw new Error(`Incorrect value for "this.authSafe.contentType": ${this.authSafe.contentType}`); //#endregion } //#endregion } }