Введение

Этот документ содержит инструкции по встраиванию PC SDK в веб-приложение. Чтобы лучше понять, как работать с PC, рекомендуем также ознакомиться со следующими документами:

PC SDK для Web позволяет реализовать всю функциональность PC прямо в вашем веб-приложении.

PC SDK может хранить ключи пользователей в двух типах хранилищ:

  1. Рутокен ЭЦП 3.0 (далее по тексту - токен). Может использоваться только для алгоритма ГОСТ.
  2. IndexedDB браузера. Может использоваться только для алгоритма ECDSA.

Для работы с Рутокен ЭЦП 3.0 используется Рутокен Плагин.
PC WEB SDK самостоятельно проверяет его наличие во время инициализации (при необходимости).

Интеграция в проект

Библиотеку PC SDK можно импортировать в ваш проект из репозитория npm SafeTech.

Необходимо добавить следующую строку в файл .npmrc в корне проекта

@safetech:registry=https://nexus.paycontrol.org/repository/npm-external/

Затем установить pcsdk.

npm i @safetech/pcsdk

После установки библиотеки PCSDK её можно импортировать в проект:

import PCSDK from @safetech/pcsdk

Ваше веб-приложение должно использовать один экземпляр класса PCSDK во время работы.

Инициализация SDK

Чтобы инициализировать библиотеку PC SDK, необходимо вызвать асинхронный метод

PCSDK.init(callback?, errorCallback?, options?)

Метод принимает 3 необязательных параметра:

  1. callback(status) - вызывается при изменении состояния подключённых токенов (только если есть новые подключённые или отключённые токены).
    В функцию передаётся объект:
    { connected: Array<number>, disconnected: Array<number> }
    • connected — массив идентификаторов подключённых токенов
    • disconnected — массив идентификаторов токенов, которые были отключены
  2. errorCallback(error) - вызывается, если во время периодического опроса состояния токенов произошла ошибка (например, если плагин для работы с токенами перестал отвечать).
  3. options — объект с необязательными параметрами конфигурации:
    { rutokenOnly?: boolean, dev?: boolean, unixTime?: number }
    • Параметр rutokenOnly отключает работу с IndexedDB (по умолчанию true)
    • Параметр dev — включает режим отладки SDK. При значении true в консоль выводятся диагностические сообщения о работе SDK. По умолчанию логирование отключено.
    • Параметр unixTime задаёт время системы в формате Unix timestamp (в секундах):
      • Если параметр не передан — время автоматически синхронизируется через use.ntpjs.org.
      • Если передан 0 — используется локальное время машины пользователя.
      • Если передано число > 0 — используется указанное значение времени.

Если параметр rutokenOnly === true (значение по умолчанию), то при ошибке инициализации Рутокен Плагин метод PCSDK.init() генерирует исключение (throw PCError).

В этом режиме библиотека ожидает, что Рутокен Плагин должен быть доступен и корректно загружен, поэтому любая ошибка загрузки приводит к немедленному прерыванию инициализации.

Если же rutokenOnly === false, то ошибка загрузки плагина не вызывает исключение - она будет возвращена в результате Promise в виде:

plugin: { ok: false, error: PCError }

Метод PCSDK.init(callback, errorCallback, options) — возвращает Promise, результатом которого является объект:

{
  db: { ok: boolean, error: PCError | null },
  plugin: { ok: boolean, error: PCError | null }
}
import PCSDK from @safetech/pcsdk
try {
  let options = { rutokenOnly: false, dev: false }

  const result = await PCSDK.init(callback, errorCallback, options);

  if (!result.db.ok) {
    // Требуется обработать ошибку инициализации хранилища пользователей
  }

  if (!result.plugin.ok) {
    // Требуется обработать ошибку инициализации Рутокен плагина
  }
} catch (error) {
  console.error('Ошибка инициализации PCSDK:', error);
}

Структура объекта error

Объект plugin.error присутствует в ответе всегда и отражает состояние плагина.

  • Если плагин инициализирован корректно - значение plugin.error будет равно null.
  • Если при инициализации возникла ошибка - в plugin.error возвращается объект типа PCError, содержащий информацию о причине сбоя.

Пример успешной инициализации

{
  db: { ok: true, error: null },
  plugin: { ok: true, error: null }
}

