# Виртуальный плагин «Ключ Онлайн»

Библиотека, которая для сайта выглядит как браузерный плагин КриптоПро, но выполняет подпись
на другом компьютере — на том, где физически вставлен токен.

Сайту не нужно ничего переписывать: он продолжает работать с `window.cadesplugin`.
Меняется одна строка — подключение скрипта.

---

## Зачем это нужно

Обычная схема требует, чтобы у каждого подписанта на его собственном компьютере стояли
КриптоПро CSP, браузерный плагин и был вставлен токен. Это и есть главная причина, по которой
подпись «не работает»: не тот браузер, не установлен плагин, токен у другого сотрудника,
человек в отпуске.

«Ключ Онлайн» разрывает эту связку. Токен остаётся на одном компьютере — например, в бухгалтерии.
Подписывать можно с любого другого устройства и из любого браузера: команда уходит на сервер,
сервер передаёт её программе на компьютере с токеном, оттуда возвращается готовая подпись.

---

## Подключение: 3 шага

### Шаг 1. Получите доступы

В кабинете «Ключ Онлайн» вам выдадут:

| Что | Зачем |
|---|---|
| `server` | адрес сервера, например `https://sign.rimira.ru` |
| `siteKey` | ключ вашего сайта, уходит в заголовке `X-Site-Key` |
| `deviceRef` | какой компьютер с токеном обслуживает ваши запросы |

Домен вашего сайта нужно назвать заранее — сервер отвечает только разрешённым источникам (CORS).

### Шаг 2. Подключите библиотеку

Вместо скрипта плагина КриптоПро подключите наш:

```html
<script>
  window.KeyOnline = {
    server:    'https://sign.rimira.ru',
    siteKey:   'ваш-ключ-сайта',
    deviceRef: 'идентификатор-устройства'
  };
</script>
<script src="https://sign.rimira.ru/keyonline-plugin.js"></script>
```

Либо короче, через атрибуты самого тега:

```html
<script src="https://sign.rimira.ru/keyonline-plugin.js"
        data-server="https://sign.rimira.ru"
        data-site-key="ваш-ключ-сайта"
        data-device-ref="идентификатор-устройства"></script>
```

### Шаг 3. Ничего больше не меняйте

Код подписи остаётся прежним.

---

## Было → стало

**Было** (сайт под плагин КриптоПро):

```html
<script src="/js/cadesplugin_api.js"></script>
```

**Стало:**

```html
<script src="https://sign.rimira.ru/keyonline-plugin.js"
        data-server="https://sign.rimira.ru"
        data-site-key="ваш-ключ-сайта"
        data-device-ref="идентификатор-устройства"></script>
```

Весь остальной код сайта не трогается. Например, этот фрагмент работает без единой правки
и до, и после замены:

```js
await cadesplugin;

const store = await cadesplugin.CreateObjectAsync('CAdESCOM.Store');
await store.Open(
  cadesplugin.CAPICOM_CURRENT_USER_STORE,
  cadesplugin.CAPICOM_MY_STORE,
  cadesplugin.CAPICOM_STORE_OPEN_MAXIMUM_ALLOWED
);

const certs = await store.Certificates;
const count = await certs.Count;          // нумерация с ЕДИНИЦЫ
const cert  = await certs.Item(1);
const name  = await cert.SubjectName;     // свойства асинхронные — как в настоящем плагине

const signer = await cadesplugin.CreateObjectAsync('CAdESCOM.CPSigner');
await signer.propset_Certificate(cert);
await signer.propset_Options(cadesplugin.CAPICOM_CERTIFICATE_INCLUDE_WHOLE_CHAIN);

const signedData = await cadesplugin.CreateObjectAsync('CAdESCOM.CadesSignedData');
await signedData.propset_ContentEncoding(cadesplugin.CADESCOM_BASE64_TO_BINARY);
await signedData.propset_Content(contentBase64);

const signature = await signedData.SignCades(signer, cadesplugin.CADESCOM_CADES_BES, false);

await store.Close();
```

---

## Что поддержано

### Сам объект `cadesplugin`

| Вызов | Поведение |
|---|---|
| `await cadesplugin` / `.then(...)` | готовность плагина; резолвится пустым значением, как настоящий |
| `CreateObjectAsync(имя)` | создание объектов из таблицы ниже |
| константы `CADESCOM_*`, `CAPICOM_*` | полный привычный набор |
| `getLastError(e)` | текст последней ошибки |
| `set_log_level(...)` | принимается и игнорируется |
| событие `cadesplugin_loaded` | рассылается после загрузки |

Дополнительно наши: `cadesplugin.KeyOnline === true` (отличить нас от настоящего плагина),
`KeyOnlineInfo()` (текущие настройки), `KeyOnlineReset()` (сбросить кэш сертификатов).

### Объекты `CreateObjectAsync`

