From cb40662e4e026efa26268dd0e91ed41980153d3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Wed, 14 Oct 2026 12:21:16 +0300 Subject: [PATCH] v3.26.0: per-TAR default keysets (the preset/SCP80/RAM rework) Every preset TAR entry carries `keyset` (int 1-15) - the keyset every operation addressed to that TAR uses: mandatory for any non-zero MSL (the counters are keyset-linked), optional for MSL 00 (allowed: some cards reject the no-security 00/00 KIc/KID form). The store fills it automatically when the preset has a single keyset, the validation refuses an entry naming an undefined keyset, and the schema is 6 (a v5 file loads leniently; editing it forces the fix). The keyset selectors are gone: the SCP80 TAR field became a select of the preset's entries (applying the TAR's keyset to the form and seeding the SPI1 from its MSL); the RAM operations use the preset's ISD role entry (the PWA sends only the preset id, the ISD TAR, SPI and the counter override - the server resolves the keyset and the keys: presets.tar_keyset, _preset_tar_keyset, _resolve_operation_keyset, _op_keyset, used by send-ota's APDU path, ram-install, ram-install-app and cap-compat); the counter probe and the test runner's scp80 steps default to their TAR's keyset (an explicit kvn stays an override). A security TAR without a keyset refuses the operation ("set it in the Cards tab") - never a silent keyless send - and the start buttons wait for a usable operation-TAR keyset. The TAR probe checks the preset's TAR list only (the standard TAR_PROBE_TARS list and the custom-TAR localStorage are gone; a `tars` list selects a subset, a TAR outside the preset is refused), each row using its own MSL and default keyset. The Cards editor's TAR table gained a Keyset column (the only keyset preselected, an amber marker when a security MSL lacks one). Docs: help EN/RU, api.md's tar-probe contract, both AGENTS. Tests: the store (the field, the auto-fill, the schema, the string forms), the server resolution (send-ota + the refusal, the probe's per-row keysets and refusals), the PWA (the TAR select, the RAM ISD resolution, the keyset-free requests) - several fixtures updated for the new schema. 891 frontend + 885 Python; sw cache simple-v434. --- docs/api.md | 32 +- frontend/help-ru.html | 12 +- frontend/help.html | 12 +- frontend/index.html | 539 +++++++++++------------- frontend/sw.js | 2 +- frontend/tests/cards_counter.test.js | 31 +- frontend/tests/cards_form.test.js | 115 +++-- frontend/tests/indicator.test.js | 6 + frontend/tests/keysets.test.js | 103 ++--- frontend/tests/ram.test.js | 23 +- frontend/tests/ram_counter_flow.test.js | 10 +- frontend/tests/tar_probe.test.js | 98 ++--- pyproject.toml | 2 +- pysim_simple_server/presets.py | 62 ++- pysim_simple_server/server.py | 315 ++++++++++---- tests/test_fetch_routes.py | 118 ++++-- tests/test_presets.py | 72 +++- tests/test_testscript.py | 14 +- 18 files changed, 955 insertions(+), 611 deletions(-) diff --git a/docs/api.md b/docs/api.md index e6c8897..3bc06bb 100644 --- a/docs/api.md +++ b/docs/api.md @@ -341,28 +341,30 @@ first attempt instead of hammering the card. ### `POST /api/tar-probe` Probe which OTA applications the card has registered: one **secured packet per -TAR** (the standard allocations of TS 101 220 V18.3.0 Annex D, Table D.1 unless -a `tars` list is given), each carrying a harmless C-APDU (`SELECT MF` by -default), with the preset's keyset. Each checked TAR uses **its own MSL** when -the preset carries the entry, otherwise the request's `spi1`; a TAR that -resolves to neither is refused with a hint **before any packet is sent**. The -resolved SPI1 must carry a counter check (b5b4 ≠ 00) and ask for a PoR. -Every accepted packet consumes a counter, which is persisted into the keyset -like any other operation, so the probe is refused when the SPI1 has no counter -check or the named preset does not define the keyset number. A missing -`cntr` is seeded from that keyset (v3.20.0; an explicit value wins but never -lowers the stored counter). The delivered SMS carries the OTA CPI IE by +TAR of the named preset** (v3.26.0 — the probe checks the preset's TAR list +only; a `tars` list may select a subset, and a TAR outside the preset is +refused with `400`), each carrying a harmless C-APDU (`SELECT MF` by default). +Each row uses **its own MSL** (the request's `spi1` is only a fallback for a +TAR the preset does not carry) and **its own default keyset** — the keyset +resolved from that TAR's entry, so the request does not carry KIc/KID/keys and +the probe's keyset selector is gone. A TAR whose MSL requires security +(non-zero) without a default keyset, or one naming an undefined keyset, is +refused with a hint **before any packet is sent**. The resolved SPI1 must +carry a counter check (b5b4 ≠ 00) and ask for a PoR. Every accepted packet +consumes a counter, which is persisted into that row's keyset like any other +operation (each keyset has its own counter, TS 102 225 A.1); an explicit +`cntr` seeds the first row only. The delivered SMS carries the OTA CPI IE by default (`includeCpi`, as in `/api/send-ota`). ```json -{"preset_id": "…", "kic": "25", "kid": "25", "kicKey": "…", "kidKey": "…", - "cntr": "0000000187", "apdu": "00A40000023F00", "tars": ["000000", "B00000"]} +{"preset_id": "…", "spi1": "16", "apdu": "00A40000023F00", + "tars": ["000000", "B00000"]} ``` **Response:** `{"success": true, "registered": N, "total": M, "final_cntr": "…", "kvn": 2, "spi1": "16", "spi2": "01", - "results": [{"tar": "000000", "label": "Issuer Security Domain (compact)", - "spi1": "16", "verdict": "registered", "sw": "9000", + "results": [{"tar": "000000", "label": "ISD", "spi1": "16", "kvn": 2, + "verdict": "registered", "sw": "9000", "por_status": "por_ok", "por_sw": "6D00", "por_data": ""}], "steps": [...]}` diff --git a/frontend/help-ru.html b/frontend/help-ru.html index 3097ef1..85e9b75 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -283,7 +283,7 @@

Верхнеуровневая вкладка SCP80 объединяет разделы, связанные с SCP80. Переключение — тремя переключателями: Secured Packet, RAM и TAR-сканер. Собирает защищённые пакеты SCP80 по ETSI TS 102 225. При подключении карты сервер читает её EF.ICCID (2FE2); если в пресете указан тот же номер (цифрами или сырым hex из EF), он автоматически выбирается во всех трёх представлениях SCP80. Вердикт низкого счётчика (cntr_low или предупреждающий ответ ENVELOPE, в котором всё ещё есть такой PoR) не исправляется повторной отправкой: любая операция SCP80/RAM немедленно останавливается. Карта не сообщает свой счётчик (поле CNTR в PoR — копия счётчика пакета), поэтому в уведомлении есть ограниченный пробник «Синхронизировать счётчик» — read-only команда с возрастающими значениями счётчика (шаг удваивается, потолок 32 бита, не более 40 пакетов), пока карта не примет одно; затем сохраняется принятое+1 (поиск, остановленный вердиктом безопасности после отказа по счётчику, тоже сохраняет значение — с янтарным предупреждением проверить настройки безопасности) — и кнопка К пресету для ручного повышения; больше ничего автоматически не записывается.

3.1 Secured Packet

-

Собирает защищённые пакеты SCP80 по ETSI TS 102 225. Селектор Keyset рядом с выбором пресета указывает, из какого keyset берутся ключи и счётчик; отправка отклоняется, если в пресете нет такого номера keyset или KIc/KID несут разные номера (TS 102 225 A.2). Селектор недоступен, пока не выбран пресет карты. Кнопка запуска недоступна, пока не выбран пресет карты и один из его keysets. Поле TAR предлагает записи TAR пресета (с описанием и MSL); при применении пресета TAR операции подставляется, а SPI1 берётся из его MSL (минимальный SPI1, TS 102 226 §8.2.1.3.2.4 — карта проверяет его до обработки защиты и отвечает 0A, если SPI1 пакета ниже). Значение ниже MSL показывает жёлтое предупреждение под формой; введённый вручную TAR сохраняет SPI1 оператора. Поле Счётчик — это переопределение: оно пустое, а следующий счётчик пресета показывается подсказкой (placeholder) — пустое поле позволяет серверу взять счётчик из хранилища и сохранить принятое картой значение; введённое значение используется только для этой отправки и никогда не записывается в пресет, а меньшее значение не откатывает сохранённый счётчик (сохраняются только движения вперёд).

+

Собирает защищённые пакеты SCP80 по ETSI TS 102 225. Селектор TAR перечисляет записи TAR выбранного пресета (метка роли или описание, MSL и keyset по умолчанию); выбор записи применяет её keyset по умолчанию к форме (её KIc/KID и ключи — v3.26.0: селектор keyset убран, его определяет TAR) и подставляет SPI1 из её MSL (минимальный SPI1, TS 102 226 §8.2.1.3.2.4 — карта проверяет его до обработки защиты и отвечает 0A, если SPI1 пакета ниже). Значение ниже MSL показывает жёлтое предупреждение под формой. Кнопка запуска недоступна, пока не выбран пресет карты и keyset TAR операции не пригоден: TAR, чей MSL требует защиты (любой ненулевой MSL — счётчики привязаны к keyset), обязан иметь keyset по умолчанию в пресете, а TAR с MSL 00 может идти без ключей или всё же нести их (некоторые карты отвергают форму 00/00 без защиты). Отправка отклоняется, когда в пресете нет указанного номера keyset или KIc/KID несут разные номера (TS 102 225 A.2). Поле Счётчик — это переопределение: оно пустое, а следующий счётчик пресета показывается подсказкой (placeholder) — пустое поле позволяет серверу взять счётчик из хранилища и сохранить принятое картой значение; введённое значение используется только для этой отправки и никогда не записывается в пресет, а меньшее значение не откатывает сохранённый счётчик (сохраняются только движения вперёд).

Структура пакета

@@ -315,7 +315,7 @@