Пример с ошибкой плагина

{
  db: { ok: true, error: null },
  plugin: {
    ok: false,
    error: {
      code: 3,
      errorName: "PC_ERROR_RUTOKEN_EXTENSION_NOT_INSTALLED",
      link: "https://chromewebstore.google.com/detail/ohedcglhbbfdgaogjhcclacoccbagkjg",
      message: "Расширение Рутокен не установлено",
      name: "PCError"
    }
  }
}

Регистрация пользователей

Процесс регистрации пользователя включает следующие этапы:

  1. Получение данных персонализации в виде JSON (в том числе из QR или deeplink) и, опционально, кода активации. Подробнее см. в документе Архитектура и принципы работы
  2. Регистрация пользователя PCUser на сервере PC. Во время этого процесса PC SDK генерирует необходимые наборы ключей
  3. Сохранение ключей пользователя в хранилище (токен или IndexedDB) для дальнейшего использования

Шаг 1. Импорт PCUser

Определение, где хранить ключи

Если планируется использование ГОСТ-алгоритмов, то это возможно только с токенами.
В этом случае, прежде всего, Вам необходимо получить список подключенных токенов и информацию о них, используя метод getTokensInfo(), а также выбрать deviceId токена, на котором будут сохранены ключи.

import PCSDK, {PCUsersManager} from '@safetech/pcsdk';

try {
  /*
    devices возвращает:
    Array<{
    label: string,
    serial: string,
    leftSpace: number,
    deviceId: number,
    }>
  */
  const devices = await PCSDK.getTokensInfo();

  if (!devices.length) {
    console.log('Нет подключённых токенов');
    return;
  }

  // Берём первый подключенный токен
  const {deviceId} = devices[0];
} catch (error) {
  console.error('Ошибка при получении списка токенов:', error);
}

Если же Вы планируете работать только с ECDSA и IndexedDB, то deviceId не понадобится

const deviceId = null;

Импорт

Затем нужно создать объект PCUser из строки JSON, указав deviceId.

Метод

PCUsersManager.importUser(source: string | PCUserJSON, deviceId: number | null)

Возвращает Promise, результатом которого является объект PCUser.

import PCSDK, { PCUsersManager } from @safetech/pcsdk’;

// Определение deviceId

try {
  const user = await PCUsersManager.importUser(source, deviceId);
  console.log('Импорт завершён:', user);
} catch(error) {
  console.error('Ошибка при импорте пользователя:', error);
}

Шаг 2. Проверка необходимости активации PCUser

Данные для персонализации могут передаваться с обязательным кодом активации (см. Архитектура и принципы работы).

Поэтому после импорта объекта PCUser необходимо:

  1. Проверить, требуется ли активация user.isActivated();
  2. При необходимости выполнить активацию с помощью кода активации.

Для активации используется метод

PCUsersManager.activate(user: PCUser, activationCode: string)

Метод выполняет проверку кода активации для объекта PCUser

import PCSDK, {PCUsersManager} from '@safetech/pcsdk';

// Импорт PCUser

try {
  if (!user.isActivated()) {
    // Для активации PCUser необходим код активации
    const activationCode = getActivationCode(); // код активации получен от пользователя или другим способом
    await PCUsersManager.activate(user, activationCode);
    console.log('Пользователь успешно активирован');
  } else {
    console.log('Активация не требуется');
  }

  // Если ошибок нет — пользователь успешно импортирован и готов к регистрации
} catch (error) {
  console.error('Ошибка при импорте или активации пользователя:', error);
}

Шаг 3. Регистрация ключа на сервере PC

Теперь можно зарегистрировать объект PCUser на сервере PC.

Для регистрации используется метод

PCUsersManager.register(user: PCUser, password: string)

Метод выполняет генерацию необходимых ключей и их регистрацию на сервере.

Параметр password может иметь два смысла, в зависимости от использования токена или IndexedDB:

  • при работе с токеном - это ПИН-код от токена
  • при работе с IndexedDB - это пароль для доступа к ключам, который будет необходимо предъявлять при формировании подтверждений
import {PCUsersManager} from '@safetech/pcsdk';

try {
  // запрос ПИН-кода токена или пароля для сохранения в IndexedDB
  const password = promptPassword();

  await PCUsersManager.register(user, password);
} catch (error) {
  console.error('При регистрации произошла ошибка', error);
}

