PFX.ts (17995B)
1 import * as asn1js from "asn1js"; 2 import * as pvutils from "pvutils"; 3 import * as common from "./common"; 4 import { ContentInfo, ContentInfoJson, ContentInfoSchema } from "./ContentInfo"; 5 import { MacData, MacDataJson, MacDataSchema } from "./MacData"; 6 import { DigestInfo } from "./DigestInfo"; 7 import { AlgorithmIdentifier } from "./AlgorithmIdentifier"; 8 import { SignedData } from "./SignedData"; 9 import { EncapsulatedContentInfo } from "./EncapsulatedContentInfo"; 10 import { Attribute } from "./Attribute"; 11 import { SignerInfo } from "./SignerInfo"; 12 import { IssuerAndSerialNumber } from "./IssuerAndSerialNumber"; 13 import { SignedAndUnsignedAttributes } from "./SignedAndUnsignedAttributes"; 14 import { AuthenticatedSafe } from "./AuthenticatedSafe"; 15 import * as Schema from "./Schema"; 16 import { Certificate } from "./Certificate"; 17 import { ArgumentError, AsnError, ParameterError } from "./errors"; 18 import { PkiObject, PkiObjectParameters } from "./PkiObject"; 19 import { BufferSourceConverter } from "pvtsutils"; 20 import { EMPTY_STRING } from "./constants"; 21 22 const VERSION = "version"; 23 const AUTH_SAFE = "authSafe"; 24 const MAC_DATA = "macData"; 25 const PARSED_VALUE = "parsedValue"; 26 const CLERA_PROPS = [ 27 VERSION, 28 AUTH_SAFE, 29 MAC_DATA 30 ]; 31 32 export interface IPFX { 33 version: number; 34 authSafe: ContentInfo; 35 macData?: MacData; 36 parsedValue?: PFXParsedValue; 37 } 38 39 export interface PFXJson { 40 version: number; 41 authSafe: ContentInfoJson; 42 macData?: MacDataJson; 43 } 44 45 export type PFXParameters = PkiObjectParameters & Partial<IPFX>; 46 47 export interface PFXParsedValue { 48 authenticatedSafe?: AuthenticatedSafe; 49 integrityMode?: number; 50 } 51 52 export type MakeInternalValuesParams = 53 object 54 | 55 { 56 iterations: number; 57 pbkdf2HashAlgorithm: Algorithm; 58 hmacHashAlgorithm: string; 59 password: ArrayBuffer; 60 } 61 | 62 { 63 signingCertificate: Certificate; 64 privateKey: CryptoKey; 65 hashAlgorithm: string; 66 }; 67 68 /** 69 * Represents the PFX structure described in [RFC7292](https://datatracker.ietf.org/doc/html/rfc7292) 70 */ 71 export class PFX extends PkiObject implements IPFX { 72 73 public static override CLASS_NAME = "PFX"; 74 75 public version!: number; 76 public authSafe!: ContentInfo; 77 public macData?: MacData; 78 public parsedValue?: PFXParsedValue; 79 80 /** 81 * Initializes a new instance of the {@link PFX} class 82 * @param parameters Initialization parameters 83 */ 84 constructor(parameters: PFXParameters = {}) { 85 super(); 86 87 this.version = pvutils.getParametersValue(parameters, VERSION, PFX.defaultValues(VERSION)); 88 this.authSafe = pvutils.getParametersValue(parameters, AUTH_SAFE, PFX.defaultValues(AUTH_SAFE)); 89 if (MAC_DATA in parameters) { 90 this.macData = pvutils.getParametersValue(parameters, MAC_DATA, PFX.defaultValues(MAC_DATA)); 91 } 92 if (PARSED_VALUE in parameters) { 93 this.parsedValue = pvutils.getParametersValue(parameters, PARSED_VALUE, PFX.defaultValues(PARSED_VALUE)); 94 } 95 96 if (parameters.schema) { 97 this.fromSchema(parameters.schema); 98 } 99 } 100 101 /** 102 * Returns default values for all class members 103 * @param memberName String name for a class member 104 * @returns Default value 105 */ 106 public static override defaultValues(memberName: typeof VERSION): number; 107 public static override defaultValues(memberName: typeof AUTH_SAFE): ContentInfo; 108 public static override defaultValues(memberName: typeof MAC_DATA): MacData; 109 public static override defaultValues(memberName: typeof PARSED_VALUE): PFXParsedValue; 110 public static override defaultValues(memberName: string): any { 111 switch (memberName) { 112 case VERSION: 113 return 3; 114 case AUTH_SAFE: 115 return (new ContentInfo()); 116 case MAC_DATA: 117 return (new MacData()); 118 case PARSED_VALUE: 119 return {}; 120 default: 121 return super.defaultValues(memberName); 122 } 123 } 124 125 /** 126 * Compare values with default values for all class members 127 * @param memberName String name for a class member 128 * @param memberValue Value to compare with default value 129 */ 130 public static compareWithDefault(memberName: string, memberValue: any): boolean { 131 switch (memberName) { 132 case VERSION: 133 return (memberValue === PFX.defaultValues(memberName)); 134 case AUTH_SAFE: 135 return ((ContentInfo.compareWithDefault("contentType", memberValue.contentType)) && 136 (ContentInfo.compareWithDefault("content", memberValue.content))); 137 case MAC_DATA: 138 return ((MacData.compareWithDefault("mac", memberValue.mac)) && 139 (MacData.compareWithDefault("macSalt", memberValue.macSalt)) && 140 (MacData.compareWithDefault("iterations", memberValue.iterations))); 141 case PARSED_VALUE: 142 return ((memberValue instanceof Object) && (Object.keys(memberValue).length === 0)); 143 default: 144 return super.defaultValues(memberName); 145 } 146 } 147 148 /** 149 * @inheritdoc 150 * @asn ASN.1 schema 151 * ```asn 152 * PFX ::= SEQUENCE { 153 * version INTEGER {v3(3)}(v3,...), 154 * authSafe ContentInfo, 155 * macData MacData OPTIONAL 156 * } 157 *``` 158 */ 159 public static override schema(parameters: Schema.SchemaParameters<{ 160 version?: string; 161 authSafe?: ContentInfoSchema; 162 macData?: MacDataSchema; 163 }> = {}): Schema.SchemaType { 164 const names = pvutils.getParametersValue<NonNullable<typeof parameters.names>>(parameters, "names", {}); 165 166 return (new asn1js.Sequence({ 167 name: (names.blockName || EMPTY_STRING), 168 value: [ 169 new asn1js.Integer({ name: (names.version || VERSION) }), 170 ContentInfo.schema(names.authSafe || { 171 names: { 172 blockName: AUTH_SAFE 173 } 174 }), 175 MacData.schema(names.macData || { 176 names: { 177 blockName: MAC_DATA, 178 optional: true 179 } 180 }) 181 ] 182 })); 183 } 184 185 public fromSchema(schema: Schema.SchemaType): void { 186 // Clear input data first 187 pvutils.clearProps(schema, CLERA_PROPS); 188 189 // Check the schema is valid 190 const asn1 = asn1js.compareSchema(schema, 191 schema, 192 PFX.schema({ 193 names: { 194 version: VERSION, 195 authSafe: { 196 names: { 197 blockName: AUTH_SAFE 198 } 199 }, 200 macData: { 201 names: { 202 blockName: MAC_DATA 203 } 204 } 205 } 206 }) 207 ); 208 AsnError.assertSchema(asn1, this.className); 209 210 // Get internal properties from parsed schema 211 this.version = asn1.result.version.valueBlock.valueDec; 212 this.authSafe = new ContentInfo({ schema: asn1.result.authSafe }); 213 if (MAC_DATA in asn1.result) 214 this.macData = new MacData({ schema: asn1.result.macData }); 215 } 216 217 public toSchema(): asn1js.Sequence { 218 //#region Construct and return new ASN.1 schema for this object 219 const outputArray = [ 220 new asn1js.Integer({ value: this.version }), 221 this.authSafe.toSchema() 222 ]; 223 224 if (this.macData) { 225 outputArray.push(this.macData.toSchema()); 226 } 227 228 return (new asn1js.Sequence({ 229 value: outputArray 230 })); 231 //#endregion 232 } 233 234 public toJSON(): PFXJson { 235 const output: PFXJson = { 236 version: this.version, 237 authSafe: this.authSafe.toJSON() 238 }; 239 240 if (this.macData) { 241 output.macData = this.macData.toJSON(); 242 } 243 244 return output; 245 } 246 247 /** 248 * Making ContentInfo from PARSED_VALUE object 249 * @param parameters Parameters, specific to each "integrity mode" 250 * @param crypto Crypto engine 251 */ 252 public async makeInternalValues(parameters: MakeInternalValuesParams = {}, crypto = common.getCrypto(true)) { 253 //#region Check mandatory parameter 254 ArgumentError.assert(parameters, "parameters", "object"); 255 if (!this.parsedValue) { 256 throw new Error("Please call \"parseValues\" function first in order to make \"parsedValue\" data"); 257 } 258 ParameterError.assertEmpty(this.parsedValue.integrityMode, "integrityMode", "parsedValue"); 259 ParameterError.assertEmpty(this.parsedValue.authenticatedSafe, "authenticatedSafe", "parsedValue"); 260 //#endregion 261 262 //#region Makes values for each particular integrity mode 263 switch (this.parsedValue.integrityMode) { 264 //#region HMAC-based integrity 265 case 0: 266 { 267 //#region Check additional mandatory parameters 268 if (!("iterations" in parameters)) 269 throw new ParameterError("iterations"); 270 ParameterError.assertEmpty(parameters.pbkdf2HashAlgorithm, "pbkdf2HashAlgorithm"); 271 ParameterError.assertEmpty(parameters.hmacHashAlgorithm, "hmacHashAlgorithm"); 272 ParameterError.assertEmpty(parameters.password, "password"); 273 //#endregion 274 275 //#region Initial variables 276 const saltBuffer = new ArrayBuffer(64); 277 const saltView = new Uint8Array(saltBuffer); 278 279 crypto.getRandomValues(saltView); 280 281 const data = this.parsedValue.authenticatedSafe.toSchema().toBER(false); 282 283 this.authSafe = new ContentInfo({ 284 contentType: ContentInfo.DATA, 285 content: new asn1js.OctetString({ valueHex: data }) 286 }); 287 //#endregion 288 289 //#region Call current crypto engine for making HMAC-based data stamp 290 const result = await crypto.stampDataWithPassword({ 291 password: parameters.password, 292 hashAlgorithm: parameters.hmacHashAlgorithm, 293 salt: saltBuffer, 294 iterationCount: parameters.iterations, 295 contentToStamp: data 296 }); 297 //#endregion 298 299 //#region Make MAC_DATA values 300 this.macData = new MacData({ 301 mac: new DigestInfo({ 302 digestAlgorithm: new AlgorithmIdentifier({ 303 algorithmId: crypto.getOIDByAlgorithm({ name: parameters.hmacHashAlgorithm }, true, "hmacHashAlgorithm"), 304 }), 305 digest: new asn1js.OctetString({ valueHex: result }) 306 }), 307 macSalt: new asn1js.OctetString({ valueHex: saltBuffer }), 308 iterations: parameters.iterations 309 }); 310 //#endregion 311 //#endregion 312 } 313 break; 314 //#endregion 315 //#region publicKey-based integrity 316 case 1: 317 { 318 //#region Check additional mandatory parameters 319 if (!("signingCertificate" in parameters)) { 320 throw new ParameterError("signingCertificate"); 321 } 322 ParameterError.assertEmpty(parameters.privateKey, "privateKey"); 323 ParameterError.assertEmpty(parameters.hashAlgorithm, "hashAlgorithm"); 324 //#endregion 325 326 //#region Making data to be signed 327 // NOTE: all internal data for "authenticatedSafe" must be already prepared. 328 // Thus user must call "makeValues" for all internal "SafeContent" value with appropriate parameters. 329 // Or user can choose to use values from initial parsing of existing PKCS#12 data. 330 331 const toBeSigned = this.parsedValue.authenticatedSafe.toSchema().toBER(false); 332 //#endregion 333 334 //#region Initial variables 335 const cmsSigned = new SignedData({ 336 version: 1, 337 encapContentInfo: new EncapsulatedContentInfo({ 338 eContentType: "1.2.840.113549.1.7.1", // "data" content type 339 eContent: new asn1js.OctetString({ valueHex: toBeSigned }) 340 }), 341 certificates: [parameters.signingCertificate] 342 }); 343 //#endregion 344 345 //#region Making additional attributes for CMS Signed Data 346 //#region Create a message digest 347 const result = await crypto.digest({ name: parameters.hashAlgorithm }, new Uint8Array(toBeSigned)); 348 //#endregion 349 350 //#region Combine all signed extensions 351 //#region Initial variables 352 const signedAttr: Attribute[] = []; 353 //#endregion 354 355 //#region contentType 356 signedAttr.push(new Attribute({ 357 type: "1.2.840.113549.1.9.3", 358 values: [ 359 new asn1js.ObjectIdentifier({ value: "1.2.840.113549.1.7.1" }) 360 ] 361 })); 362 //#endregion 363 //#region signingTime 364 signedAttr.push(new Attribute({ 365 type: "1.2.840.113549.1.9.5", 366 values: [ 367 new asn1js.UTCTime({ valueDate: new Date() }) 368 ] 369 })); 370 //#endregion 371 //#region messageDigest 372 signedAttr.push(new Attribute({ 373 type: "1.2.840.113549.1.9.4", 374 values: [ 375 new asn1js.OctetString({ valueHex: result }) 376 ] 377 })); 378 //#endregion 379 380 //#region Making final value for "SignerInfo" type 381 cmsSigned.signerInfos.push(new SignerInfo({ 382 version: 1, 383 sid: new IssuerAndSerialNumber({ 384 issuer: parameters.signingCertificate.issuer, 385 serialNumber: parameters.signingCertificate.serialNumber 386 }), 387 signedAttrs: new SignedAndUnsignedAttributes({ 388 type: 0, 389 attributes: signedAttr 390 }) 391 })); 392 //#endregion 393 //#endregion 394 //#endregion 395 396 //#region Signing CMS Signed Data 397 await cmsSigned.sign(parameters.privateKey, 0, parameters.hashAlgorithm, undefined, crypto); 398 //#endregion 399 400 //#region Making final CMS_CONTENT_INFO type 401 this.authSafe = new ContentInfo({ 402 contentType: "1.2.840.113549.1.7.2", 403 content: cmsSigned.toSchema(true) 404 }); 405 //#endregion 406 } 407 break; 408 //#endregion 409 //#region default 410 default: 411 throw new Error(`Parameter "integrityMode" has unknown value: ${this.parsedValue.integrityMode}`); 412 //#endregion 413 } 414 //#endregion 415 } 416 417 public async parseInternalValues(parameters: { 418 checkIntegrity?: boolean; 419 password?: ArrayBuffer; 420 }, crypto = common.getCrypto(true)) { 421 //#region Check input data from "parameters" 422 ArgumentError.assert(parameters, "parameters", "object"); 423 424 if (parameters.checkIntegrity === undefined) { 425 parameters.checkIntegrity = true; 426 } 427 //#endregion 428 429 //#region Create value for "this.parsedValue.authenticatedSafe" and check integrity 430 this.parsedValue = {}; 431 432 switch (this.authSafe.contentType) { 433 //#region data 434 case ContentInfo.DATA: 435 { 436 //#region Check additional mandatory parameters 437 ParameterError.assertEmpty(parameters.password, "password"); 438 //#endregion 439 440 //#region Integrity based on HMAC 441 this.parsedValue.integrityMode = 0; 442 //#endregion 443 444 //#region Check that we do have OCTETSTRING as "content" 445 ArgumentError.assert(this.authSafe.content, "authSafe.content", asn1js.OctetString); 446 //#endregion 447 448 //#region Check we have "constructive encoding" for AuthSafe content 449 const authSafeContent = this.authSafe.content.getValue(); 450 //#endregion 451 452 //#region Set "authenticatedSafe" value 453 this.parsedValue.authenticatedSafe = AuthenticatedSafe.fromBER(authSafeContent); 454 //#endregion 455 456 //#region Check integrity 457 if (parameters.checkIntegrity) { 458 //#region Check that MAC_DATA exists 459 if (!this.macData) { 460 throw new Error("Absent \"macData\" value, can not check PKCS#12 data integrity"); 461 } 462 //#endregion 463 464 //#region Initial variables 465 const hashAlgorithm = crypto.getAlgorithmByOID(this.macData.mac.digestAlgorithm.algorithmId, true, "digestAlgorithm"); 466 //#endregion 467 468 //#region Call current crypto engine for verifying HMAC-based data stamp 469 const result = await crypto.verifyDataStampedWithPassword({ 470 password: parameters.password, 471 hashAlgorithm: hashAlgorithm.name, 472 salt: BufferSourceConverter.toArrayBuffer(this.macData.macSalt.valueBlock.valueHexView), 473 iterationCount: this.macData.iterations || 1, 474 contentToVerify: authSafeContent, 475 signatureToVerify: BufferSourceConverter.toArrayBuffer(this.macData.mac.digest.valueBlock.valueHexView), 476 }); 477 //#endregion 478 479 //#region Verify HMAC signature 480 if (!result) { 481 throw new Error("Integrity for the PKCS#12 data is broken!"); 482 } 483 //#endregion 484 } 485 //#endregion 486 } 487 break; 488 //#endregion 489 //#region signedData 490 case ContentInfo.SIGNED_DATA: 491 { 492 //#region Integrity based on signature using public key 493 this.parsedValue.integrityMode = 1; 494 //#endregion 495 496 //#region Parse CMS Signed Data 497 const cmsSigned = new SignedData({ schema: this.authSafe.content }); 498 //#endregion 499 500 //#region Check that we do have OCTET STRING as "content" 501 const eContent = cmsSigned.encapContentInfo.eContent; 502 ParameterError.assert(eContent, "eContent", "cmsSigned.encapContentInfo"); 503 ArgumentError.assert(eContent, "eContent", asn1js.OctetString); 504 //#endregion 505 506 //#region Create correct data block for verification 507 const data = eContent.getValue(); 508 //#endregion 509 510 //#region Set "authenticatedSafe" value 511 this.parsedValue.authenticatedSafe = AuthenticatedSafe.fromBER(data); 512 //#endregion 513 514 //#region Check integrity 515 const ok = await cmsSigned.verify({ signer: 0, checkChain: false }, crypto); 516 if (!ok) { 517 throw new Error("Integrity for the PKCS#12 data is broken!"); 518 } 519 //#endregion 520 } 521 break; 522 //#endregion 523 //#region default 524 default: 525 throw new Error(`Incorrect value for "this.authSafe.contentType": ${this.authSafe.contentType}`); 526 //#endregion 527 } 528 //#endregion 529 } 530 531 }