summaryrefslogtreecommitdiffstats
path: root/third_party/js/PKI.js/src/PFX.ts
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/js/PKI.js/src/PFX.ts')
-rw-r--r--third_party/js/PKI.js/src/PFX.ts533
1 files changed, 533 insertions, 0 deletions
diff --git a/third_party/js/PKI.js/src/PFX.ts b/third_party/js/PKI.js/src/PFX.ts
new file mode 100644
index 0000000000..25885f4f83
--- /dev/null
+++ b/third_party/js/PKI.js/src/PFX.ts
@@ -0,0 +1,533 @@
+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<IPFX>;
+
+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<NonNullable<typeof parameters.names>>(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
+ }
+
+}