// Если ошибок нет — пользователь успешно зарегистрирован

Шаг 4. Сохранение объекта PCUser

На последнем этапе нужно сохранить объект PCUser.

Каждый пользователь сохраняется под уникальным именем (keyName), которое задаётся Вашим приложением (или запрашивается у Пользователя).

Если Вы сохраняете объект PCUser в IndexedDB и устройство поддерживает WebAuthn (например, Touch ID, Face ID или Windows Hello), то возможно использование биометрической аутентификации вместо пароля.

Для активации биометрии необходимо выполнить три проверки флагов и типа объекта PCUser (см. PC Server API Reference -> Create User):

  • user.type === 0 - проверка, что тип ключей не-ГОСТ
  • user.hasOnlineCredentials() === true - проверка, что у пользователя активирован keyFlag, разрешающий использование online credentials
  • user.isWebAuthnAllowed() === true - проверка, что пользователю разрешено использовать WebAuthn

Метод

PCUsersManager.store(user: PCUser, keyName: string, password: string, useBiometry: boolean)

сохраняет объект пользователя в хранилище токена или IndexedDB, включая активацию biometry-mode при useBiometry === true

Пример проверки WebAuthn

function checkBiometry() {
    let useBiometry = false;

    try {
    const isPlatformAuthenticatorAvailable =
        typeof window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable === 'function'
        ? await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
        : false;

    if (
        user.type === 0 &&
        isPlatformAuthenticatorAvailable &&
        user.isWebAuthnAllowed() &&
        user.hasOnlineCredentials()
    ) {
        useBiometry = true;
    } else {
        console.warn('Биометрические данные недоступны');
    }

    } catch (error) {
        console.warn('Произошла ошибка при использовании биометрии', e);
        useBiometry = false;
    }

    return useBiometry
}

Пример процесса сохранения ключа

// запрос уникального имени ключа от клиента или генерация в приложении
const keyName = promptKeyName();

// запрос ПИН-кода токена или пароля для сохранения в IndexedDB
const password = promptPassword();

// использовать ли биометрию (WebAuthn)
const useBiometry = await checkBiometry();

// сохранение ключа в хранилище
try {
  await PCUsersManager.store(user, keyName, password, useBiometry);
} catch (error) {
  console.error('Ошибка при сохранении пользователя:', error);
}

// Если ошибок нет — пользователь успешно сохранён

Общая схема регистрации ключа

import PCSDK, {PCUsersManager} from '@safetech/pcsdk';

// 1. Импорт PCUser
let user = null;

try {
  // используем ли ГОСТ и токены?
  const useGOST = false;
  let deviceId = null;

  if (useGOST) {
    const devices = await PCSDK.getTokensInfo();

    if (!devices.length) {
      console.log('Нет подключённых токенов');
      return;
    }

    deviceId = devices[0].deviceId;
  }

  user = await PCUsersManager.importUser(source, deviceId);
} catch (error) {
  console.error('Ошибка при импорте пользователя:', error);
}

// 2. Проверка необходимости активации
try {
  if (!user.isActivated()) {
    const activationCode = getActivationCode();
    await PCUsersManager.activate(user, activationCode);
  } else {
    // активация не требуется
  }
} catch (error) {
  console.error('Ошибка при активации пользователя:', error);
}

// 3. Регистрация пользователя
const password = promptPassword();
try {
  await PCUsersManager.register(user, password);
} catch (error) {
  console.error('При регистрации произошла ошибка', error);
}

// 4. Сохранение ключа
const keyName = promptKeyName();
const useBiometry = await checkBiometry();

try {
  await PCUsersManager.store(user, keyName, password, useBiometry);
} catch (error) {
  console.error('Ошибка при сохранении пользователя:', error);
}

Удаление пользователя

Метод

PCUsersManager.delete(user: PCUser, password: string | null)

Используется для удаления пользователя из хранилища SDK.

В зависимости от типа пользователя требуется разное поведение по паролю:

  • ECDSA (user.type === 0) - пароль для удаления не требуется, параметр password можно передать как null
  • ГОСТ (user.type === 1) - пароль обязателен, так как удаление выполняется на токене

Пример использования