Кнопка Проверить в pySim сверяет собранный пакет с эталонной реализацией OtaDialectSms.encode_cmd. Кнопка Отправить на карту доставляет пакет через ENVELOPE SMS-PP-DOWNLOAD (при подключении к серверу). 2-байтовый префикс CPL передаётся, когда пакет зашифрован или несёт RC/CC/DS (входит в их входные данные), а также при конкатенации SMS; одиночное незащищённое SMS сохраняет форму pySim, начинающуюся с CHL (TS 31.115 Table 1 NOTE). Под полем пакета показывается его размер и число SMS; пакет, не помещающийся в одно SMS, отправляется конкатенированной загрузкой (TS 31.115 §4.3) — пакет делится на части SMS-пользовательских данных (первое SMS 132 октета, последующие по 134; первое дополнительно несёт информационные элементы конкатенации и CPI), и сегменты отправляются по порядку. Более 5 сегментов отклоняется: это практический предел буфера конкатенации карты. Полученный Proof of Receipt декодируется и показывается строкой статуса PoR (статус, TAR, счётчик, сырой PoR); статусное слово и данные ответа последней команды подставляются в подвкладку «Разбор R-APDU» (Remote APDU → «Разбор»), а успешный PoR увеличивает счётчик повторов (когда в SPI1 есть проверка счётчика — b5 b4 ≠ 00; без неё счётчик не увеличивается и не сохраняется) и очищает пакет — сервер сохраняет принятый счётчик в пресет, а поле счётчика остаётся пустым (подсказка следует за сохранённым значением), если оператор не ввёл переопределение.

3.2 RAM

-

Выполняет операции удалённого управления приложениями (Remote Application Management) как защищённые пакеты SCP80 через SMS-PP-DOWNLOAD ENVELOPE. Карта должна поддерживать SCP03 (AES или 3DES). Пресет карты со вкладки Карты обеспечивает ключи, таблицу TAR и счётчик (RAM использует ISD TAR пресета, по умолчанию 000000; SPI1 берётся из MSL этого TAR). Селектор Keyset рядом с выбором пресета указывает, из какого keyset берутся ключи и счётчик операции. Селектор недоступен, пока не выбран пресет карты. Кнопка запуска недоступна, пока не выбран пресет карты и один из его keysets. Селектор Формат фиксирует формат удалённой команды: Авто (определение) опрашивает read-only команду GET STATUS в компактном виде и в обоих expanded-кодированиях и оставляет первое, которое карта выполнила; Компактный отправляет голую C-APDU; Expanded (AA, определённая длина) / Expanded (AE 80, неопределённая длина) заворачивают её в шаблон Command Scripting из TS 102 226 §5.2.1 (на живой карте выполняются обе; шаблон, у которого заявленная длина не совпадает с телом, получает Bad format TLV — операция показывает это как ошибку шага). Запросы листингов и реестра следуют этому формату: компактные листинги несут сцепленный GET RESPONSE, а expanded-прогон отправляет те же запросы P2=00 обёрнутыми и без цепочки (форма эталонного загрузчика TCA; Le=’00’ возвращает листинг сразу), и запрос реестра, отклонённый картой в текущем формате, повторяется в остальных — отчёт называет тот, который сработал. Страница следующего вхождения (P2=03), отклонённая форматом, тоже повторяется в остальных форматах — сначала expanded-кодирования (первым то, что обслужило предыдущую страницу, если оно expanded), компактная командная строка последней: компактная попытка страницы может израсходовать страницу, не доставив её (карта отвечает 61xx, а сцепленный GET RESPONSE отклоняется — все последующие форматы видят вхождение уже использованным) — некоторые карты отдают expanded-страницу только в шаблоне AA — и пометка (только первая страница) ставится, только если карта обещала продолжение (6310), а ни один формат не смог его получить; в режиме Авто проводник также опрашивает по AID записи, оставшиеся без деталей (несколько дополнительных пакетов). Блок ресурсов карты также называет предполагаемый релиз ETSI карты (версия uicc.toolkit → REL-N, то же сопоставление, что и в анализе CAP; показывается, только если расширенный реестр вернул версии). Пакет, чей собственный листинг не несёт версии, берёт её у своего экземпляра апплета, если он есть — модульные AID пакета это AID экземпляров апплета, и строка получает соответствующую пометку — так что карта, отклоняющая все формы со списком тегов, всё равно показывает версии пакетов везде, где есть экземпляр апплета.

+

Выполняет операции удалённого управления приложениями (Remote Application Management) как защищённые пакеты SCP80 через SMS-PP-DOWNLOAD ENVELOPE. Карта должна поддерживать SCP03 (AES или 3DES). Пресет карты со вкладки Карты обеспечивает ключи, таблицу TAR и счётчик (RAM использует ISD TAR пресета, по умолчанию 000000; SPI1 берётся из MSL этого TAR). v3.26.0: keyset операции — это keyset по умолчанию записи ISD TAR (селектор keyset убран), сервер разрешает его (и его ключи) из хранилища пресета. Кнопка запуска недоступна, пока не выбран пресет карты и keyset ISD TAR не пригоден: при ненулевом MSL нужен keyset по умолчанию, TAR с MSL 00 может идти без ключей. Селектор Формат фиксирует формат удалённой команды: Авто (определение) опрашивает read-only команду GET STATUS в компактном виде и в обоих expanded-кодированиях и оставляет первое, которое карта выполнила; Компактный отправляет голую C-APDU; Expanded (AA, определённая длина) / Expanded (AE 80, неопределённая длина) заворачивают её в шаблон Command Scripting из TS 102 226 §5.2.1 (на живой карте выполняются обе; шаблон, у которого заявленная длина не совпадает с телом, получает Bad format TLV — операция показывает это как ошибку шага). Запросы листингов и реестра следуют этому формату: компактные листинги несут сцепленный GET RESPONSE, а expanded-прогон отправляет те же запросы P2=00 обёрнутыми и без цепочки (форма эталонного загрузчика TCA; Le=’00’ возвращает листинг сразу), и запрос реестра, отклонённый картой в текущем формате, повторяется в остальных — отчёт называет тот, который сработал. Страница следующего вхождения (P2=03), отклонённая форматом, тоже повторяется в остальных форматах — сначала expanded-кодирования (первым то, что обслужило предыдущую страницу, если оно expanded), компактная командная строка последней: компактная попытка страницы может израсходовать страницу, не доставив её (карта отвечает 61xx, а сцепленный GET RESPONSE отклоняется — все последующие форматы видят вхождение уже использованным) — некоторые карты отдают expanded-страницу только в шаблоне AA — и пометка (только первая страница) ставится, только если карта обещала продолжение (6310), а ни один формат не смог его получить; в режиме Авто проводник также опрашивает по AID записи, оставшиеся без деталей (несколько дополнительных пакетов). Блок ресурсов карты также называет предполагаемый релиз ETSI карты (версия uicc.toolkit → REL-N, то же сопоставление, что и в анализе CAP; показывается, только если расширенный реестр вернул версии). Пакет, чей собственный листинг не несёт версии, берёт её у своего экземпляра апплета, если он есть — модульные AID пакета это AID экземпляров апплета, и строка получает соответствующую пометку — так что карта, отклоняющая все формы со списком тегов, всё равно показывает версии пакетов везде, где есть экземпляр апплета.

Операции

ПолеРазмерОписание
@@ -343,13 +343,13 @@

3.3 TAR-сканер

-

Третья пилюля вкладки SCP80 показывает, какие OTA-приложения зарегистрированы на карте: отправляется по одному защищённому пакету на каждый выбранный TAR — стандартные значения из TS 101 220 V18.3.0 Annex D, Table D.1, каждая строка несёт документ-источник — с безобидной C-APDU (по умолчанию SELECT MF), пресетом и keyset этого представления — каждый проверяемый TAR использует свой MSL, если запись есть в пресете, иначе SPI1 из общей формы; TAR, для которого нет ни того, ни другого, отклоняется до отправки. Разрешённый SPI1 должен иметь проверку счётчика (b5b4 ≠ 00) и запрашивать PoR. Кнопка Execute недоступна, пока не выбран пресет карты и один из его keysets.

-

Список предзаполнен стандартными TAR (все отмечены; кнопки Выбрать все / Снять все переключают быстро), свои TAR добавляются кнопкой Добавить TAR (три hex-байта и необязательное описание). Пользовательские записи хранятся в localStorage по возможности — их потеря безвредна.

+

Третья пилюля вкладки SCP80 показывает, какие OTA-приложения зарегистрированы на карте: отправляется по одному защищённому пакету на каждый выбранный TAR с безобидной C-APDU (по умолчанию SELECT MF). v3.26.0: строки — это список TAR выбранного пресета (никаких произвольных TAR), каждая строка использует свой MSL и свой keyset по умолчанию (разрешается из хранилища пресета — селектор keyset у сканера убран), а TAR, чей MSL требует защиты и у которого нет keyset по умолчанию, отклоняется до отправки. Разрешённый SPI1 должен иметь проверку счётчика (b5b4 ≠ 00) и запрашивать PoR. Кнопка Execute недоступна, пока не выбран пресет карты.

+

Список показывает записи TAR выбранного пресета (все отмечены; кнопки Выбрать все / Снять все переключают быстро); подсказка строки называет её MSL и keyset по умолчанию.

Каждый TAR классифицируется по ответу карты: зарегистрировано (ENVELOPE 9000 + PoR со SW и данными приложения), принято, нет ответа (PoR без R-APDU), принято, нет PoR (пакет молча отброшен) и отклонено (ENVELOPE не 9000, обычно 6200 для TAR без зарегистрированного приложения). Таблица результата появляется в окне операции; строка под кнопкой Execute показывает зарегистрировано N / M.

-

Каждый принятый картой пакет расходует счётчик, который сохраняется в keyset пресета — проба отклоняется, если в разрешённом SPI1 нет проверки счётчика или в пресете нет такого номера keyset. На живой карте: 000000, B00000 и B00001 зарегистрированы (приложения компактного формата), расширенный формат, USAT, мультиплексирование и CASD отклонены с 6200.

+

