/* eslint-disable @typescript-eslint/ban-ts-comment */ import * as pvtsutils from "pvtsutils"; import { IS_CONSTRUCTED, EMPTY_STRING, NAME, ID_BLOCK, FROM_BER, TO_BER, TAG_CLASS, TAG_NUMBER, IS_HEX_ONLY, LOCAL, VALUE_HEX_VIEW } from "./internals/constants"; import { Any } from "./Any"; import { Choice } from "./Choice"; import { Repeated } from "./Repeated"; import { localFromBER } from "./parser"; import { AsnType, typeStore } from "./TypeStore"; export type AsnSchemaType = AsnType | Any | Choice | Repeated; export interface CompareSchemaSuccess { verified: true; result: AsnType & { [key: string]: any; }; } export interface CompareSchemaFail { verified: false; name?: string; result: AsnType | { error: string; }; } export type CompareSchemaResult = CompareSchemaSuccess | CompareSchemaFail; /** * Compare of two ASN.1 object trees * @param root Root of input ASN.1 object tree * @param inputData Input ASN.1 object tree * @param inputSchema Input ASN.1 schema to compare with * @return Returns result of comparison */ export function compareSchema(root: AsnType, inputData: AsnType, inputSchema: AsnSchemaType): CompareSchemaResult { //#region Special case for Choice schema element type if (inputSchema instanceof Choice) { const choiceResult = false; for (let j = 0; j < inputSchema.value.length; j++) { const result = compareSchema(root, inputData, inputSchema.value[j]); if (result.verified) { return { verified: true, result: root }; } } if (choiceResult === false) { const _result: CompareSchemaResult = { verified: false, result: { error: "Wrong values for Choice type" }, }; if (inputSchema.hasOwnProperty(NAME)) _result.name = inputSchema.name; return _result; } } //#endregion //#region Special case for Any schema element type if (inputSchema instanceof Any) { //#region Add named component of ASN.1 schema if (inputSchema.hasOwnProperty(NAME)) (root as any)[inputSchema.name] = inputData; // TODO Such call may replace original field of the object (eg idBlock) //#endregion return { verified: true, result: root }; } //#endregion //#region Initial check if ((root instanceof Object) === false) { return { verified: false, result: { error: "Wrong root object" } }; } if ((inputData instanceof Object) === false) { return { verified: false, result: { error: "Wrong ASN.1 data" } }; } if ((inputSchema instanceof Object) === false) { return { verified: false, result: { error: "Wrong ASN.1 schema" } }; } if ((ID_BLOCK in inputSchema) === false) { return { verified: false, result: { error: "Wrong ASN.1 schema" } }; } //#endregion //#region Comparing idBlock properties in ASN.1 data and ASN.1 schema //#region Encode and decode ASN.1 schema idBlock /// This encoding/decoding is necessary because could be an errors in schema definition if ((FROM_BER in inputSchema.idBlock) === false) { return { verified: false, result: { error: "Wrong ASN.1 schema" } }; } if ((TO_BER in inputSchema.idBlock) === false) { return { verified: false, result: { error: "Wrong ASN.1 schema" } }; } const encodedId = inputSchema.idBlock.toBER(false); if (encodedId.byteLength === 0) { return { verified: false, result: { error: "Error encoding idBlock for ASN.1 schema" } }; } const decodedOffset = inputSchema.idBlock.fromBER(encodedId, 0, encodedId.byteLength); if (decodedOffset === -1) { return { verified: false, result: { error: "Error decoding idBlock for ASN.1 schema" } }; } //#endregion //#region tagClass if (inputSchema.idBlock.hasOwnProperty(TAG_CLASS) === false) { return { verified: false, result: { error: "Wrong ASN.1 schema" } }; } if (inputSchema.idBlock.tagClass !== inputData.idBlock.tagClass) { return { verified: false, result: root }; } //#endregion //#region tagNumber if (inputSchema.idBlock.hasOwnProperty(TAG_NUMBER) === false) { return { verified: false, result: { error: "Wrong ASN.1 schema" } }; } if (inputSchema.idBlock.tagNumber !== inputData.idBlock.tagNumber) { return { verified: false, result: root }; } //#endregion //#region isConstructed if (inputSchema.idBlock.hasOwnProperty(IS_CONSTRUCTED) === false) { return { verified: false, result: { error: "Wrong ASN.1 schema" } }; } if (inputSchema.idBlock.isConstructed !== inputData.idBlock.isConstructed) { return { verified: false, result: root }; } //#endregion //#region isHexOnly if (!(IS_HEX_ONLY in inputSchema.idBlock)) // Since 'isHexOnly' is an inherited property { return { verified: false, result: { error: "Wrong ASN.1 schema" } }; } if (inputSchema.idBlock.isHexOnly !== inputData.idBlock.isHexOnly) { return { verified: false, result: root }; } //#endregion //#region valueHex if (inputSchema.idBlock.isHexOnly) { if ((VALUE_HEX_VIEW in inputSchema.idBlock) === false) // Since 'valueHex' is an inherited property { return { verified: false, result: { error: "Wrong ASN.1 schema" } }; } const schemaView = inputSchema.idBlock.valueHexView; const asn1View = inputData.idBlock.valueHexView; if (schemaView.length !== asn1View.length) { return { verified: false, result: root }; } for (let i = 0; i < schemaView.length; i++) { if (schemaView[i] !== asn1View[1]) { return { verified: false, result: root }; } } } //#endregion //#endregion //#region Add named component of ASN.1 schema if (inputSchema.name) { inputSchema.name = inputSchema.name.replace(/^\s+|\s+$/g, EMPTY_STRING); if (inputSchema.name) (root as any)[inputSchema.name] = inputData; // TODO check field existence. If exists throw an error } //#endregion //#region Getting next ASN.1 block for comparison if (inputSchema instanceof typeStore.Constructed) { // TODO not clear how it works for OctetString and BitString //* if (inputSchema.idBlock.isConstructed) { let admission = 0; let result: CompareSchemaResult = { verified: false, result: { error: "Unknown error", } }; let maxLength = inputSchema.valueBlock.value.length; if (maxLength > 0) { if (inputSchema.valueBlock.value[0] instanceof Repeated) { // @ts-ignore maxLength = inputData.valueBlock.value.length; // TODO debug it } } //#region Special case when constructive value has no elements if (maxLength === 0) { return { verified: true, result: root }; } //#endregion //#region Special case when "inputData" has no values and "inputSchema" has all optional values // @ts-ignore // TODO debug it if ((inputData.valueBlock.value.length === 0) && (inputSchema.valueBlock.value.length !== 0)) { let _optional = true; for (let i = 0; i < inputSchema.valueBlock.value.length; i++) _optional = _optional && (inputSchema.valueBlock.value[i].optional || false); if (_optional) { return { verified: true, result: root }; } //#region Delete early added name of block if (inputSchema.name) { inputSchema.name = inputSchema.name.replace(/^\s+|\s+$/g, EMPTY_STRING); if (inputSchema.name) delete (root as any)[inputSchema.name]; } //#endregion root.error = "Inconsistent object length"; return { verified: false, result: root }; } //#endregion for (let i = 0; i < maxLength; i++) { //#region Special case when there is an OPTIONAL element of ASN.1 schema at the end // @ts-ignore if ((i - admission) >= inputData.valueBlock.value.length) { if (inputSchema.valueBlock.value[i].optional === false) { const _result: CompareSchemaResult = { verified: false, result: root }; root.error = "Inconsistent length between ASN.1 data and schema"; //#region Delete early added name of block if (inputSchema.name) { inputSchema.name = inputSchema.name.replace(/^\s+|\s+$/g, EMPTY_STRING); if (inputSchema.name) { delete (root as any)[inputSchema.name]; _result.name = inputSchema.name; } } //#endregion return _result; } } //#endregion else { //#region Special case for Repeated type of ASN.1 schema element if (inputSchema.valueBlock.value[0] instanceof Repeated) { // @ts-ignore result = compareSchema(root, inputData.valueBlock.value[i], inputSchema.valueBlock.value[0].value); if (result.verified === false) { if (inputSchema.valueBlock.value[0].optional) admission++; else { //#region Delete early added name of block if (inputSchema.name) { inputSchema.name = inputSchema.name.replace(/^\s+|\s+$/g, EMPTY_STRING); if (inputSchema.name) delete (root as any)[inputSchema.name]; } //#endregion return result; } } if ((NAME in inputSchema.valueBlock.value[0]) && (inputSchema.valueBlock.value[0].name.length > 0)) { let arrayRoot: Record = {}; if ((LOCAL in inputSchema.valueBlock.value[0]) && (inputSchema.valueBlock.value[0].local)) arrayRoot = inputData; else arrayRoot = root; if (typeof arrayRoot[inputSchema.valueBlock.value[0].name] === "undefined") arrayRoot[inputSchema.valueBlock.value[0].name] = []; // @ts-ignore arrayRoot[inputSchema.valueBlock.value[0].name].push(inputData.valueBlock.value[i]); } } //#endregion else { // @ts-ignore result = compareSchema(root, inputData.valueBlock.value[i - admission], inputSchema.valueBlock.value[i]); if (result.verified === false) { if (inputSchema.valueBlock.value[i].optional) admission++; else { //#region Delete early added name of block if (inputSchema.name) { inputSchema.name = inputSchema.name.replace(/^\s+|\s+$/g, EMPTY_STRING); if (inputSchema.name) delete (root as any)[inputSchema.name]; } //#endregion return result; } } } } } if (result.verified === false) // The situation may take place if last element is OPTIONAL and verification failed { const _result: CompareSchemaResult = { verified: false, result: root }; //#region Delete early added name of block if (inputSchema.name) { inputSchema.name = inputSchema.name.replace(/^\s+|\s+$/g, EMPTY_STRING); if (inputSchema.name) { delete (root as any)[inputSchema.name]; _result.name = inputSchema.name; } } //#endregion return _result; } return { verified: true, result: root }; } //#endregion //#region Ability to parse internal value for primitive-encoded value (value of OctetString, for example) if (inputSchema.primitiveSchema && (VALUE_HEX_VIEW in inputData.valueBlock)) { //#region Decoding of raw ASN.1 data const asn1 = localFromBER(inputData.valueBlock.valueHexView); if (asn1.offset === -1) { const _result: CompareSchemaResult = { verified: false, result: asn1.result }; //#region Delete early added name of block if (inputSchema.name) { inputSchema.name = inputSchema.name.replace(/^\s+|\s+$/g, EMPTY_STRING); if (inputSchema.name) { delete (root as any)[inputSchema.name]; _result.name = inputSchema.name; } } //#endregion return _result; } //#endregion return compareSchema(root, asn1.result, inputSchema.primitiveSchema); } return { verified: true, result: root }; //#endregion } /** * ASN.1 schema verification for ArrayBuffer data * @param inputBuffer Input BER-encoded ASN.1 data * @param inputSchema Input ASN.1 schema to verify against to * @return */ export function verifySchema(inputBuffer: pvtsutils.BufferSource, inputSchema: AsnSchemaType): CompareSchemaResult { //#region Initial check if ((inputSchema instanceof Object) === false) { return { verified: false, result: { error: "Wrong ASN.1 schema type" } }; } //#endregion //#region Decoding of raw ASN.1 data const asn1 = localFromBER(pvtsutils.BufferSourceConverter.toUint8Array(inputBuffer)); if (asn1.offset === -1) { return { verified: false, result: asn1.result }; } //#endregion //#region Compare ASN.1 struct with input schema return compareSchema(asn1.result, asn1.result, inputSchema); //#endregion }