import {PCUsersManager} from '@safetech/pcsdk';

try {
  const users = await PCUsersManager.listStorage();

  if (!users.length) {
    console.log('Пользователи отсутствуют');
    return;
  }

  const user = users[0];

  const password = user.type === 1 ? prompt('Введите пароль Рутокен') : null;

  await PCUsersManager.delete(user, password);

  console.log('Пользователь успешно удалён');
} catch (error) {
  console.error('Ошибка при удалении пользователя:', error);
}

Получение информации о сертификате пользователя

Если в Вашей системе PC используется совместно со средствами УЦ (PKI), то у пользователя может быть выпущен сертификат (см. PC Server API Reference -> PKI endpoint)

Для получения информации о сертификате используется метод

PCUsersManager.getCertificateInfo(user: PCUser)

Метод возвращает Promise со следующей структурой:

{
  certRequest?: string,
  certificate?: string,
  status: CertificateStatus
}

Статусы сертификата

  • UNDEFINED - запрос и сертификат отсутствуют
  • CERT_REQUEST_INFO_CREATED - информация о запросе создана, данных сертификата нет
  • CERT_REQUEST_CREATED - создан запрос на сертификат (certRequest присутствует), сертификата нет
  • CERT_ISSUED - сертификат выдан (certRequest и certificate присутствуют)
  • CERT_ISSUE_ERROR - ошибка выпуска сертификата
  • CERT_REVOKED - сертификат отозван
  • CERT_EXPIRED - сертификат истёк

Пример использования

import {PCUsersManager} from '@safetech/pcsdk';

try {
  const users = await PCUsersManager.listStorage();

  if (!users.length) {
    console.log('Пользователи отсутствуют');
    return;
  }

  const user = users[0];

  const result = await PCUsersManager.getCertificateInfo(user);

  console.log('Статус сертификата:', result.status);

  if (result.certRequest) {
    console.log('Запрос сертификата:', result.certRequest);
  }

  if (result.certificate) {
    console.log('Сертификат:', result.certificate);
  }
} catch (error) {
  console.error('Ошибка при получении информации о сертификате:', error);
}

Обработка транзакций

Общий сценарий обработки транзакции включает следующие шаги:

  1. Вызвать PCUsersManager.listStorage() для получения списка пользователей из хранилища
  2. Отфильтровать пользователей, для которых нужно загрузить транзакции
  3. Для каждого пользователя вызвать PCTransactionsManager.getTransactionList(user: PCUser) и проверить наличие транзакций
  4. Для каждой транзакции вызвать PCTransactionsManager.getTransaction(user: PCUser, transactionId: string) для получения данных
    • При наличии вложений использовать PCTransactionsManager.getTransactionBinaryData(user: PCUser, transaction: PCTransaction)
  5. Отобразить загруженные транзакции пользователю с кнопками подтверждения и отклонения
  6. Обработать действия пользователя:
    • запросить пароль для доступа к ключам
    • для подтверждения транзакции вызвать метод PCTransactionsManager.sign(user: PCUser, transaction: PCTransaction, password: string)
    • для отклонения транзакции вызвать метод PCTransactionsManager.decline(user: PCUser, transaction: PCTransaction, password: string)

Получение данных транзакции

Пример загрузки доступных транзакций:

import {PCUsersManager, PCTransactionsManager} from '@safetech/pcsdk';

try {
  const users = await PCUsersManager.listStorage();

  // фильтрация списка пользователей
  for (let user of users) {
    if (user.userId === TARGET_USER_ID || user.keyName === TARGET_KEY_NAME) {
      // загрузка списка транзакций
      const transactionsIdArr = await PCTransactionsManager.getTransactionList(user);

      if (transactionsIdArr.length) {
        for (let transactionId of transactionsIdArr) {
          const transaction = await PCTransactionsManager.getTransaction(user, transactionId);

          if (transaction.hasBinaryData()) {
            await PCTransactionsManager.getTransactionBinaryData(user, transaction);
          } else {
            // нет вложений — можно сразу отображать
          }
        }
      }
    }
  }
} catch (error) {
  console.error('Произошла ошибка при получении транзакции', error);
}

Методы PCTransactionsManager.getTransactionList() и PCTransactionsManager.getTransaction() лёгкие и создают небольшой сетевой трафик (несколько килобайт).