Каждый принятый картой пакет расходует счётчик, который сохраняется в keyset пресета — проба отклоняется, если в разрешённом SPI1 нет проверки счётчика, если в пресете нет keyset по умолчанию для строки (при ненулевом MSL он обязателен) или запись называет несуществующий номер keyset. На живой карте: 000000, B00000 и B00001 зарегистрированы (приложения компактного формата), расширенный формат, USAT, мультиплексирование и CASD отклонены с 6200.

4. Вкладка «Карты»

-

Хранит пресеты карт на сервере (~/.pysim-simple-server/card_presets.json; переопределяется --card-presets), чтобы представление Secured Packet могло автоматически подставлять ключи и параметры, слушатель SCP81 HTTP OTA — находить PSK-ключи, а счётчики SCP80, израсходованные операцией, сохранялись самой операцией — закрытая вкладка или второе окно браузера больше не могут потерять инкремент. Пресет содержит несколько keysets — номер keyset задаётся нибблом b8..b5 его KIc/KID (TS 102 225 §5.1.2/A.2): KIc и KID одного keyset должны нести один и тот же номер (01–0F; A.2 отвергает несовпадение), и у каждого keyset на карте свой счётчик (Annex A.1), поэтому счётчики ведутся по keysets. В форме keysets редактируются строками — по одной на номер, с кнопками Добавить keyset / Удалить keyset; номер в строке вычисляется из байта KIc/KID, а 00 («без защиты») не принимается как keyset. Список загружается при подключении PWA и экспортируется/импортируется как JSON (импорт принимает и пресеты старых сборок — плоские и с keysets). «Карты» — верхнеуровневая вкладка: по умолчанию показывается список пресетов, а Добавить пресет / Редактировать открывают редактор (отдельное представление) — Сохранить сохраняет пресет и возвращает к списку, Отмена отменяет правки. При подключении карты читается её EF.ICCID, и пресет с тем же ICCID автоматически выбирается во всех трёх представлениях SCP80; если подключённая карта не совпадает ни с одним пресетом — или выбранный пресет принадлежит другой карте — все три представления SCP80 показывают жёлтое предупреждение над выбором пресета. В списке подсвечивается пресет подключённой карты. Поля редактора сгруппированы в блоки с рамкой: PIN/ADM (необязательные коды карты — PIN1/PUK1/PIN2/PUK2 по 4–8 десятичных цифр и ключ ADM — сохраняются для будущего использования), TAR (таблица TAR), SCP80 (GSM 03.48, ETSI TS 102 225) (keysets и их счётчики) и SCP81 (HTTP OTA) (пара PSK identity/ключ с пояснением о выборе ключа). В шапке редактора также выбирается релиз CAT (USAT) карты — по нему механизм релизов скрывает элементы CAT, появившиеся позже (параметры событий в Симуляторе); «не указан» означает Rel-12, и неуказанный релиз ничего не фильтрует. В шапке отображаются серые маркеры SCP80/SCP81 и значок ключа на бейдже ADM, когда в пресете с ICCID подключённой карты эти поля заполнены.

+

Хранит пресеты карт на сервере (~/.pysim-simple-server/card_presets.json; переопределяется --card-presets), чтобы представление Secured Packet могло автоматически подставлять ключи и параметры, слушатель SCP81 HTTP OTA — находить PSK-ключи, а счётчики SCP80, израсходованные операцией, сохранялись самой операцией — закрытая вкладка или второе окно браузера больше не могут потерять инкремент. Пресет содержит несколько keysets — номер keyset задаётся нибблом b8..b5 его KIc/KID (TS 102 225 §5.1.2/A.2): KIc и KID одного keyset должны нести один и тот же номер (01–0F; A.2 отвергает несовпадение), и у каждого keyset на карте свой счётчик (Annex A.1), поэтому счётчики ведутся по keysets. В форме keysets редактируются строками — по одной на номер, с кнопками Добавить keyset / Удалить keyset; номер в строке вычисляется из байта KIc/KID, а 00 («без защиты») не принимается как keyset. Список загружается при подключении PWA и экспортируется/импортируется как JSON (импорт принимает и пресеты старых сборок — плоские и с keysets). «Карты» — верхнеуровневая вкладка: по умолчанию показывается список пресетов, а Добавить пресет / Редактировать открывают редактор (отдельное представление) — Сохранить сохраняет пресет и возвращает к списку, Отмена отменяет правки. При подключении карты читается её EF.ICCID, и пресет с тем же ICCID автоматически выбирается во всех трёх представлениях SCP80; если подключённая карта не совпадает ни с одним пресетом — или выбранный пресет принадлежит другой карте — все три представления SCP80 показывают жёлтое предупреждение над выбором пресета. В списке подсвечивается пресет подключённой карты. Поля редактора сгруппированы в блоки с рамкой: PIN/ADM (необязательные коды карты — PIN1/PUK1/PIN2/PUK2 по 4–8 десятичных цифр и ключ ADM — сохраняются для будущего использования), TAR (таблица TAR — у каждой записи свой TAR, MSL и keyset по умолчанию: keyset, который используют все операции к этому TAR; обязателен при ненулевом MSL — счётчики привязаны к keyset — и необязателен при MSL 00; пресет с единственным keyset заполняет его автоматически), SCP80 (GSM 03.48, ETSI TS 102 225) (keysets и их счётчики) и SCP81 (HTTP OTA) (пара PSK identity/ключ с пояснением о выборе ключа). В шапке редактора также выбирается релиз CAT (USAT) карты — по нему механизм релизов скрывает элементы CAT, появившиеся позже (параметры событий в Симуляторе); «не указан» означает Rel-12, и неуказанный релиз ничего не фильтрует. В шапке отображаются серые маркеры SCP80/SCP81 и значок ключа на бейдже ADM, когда в пресете с ICCID подключённой карты эти поля заполнены.

diff --git a/frontend/help.html b/frontend/help.html index 4e7e867..3315a59 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -282,7 +282,7 @@

The SCP80 top-level tab groups the SCP80-related views. It is switched by three pills: Secured Packet, RAM and TAR probe. Assembles SCP80 secured packets per ETSI TS 102 225. When a card is equipped the server reads its EF.ICCID (2FE2); if a card preset carries the same number (digits, or the raw EF hex), it is selected automatically in all three SCP80 views.

3.1 Secured Packet

-