| Объект | Что поддержано |
|---|---|
| `CAdESCOM.About` | `Version`, `PluginVersion`, `MajorVersion`, `MinorVersion`, `BuildVersion`, `CSPName()`, `CSPVersion()` |
| `CAdESCOM.Store` | `Open(...)`, `Certificates`, `Close()`, `Location`, `Name` |
| — `Certificates` | `Count`, `Item(i)` — **нумерация с единицы** |
| — сертификат | `Thumbprint`, `SubjectName`, `IssuerName`, `ValidFromDate`, `ValidToDate`, `SerialNumber`, `Version`, `HasPrivateKey()`, `IsValid()`, `Export(0)` |
| `CAdESCOM.CPSigner` | `propset_Certificate()`, `propset_Options()`, `propset_TSAAddress()`, `propset_CheckCertificate()`, `Certificate`, `Options`, `AuthenticatedAttributes2` |
| `CAdESCOM.CadesSignedData` | `propset_Content()`, `propset_ContentEncoding()`, `propset_DisplayData()`, `SignCades(signer, тип, detached)` |
| `CAdESCOM.RawSignature` | `SignHash(hashedData, cert)` — возвращает HEX-строку, как настоящий плагин |
| `CAdESCOM.CPHashedData` | `propset_Algorithm()`, `propset_DataEncoding()`, `Hash(данные)` |
| `CAdESCOM.CPAttribute` | `propset_Name()`, `propset_Value()` — принимается, но не влияет на подпись |

Все обращения асинхронные, включая чтение свойств: пишите `await cert.Thumbprint`,
как и с настоящим плагином.

Обращение к неподдерживаемому свойству или объекту не возвращает `undefined`, а сразу
бросает понятную ошибку с названием вызова — чтобы несовместимость обнаруживалась
на первом же тесте, а не в виде испорченной подписи у клиента.

---

## Ограничения — честно

Это не плагин КриптоПро, и разница видна в нескольких местах.

**Задержка.** Подпись занимает секунды, а не миллисекунды: запрос идёт на сервер, оттуда на
компьютер с токеном и обратно. Ожидание — 2–5 секунд, предел — 120 секунд (настраивается через
`window.KeyOnline.timeout`). Показывайте пользователю состояние «подписываем», иначе он
нажмёт кнопку второй раз.

**PIN-код вводится на удалённой машине.** Мы принципиально не передаём PIN через браузер:
`propset_KeyPin` всегда возвращает ошибку. Для подписи без участия человека PIN должен быть
заранее сохранён на компьютере с токеном.

**Подпись видна владельцу токена.** Каждая операция попадает в журнал устройства и в кабинет.
Это сделано намеренно: удалённая подпись отличается от кражи ключа только согласием и журналом.

**Не поддержано:**

| Вызов | Почему |
|---|---|
| `CreateObject` (синхронный) | за каждым вызовом стоит сеть; синхронного режима нет и в современном КриптоПро |
| `CPHashedData.Value` | ГОСТ-хеш нечем посчитать в браузере — его считает компьютер с токеном в момент подписи |
| `CPHashedData.SetHashValue` | для удалённой подписи нужны исходные данные, а не готовый хеш |
| `VerifyCades`, `VerifyHash` | проверять подпись в браузере бессмысленно — делайте это на своём сервере |
| `Certificates.Find` | переберите `Item(1..Count)` самостоятельно |
| метка времени (TSA) | адрес принимается, но игнорируется |
| усиленные виды подписи (CAdES-T, CAdES-X Long Type 1) | вид подписи задаёт программа на компьютере с токеном; запрос усиленного вида даёт предупреждение в консоли |
| подписанные атрибуты (`AuthenticatedAttributes2`) | принимаются и игнорируются: время подписания проставляет криптопровайдер на удалённой машине |
| `Certificate.Export()` | работает, только если сервер отдаёт тело сертификата (поле `certBase64`) |
| XML-подпись (`CAdESCOM.SignedXML`) | не реализована |
| работа с несколькими устройствами одновременно | одна страница — одно устройство (`deviceRef`) |

**Если плагин КриптоПро уже установлен**, библиотека НЕ подменяет `window.cadesplugin` и пишет
предупреждение в консоль — иначе сайт, прекрасно работавший локально, начал бы неожиданно
отправлять подпись наружу. Подменить осознанно можно флагом `window.KeyOnline.force = true`;
без подмены библиотека всегда доступна как `window.KeyOnlinePlugin`.

---

## Отладка без сервера

Пока сервер не поднят или устройство не привязано, включите режим макета:

```js
window.KeyOnline = { mock: true };
```

Библиотека ответит тремя заранее заданными сертификатами (один из них намеренно просроченный)
и фиктивной подписью правдоподобной длины. Всю логику страницы — получение списка, выбор,
подпись, показ результата — так можно проверить целиком, не трогая криптографию.

Ещё полезно: `window.KeyOnline.debug = true` печатает в консоль все обращения к серверу.

Готовые примеры лежат рядом: `demo.html` (вход по ЭЦП) и `demo-sign.html` (подпись
произвольного текста). Обе страницы открываются и работают в режиме макета сразу.

---

## Что происходит под капотом

Библиотека делает всего два вида запросов к серверу «Ключ Онлайн».

**Список сертификатов** — кэшируется на 60 секунд, потому что сайты запрашивают его при каждой
загрузке страницы, а гонять ради этого задание на чужой компьютер незачем:

```
POST {server}/api/sign/certificates
X-Site-Key: {siteKey}
{ "deviceRef": "..." }

→ { "ok": true, "certs": [ { "thumbprint": "...", "subject": "...",
                             "validFrom": "...", "validTo": "..." } ] }
```

**Подпись:**

```
POST {server}/api/sign/sign
X-Site-Key: {siteKey}
{ "deviceRef": "...", "thumbprint": "...", "contentBase64": "...",
  "detached": false, "hash": false, "label": "имя вашего сайта" }

→ { "ok": true, "signatureBase64": "..." }
→ { "ok": false, "error": "текст для показа пользователю" }
```

Cookie не используются: сайт авторизуется ключом в заголовке, поэтому кража cookie у посетителя
ничего не даёт. Текст поля `error` библиотека показывает как есть — сервер пишет его сразу
по-русски и для человека.