Метод PCTransactionsManager.getTransactionBinaryData() может использовать значительно больше трафика при загрузке больших вложений.

Отображение данных транзакции

Транзакцию нужно показать пользователю перед подтверждением или отклонением. Для удобного отображения можно использовать методы:

  • PCTransaction.getTransactionText() - возвращает текст транзакции (может быть null, если есть только вложение).
  • PCTransaction.getSnippet() - возвращает краткое описание (может быть null).
  • PCTransaction.getStoredBinaryData() - возвращает Uint8Array с бинарным вложением (например, PDF).
  • PCTransaction.getTextRenderType() - возвращает тип текста: null или 'raw' для обычного текста, 'markdown' для markdown-формата.
  • PCTransaction.getSnippetRenderType() - аналогично предыдущему, но для краткого описания.

Подтверждение или отклонение транзакции

После загрузки и отображения данных транзакции её можно подтвердить методом sign() или отклонить методом decline() класса PCTransactionsManager.

Пример процесса подтверждения

import { PCTransactionsManager } from @safetech/pcsdk’;

const password = promptPassword();

if (confirmationButtonPressed) {
  PCTransactionsManager.sign(user, transaction, password)
    .then(/* уведомить пользователя об успешной подписи */)
    .catch((error) => {/* обработка ошибки */})
} else {
  PCTransactionsManager.decline(user, transaction, password)
    .then(/* уведомить пользователя об успешном отклонении */)
    .catch((error) => {/* обработка ошибки */})
}

Если транзакция содержит вложение, но оно не было загружено через PCTransactionsManager.getTransactionBinaryData(), транзакция не будет обработана.

Работа с операциями

Операция (PCOperation) - это набор связанных транзакций (PCTransaction), которые могут быть обработаны одним запросом. Это позволяет уменьшить количество сетевых вызовов и ускорить выполнение.

Общий сценарий обработки операции включает следующие шаги:

  1. Вызвать PCUsersManager.listStorage() для получения списка пользователей из хранилища
  2. Отфильтровать пользователей, для которых нужно загрузить операции
  3. Для PCUser вызвать PCOperationsManager.getOperationsList(user: PCUser)
  4. Если доступны операции, получить каждую из них с помощью PCOperationsManager.getOperation(user: PCUser, operationId: string).
    Объект PCOperation содержит данные обо всех входящих в неё транзакциях
  5. Для каждой транзакции в PCOperation.getTransactions() проверить, требуется ли загрузка бинарных данных, вызвав PCTransaction.hasBinaryData(), и при необходимости загрузить их через PCTransactionsManager.getTransactionBinaryData(user: PCUser, transaction: PCTransaction)
  6. Отобразить операцию желаемым образом. Подробнее о методах отображения транзакций см. раздел «Отображение данных транзакции». Помимо данных транзакций, также должно отображаться описание операции (PCOperation.getDescription())
    Операции поддерживают частичную обработку: Пользователь может подтвердить одни транзакции сразу, а другие — отложить. Поэтому следующим шагом будет предоставление интерфейса, позволяющего Пользователю выбирать, какие транзакции он хочет обработать сейчас
  7. Вызвать PCTransaction.processOperation(user: PCUser, operation: PCOperation, password: string, transactionsToConfirm: Array<PCTransaction>, transactionsToDecline: Array<PCTransaction>) для обработки операции

Если часть транзакций не была обработана, Пользователь может вернуться к ним позднее

Получение данных операции

import {PCUsersManager, PCOperationsManager} from '@safetech/pcsdk';