Builds SCP80 secured packets per ETSI TS 102 225. The Keyset selector next to the card preset selector picks which keyset of the preset supplies the keys and the counter; the send is refused when the preset does not define that keyset number or when its KIc/KID carry different numbers (TS 102 225 A.2). It is disabled until a card preset is selected. The start button stays disabled until a card preset and one of its keysets are selected. The packet's TAR field suggests the preset's TAR entries (each with its description and MSL); applying a preset sets the operation's TAR and seeds the SPI1 from that TAR's MSL (Minimum SPI1, TS 102 226 §8.2.1.3.2.4 — the card checks it before the security processing and answers 0A when the packet's SPI1 is below it). A value below the MSL shows an amber warning under the form; a hand-typed TAR keeps the operator's SPI1. The Counter field is an override: it starts empty and shows the preset's next counter as its placeholder — an empty field lets the server seed the packet from the stored counter and persist the value the card accepted; a typed value is used for that send only, is never written to the preset, and a lower one never rewinds the stored counter (only forward moves are persisted).

+

Builds SCP80 secured packets per ETSI TS 102 225. The TAR selector lists the selected preset's TAR entries (role label or description, MSL and default keyset); choosing one applies that TAR's default keyset to the form (its KIc/KID and keys — v3.26.0: the keyset selector is gone, the TAR drives it) and seeds the SPI1 from its MSL (Minimum SPI1, TS 102 226 §8.2.1.3.2.4 — the card checks it before the security processing and answers 0A when the packet's SPI1 is below it). A value below the MSL shows an amber warning under the form. The start button stays disabled until a card preset is selected and the operation TAR's keyset is usable: a TAR whose MSL requires security (any non-zero MSL — the counters are keyset-linked) must have a default keyset in the preset, while an MSL 00 TAR may go keyless or still carry one (some cards reject the no-security 00/00 KIc/KID form). The send is refused when the preset does not define the named keyset number or when its KIc/KID carry different numbers (TS 102 225 A.2). The Counter field is an override: it starts empty and shows the preset's next counter as its placeholder — an empty field lets the server seed the packet from the stored counter and persist the value the card accepted; a typed value is used for that send only, is never written to the preset, and a lower one never rewinds the stored counter (only forward moves are persisted).

Packet structure

ПолеОписание
@@ -314,7 +314,7 @@

A “Verify vs pySim” button cross-checks the assembled packet against pySim’s reference OtaDialectSms.encode_cmd. A “Send to Card” button delivers it via SMS-PP-DOWNLOAD ENVELOPE (when connected to the server). The 2-byte CPL prefix is transmitted whenever the packet is ciphered or carries RC/CC/DS (it is part of their input) and whenever the packet is concatenated; a single unprotected SMS keeps pySim’s CHL-first form (TS 31.115 Table 1 NOTE). Under the packet field the app reports its size and how many SMS it takes; a packet that does not fit one SMS is sent as a concatenated download (TS 31.115 §4.3) — the packet is split into SMS user-data parts (first SM 132 octets, following ones 134; the first one additionally carries the concatenation and CPI information elements) and the segments are sent in order. More than 5 segments is refused, that being the practical limit of the card’s concatenation buffer. The returned Proof of Receipt is decoded and shown as a PoR status line (status, TAR, counter, raw PoR); the last command’s status word and response data are filled into the R-APDU parser panel, and a successful PoR advances the replay counter (when SPI1 carries a counter check — b5 b4 ≠ 00; without one no counter is advanced or saved) and clears the packet — the server persists the accepted counter into the preset, the form’s counter field stays empty (its placeholder follows the stored value) unless the operator typed an override. A low-counter verdict (cntr_low, or a warning ENVELOPE answer that still carries such a PoR) is not recoverable by retrying: every SCP80/RAM operation stops immediately. The card never reports its counter (the PoR’s CNTR field is a copy of the packet’s), so the notice offers a bounded Sync counter probe — a read-only command with increasing counter values (doubling steps, a 32-bit ceiling, at most 40 packets) until the card accepts one, then accepted+1 is stored (a search stopped by a security verdict after a counter rejection saves the value too, with an amber warning to check the security settings) — and a Go to preset button for raising the counter by hand; nothing else is written automatically.

3.2 RAM

-

Delivers Remote Application Management operations as SCP80 secured packets via SMS-PP-DOWNLOAD ENVELOPE. The card must support SCP03 (AES or 3DES). A saved card preset from the Cards tab provides the keys, the TAR table and the counter (RAM uses the preset's ISD TAR, 000000 by default; the SPI1 is derived from that TAR's MSL). The Keyset selector next to the preset selector picks the keyset whose keys and counter the operation uses. It is disabled until a card preset is selected. The start button stays disabled until a card preset and one of its keysets are selected. The Format selector pins the remote command format: Auto (detect) probes the read-only GET STATUS command compact and in both expanded codings and keeps the first that the card executes, Compact sends the bare C-APDU, and Expanded (AA, definite length) / Expanded (AE 80, indefinite length) wrap it in the TS 102 226 §5.2.1 scripting template (both execute on the live ISD; a template whose declared lengths do not match its body draws a Bad format TLV, which the operation shows as the step failure). The listing and registry queries follow that format: compact listings keep the chained GET RESPONSE, while an expanded run sends the same P2=00 queries wrapped and unchained (the reference TCA loader’s form, Le=’00’ returns the listing directly), and a registry query the card refuses in the current format is retried in the other formats — the report names the one that served it. A next-occurrence page (P2=03) that a format refuses is retried in the other formats too — the expanded codings first (the serving one first when it is expanded), the compact command string last, because a compact page attempt can consume the page without delivering it (the card answers 61xx and the chained GET RESPONSE fails; every later format then sees the occurrence already gone) — some cards serve the expanded next page only wrapped in the AA template — and a listing is marked (first page only) only when the card promised more (6310) and no format could fetch the follow-up; in Auto the explorer also queries entries the bulk listing left without details individually by AID (a few extra packets). The card-resources block also names the card’s inferred ETSI release (uicc.toolkit version → REL-N, the same mapping as the CAP analysis; shown only when the expanded registry returned the versions). A package whose own listing carries no version derives it from its applet instance when one exists — the package’s module AIDs are the applet instance AIDs, and the row is annotated as coming from the app instance — so a card that refuses every tag-list form still shows package versions wherever an applet instance exists.

+

Delivers Remote Application Management operations as SCP80 secured packets via SMS-PP-DOWNLOAD ENVELOPE. The card must support SCP03 (AES or 3DES). A saved card preset from the Cards tab provides the keys, the TAR table and the counter (RAM uses the preset's ISD TAR, 000000 by default; the SPI1 is derived from that TAR's MSL). v3.26.0: the operation's keyset is the ISD TAR entry's default keyset — the keyset selector is gone — and the server resolves it (and its keys) from the preset store. The start button stays disabled until a card preset is selected and the ISD TAR's keyset is usable: a non-zero MSL requires a default keyset, an MSL 00 TAR may go keyless. The Format selector pins the remote command format: Auto (detect) probes the read-only GET STATUS command compact and in both expanded codings and keeps the first that the card executes, Compact sends the bare C-APDU, and Expanded (AA, definite length) / Expanded (AE 80, indefinite length) wrap it in the TS 102 226 §5.2.1 scripting template (both execute on the live ISD; a template whose declared lengths do not match its body draws a Bad format TLV, which the operation shows as the step failure). The listing and registry queries follow that format: compact listings keep the chained GET RESPONSE, while an expanded run sends the same P2=00 queries wrapped and unchained (the reference TCA loader’s form, Le=’00’ returns the listing directly), and a registry query the card refuses in the current format is retried in the other formats — the report names the one that served it. A next-occurrence page (P2=03) that a format refuses is retried in the other formats too — the expanded codings first (the serving one first when it is expanded), the compact command string last, because a compact page attempt can consume the page without delivering it (the card answers 61xx and the chained GET RESPONSE fails; every later format then sees the occurrence already gone) — some cards serve the expanded next page only wrapped in the AA template — and a listing is marked (first page only) only when the card promised more (6310) and no format could fetch the follow-up; in Auto the explorer also queries entries the bulk listing left without details individually by AID (a few extra packets). The card-resources block also names the card’s inferred ETSI release (uicc.toolkit version → REL-N, the same mapping as the CAP analysis; shown only when the expanded registry returned the versions). A package whose own listing carries no version derives it from its applet instance when one exists — the package’s module AIDs are the applet instance AIDs, and the row is annotated as coming from the app instance — so a card that refuses every tag-list form still shows package versions wherever an applet instance exists.

Operations

FieldSizeDescription
@@ -342,13 +342,13 @@

3.3 TAR probe

-

The third SCP80 pill probes which OTA applications the card has registered: it sends one secured packet per selected TAR — the standard allocations of TS 101 220 V18.3.0 Annex D, Table D.1, each row carrying the document it comes from — with a harmless C-APDU (default SELECT MF) using the view's preset and keyset — each checked TAR uses its own MSL when the preset carries the entry, otherwise the SPI1 from the shared form; a TAR resolving to neither is refused before anything is sent. The resolved SPI1 must carry a counter check (b5b4 ≠ 00) and request a PoR. The Execute button stays disabled until a card preset and one of its keysets are selected.

-

The list is pre-filled with the standard TARs (all checked; Select all / Select none switch them quickly) and custom TARs can be added with Add TAR (three hex bytes plus an optional description). Custom entries are kept in localStorage on a best-effort basis — losing them is harmless.

+

The third SCP80 pill probes which OTA applications the card has registered: it sends one secured packet per selected TAR with a harmless C-APDU (default SELECT MF). v3.26.0: the rows are the selected preset's TAR list (no arbitrary TARs), each row uses its own MSL and its own default keyset (resolved from the preset store — the probe's keyset selector is gone), and a TAR whose MSL requires security without a default keyset is refused before anything is sent. The resolved SPI1 must carry a counter check (b5b4 ≠ 00) and request a PoR. The Execute button stays disabled until a card preset is selected.

+

The list shows the selected preset's TAR entries (all checked; Select all / Select none switch them quickly); each row's tooltip names its MSL and default keyset.

Every TAR is classified from the card's answer: registered (ENVELOPE 9000 + a PoR carrying the application's own status word and data), accepted, no answer (a PoR without an R-APDU), accepted, no PoR (the packet was silently dropped) and refused (a non-9000 ENVELOPE, typically 6200 for a TAR without a registered application). The result table appears in the operation popup; the line under Execute summarises registered N / M.

-

Each packet the card accepts consumes a counter, which is saved into the preset's keyset — the probe is refused when the resolved SPI1 has no counter check or the named preset does not define the keyset number. Observed on a live card: 000000, B00000 and B00001 registered (compact-format applications), the expanded-format TARs, USAT, multiplexing and CASD refused with 6200.

+

Each packet the card accepts consumes a counter, which is saved into the preset's keyset — the probe is refused when the resolved SPI1 has no counter check, when the preset lacks the row's default keyset (a non-zero MSL requires one) or the entry names an undefined keyset number. Observed on a live card: 000000, B00000 and B00001 registered (compact-format applications), the expanded-format TARs, USAT, multiplexing and CASD refused with 6200.

4. Cards tab

-

