ncalayer-client.js

// @ts-check

((exports, WebSocket, window) => { // eslint-disable-line max-classes-per-file
  /**
   * Класс ошибок NCALayerError.
   */
  class NCALayerError extends Error {
    constructor(message, canceledByUser) {
      super(message);
      this.name = 'NCALayerError';
      this.canceledByUser = canceledByUser;
    }
  }

  /**
   * Класс клиента NCALayer.
   */
  class NCALayerClient {
    /**
     * Конструктор.
     *
     * @param {String} [url = 'wss://127.0.0.1:13579'] опциональный URL для подключения к NCALayer.
     * @param {Boolean} [allowKmdHttpApi = true] допустимо ли использовать HTTP API
     * KAZTOKEN mobile/desktop
     * (https://kaztoken.kz/products/kaztoken-desktop/#%D0%BE%D0%BF%D0%B8%D1%81%D0%B0%D0%BD%D0%B8%D0%B5-api-%D0%BC%D1%83%D0%BB%D1%8C%D1%82%D0%B8%D0%BF%D0%BE%D0%B4%D0%BF%D0%B8%D1%81%D0%B0%D0%BD%D0%B8%D1%8F-sigex),
     * этот API работает в поточном режиме и позволяет подписывать документы очень больших размеров.
     * На данный момент реализована поддержка этого API только в функции `basicsSignCMS`.
     */
    constructor(url = 'wss://127.0.0.1:13579', allowKmdHttpApi = true) {
      this.url = url;
      this.wsConnection = null;
      this.responseProcessed = false;
      this.isKmd = false; // Работаем с KAZTOKEN mobile/desktop?
      this.allowKmdHttpApi = allowKmdHttpApi;
      this.kmdHttpApiUrl = 'https://127.0.0.1:24680/';
      this.isKmdHttpApiAvailable = false; // Доступен ли HTTP API KAZTOKEN mobile/desktop?
      this.KmdHTTPAPIOperationId = null;
      this.KmdHTTPAPIOperationInBase64 = false;
      this.KmdHTTPAPIOperationTotal = 0;
      this.KmdHTTPAPIOperationProcessed = 0;

      // Используются для упрощения тестирования
      this.onRequestReady = null;
      this.onResponseReady = null;
    }

    /**
     * Подключиться к NCALayer.
     *
     * @returns {Promise<String>} версию NCALayer.
     *
     * @throws NCALayerError
     */
    async connect() {
      if (this.wsConnection) {
        throw new NCALayerError('Подключение уже выполнено.');
      }

      this.wsConnection = new WebSocket(this.url);

      return new Promise((resolve, reject) => {
        this.responseProcessed = false;
        this.setHandlers(resolve, reject);

        this.wsConnection.onmessage = async (msg) => {
          if (this.responseProcessed) {
            return;
          }
          this.responseProcessed = true;

          if (this.onResponseReady) {
            this.onResponseReady(msg.data);
          }

          const response = JSON.parse(msg.data);

          if (!response.result || !response.result.version) {
            reject(new NCALayerError('Ошибка взаимодействия с NCALayer.'));
            return;
          }

          // Идентификация KAZTOKEN mobile/desktop
          try {
            const request = {
              module: 'kz.digiflow.mobile.extensions',
              method: 'getVersion',
            };

            this.sendRequest(request);

            await new Promise((resolveInner, rejectInner) => {
              this.setHandlers(resolveInner, rejectInner);
            });
            this.isKmd = true;
          } catch (err) {
            /* игнорируем */
          }

          // Идентификация KAZTOKEN mobile/desktop HTTP API
          (async () => {
            try {
              const httpResponse = await fetch(
                this.kmdHttpApiUrl,
                { signal: AbortSignal.timeout(1000) },
              );

              if (httpResponse.ok) {
                this.isKmdHttpApiAvailable = true;
              }
            } catch (err) {
              /* игнорируем */
            }
          })();

          resolve(response.result.version);
        };
      });
    }

    /**
     * Доступна ли функия мультиподписания (подписание нескольких документов одной операцией).
     */
    get multisignAvailable() { // eslint-disable-line class-methods-use-this
      // eslint отключен для обеспечения обратной совместимости,
      // так как раньше этот метод использовал `this`.
      return true;
    }

    //
    // Типы хранилищ
    //

    /**
     * KAZTOKEN
     */
    static get basicsStorageKAZTOKEN() {
      return ['AKKaztokenStore'];
    }

    /**
     * Удостоверение личности
     */
    static get basicsStorageIDCard() {
      return ['AKKZIDCardStore'];
    }

    /**
     * eToken 72k
     */
    static get basicsStorageEToken72k() {
      return ['AKEToken72KStore'];
    }

    /**
     * eToken 5110
     */
    static get basicsStorageEToken5110() {
      return ['AKEToken5110Store'];
    }

    /**
     * JaCarta
     */
    static get basicsStorageJaCarta() {
      return ['AKJaCartaStore'];
    }

    /**
     * aKey
     */
    static get basicsStorageAKey() {
      return ['AKAKEYStore'];
    }

    /**
     * Файловле хранилище PKCS#12
     */
    static get basicsStoragePKCS12() {
      return ['PKCS12'];
    }

    /**
     * Файловле хранилище JKS
     */
    static get basicsStorageJKS() {
      return ['JKS'];
    }

    /**
     * Любые хранилища.
     */
    static get basicsStorageAll() {
      return null;
    }

    /**
     * Только аппаратные хранилища.
     */
    static get basicsStorageHardware() {
      return [
        'AKKaztokenStore',
        'AKKZIDCardStore',
        'AKEToken72KStore',
        'AKEToken5110Store',
        'AKAKEYStore',
      ];
    }

    //
    // Параметры подписания
    //

    /**
     * Параметры подписания для формирования CMS по умолчанию.
     */
    static get basicsCMSParams() {
      return {};
    }

    /**
     * Параметры подписания для формирования CMS без вложенных данных из данных в Base64.
     */
    static get basicsCMSParamsDetached() {
      return {
        decode: true,
        encapsulate: false,
        digested: false,
        tsaProfile: {},
      };
    }

    /**
     * Параметры подписания для формирования CMS без вложенных данных из хеша данных в Base64.
     */
    static get basicsCMSParamsDetachedHash() {
      return {
        decode: true,
        encapsulate: false,
        digested: true,
        tsaProfile: {},
      };
    }

    /**
     * Параметры подписания для формирования CMS с вложенными данными из данных в Base64.
     */
    static get basicsCMSParamsAttached() {
      return {
        decode: true,
        encapsulate: true,
        digested: false,
        tsaProfile: {},
      };
    }

    /**
     * Параметры подписания для формирования XML по умолчанию.
     */
    static get basicsXMLParams() {
      return {};
    }

    //
    // Параметры выбора сертификата
    //

    /**
     * Любой сертификат выпущенный боевым УЦ НУЦ.
     */
    static get basicsSignerAny() {
      return {
        extKeyUsageOids: [],
      };
    }

    /**
     * Любой сертификат для подписания выпущенный боевым УЦ НУЦ.
     */
    static get basicsSignerSignAny() {
      return {
        extKeyUsageOids: ['1.3.6.1.5.5.7.3.4'],
      };
    }

    /**
     * Сертификат физического лица для подписания выпущенный боевым УЦ НУЦ.
     */
    static get basicsSignerSignPerson() {
      return {
        extKeyUsageOids: ['1.3.6.1.5.5.7.3.4', '1.2.398.3.3.4.1.1'],
      };
    }

    /**
     * Сертификат любого сотрудника юридического лица для подписания выпущенный боевым УЦ НУЦ.
     */
    static get basicsSignerSignOrg() {
      return {
        extKeyUsageOids: ['1.3.6.1.5.5.7.3.4', '1.2.398.3.3.4.1.2'],
      };
    }

    /**
     * Сертификат руководителя юридического лица для подписания выпущенный боевым УЦ НУЦ.
     */
    static get basicsSignerSignHead() {
      return {
        extKeyUsageOids: ['1.3.6.1.5.5.7.3.4', '1.2.398.3.3.4.1.2.1'],
      };
    }

    /**
     * Сертификат лица с правом подписи юридического лица для подписания выпущенный боевым УЦ НУЦ.
     */
    static get basicsSignerSignTrusted() {
      return {
        extKeyUsageOids: ['1.3.6.1.5.5.7.3.4', '1.2.398.3.3.4.1.2.2'],
      };
    }

    /**
     * Сертификат сотрудника юридического лица для подписания выпущенный боевым УЦ НУЦ.
     */
    static get basicsSignerSignEmployee() {
      return {
        extKeyUsageOids: ['1.3.6.1.5.5.7.3.4', '1.2.398.3.3.4.1.2.5'],
      };
    }

    /**
     * Любой сертификат для аутентификации выпущенный боевым УЦ НУЦ.
     */
    static get basicsSignerAuthAny() {
      return {
        extKeyUsageOids: ['1.3.6.1.5.5.7.3.2'],
      };
    }

    /**
     * Сертификат физического лица для аутентификации выпущенный боевым УЦ НУЦ.
     */
    static get basicsSignerAuthPerson() {
      return {
        extKeyUsageOids: ['1.3.6.1.5.5.7.3.2', '1.2.398.3.3.4.1.1'],
      };
    }

    /**
     * Сертификат любого сотрудника юридического лица для аутентификации выпущенный боевым УЦ НУЦ.
     */
    static get basicsSignerAuthOrg() {
      return {
        extKeyUsageOids: ['1.3.6.1.5.5.7.3.2', '1.2.398.3.3.4.1.2'],
      };
    }

    /**
     * Сертификат руководителя юридического лица для аутентификации выпущенный боевым УЦ НУЦ.
     */
    static get basicsSignerAuthHead() {
      return {
        extKeyUsageOids: ['1.3.6.1.5.5.7.3.2', '1.2.398.3.3.4.1.2.1'],
      };
    }

    /**
     * Сертификат лица с правом подписи юридического лица для аутентификации выпущенный боевым УЦ
     * НУЦ.
     */
    static get basicsSignerAuthRight() {
      return {
        extKeyUsageOids: ['1.3.6.1.5.5.7.3.2', '1.2.398.3.3.4.1.2.2'],
      };
    }

    /**
     * Сертификат сотрудника юридического лица для аутентификации выпущенный боевым УЦ НУЦ.
     */
    static get basicsSignerAuthEmployee() {
      return {
        extKeyUsageOids: ['1.3.6.1.5.5.7.3.2', '1.2.398.3.3.4.1.2.5'],
      };
    }

    /**
     * Любой сертификат выпущенный боевым или тестовым УЦ НУЦ.
     */
    static get basicsSignerTestAny() {
      return {
        extKeyUsageOids: [],
        chain: [],
      };
    }

    /**
     * Вычислить подпись под данными с указанными параметрами. **Новая функция sign 2022 года из
     * модуля kz.gov.pki.knca.basics (https://github.com/pkigovkz/sdkinfo/wiki/KNCA-Basics-Module)**.
     * Сигнатура функции сложная, поэтому рекомендуем пользоваться функциями помощниками
     * basicsSignXLM и basicsSignCMS.
     *
     * @param {Array | null} allowedStorages массив строк с константами допустимых для использования
     * типов хранилищ (см. константы basicsStorage*).
     *
     * @param {String} format тип вычисляемой подписи: 'xml', либо 'cms'.
     *
     * @param {String} data подписываемые данные.
     *
     * @param {Object} signingParams параметры подписания (см. basicsCMSParams* и basicsXMLParams*).
     *
     * @param {Object} signerParams параметры выбора сертификата для подписания (см. константы
     * basicsSigner*).
     *
     * @param {String} locale язык пользовательского интерфейса.
     *
     * @param {Boolean} forceSingleSignature возвращать только одну подпись даже если получили
     * массив, используется для обеспечения обратной совместимости работы с CMS.
     *
     * @returns {Promise<String>} подпись.
     *
     * @throws NCALayerError
     */
    async basicsSign(
      allowedStorages,
      format,
      data,
      signingParams,
      signerParams,
      locale,
      forceSingleSignature = false,
    ) {
      const request = {
        module: 'kz.gov.pki.knca.basics',
        method: 'sign',
        args: {
          allowedStorages,
          format,
          data,
          signingParams,
          signerParams,
          locale,
        },
      };

      this.sendRequest(request);

      return new Promise((resolve, reject) => {
        this.setHandlers(resolve, reject, forceSingleSignature);
      });
    }

    /**
     * Вычислить CMS подпись под данными с указанными параметрами, это функция-помощник для
     * упрощения работы с функцией basicsSign.
     *
     * В том случае, если библиотека смогла обнаружить HTTP API KAZTOKEN desktop на локальном
     * компьютере, она будет пробовать использовать его для подписания в том случае, если это
     * не было запрещено при вызове конструктора (параметр `allowKmdHttpApi`).
     *
     * @param {Array | null} allowedStorages массив строк с константами допустимых для использования
     * типов хранилищ (см. константы basicsStorage*).
     *
     * @param {String | ArrayBuffer | Blob | File | Array<String | ArrayBuffer | Blob | File>} data
     * данные, которые нужно подписать, в виде строки Base64, либо ArrayBuffer, Blob или File.
     * Так же поддерживается массив строк Base64, ArrayBuffer, Blob или File, но это будет работать
     * только с приложениями KAZTOKEN mobile/desktop, NCALayer не умеет подписывать массив
     * документов.
     *
     * @param {Object} signingParams параметры подписания (см basicsCMSParams*).
     *
     * @param {Object} signerParams параметры выбора сертификата для подписания (см. константы
     * basicsSigner*).
     *
     * @param {String} [locale = 'ru'] язык пользовательского интерфейса.
     *
     * @returns {Promise<String | Array>} подпись, либо массив подписей если на подписание был
     * передан массиов документов.
     *
     * @throws NCALayerError
     */
    async basicsSignCMS(allowedStorages, data, signingParams, signerParams, locale = 'ru') {
      const dataIsArray = Array.isArray(data);

      // Использование HTTP API KAZTOKEN mobile/desktop
      if (this.allowKmdHttpApi && this.isKmdHttpApiAvailable) {
        try {
          const documents = dataIsArray ? data : [data];
          const base64 = (typeof (documents[0]) === 'string');

          let response = await fetch(
            this.kmdHttpApiUrl,
            {
              method: 'POST',
              mode: 'cors',
              credentials: 'include',
              body: JSON.stringify({
                numberOfDocuments: documents.length,
                base64,
                encapsulateContent: signingParams.encapsulate,
              }),
            },
          );

          if (!response) {
            throw new NCALayerError('Ошибка взаимодействия с KAZTOKEN mobile/desktop.');
          }

          if (!response.ok) {
            if (response.status === 409) {
              throw new NCALayerError('Операция отменена пользователем', true);
            }
            throw new NCALayerError(`KAZTOKEN mobile/desktop вернул ошибку '${response.status}: ${response.statusText}'`);
          }

          const operationId = await response.text();

          const signatures = [];
          // eslint-disable-next-line no-restricted-syntax
          for (const document of documents) {
            // eslint-disable-next-line no-await-in-loop
            response = await fetch(
              `${this.kmdHttpApiUrl}${operationId}`,
              {
                method: 'POST',
                mode: 'cors',
                credentials: 'include',
                body: document,
              },
            );

            if (!response) {
              throw new NCALayerError('Ошибка взаимодействия с KAZTOKEN mobile/desktop.');
            }

            if (!response.ok) {
              if (response.status === 401) {
                throw new NCALayerError('Операция отменена пользователем', true);
              }
              throw new NCALayerError(`KAZTOKEN mobile/desktop вернул ошибку '${response.status}: ${response.statusText}'`);
            }

            let signature = '';
            if (base64) {
              // eslint-disable-next-line no-await-in-loop
              signature = await response.text();
            } else {
              // eslint-disable-next-line no-await-in-loop
              const signatureBytes = await response.arrayBuffer();
              signature = NCALayerClient.arrayBufferToB64(signatureBytes);
            }

            signatures.push(signature);
          }

          return dataIsArray ? signatures : signatures[0];
        } catch (err) {
          throw new NCALayerError(`Ошибка взаимодействия с KAZTOKEN mobile/desktop: ${err}`);
        }
      }

      const forceSingleSignature = !dataIsArray;
      return this.basicsSign(
        allowedStorages,
        'cms',
        await NCALayerClient.normalizeDataToSign(data),
        signingParams,
        signerParams,
        locale,
        forceSingleSignature,
      );
    }

    /**
     * Вычислить XML подпись под данными с указанными параметрами, это функция-помощник для
     * упрощения работы с функцией basicsSign.
     *
     * @param {Array | null} allowedStorages массив строк с константами допустимых для использования
     * типов хранилищ (см. константы basicsStorage*).
     *
     * @param {String} data подписываемые данные.
     *
     * @param {Object} signingParams параметры подписания (см basicsXMLParams*).
     *
     * @param {Object} signerParams параметры выбора сертификата для подписания (см. константы
     * basicsSigner*).
     *
     * @param {String} [locale = 'ru'] язык пользовательского интерфейса.
     *
     * @returns {Promise<String>} подпись.
     *
     * @throws NCALayerError
     */
    async basicsSignXML(allowedStorages, data, signingParams, signerParams, locale = 'ru') {
      return this.basicsSign(
        allowedStorages,
        'xml',
        data,
        signingParams,
        signerParams,
        locale,
      );
    }

    /**
     * Проверить доступность функции мультиподписания через HTTP API KAZTOKEN mobile/desktop.
     *
     * @returns {Promise<Boolean>} доступна ли функция.
     */
    async kmdMultisignAvailable() {
      try {
        const httpResponse = await fetch(
          this.kmdHttpApiUrl,
          { signal: AbortSignal.timeout(1000) },
        );

        if (httpResponse.ok) {
          return true;
        }
      } catch (err) {
        /* игнорируем */
      }

      return false;
    }

    /**
     * Инициировать процедуру мультиподписания через HTTP API KAZTOKEN mobile/desktop.
     * Не требует предварительного вызова `connect()`.
     *
     * @param {Number} numberOfDocuments количество документов которые будут подписаны
     * в рамках процедуры мультиподписания.
     *
     * @param {Boolean} base64 будут ли данные передаваться в base64 или в бинарном виде.
     *
     * @param {Boolean} encapsulateContent следудует ли встраивать подписываемые данные в подписи
     * (не рекомендуется, так как в этом случае требуется значительно больше ОЗУ для обработки).
     *
     * @throws NCALayerError
     */
    async startKmdMultisign(numberOfDocuments, base64, encapsulateContent) {
      let response;
      try {
        response = await fetch(
          this.kmdHttpApiUrl,
          {
            method: 'POST',
            mode: 'cors',
            credentials: 'include',
            body: JSON.stringify({
              numberOfDocuments,
              base64,
              encapsulateContent,
            }),
          },
        );
      } catch (err) {
        throw new NCALayerError(`Ошибка взаимодействия с KAZTOKEN mobile/desktop: ${err}`);
      }

      if (!response) {
        throw new NCALayerError('Ошибка взаимодействия с KAZTOKEN mobile/desktop');
      }

      if (!response.ok) {
        if (response.status === 409) {
          throw new NCALayerError('Операция отменена пользователем', true);
        }
        throw new NCALayerError(`KAZTOKEN mobile/desktop вернул ошибку '${response.status}: ${response.statusText}'`);
      }

      try {
        this.KmdHTTPAPIOperationId = await response.text();
        this.KmdHTTPAPIOperationInBase64 = base64;
        this.KmdHTTPAPIOperationTotal = numberOfDocuments;
        this.KmdHTTPAPIOperationProcessed = 0;
      } catch (err) {
        throw new NCALayerError(`Ошибка взаимодействия с KAZTOKEN mobile/desktop: ${err}`);
      }
    }

    /**
     * Вычислить CMS подпись под данными в рамках процедуры мультиподписания через HTTP API
     * KAZTOKEN mobile/desktop.
     *
     * Можно вызывать только после того как процедура была инициализирована с помощью
     * `StartKmdMultisign` и только для того количества документов, которое было
     * указано при инициализации.
     *
     * @param {String | ArrayBuffer | Blob | File} data
     * данные, которые нужно подписать, в виде строки Base64, либо ArrayBuffer, Blob или File.
     *
     * @returns {Promise<String>} подпись в base64.
     *
     * @throws NCALayerError
     */
    async kmdMultisignNext(data) {
      if (!this.KmdHTTPAPIOperationId) {
        throw new NCALayerError('Процедура мультиподписания не была инициализирована');
      }

      let response;
      try {
        response = await fetch(
          `${this.kmdHttpApiUrl}${this.KmdHTTPAPIOperationId}`,
          {
            method: 'POST',
            mode: 'cors',
            credentials: 'include',
            body: data,
          },
        );
      } catch (err) {
        throw new NCALayerError(`Ошибка взаимодействия с KAZTOKEN mobile/desktop: ${err}`);
      }

      if (!response) {
        throw new NCALayerError('Ошибка взаимодействия с KAZTOKEN mobile/desktop');
      }

      if (!response.ok) {
        if (response.status === 401) {
          throw new NCALayerError('Операция отменена пользователем', true);
        }
        throw new NCALayerError(`KAZTOKEN mobile/desktop вернул ошибку '${response.status}: ${response.statusText}'`);
      }

      let signature = '';
      if (this.KmdHTTPAPIOperationInBase64) {
        signature = await response.text();
      } else {
        const signatureBytes = await response.arrayBuffer();
        signature = NCALayerClient.arrayBufferToB64(signatureBytes);
      }

      this.KmdHTTPAPIOperationProcessed += 1;
      if (this.KmdHTTPAPIOperationProcessed === this.KmdHTTPAPIOperationTotal) {
        this.KmdHTTPAPIOperationId = null;
        this.KmdHTTPAPIOperationProcessed = 0;
        this.KmdHTTPAPIOperationTotal = 0;
      }

      return signature;
    }

    /**
     * Получить список активных типов устройств.
     *
     * @returns {Promise<String[]>} массив содержащий типы хранилищ экземпляры которых доступны в
     * данный момент.
     *
     * @throws NCALayerError
     */
    async getActiveTokens() {
      const request = {
        module: 'kz.gov.pki.knca.commonUtils',
        method: 'getActiveTokens',
      };

      this.sendRequest(request);

      return new Promise((resolve, reject) => { this.setHandlers(resolve, reject); });
    }

    /**
     * Получить информацию об одной записи (ключевой паре с сертификатом).
     *
     * @param {String} storageType тип хранилища на экземплярах которого следует искать записи.
     *
     * @returns {Promise<Object>} объект с информацией о записи.
     *
     * @throws NCALayerError
     */
    async getKeyInfo(storageType) {
      const request = {
        module: 'kz.gov.pki.knca.commonUtils',
        method: 'getKeyInfo',
        args: [
          storageType,
        ],
      };

      this.sendRequest(request);

      return new Promise((resolve, reject) => { this.setHandlers(resolve, reject); });
    }

    /**
     * Вычислить подпись под данными и сформировать CMS (CAdES).
     *
     * @param {String} storageType тип хранилища который следует использовать для подписания.
     *
     * @param {String | ArrayBuffer | Blob | File | Array<String | ArrayBuffer | Blob | File>} data
     * данные, которые нужно подписать, в виде строки Base64, либо ArrayBuffer, Blob или File.
     * Так же поддерживается массив строк Base64, ArrayBuffer, Blob или File, но это будет работать
     * только с приложениями KAZTOKEN mobile/desktop, NCALayer не умеет подписывать массив
     * документов.
     *
     * @param {String} [keyType = 'SIGNATURE'] каким типом ключа следует подписывать, поддерживаемые
     * варианты 'SIGNATURE' и 'AUTHENTICATION', иное значение позволит пользователю выбрать
     * любой доступный в хранилище ключа.
     *
     * @param {Boolean} [attach = false] следует ли включить в подпись подписываемые данные.
     *
     * @returns {Promise<String>} CMS подпись в виде Base64 строки.
     *
     * @throws NCALayerError
     */
    async createCAdESFromBase64(storageType, data, keyType = 'SIGNATURE', attach = false) {
      const request = {
        module: 'kz.gov.pki.knca.commonUtils',
        method: 'createCAdESFromBase64',
        args: [
          storageType,
          keyType,
          await NCALayerClient.normalizeDataToSign(data),
          attach,
        ],
      };

      this.sendRequest(request);

      return new Promise((resolve, reject) => { this.setHandlers(resolve, reject); });
    }

    /**
     * Вычислить подпись под хешем данных и сформировать CMS (CAdES).
     *
     * @param {String} storageType тип хранилища который следует использовать для подписания.
     *
     * @param {String | ArrayBuffer | Blob | File | Array<String | ArrayBuffer | Blob | File>} hash
     * хеш данных в виде строки Base64, либо ArrayBuffer, Blob или File.
     * Так же поддерживается массив строк Base64, ArrayBuffer, Blob или File, но это будет работать
     * только с приложениями KAZTOKEN mobile/desktop, NCALayer не умеет подписывать массив
     * хешей.
     *
     * @param {String} [keyType = 'SIGNATURE'] каким типом ключа следует подписывать, поддерживаемые
     * варианты 'SIGNATURE' и 'AUTHENTICATION', иное значение позволит пользователю выбрать
     * любой доступный в хранилище ключа.
     *
     * @returns {Promise<String>} CMS подпись в виде Base64 строки.
     *
     * @throws NCALayerError
     */
    async createCAdESFromBase64Hash(storageType, hash, keyType = 'SIGNATURE') {
      const request = {
        module: 'kz.gov.pki.knca.commonUtils',
        method: 'createCAdESFromBase64Hash',
        args: [
          storageType,
          keyType,
          await NCALayerClient.normalizeDataToSign(hash),
        ],
      };

      this.sendRequest(request);

      return new Promise((resolve, reject) => { this.setHandlers(resolve, reject); });
    }

    /**
     * Подписать блок данных и сформировать CMS (CAdES) подпись с интегрированной меткой времени
     * TSP. **Не рекомендуется использовать, разработчики NCALayer пометили как DEPRECATED (https://forum.pki.gov.kz/t/podpis-s-metkoj-vremeni-na-js/704/7)!**
     *
     * @param {String} storageType тип хранилища который следует использовать для подписания.
     *
     * @param {String | ArrayBuffer | Blob | File | Array<String | ArrayBuffer | Blob | File>} data
     * данные, которые нужно подписать, в виде строки Base64, либо ArrayBuffer, Blob или File.
     * Так же поддерживается массив строк Base64, ArrayBuffer, Blob или File, но это будет работать
     * только с приложениями KAZTOKEN mobile/desktop, NCALayer не умеет подписывать массив
     * документов.
     *
     * @param {String} [keyType = 'SIGNATURE'] каким типом ключа следует подписывать, поддерживаемые
     * варианты 'SIGNATURE' и 'AUTHENTICATION', иное значение позволит пользователю выбрать
     * любой доступный в хранилище ключа.
     *
     * @param {Boolean} [attach = false] следует ли включить в подпись подписываемые данные.
     *
     * @returns {Promise<String>} CMS подпись в виде Base64 строки.
     *
     * @throws NCALayerError
     */
    async createCMSSignatureFromBase64(storageType, data, keyType = 'SIGNATURE', attach = false) {
      const request = {
        module: 'kz.gov.pki.knca.commonUtils',
        method: 'createCMSSignatureFromBase64',
        args: [
          storageType,
          keyType,
          await NCALayerClient.normalizeDataToSign(data),
          attach,
        ],
      };

      this.sendRequest(request);

      return new Promise((resolve, reject) => { this.setHandlers(resolve, reject); });
    }

    /**
     * Вычислить подпись под документом в формате XML.
     *
     * @param {String} storageType тип хранилища который следует использовать для подписания.
     *
     * @param {String} xml XML данные которые нужно подписать.
     *
     * @param {String} [keyType = 'SIGNATURE'] каким типом ключа следует подписывать, поддерживаемые
     * варианты 'SIGNATURE' и 'AUTHENTICATION', иное значение позволит пользователю выбрать
     * любой доступный в хранилище ключа.
     *
     * @param {String} [tbsElementXPath = ''] путь к подписываемому узлу XML.
     *
     * @param {String} [signatureParentElementXPath = ''] путь к узлу в который необходимо добавить
     * сформированную подпись.
     *
     * @returns {Promise<String>} XML документ содержащий XMLDSIG подпись.
     *
     * @throws NCALayerError
     */
    async signXml(storageType, xml, keyType = 'SIGNATURE', tbsElementXPath = '', signatureParentElementXPath = '') {
      const request = {
        module: 'kz.gov.pki.knca.commonUtils',
        method: 'signXml',
        args: [
          storageType,
          keyType,
          xml,
          tbsElementXPath,
          signatureParentElementXPath,
        ],
      };

      this.sendRequest(request);

      return new Promise((resolve, reject) => { this.setHandlers(resolve, reject); });
    }

    /**
     * Вычислить подпись под каждым из массива документов в формате XML.
     *
     * @param {String} storageType тип хранилища который следует использовать для подписания.
     *
     * @param {String[]} xmls массив XML данных которые нужно подписать.
     *
     * @param {String} [keyType = 'SIGNATURE'] каким типом ключа следует подписывать, поддерживаемые
     * варианты 'SIGNATURE' и 'AUTHENTICATION', иное значение позволит пользователю выбрать
     * любой доступный в хранилище ключа.
     *
     * @param {String} [tbsElementXPath = ''] путь к подписываемому узлу XML.
     *
     * @param {String} [signatureParentElementXPath = ''] путь к узлу в который необходимо добавить
     * сформированную подпись.
     *
     * @returns {Promise<String[]>} массив XML документов содержащих XMLDSIG подписи.
     *
     * @throws NCALayerError
     */
    async signXmls(storageType, xmls, keyType = 'SIGNATURE', tbsElementXPath = '', signatureParentElementXPath = '') {
      const request = {
        module: 'kz.gov.pki.knca.commonUtils',
        method: 'signXmls',
        args: [
          storageType,
          keyType,
          xmls,
          tbsElementXPath,
          signatureParentElementXPath,
        ],
      };

      this.sendRequest(request);

      return new Promise((resolve, reject) => { this.setHandlers(resolve, reject); });
    }

    /**
     * Изменить язык интерфейса NCALayer.
     *
     * @param {String} localeId новый идентификатор языка.
     *
     * @throws NCALayerError
     */
    async changeLocale(localeId) {
      const request = {
        module: 'kz.gov.pki.knca.commonUtils',
        method: 'changeLocale',
        args: [
          localeId,
        ],
      };

      this.sendRequest(request);

      return new Promise((resolve, reject) => { this.setHandlers(resolve, reject); });
    }

    /**
     * Константа определяющая имя файлового хранилища.
     */
    static get fileStorageType() {
      return 'PKCS12';
    }

    sendRequest(request) {
      if (!this.wsConnection) {
        throw new NCALayerError('Подключение к NCALayer не установлено.');
      }

      const jsonRequest = JSON.stringify(request);
      if (this.onRequestReady) {
        this.onRequestReady(jsonRequest);
      }

      this.wsConnection.send(jsonRequest);
    }

    setHandlers(resolve, reject, forceSingleSignature) {
      this.responseProcessed = false;

      this.wsConnection.onerror = () => {
        if (this.responseProcessed) {
          return;
        }
        this.responseProcessed = true;

        reject(new NCALayerError('Ошибка взаимодействия с NCALayer. В том случае, если на вашем компьютере не установлен NCALayer, пожалуйста установите его c портала НУЦ РК (https://ncl.pki.gov.kz/). Если же NCALayer установлен, но портал выдает ошибку, свяжитесь, пожалуйста, с нашей технической поддержкой.'));
      };

      this.wsConnection.onclose = () => {
        if (this.responseProcessed) {
          return;
        }
        this.responseProcessed = true;

        reject(new NCALayerError('NCALayer закрыл соединение.'));
      };

      this.wsConnection.onmessage = (msg) => {
        if (this.responseProcessed) {
          return;
        }
        this.responseProcessed = true;

        if (this.onResponseReady) {
          this.onResponseReady(msg.data);
        }

        const response = JSON.parse(msg.data);

        // basics response
        if (response.hasOwnProperty('status')) { // eslint-disable-line no-prototype-builtins
          if (!response.status) {
            reject(new NCALayerError(`${response.code}: ${response.message} (${response.details})`));
            return;
          }

          if (!response.body.hasOwnProperty('result')) { // eslint-disable-line no-prototype-builtins
            reject(new NCALayerError('cancelled by user', true));
            return;
          }

          let { result } = response.body;
          if (forceSingleSignature && Array.isArray(result)) {
            [result] = result;
          }
          resolve(result);
          return;
        }

        // commonUtils response
        if (response.code !== '200') {
          reject(new NCALayerError(`${response.code}: ${response.message}`));
          return;
        }

        resolve(response.responseObject);
      };
    }

    static arrayBufferToB64(arrayBuffer) {
      let binary = '';
      const bytes = new Uint8Array(arrayBuffer);
      const len = bytes.byteLength;
      for (let i = 0; i < len; i += 1) {
        binary += String.fromCharCode(bytes[i]);
      }
      return window.btoa(binary);
    }

    static async normalizeDataToSign(data) {
      const normalizeDataBlock = async (dataBlock) => {
        if (typeof dataBlock === 'string') {
          return dataBlock;
        }

        let dataBlockArrayBuffer = dataBlock;
        if (dataBlock instanceof Blob) {
          dataBlockArrayBuffer = await dataBlock.arrayBuffer();
        }

        return NCALayerClient.arrayBufferToB64(dataBlockArrayBuffer);
      };

      if (Array.isArray(data)) {
        return Promise.all(data.map(normalizeDataBlock));
      }

      return normalizeDataBlock(data);
    }
  }

  exports.NCALayerClient = NCALayerClient; // eslint-disable-line no-param-reassign
})(
  typeof exports === 'undefined' ? this : exports,
  typeof WebSocket === 'undefined' ? require('ws') : WebSocket,
  typeof window === 'undefined' ? { btoa(x) { return x; } } : window // eslint-disable-line comma-dangle
); // Заглушка для NodeJS