try {
  let targetUser = null;
  // 1. Получить список пользователей
  const users = await PCUsersManager.listStorage();

  // 2. Отфильтровать список
  for (let user of users) {
    if (user.userId === TARGET_USER_ID || user.keyName === TARGET_KEY_NAME) {
      targetUser = user;
    }
  }
  if (null == targetUser) {
    return;
  }

  // 3. Получить список операций пользователя
  const operationIds = await PCOperationsManager.getOperationsList(targetUser);

  if (!operationIds || operationIds.length === 0) {
    // Список пуст
    return;
  }

  // 4. Загрузить данные операции
  const operationId = operationIds[0];
  const pcOperation = await PCOperationsManager.getOperation(targetUser, operationId);

  // Описание операции
  const description = pcOperation.getDescription();

  // Список транзакций
  const transactions = pcOperation.getTransactions();

  // 5. Загрузить бинарные данные транзакций (если есть)
  for (const transaction of transactions) {
    if (transaction.hasBinaryData()) {
      try {
        // Загружаем бинарные данные транзакции
        await PCTransactionsManager.getTransactionBinaryData(targetUser, transaction);

        // Теперь транзакция полностью загружена и готова к обработке
      } catch (error) {
        console.error('Ошибка загрузки бинарных данных транзакции:', err);
      }
    } else {
      // У транзакции нет бинарных данных — можно сразу отображать/обрабатывать
    }
  }
} catch (error) {
  console.error('Произошла ошибка при загрузке операций:', error);
}

Обработка операции

Для обработки операции необходимо указать список транзакций для подтверждения и список транзакций для отклонения.

Каждый из списков может быть пустым, но не оба одновременно. Также списки не должны пересекаться.

Пример ниже демонстрирует, как может быть обработана PCOperation.

import {PCUsersManager, PCOperationsManager} from '@safetech/pcsdk';

// pcOperation создан, транзакции загружены вместе с бинарными данными

/*
 * 6. Пользователь выбирает транзакции для обработки
 * Каждая транзакция - объект из PCOperation.getTransactions()
 */
const transactionsToConfirm = getTransactionsSelectedForConfirmation();
const transactionsToDecline = getTransactionsSelectedForDeclination();

// Запрашиваем ПИН-код токена или пароль для IndexedDB
const password = promptPassword();

// 7. Обработка операции
async function processOperation() {
  try {
    const {confirmationResults, declinationResults} = await PCOperationsManager.processOperation(
      pcUser,
      pcOperation,
      password,
      transactionsToConfirm,
      transactionsToDecline,
    );

    const allResults = [...confirmationResults, ...declinationResults];

    const hasErrors = allResults.some((r) => r.result.errorCode !== 0);

    if (!hasErrors) {
      console.log('Все транзакции обработаны успешно.');
    } else {
      console.warn('Некоторые транзакции не были обработаны.');
    }
  } catch (error) {
    console.error('Произошла ошибка при обработке операции:', error);
  }
}

Метод

PCOperationsManager.processOperation(
    user: PCUser,
    operation: PCOperation,
    password: string,
    transactionsToConfirm: Array<PCTransaction>,
    transactionsToDecline: Array<PCTransaction>)

обрабатывает переданные транзакции и возвращает объект с двумя полями:

  • confirmationResults - результаты обработки транзакций, отправленных на подтверждение
  • declinationResults - результаты обработки транзакций, отправленных на отклонение

Каждый элемент в этих массивах содержит информацию о конкретной транзакции и итог обработки. Формат результата выглядит так:

const confirmationResults = {
  transactionId: 'string',
  result: {
    errorCode: 0,
    errorMessage: 'Success',
  },
};

Где errorCode === 0 означает успешную обработку, а любые другие значения обозначают ошибку.

После обработки транзакций объект PCOperation не обновляется автоматически.

Чтобы получить актуальное состояние, операции, необходимо заново запросить её с сервера с помощью PCOperationsManager.getOperation(targetUser, operationId).

История изменений

Версия: 2.1.0

Новые функции

  • В методе PCSDK.init в options добавлен параметр unixTime для управления источником системного времени (Unix timestamp в секундах)

Версия: 2.0.1

Новые функции

  • Переход с Webpack на Vite
  • Механизм сбора событий при наличии флага collect_events
  • Метод получения названия вложения для транзакций getDataFilename()
  • Возможность подписания CMS-транзакции
  • Возможности подписания CSR-транзакции
  • Метод получения информации о сертификате и запросе на сертификат PCUsersManager.getCertificateInfo(user: PCUser)
  • Удаление ключа без ввода пароля для ECDSA
  • Функциональность обновления пользователя
  • Метод PCUsersManager.importUser(source, deviceId: number | null) для ECDSA deviceId ожидается null

Исправления

  • Теперь Рутокен Lite не отображается в списке токенов
  • Загрузка больших вложений в транзакциях не обрывается по таймауту
  • Разные исправления и улучшения