Stores card presets on the server (~/.pysim-simple-server/card_presets.json; --card-presets overrides it) so the Secured Packet view can auto-fill keys and parameters, the SCP81 HTTP OTA listener can look up PSK keys, and the SCP80 counters an operation consumed are persisted with the operation itself — a closed tab or a second browser window can no longer lose an increment. A preset carries several keysets — a keyset's number is the b8..b5 nibble of its KIc/KID (TS 102 225 §5.1.2/A.2): KIc and KID of a keyset must carry the same number (01–0F; A.2 rejects a mismatch), and each keyset has its own counter on the card (Annex A.1), so counters are tracked per keyset. In the form the keysets are edited as rows — one per number, with Add keyset / Remove keyset; each row shows the number derived from its KIc/KID byte, and 00 (no security) is not accepted as a keyset. The list is loaded when the PWA connects and can be exported/imported as JSON (the import also takes presets exported by older builds, flat or keyset-shaped). Cards is a top-level tab: it shows the preset list by default, and Add preset / Edit open the editor (a separate view) — Save stores the preset and returns to the list, Cancel discards the edits. When the card is equipped its EF.ICCID is read and the preset with the same ICCID is selected automatically in all three SCP80 views; when the equipped card matches no preset — or the selected preset belongs to another card — all three SCP80 views show an amber warning above the preset selector. The list highlights the equipped card's preset. The editor groups the fields into bordered blocks: PIN/ADM (the optional card codes — PIN1/PUK1/PIN2/PUK2 as 4–8 decimal digits and the ADM key — stored for later use), TAR (the TAR table), SCP80 (GSM 03.48, ETSI TS 102 225) (the keysets and their counters) and SCP81 (HTTP OTA) (the PSK identity/key pair with the key-selection note). The editor's header row also carries the card's CAT (USAT) release — the release guard hides CAT items introduced after it (the Simulator's event parameters); unspecified means Rel-12, and an unset release never filters anything away. The header shows gray SCP80/SCP81 markers and a key glyph on the ADM badge when the preset matching the equipped card's ICCID has those settings filled in.

+

Stores card presets on the server (~/.pysim-simple-server/card_presets.json; --card-presets overrides it) so the Secured Packet view can auto-fill keys and parameters, the SCP81 HTTP OTA listener can look up PSK keys, and the SCP80 counters an operation consumed are persisted with the operation itself — a closed tab or a second browser window can no longer lose an increment. A preset carries several keysets — a keyset's number is the b8..b5 nibble of its KIc/KID (TS 102 225 §5.1.2/A.2): KIc and KID of a keyset must carry the same number (01–0F; A.2 rejects a mismatch), and each keyset has its own counter on the card (Annex A.1), so counters are tracked per keyset. In the form the keysets are edited as rows — one per number, with Add keyset / Remove keyset; each row shows the number derived from its KIc/KID byte, and 00 (no security) is not accepted as a keyset. The list is loaded when the PWA connects and can be exported/imported as JSON (the import also takes presets exported by older builds, flat or keyset-shaped). Cards is a top-level tab: it shows the preset list by default, and Add preset / Edit open the editor (a separate view) — Save stores the preset and returns to the list, Cancel discards the edits. When the card is equipped its EF.ICCID is read and the preset with the same ICCID is selected automatically in all three SCP80 views; when the equipped card matches no preset — or the selected preset belongs to another card — all three SCP80 views show an amber warning above the preset selector. The list highlights the equipped card's preset. The editor groups the fields into bordered blocks: PIN/ADM (the optional card codes — PIN1/PUK1/PIN2/PUK2 as 4–8 decimal digits and the ADM key — stored for later use), TAR (the TAR table — each entry carries its TAR, its MSL and its default keyset: the keyset every operation addressed to that TAR uses; mandatory for a non-zero MSL — the counters are keyset-linked — optional for MSL 00, and a preset with a single keyset fills it automatically), SCP80 (GSM 03.48, ETSI TS 102 225) (the keysets and their counters) and SCP81 (HTTP OTA) (the PSK identity/key pair with the key-selection note). The editor's header row also carries the card's CAT (USAT) release — the release guard hides CAT items introduced after it (the Simulator's event parameters); unspecified means Rel-12, and an unset release never filters anything away. The header shows gray SCP80/SCP81 markers and a key glyph on the ADM badge when the preset matching the equipped card's ICCID has those settings filled in.

diff --git a/frontend/index.html b/frontend/index.html index 30edae4..637e49c 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -528,8 +528,6 @@ - - @@ -650,9 +648,8 @@
- - - + +
@@ -698,10 +695,6 @@
-
- - -
-
- - -
- TARs to probe — TS 101 220 V18.3.0 Annex D, Table D.1 + TARs to probe (the preset's TAR list)
-
- - - -
@@ -1719,6 +1703,7 @@ + @@ -1907,7 +1892,7 @@ // ===== Version ===== // Single source of truth for the PWA version: shown in the header and used // by the server version check in pysimConnect(). -const SIMPLE_VERSION = '3.25.8'; +const SIMPLE_VERSION = '3.26.0'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== view history (the browser Back/Forward buttons) ===== @@ -5032,20 +5017,71 @@ function spPresetTar(role) { return cardsRoleTar(c, role); } -// The TAR field's suggestions: the preset's TAR table, each entry labelled -// with its description (or its role) and MSL - free text stays possible. -function spTarListRender(preset) { - const list = document.getElementById('sp-tar-list'); - if (!list) return; +// The SCP80 TAR selector (v3.26.0): the preset's TAR table - the role entries +// labelled by their role, the free rows by their description - each with its +// MSL and default keyset. A change applies the TAR's keyset to the form and +// seeds the SPI1 from its MSL. +function spTarOptionsHtml(preset) { const roleLabel = { isd: 'ISD', uiccRfm: 'UICC RFM', usimRfm: 'ADF RFM' }; - list.innerHTML = cardsTarList(preset).map(e => { + const list = cardsTarList(preset); + if (!list.length) return ''; + return list.map(e => { + const bits = [e.tar || '']; const label = e.desc || roleLabel[e.role] || ''; - const text = label + (e.msl ? (label ? ' ' : '') + '(MSL ' + e.msl + ')' : ''); - return ''; + if (label) bits.push(label); + if (e.msl) bits.push('MSL ' + e.msl); + if (e.keyset) bits.push(t('Keyset') + ' ' + e.keyset); + return ''; }).join(''); } +// Render the SCP80 TAR select from a preset (the view's current one when not +// given); the previous selection is kept when the new list carries it. +function spTarRender(preset) { + const sel = document.getElementById('sp-tar'); + if (!sel) return; + if (preset === undefined) { + const idx = spPresetIdx(); + preset = idx >= 0 ? cards[idx] : null; + } + const prev = sel.value; + sel.disabled = !preset; + sel.innerHTML = preset ? spTarOptionsHtml(preset) + : ''; + if (prev && Array.prototype.some.call(sel.options, o => o.value === prev)) sel.value = prev; + else if (sel.options.length) sel.selectedIndex = 0; +} + +// Seed the SPI1 field from a TAR's MSL (the option is added when the selector +// does not carry it - the MSL is the card's Minimum SPI1, TS 102 226 +// 8.2.1.3.2.4). +function spSpi1Seed(msl) { + const spi1Sel = document.getElementById('sp-spi1'); + if (!spi1Sel || !msl) return; + if (spi1Sel.options && typeof Option === 'function' && + !Array.prototype.some.call(spi1Sel.options, o => o.value === msl)) { + spi1Sel.appendChild(new Option(msl + ' \u2014 MSL', msl)); + } + spi1Sel.value = msl; +} + +// A TAR selection applies its preset entry's default keyset (KIc/KID and the +// keys; the counter override is cleared) and seeds the SPI1 from its MSL. +function spTarChanged() { + const sel = document.getElementById('sp-tar'); + const idx = spPresetIdx(); + const preset = idx >= 0 ? cards[idx] : null; + const entry = preset ? cardsTarEntry(preset, sel ? sel.value : '') : null; + if (entry) { + const ks = entry.keyset ? spKeysetFor(preset, entry.keyset) : null; + if (ks) spKeysetApply(ks); + if (entry.msl) spSpi1Seed(entry.msl); + } + spInvalidate(); + updateSp(); + spKeysetSync(); +} + function packToSp(sourceId) { const val = document.getElementById(sourceId).value; if (!val) return; @@ -5055,7 +5091,10 @@ function packToSp(sourceId) { const key = spTarKeyForPack(sourceId); if (key) { _spTarKey = key; - document.getElementById('sp-tar').value = spPresetTar(key); + spTarRender(); + const sel = document.getElementById('sp-tar'); + const tar = spPresetTar(key); + if (sel && Array.prototype.some.call(sel.options, o => o.value === tar)) sel.value = tar; } _spRamFormat = sourceId === 'chain-ram-preview' ? 'auto' : null; } @@ -10436,8 +10475,10 @@ function scriptsCapFileChanged() { // Collect SP params from the SP form (populated by ramApplyCard -> cardsApply) function getRamSpParams(selId) { // `preset_id` names the server-side preset the operation belongs to: the - // server persists the counter it consumes there (v3.8.0). `selId` picks - // the view's preset selector (RAM by default, the TAR probe its own). + // server persists the counter it consumes there (v3.8.0) and resolves the + // keyset from the preset's ISD TAR entry (v3.26.0 - the RAM keyset + // selector is gone). `selId` picks the view's preset selector (RAM by + // default, the TAR probe its own). const sel = document.getElementById(selId || 'ram-card-sel'); const idx = sel ? parseInt(sel.value, 10) : NaN; const preset = (!isNaN(idx) && cards[idx]) ? cards[idx] : null; @@ -10445,11 +10486,7 @@ function getRamSpParams(selId) { preset_id: (preset && preset.id) || '', spi1: document.getElementById('sp-spi1').value, spi2: document.getElementById('sp-spi2-hex').value, - kic: document.getElementById('sp-kic-hex').value, - kid: document.getElementById('sp-kid-hex').value, - tar: (document.getElementById('sp-tar').value || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase().padEnd(6, '0').slice(0, 6), - kicKey: (document.getElementById('sp-kic-key').value || '').replace(/[^0-9a-fA-F]/g, ''), - kidKey: (document.getElementById('sp-kid-key').value || '').replace(/[^0-9a-fA-F]/g, ''), + tar: preset ? cardsRoleTar(preset, 'isd') : '', ram_format: (document.getElementById('ram-format') || {}).value || 'auto', }; // Override only when typed; empty = the server seeds the counter from the @@ -11928,10 +11965,8 @@ async function ramDeleteFromExplorer(aid, withCascade) { const ksErr = ramKeysetGuard(); if (ksErr) { alert(ksErr); return; } const sp = getRamSpParams(); - if (!sp.kicKey || !sp.kidKey) { - alert(t('Select a card preset with keys first (RAM subtab → Card preset)')); - return; - } + // v3.26.0: the keyset fields no longer travel - the server resolves the + // keyset (and its keys) from the preset's ISD TAR entry const label = withCascade ? t('Delete') + ' (' + t('cascade') + ')' : t('Delete'); if (!confirm(label + ' — ' + t('AID:') + ' ' + aid + '?')) return; const apdu = ramDeleteApdu(aid, withCascade); @@ -12494,59 +12529,27 @@ async function ramInstallApp(sp, op) { } // ===== TAR probe (v3.8.0) ===== -// One secured packet per TAR from the preset's keyset, classified by the -// card's ENVELOPE verdict and the application's PoR. The standard TAR list -// lives on the server (TS 101 220 V18.3.0 Annex D, Table D.1) and is used when -// the list field is empty. Each packet the card accepts consumes a counter, -// which the server persists into the keyset (one per accepted TAR). +// One secured packet per TAR of the selected preset, classified by the card's +// ENVELOPE verdict and the application's PoR. v3.26.0: the probe checks the +// preset's TAR list only - each row uses its TAR's MSL and default keyset, and +// every packet the card accepts consumes a counter, which the server persists +// into that keyset (one per accepted TAR). -// TAR values allocated by ETSI (TS 101 220 V18.3.0 Annex D, Table D.1) with -// the document each allocation comes from. Kept in sync with the server's -// TAR_PROBE_TARS by a test; custom TARs live in localStorage (best effort). -const TAR_PROBE_TARS = [ - { tar: '000000', label: 'Issuer Security Domain (compact)', doc: 'TS 102 226' }, - { tar: 'B20100', label: 'Issuer Security Domain (expanded)', doc: 'TS 102 226' }, - { tar: 'B00000', label: 'UICC shared file system RFM (compact)', doc: 'TS 102 226' }, - { tar: 'B00001', label: 'ADF RFM (compact)', doc: 'TS 131 116' }, - { tar: 'B00010', label: 'SIM file system RFM (compact)', doc: 'TS 131 116' }, - { tar: 'B00120', label: 'UICC shared file system RFM (expanded)', doc: 'TS 102 226' }, - { tar: 'B00130', label: 'SIM file system RFM (expanded)', doc: 'TS 131 116' }, - { tar: 'B00140', label: 'ADF RFM (expanded)', doc: 'TS 131 116' }, - { tar: 'B20000', label: 'USAT interpreter', doc: 'TS 131 114' }, - { tar: 'B20200', label: 'Multiplexing application', doc: 'TS 102 225' }, - { tar: 'B20201', label: 'Controlling Authority Security Domain', doc: 'GP Amendment A' }, - { tar: 'B20202', label: 'OMA BCAST audience measurement', doc: 'OMA' }, - { tar: 'B20203', label: 'OMA DM LWM2M application', doc: 'OMA' }, - { tar: 'B00200', label: 'RFU (control)', doc: '' }, -]; -const TAR_PROBE_STORAGE = 'simple_tar_probe'; - -// The custom TARs added by the user (best effort: losing them is harmless). -function tarProbeCustomLoad() { - try { - const d = JSON.parse(localStorage.getItem(TAR_PROBE_STORAGE) || '[]'); - if (!Array.isArray(d)) return []; - return d.filter(e => e && /^[0-9A-F]{6}$/.test(String(e.tar || '').toUpperCase())) - .map(e => ({ tar: String(e.tar).toUpperCase(), label: String(e.label || ''), custom: true })); - } catch (e) { - return []; - } -} - -function tarProbeCustomSave(list) { - try { - localStorage.setItem(TAR_PROBE_STORAGE, JSON.stringify( - (list || []).map(e => ({ tar: e.tar, label: e.label || '' })))); - } catch (e) { /* best effort */ } -} - -// The standard rows plus the custom ones. +// The probe's rows: the selected preset's TAR list (the role entries labelled +// by their role, the free rows by their description; the tooltip carries the +// MSL and the default keyset). There is no custom-TAR input anymore. function tarProbeAllRows() { - return TAR_PROBE_TARS.concat(tarProbeCustomLoad()); + const idx = tarPresetIdx(); + const preset = idx >= 0 ? cards[idx] : null; + const roleLabel = { isd: 'ISD', uiccRfm: 'UICC RFM', usimRfm: 'ADF RFM' }; + return cardsTarList(preset).map(e => ({ + tar: String(e.tar || '').toUpperCase(), + label: e.desc || roleLabel[e.role] || '', + doc: e.msl ? 'MSL ' + e.msl + (e.keyset ? ' \u00b7 ' + t('Keyset') + ' ' + e.keyset : '') : '', + })); } -// The checklist rows (pure): a checkbox per TAR, checked by default, the spec -// document in the tooltip and a remove button on custom rows. +// The checklist rows (pure): a checkbox per TAR, checked by default. function tarProbeRowsHtml(rows) { return (rows || []).map(r => { const title = r.doc ? ' title="' + esc(r.doc) + '"' : ''; @@ -12554,9 +12557,6 @@ function tarProbeRowsHtml(rows) { '' + '' + esc(r.tar) + '' + '' + esc(r.label || '') + '' + - (r.custom - ? '' - : '') + ''; }).join(''); } @@ -12570,34 +12570,20 @@ function tarProbeChecked() { function tarProbeRender() { const box = document.getElementById('tar-list'); - if (box) box.innerHTML = tarProbeRowsHtml(tarProbeAllRows()); + if (!box) return; + const rows = tarProbeAllRows(); + if (!rows.length) { + box.innerHTML = '' + + esc(t('Select a card preset with TARs first')) + ''; + return; + } + box.innerHTML = tarProbeRowsHtml(rows); } function tarProbeSelectAll(on) { document.querySelectorAll('#tar-list input[type="checkbox"]').forEach(cb => { cb.checked = !!on; }); } -function tarProbeAddCustom() { - const el = document.getElementById('tar-custom'); - const noteEl = document.getElementById('tar-custom-note'); - const tar = (el && el.value || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); - if (!/^[0-9A-F]{6}$/.test(tar)) { alert(t('A TAR is three hex bytes, e.g. B00001')); return; } - const custom = tarProbeCustomLoad(); - if (!custom.some(e => e.tar === tar)) { - custom.push({ tar: tar, label: (noteEl && noteEl.value || '').trim(), custom: true }); - tarProbeCustomSave(custom); - } - if (el) el.value = ''; - if (noteEl) noteEl.value = ''; - tarProbeRender(); -} - -function tarProbeRemoveCustom(tar) { - const want = String(tar || '').toUpperCase(); - tarProbeCustomSave(tarProbeCustomLoad().filter(e => e.tar !== want)); - tarProbeRender(); -} - function tarProbeVerdictLabel(verdict) { return { registered: 'registered', no_answer: 'accepted, no answer', no_por: 'accepted, no PoR', refused: 'refused' }[verdict] || verdict; @@ -12694,46 +12680,29 @@ function tarPresetIdx() { } function tarApplyCard(idx) { - // copy the selected preset into the shared SP form fields (the probe reads - // kic/kid/keys/counter from there, like the RAM view does) + // v3.26.0: the probe reads its rows from the selected preset (each TAR + // carries its own keyset); the shared SP form fields are only the SPI1 + // fallback - the ISD stays the form default for the other views. const i = parseInt(idx, 10); if (!isNaN(i) && cards[i]) _ramCardIdx = i; - _spTarKey = 'isd'; // the probe's checklist TARs are per-row; the ISD is the form default + _spTarKey = 'isd'; cardsApply(idx); + tarProbeRender(); spPresetWarningRender(); } -function tarKeysetChanged() { - const sel = document.getElementById('tar-keyset-sel'); - if (!sel) return; - const kvn = parseInt(sel.value, 10); - const idx = tarPresetIdx(); - const ks = (idx >= 0 && !isNaN(kvn)) ? spKeysetFor(cards[idx], kvn) : null; - if (ks) spKeysetApply(ks); - spKeysetSync(); -} - -function tarKeysetGuard() { - const idx = tarPresetIdx(); - const kic = (document.getElementById('sp-kic-hex') || {}).value; - const kid = (document.getElementById('sp-kid-hex') || {}).value; - return spKeysetCheck(idx >= 0 ? cards[idx] : null, kic, kid); -} - async function tarExecute() { const tars = tarProbeChecked(); if (!tars.length) { alert(t('Select at least one TAR')); return; } const cardIdx = parseInt(document.getElementById('tar-card-sel').value, 10); if (!isNaN(cardIdx) && cards[cardIdx]) _ramCardIdx = cardIdx; - spRefreshFromPreset('tar-card-sel'); - const ksErr = tarKeysetGuard(); - if (ksErr) { alert(ksErr); return; } - const sp = getRamSpParams('tar-card-sel'); - if (!sp.kicKey || !sp.kidKey) { - alert(t('Select a card preset with keys first (RAM subtab → Card preset)')); - return; - } - await tarProbe(sp, tars); + const idx = tarPresetIdx(); + const preset = idx >= 0 ? cards[idx] : null; + if (!preset) { alert(t('Select a card preset')); return; } + // v3.26.0: the probe checks the preset's TARs and resolves each row's + // keyset server-side; only the preset and the (fallback) SPI1 travel. + const spi1 = (document.getElementById('sp-spi1') || {}).value || ''; + await tarProbe({ preset_id: preset.id, spi1: spi1 }, tars); } // Measured NV footprint of an install operation: the server reads GET DATA @@ -12838,21 +12807,14 @@ async function ramSyncCounter(presetIdx, view) { const preset = (Number.isInteger(i) && i >= 0 && cards[i]) ? cards[i] : null; const box = ramCntrLowBox(view === 'sp' ? 'sp' : 'ram'); if (!preset) { alert(t('No card preset selected')); return; } - // The keyset the failed operation used: the originating view's selector - // first, the other view's as the fallback (both hold the keyset number as - // their option value). - const kvnOf = id => { - const sel = document.getElementById(id); - const v = sel && sel.value !== '' ? parseInt(sel.value, 10) : NaN; - return Number.isInteger(v) && v > 0 ? v : null; - }; - const own = view === 'sp' ? 'sp-keyset-sel' : 'ram-keyset-sel'; - const other = own === 'ram-keyset-sel' ? 'sp-keyset-sel' : 'ram-keyset-sel'; const val = id => (document.getElementById(id) || {}).value || ''; const body = { preset_id: preset.id }; - const kvn = kvnOf(own) || kvnOf(other); - if (kvn) body.kvn = kvn; - const tar = val('sp-tar').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); + // v3.26.0: the keyset follows the operation's TAR - the RAM operations are + // addressed to the ISD role entry, a plain send to its selected TAR (the + // server resolves it; an explicit `kvn` override stays possible through + // the API). + const tar = view === 'ram' ? cardsRoleTar(preset, 'isd') + : val('sp-tar').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); if (tar) body.tar = tar; if (val('sp-spi1')) body.spi1 = val('sp-spi1'); // The probe needs a PoR: use the transport the failed operation used - the @@ -13011,10 +12973,8 @@ async function ramExecute() { // changed in the Cards tab after the RAM form was filled. spRefreshFromPreset('ram-card-sel'); const sp = getRamSpParams(); - if (!sp.kicKey || !sp.kidKey) { - alert(t('Select a card preset with keys first (RAM subtab → Card preset)')); - return; - } + // v3.26.0: the keyset fields no longer travel - the server resolves the + // keyset (and its keys) from the preset's ISD TAR entry try { if (op === 'explore') await ramExplore(sp); else if (op === 'install-cap') await ramInstallCap(sp); @@ -13543,28 +13503,6 @@ function spKeysetCheck(preset, kicHex, kidHex) { return ''; } -// The '; - }); - if (kvn && !found) { - html += ''; - } - if (!html) html = ''; - return html; -} - // ===== Keyset editor (Cards form) ===== let _cardsKeysetCount = 0; @@ -13595,12 +13533,20 @@ function cardsKeysetRowsRender(keysets) { function cardsKeysetAdd() { cardsKeysetRowsRender(cardsKeysetsFromForm().concat([ { kic: '', kid: '', kicKey: '', kidKey: '', cntr: '' }])); + cardsTarKeysetRefresh(); } function cardsKeysetRemove(i) { const rows = cardsKeysetsFromForm(); rows.splice(i, 1); cardsKeysetRowsRender(rows); + cardsTarKeysetRefresh(); +} + +// Refresh the TAR table's keyset selects after the keyset table changed (the +// TAR rows' own values are read from the DOM first, so nothing is lost). +function cardsTarKeysetRefresh() { + cardsTarRowsRender(cardsTarsFromTable()); } function cardsKeysetKvnUpdate(i) { @@ -13614,6 +13560,9 @@ function cardsKeysetKvnUpdate(i) { el.classList.toggle('text-red-500', !!bad); el.title = bad ? t('KIc and KID must carry the same keyset number (TS 102 225 A.2)') : t('Keyset number (b8..b5 of KIc/KID)'); + // The TAR table's keyset selects follow the numbers as they are typed + // (v3.26.0) - a re-render keeps every field's value (read from the DOM). + cardsTarKeysetRefresh(); } // The keyset rows as preset entries (empty rows are dropped). @@ -13648,11 +13597,42 @@ const CARDS_TAR_ROLE_DESC = { usimRfm: 'ADF FS RFM compact format', }; +// The keyset cell (v3.26.0): a select of the form's current keysets, the only +// one preselected when the form carries exactly one, and an amber marker when +// a TAR whose MSL requires security has none (the server refuses to save such +// a TAR; the marker shows it before the save). +function cardsTarKeysetCell(e, i) { + const keysets = cardsKeysetsFromForm(); + const kvn = e.keyset ? Number(e.keyset) : null; + const msl = String(e.msl || '').toUpperCase(); + const auto = (!kvn && keysets.length === 1) ? spKeysetKvnOf(keysets[0].kic) : null; + const sel = kvn || auto; + const warn = !sel && msl && msl !== '00'; + let html = ''; + return html; +} + function cardsTarRowHtml(e, i) { const cls = 'border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono'; const role = CARDS_TAR_ROLES[i] || ''; let cells = '' + - ''; + '' + + cardsTarKeysetCell(e, i); if (role) { cells += '' + ''; @@ -13673,7 +13653,7 @@ function cardsTarRowsRender(entries) { const list = entries || []; const roles = CARDS_TAR_ROLES.map(role => { const e = list.find(x => x.role === role) || {}; - return { tar: e.tar || '', msl: e.msl || '' }; + return { tar: e.tar || '', msl: e.msl || '', keyset: e.keyset || null }; }); const free = list.filter(e => !e.role); _cardsTarCount = free.length; @@ -13682,7 +13662,7 @@ function cardsTarRowsRender(entries) { } function cardsTarAdd() { - cardsTarRowsRender(cardsTarsFromTable().concat([{ tar: '', msl: '', desc: '' }])); + cardsTarRowsRender(cardsTarsFromTable().concat([{ tar: '', msl: '', desc: '', keyset: null }])); } // `i` is the table row index; the role rows are fixed and never removed. @@ -13703,13 +13683,21 @@ function cardsFreeTarsFromForm() { return el ? String(el.value).trim() : ''; }; const e = { tar: get('tar').replace(/\s/g, '').toUpperCase(), - msl: get('msl').replace(/\s/g, '').toUpperCase(), desc: get('desc') }; - if (!e.tar && !e.msl && !e.desc) continue; + msl: get('msl').replace(/\s/g, '').toUpperCase(), desc: get('desc'), + keyset: cardsTarKeysetFromForm(idx) }; + if (!e.tar && !e.msl && !e.desc && !e.keyset) continue; out.push(e); } return out; } +// The keyset select of a TAR row as a number (null for none). +function cardsTarKeysetFromForm(idx) { + const el = document.getElementById('cards-tr-' + idx + '-keyset'); + const v = el && el.value ? parseInt(el.value, 10) : NaN; + return (Number.isInteger(v) && v > 0) ? v : null; +} + // The TAR table exactly as displayed: the role rows as typed (an emptied field // stays empty) plus the free rows - the re-render source for Add/Remove, so an // emptied field is not silently refilled with its default. @@ -13721,7 +13709,8 @@ function cardsTarsFromTable() { }; return { role: role, tar: get('tar').replace(/\s/g, '').toUpperCase(), - msl: get('msl').replace(/\s/g, '').toUpperCase(), desc: '' }; + msl: get('msl').replace(/\s/g, '').toUpperCase(), desc: '', + keyset: cardsTarKeysetFromForm(i) }; }); return out.concat(cardsFreeTarsFromForm()); } @@ -13732,45 +13721,24 @@ function cardsTarsFromTable() { function cardsTarsFromForm() { return cardsTarsFromTable().map(e => e.role ? { role: e.role, tar: e.tar || cardsRoleDefault(e.role), - msl: e.msl || CARDS_MSL_DEFAULT, desc: '' } + msl: e.msl || CARDS_MSL_DEFAULT, desc: '', keyset: e.keyset || null } : e); } -// The role rows back to their pre-settled defaults (TAR + MSL 16) and no free -// rows. +// The role rows back to their pre-settled defaults (TAR + MSL 16, no keyset) +// and no free rows. function cardsTarsReset() { cardsTarRowsRender(CARDS_TAR_ROLES.map(role => ({ - role: role, tar: cardsRoleDefault(role), msl: CARDS_MSL_DEFAULT, desc: '' }))); + role: role, tar: cardsRoleDefault(role), msl: CARDS_MSL_DEFAULT, desc: '', + keyset: null }))); } -// ===== Keyset selectors (SCP80 / RAM) ===== -// The selector follows the KIc/KID bytes: a keyset of the selected preset that -// matches their keyset number is selected; a number the preset does not define -// shows as "not defined" (and the packet actions refuse it). +// ===== Keyset resolution (SCP80 / RAM / TAR probe) ===== +// v3.26.0: the keyset selectors are gone - each TAR carries its default keyset +// in the preset, the SCP80 TAR select applies it to the form and the RAM +// operations use the ISD role entry (the server resolves the same rule). Only +// the start buttons' availability refresh remains here. function spKeysetSync() { - const kic = (document.getElementById('sp-kic-hex') || {}).value; - const kid = (document.getElementById('sp-kid-hex') || {}).value; - // A keyset only exists inside a card preset: with no preset selected the - // selector is disabled and cleared, so a stale "not defined in the preset" - // entry can never linger. - const sync = (selId, idxFn) => { - const sel = document.getElementById(selId); - if (!sel) return; - const idx = idxFn(); - const preset = idx >= 0 ? cards[idx] : null; - if (!preset) { - sel.disabled = true; - sel.innerHTML = ''; - return; - } - sel.disabled = false; - sel.innerHTML = spKeysetOptionsHtml(preset, kic, kid); - }; - sync('sp-keyset-sel', spPresetIdx); - sync('ram-keyset-sel', ramPresetIdx); - sync('tar-keyset-sel', tarPresetIdx); - // The start buttons follow the same state: they wait for a preset and a - // keyset defined in it. pysimApplyAvailability(); } @@ -13797,26 +13765,6 @@ function spKeysetApply(ks) { updateSp(); } -function spKeysetChanged() { - const sel = document.getElementById('sp-keyset-sel'); - if (!sel) return; - const kvn = parseInt(sel.value, 10); - const idx = spPresetIdx(); - const ks = (idx >= 0 && !isNaN(kvn)) ? spKeysetFor(cards[idx], kvn) : null; - if (ks) spKeysetApply(ks); - spKeysetSync(); -} - -function ramKeysetChanged() { - const sel = document.getElementById('ram-keyset-sel'); - if (!sel) return; - const kvn = parseInt(sel.value, 10); - const idx = ramPresetIdx(); - const ks = (idx >= 0 && !isNaN(kvn)) ? spKeysetFor(cards[idx], kvn) : null; - if (ks) spKeysetApply(ks); - spKeysetSync(); -} - // A warning for the SCP80/RAM views when the equipped card and the selected // preset disagree (pure). Autoselection keeps them in step, so this covers // what it cannot fix: a card with no preset at all, or a manually selected @@ -13869,30 +13817,64 @@ function ramPresetIdx() { // '' when the SCP80 form may build a packet, else the reason. With a preset // selected the keyset number must be one of its keysets; without one only the // byte rules apply (hand-entered keys stay possible, a workbench flow). +// The click-time guard (v3.26.0): the operation's TAR must have a usable +// default keyset in the preset - a TAR whose MSL requires security needs one +// (the counters are keyset-linked; MSL 00 may go keyless) - and the form's +// KIc/KID must stay consistent with it (the fields remain hand-editable). +function spTarKeysetGuard(preset, tar) { + if (!preset) return ''; + const entry = cardsTarEntry(preset, tar); + if (!entry) return ''; + if (entry.keyset) { + if (!spKeysetFor(preset, entry.keyset)) { + return t('Keyset') + ' ' + entry.keyset + ' ' + t('is not defined in the preset'); + } + return ''; + } + const msl = String(entry.msl || ''); + if (msl && msl !== '00') { + return t('TAR') + ' ' + entry.tar + ': ' + + t('needs a default keyset (its MSL requires security) - set it in the Cards tab'); + } + return ''; +} + function spKeysetGuard() { const idx = spPresetIdx(); + if (idx < 0) return ''; + const preset = cards[idx]; + const sel = document.getElementById('sp-tar'); + const tarErr = spTarKeysetGuard(preset, sel ? sel.value : ''); + if (tarErr) return tarErr; const kic = (document.getElementById('sp-kic-hex') || {}).value; const kid = (document.getElementById('sp-kid-hex') || {}).value; - return spKeysetCheck(idx >= 0 ? cards[idx] : null, kic, kid); + return spKeysetCheck(preset, kic, kid); } function ramKeysetGuard() { const idx = ramPresetIdx(); + if (idx < 0) return ''; + const preset = cards[idx]; + // The RAM operations are addressed to the ISD TAR (v3.26.0) + const tarErr = spTarKeysetGuard(preset, cardsRoleTar(preset, 'isd')); + if (tarErr) return tarErr; const kic = (document.getElementById('sp-kic-hex') || {}).value; const kid = (document.getElementById('sp-kid-hex') || {}).value; - return spKeysetCheck(idx >= 0 ? cards[idx] : null, kic, kid); + return spKeysetCheck(preset, kic, kid); } // Whether a view's start button may run: its preset must be selected and the -// form's keyset number one of that preset's keysets (the click-time guards keep -// the byte-level checks). '00'/'00' (no security) with a preset selected stays -// allowed - it is a deliberate form choice. +// operation TAR's keyset usable (the click-time guards keep the byte-level +// checks). '00'/'00' (no security) with a preset selected stays allowed - it +// is a deliberate form choice. function spKeysetStartReady(view) { - const idx = view === 'ram' ? ramPresetIdx() : (view === 'tar' ? tarPresetIdx() : spPresetIdx()); - if (idx < 0) return false; - const kic = (document.getElementById('sp-kic-hex') || {}).value; - const kid = (document.getElementById('sp-kid-hex') || {}).value; - return spKeysetCheck(cards[idx], kic, kid) === ''; + if (view === 'ram') { + if (ramPresetIdx() < 0) return false; + return ramKeysetGuard() === ''; + } + if (view === 'tar') return tarPresetIdx() >= 0; + if (spPresetIdx() < 0) return false; + return spKeysetGuard() === ''; } function cardsFormValues() { @@ -14257,40 +14239,22 @@ function cardsApplyFields(idx, keepTar, keepCntr) { const c = cards[parseInt(idx)]; if (!c) return false; // The TAR of the current operation (ISD for RAM/GP, the RFM TAR for the RFM - // chains) and its MSL: a preset apply seeds the SPI1 from the MSL - the - // card's Minimum SPI1 (TS 102 226 8.2.1.3.2.4). keepTar leaves the packet - // TAR alone (the plain send may target a manually entered TAR): its SPI1 is - // the operator's value too. + // chains) and its MSL + default keyset (v3.26.0): a preset apply selects + // the TAR in the selector and applies its keyset to the form, seeding the + // SPI1 from its MSL - the card's Minimum SPI1 (TS 102 226 8.2.1.3.2.4). + // keepTar leaves the packet TAR (and its keyset) alone: the plain send + // keeps the operator's selection. + spTarRender(c); if (!keepTar) { const roleTar = cardsRoleTar(c, _spTarKey); - document.getElementById('sp-tar').value = roleTar; - const msl = cardsTarMsl(c, roleTar) || CARDS_MSL_DEFAULT; - const spi1Sel = document.getElementById('sp-spi1'); - if (spi1Sel) { - if (spi1Sel.options && typeof Option === 'function' && - !Array.prototype.some.call(spi1Sel.options, o => o.value === msl)) { - spi1Sel.appendChild(new Option(msl + ' \u2014 MSL', msl)); - } - spi1Sel.value = msl; + const sel = document.getElementById('sp-tar'); + if (sel && Array.prototype.some.call(sel.options, o => o.value === roleTar)) { + sel.value = roleTar; } - } - // The keyset: keep the keyset number the form carries when this preset - // defines it, otherwise start from the preset's first keyset. - const kicNow = (document.getElementById('sp-kic-hex') || {}).value; - const kidNow = (document.getElementById('sp-kid-hex') || {}).value; - const kvnNow = spKeysetKvnOf(kicNow) || spKeysetKvnOf(kidNow) || 0; - const ks = (kvnNow && spKeysetFor(c, kvnNow)) || spKeysetList(c)[0] || null; - if (ks) { - const kicByte = parseInt(ks.kic, 16); - document.getElementById('sp-kic-idx').value = ((kicByte >> 4) & 0x0F).toString(16).toUpperCase(); - document.getElementById('sp-kic-alg').value = (kicByte & 0x0F).toString(16).padStart(2, '0'); - updateSpKic(); - const kidByte = parseInt(ks.kid, 16); - document.getElementById('sp-kid-idx').value = ((kidByte >> 4) & 0x0F).toString(16).toUpperCase(); - document.getElementById('sp-kid-alg').value = (kidByte & 0x0F).toString(16).padStart(2, '0'); - updateSpKid(); - document.getElementById('sp-kic-key').value = ks.kicKey || ''; - document.getElementById('sp-kid-key').value = ks.kidKey || ''; + const entry = cardsTarEntry(c, sel ? sel.value : roleTar); + spSpi1Seed((entry && entry.msl) || CARDS_MSL_DEFAULT); + const ks = (entry && entry.keyset) ? spKeysetFor(c, entry.keyset) : null; + if (ks) spKeysetApply(ks); } // The counter field is an override: applying a preset clears it (empty = // auto, the server seeds from the store); the pre-send refresh keeps a @@ -14301,8 +14265,6 @@ function cardsApplyFields(idx, keepTar, keepCntr) { if (cntrEl) cntrEl.value = ''; } spCntrPlaceholder(); - if (!keepTar) document.getElementById('sp-tar').value = cardsRoleTar(c, _spTarKey); - spTarListRender(c); spInvalidate(); updateSp(); spKeysetSync(); @@ -14317,7 +14279,7 @@ function cardsApply(idx, keepTar, keepCntr) { // warning must not outlive the selection it described. spKeysetSync(); spPresetWarningRender(); - spTarListRender(null); + spTarRender(null); return; } _genSpBuild(); @@ -23972,6 +23934,15 @@ const LANG_RU = { 'TARs (MSL)': 'TAR (MSL)', 'Each TAR carries its MSL (Minimum SPI1) — the card checks it before the security processing (TS 102 226 8.2.1.3.2.4); the packet SPI1 is derived from it.': 'У каждого TAR есть MSL (минимальный SPI1) — карта проверяет его до обработки защиты (TS 102 226 8.2.1.3.2.4); из него выводится SPI1 пакета.', 'More TARs — each with its MSL and an optional description': 'Дополнительные TAR — у каждого свой MSL и необязательное описание', + 'TAR (from the preset)': 'TAR (из пресета)', + 'TARs to probe (the preset\'s TAR list)': 'TAR для проверки (список TAR пресета)', + '— no TAR in the preset —': '— в пресете нет TAR —', + '— none —': '— нет —', + 'Select a card preset': 'Выберите пресет карты', + 'Select a card preset with TARs first': 'Сначала выберите пресет карты с TAR', + 'The keyset packets to this TAR use - a non-zero MSL requires one': 'Keyset, который используют пакеты к этому TAR — при MSL не 00 он обязателен', + 'This TAR needs a default keyset (its MSL requires security) - set it in the Cards tab': 'Этому TAR нужен keyset по умолчанию (его MSL требует защиты) — задайте его на вкладке «Карты»', + 'needs a default keyset (its MSL requires security) - set it in the Cards tab': 'нужен keyset по умолчанию (его MSL требует защиты) — задайте его на вкладке «Карты»', 'Add TAR': 'Добавить TAR', 'Remove TAR': 'Удалить TAR', 'Description (optional)': 'Описание (необязательно)', diff --git a/frontend/sw.js b/frontend/sw.js index 970b984..d6f48c1 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v433'; +const CACHE = 'simple-v434'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/cards_counter.test.js b/frontend/tests/cards_counter.test.js index ec9d7d1..4aff4ac 100644 --- a/frontend/tests/cards_counter.test.js +++ b/frontend/tests/cards_counter.test.js @@ -34,12 +34,12 @@ for (const fn of ['cardsTarList', 'cardsRoleDefault', 'cardsRoleTar', 'spPresetCounterValue', 'spCntrPlaceholder', 'spCntrEffective', 'spCntrAhead', 'spCntrAdjust', 'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck', - 'spKeysetOptionsHtml', 'cardsKeysetRowHtml', 'cardsKeysetRowsRender', + 'spTarOptionsHtml', 'spTarRender', 'spSpi1Seed', 'spTarKeysetGuard', 'cardsTarKeysetCell', 'cardsTarKeysetFromForm', 'cardsTarKeysetRefresh', 'cardsTarRowsRender', 'cardsTarRowHtml', 'cardsTarsFromTable', 'cardsFreeTarsFromForm', 'cardsKeysetRowHtml', 'cardsKeysetRowsRender', 'cardsKeysetAdd', 'cardsKeysetRemove', 'cardsKeysetKvnUpdate', 'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply', - 'spKeysetChanged', 'ramKeysetChanged', 'ramPresetIdx', 'tarPresetIdx', 'spKeysetGuard', - 'spTarListRender', + 'ramPresetIdx', 'tarPresetIdx', 'spKeysetGuard', 'ramKeysetGuard', + 'esc', ]) { code += extractFunc(html, fn) + '\n'; } @@ -61,9 +61,22 @@ globalThis.t = s => s; function fakeEnv(selValue, selId, presetCntr) { const els = {}; for (const id of ['sp-spi1', 'sp-spi2-hex', 'sp-kic-idx', 'sp-kic-alg', - 'sp-kid-idx', 'sp-kid-alg', 'sp-tar', 'sp-cntr', 'sp-kic-key', 'sp-kid-key']) { + 'sp-kid-idx', 'sp-kid-alg', 'sp-cntr', 'sp-kic-key', 'sp-kid-key']) { els[id] = { value: '' }; } + // the TAR is a select (v3.26.0): the stub models the surface the renderer + // touches - setting innerHTML parses the option values and selectedIndex + // moves the value, like a real -like fake (the TAR selector): setting innerHTML parses the option +// values (and the selected one) and selectedIndex moves the value, like a real +// element. The accessors need defineProperties - Object.assign would flatten +// them into data properties. +function selectEl() { + const el = fakeEl(); + el.options = []; + el._sel = 0; + el._html = ''; + el.disabled = false; + Object.defineProperties(el, { + innerHTML: { + get() { return this._html; }, + set(v) { + this._html = v; + this.options = Array.from(v.matchAll(/value="([^"]*)"/g), m => ({ value: m[1] })); + const sel = v.match(/
FieldDescription
TAR MSLKeyset Description
'; + html += ''; + if (warn) { + html += '\u26a0'; + } + html += '
' + esc(CARDS_TAR_ROLE_DESC[role] || '') + '