From 7aee1818b8fee6465e0801c1abdd7720e44021ec 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, 23 Sep 2026 07:34:21 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20remote-script=20command=20palette=20?= =?UTF-8?q?=E2=80=94=20RFM/RAM=20commands=20in=20Expanded=20Script,=20TS?= =?UTF-8?q?=20102=20226=20=C2=A79=20PUSH=20(v3.3.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 — the Expanded Script C-APDU rows are no longer hex-only: - chain containers gained a kind (chainKind/chainIsEmbedded/chainCommands), so the SIM RFM / USIM RFM / RAM-GP row editors and hex builders can be embedded as one-row "virtual" chains (ber--) with a listener hook (chainListen/chainNotify) refreshing the owning row. - the C-APDU row offers Hex / SIM RFM / USIM RFM / RAM-GP; the built APDU is echoed next to the picker and wrapped in the 22 Command TLV. GET RESPONSE is not offered in the embedded pickers (TS 102 226 5.2.1.1). Phase 2 — "→ Expanded Script" in the SIM RFM / USIM RFM / RAM-GP views imports the built chain as C-APDU rows (chainApduList drops GET RESPONSE and splits multi-APDU FID-chain selects; the RAM "To expanded" preview stays). Phase 3 — the TS 102 226 table 7.1/8.1/9.1 gaps: - RFM: SEARCH RECORD (TS 102 221 11.1.7 / SEEK per TS 151 011 9.2.7), INCREASE (11.1.8 / TS 151 011 9.2.8), CREATE FILE / DELETE FILE / RESIZE FILE (TS 102 222 6.3/6.4/6.10, with an FCP skeleton builder and CLA 80 for RESIZE), SET DATA / RETRIEVE DATA (11.3, block + SFI coding); - RAM: PUT KEY (GP Card Spec 11.8) and the TS 102 226 §9 PUSH command (80 EC 01 P2) with BIP opening (optional OPEN CHANNEL TLVs), CAT_TP (3C/39/36), TCP (35/3C/3E/47) and identification-packet variants. Tests: new frontend/tests/ts102226.test.js (byte-exact spec vectors for every new command, the FCP skeletons, chainApduList) plus C-APDU row tests in ber.test.js; 530 frontend / 390 Python green. Help EN/RU, README/RUS and AGENTS document the Command TLV taxonomy (Tables 5.5/5.9 limits), the picker and the new commands. --- README.md | 27 +- README_RUS.md | 27 +- frontend/help-ru.html | 26 +- frontend/help.html | 27 +- frontend/index.html | 530 +++++++++++++++++++++++++++++--- frontend/sw.js | 2 +- frontend/tests/ber.test.js | 45 ++- frontend/tests/sim.test.js | 2 +- frontend/tests/ts102226.test.js | 247 +++++++++++++++ pyproject.toml | 2 +- pysim_simple_server/server.py | 2 +- 11 files changed, 871 insertions(+), 66 deletions(-) create mode 100644 frontend/tests/ts102226.test.js diff --git a/README.md b/README.md index 2ed2cac..39503b4 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,9 @@ CLA = `A0` (GSM 11.11 / ISO 7816-4). | DEACTIVATE FILE | 04 | Deactivate a file | | VERIFY PIN | 20 | Verify PIN1 or PIN2 | | CHANGE PIN | 24 | Change PIN1 or PIN2 | +| UNBLOCK PIN | 2C | Unblock a PIN with the PUK | +| SEARCH RECORD (SEEK) | A2 | Search a linear fixed EF for a pattern; P2 selects type 1/2 and the mode (TS 151 011 §9.2.7) | +| INCREASE | 32 | Add a value to the last increased/updated record of a cyclic EF (3-byte value, TS 151 011 §9.2.8) | #### SELECT methods @@ -91,6 +94,18 @@ CLA = `A0` (GSM 11.11 / ISO 7816-4). CLA = `00` (ETSI TS 102 221). Same commands as SIM, but SELECT uses P1=09, P2=0C (by FID from current directory). +The UICC administrative command set is available on top of the SIM set (ETSI TS 102 221/102 222): + +| Command | INS | Description | +|---|---|---| +| SEARCH RECORD | A2 | Simple (P2=10/12, P1 = record number) and enhanced (P2=18, 2-byte indication) search per TS 102 221 §11.1.7 | +| INCREASE | 32 | Add a value to a cyclic EF record; optional SFI in P1 (TS 102 221 §11.1.8) | +| CREATE FILE | E0 | Create an EF from an FCP template; **Build template** composes a structural skeleton (TS 102 222 §6.3) | +| DELETE FILE | E4 | Delete the EF/DF with the given FID (TS 102 222 §6.4) | +| RESIZE FILE | D4 | Resize a file (FCP with FID + new size); CLA `80` per TS 102 222 Table 1 (TS 102 222 §6.10) | +| SET DATA | DB | Write a BER-TLV object; P2 selects first/next/retransmit block (+SFI) (TS 102 221 §11.3.2) | +| RETRIEVE DATA | CB | Read a BER-TLV object or the tag list (`5C`); P2 as above (TS 102 221 §11.3.1) | + #### References - ETSI TS 102 221: UICC-Terminal Interface; Physical and Logical Characteristics @@ -110,11 +125,17 @@ Two encoding variants: | Type | Tag | Description | |---|---|---| -| C-APDU | 22 | Raw APDU hex | +| C-APDU | 22 | Any remote management command: raw hex or built with the SIM RFM / USIM RFM / RAM-GP editors (TS 102 226 §5.2.1.0) | | Immediate Action | 81 | Proactive command or action indicator | | Error Action | 82 | Proactive command on error | | Script Chaining | 83 | Chaining data for multi-packet scripts | +#### C-APDU rows + +A C-APDU row starts in **Hex** mode (paste any command) and can switch to a one-row **SIM RFM**, **USIM RFM** or **RAM/GP** editor that builds the command from fields — the resulting APDU is wrapped in the `22` TLV and echoed next to the picker. GET RESPONSE is not offered there: the expanded format carries no GET RESPONSE (TS 102 226 §5.2.1.1). **→ Expanded Script** in the SIM RFM / USIM RFM / RAM-GP views appends their built commands as C-APDU rows (GET RESPONSE hops dropped). + +Only REFRESH, DISPLAY TEXT and PLAY TONE are allowed as an Immediate Action (TS 102 226 Table 5.5) and only DISPLAY TEXT and PLAY TONE as an Error Action (Table 5.9); the behaviour of the card for any other proactive command is undefined. + #### Immediate Action builder When the type is set to Immediate Action, the tool provides a structured builder for: @@ -150,9 +171,13 @@ CLA = `80` (GlobalPlatform Card Specification v2.3.1). Remote Application Manage | GET DATA | CA | tag | Read card data objects | | STORE DATA | E2 | 00/40/80/C0 | Store data (key, certificate, etc.) | | SET STATUS | F0 | 80/40/60 | Lifecycle state management | +| PUT KEY | D0 | key ver | Replace a key: P1 = key version, P2 = key identifier, data = new key material (GP Card Spec §11.8) | +| PUSH | EC | 01 | Ask an application to open a BIP channel / CAT_TP link / TCP connection or send an identification packet (TS 102 226 §9) | | EXTERNAL AUTHENTICATE | 82 | 00 | SCP host authentication | | INTERNAL AUTHENTICATE | 88 | 00 | Card challenge-response | +**PUSH** (TS 102 226 §9.2) is a C-APDU (`80 EC 01 P2`) addressed to an application that supports BIP and/or CAT_TP — not the HTTP administration trigger (that is the HTTP OTA builder above). P2: `01` BIP channel opening (OPEN CHANNEL COMPREHENSION-TLVs, optional), `02` CAT_TP link (destination port in transport level `3C` with protocol type 00, optional buffer size `39` / channel data `36`), `03` TCP connection (bearer `35`, transport level `3C` with protocol type 02 = TCP client remote, destination address `3E`, NAA/APN `47`), `04` identification packet (channel data `36`; the ICCID is used when absent). The application issues the proactive OPEN CHANNEL itself; success is `90 00`, failure `6F 00` with the Result TLV in the response data, malformed data is rejected with `6A 80`. + #### INSTALL [for install] — Privilege Builder Tag `C7` in the INSTALL data field. Built from 3 privilege bytes (GP spec Tables 11-7, 11-8, 11-9): diff --git a/README_RUS.md b/README_RUS.md index 38ce7e9..3ab77d3 100644 --- a/README_RUS.md +++ b/README_RUS.md @@ -64,6 +64,9 @@ CLA = `A0` (GSM 11.11 / ISO 7816-4). | DEACTIVATE FILE | 04 | Деактивация файла | | VERIFY PIN | 20 | Проверка PIN1 или PIN2 | | CHANGE PIN | 24 | Смена PIN1 или PIN2 | +| UNBLOCK PIN | 2C | Разблокировка PIN через PUK | +| SEARCH RECORD (SEEK) | A2 | Поиск образца в линейном фиксированном EF; P2 задаёт тип 1/2 и режим (TS 151 011 §9.2.7) | +| INCREASE | 32 | Прибавить значение к последней увеличенной/обновлённой записи циклического EF (3 байта, TS 151 011 §9.2.8) | #### Методы SELECT @@ -91,6 +94,18 @@ CLA = `A0` (GSM 11.11 / ISO 7816-4). CLA = `00` (ETSI TS 102 221). Те же команды, что и SIM, но SELECT использует P1=09, P2=0C. +Дополнительно доступны административные команды UICC (ETSI TS 102 221/102 222): + +| Команда | INS | Описание | +|---|---|---| +| SEARCH RECORD | A2 | Простой (P2=10/12, P1 = номер записи) и расширенный (P2=18, 2-байтовое indication) поиск по TS 102 221 §11.1.7 | +| INCREASE | 32 | Прибавить значение к записи циклического EF; необязательный SFI в P1 (TS 102 221 §11.1.8) | +| CREATE FILE | E0 | Создание EF из шаблона FCP; кнопка **Build template** собирает структурный каркас (TS 102 222 §6.3) | +| DELETE FILE | E4 | Удаление EF/DF по FID (TS 102 222 §6.4) | +| RESIZE FILE | D4 | Изменение размера файла (FCP с FID + новый размер); CLA `80` по TS 102 222 Table 1 (TS 102 222 §6.10) | +| SET DATA | DB | Запись BER-TLV объекта; P2 выбирает первый/следующий/повторный блок (+SFI) (TS 102 221 §11.3.2) | +| RETRIEVE DATA | CB | Чтение BER-TLV объекта или списка тегов (`5C`); P2 как выше (TS 102 221 §11.3.1) | + #### Ссылки - ETSI TS 102 221: UICC-Terminal Interface @@ -109,11 +124,17 @@ CLA = `00` (ETSI TS 102 221). Те же команды, что и SIM, но SELE | Тип | Тег | Описание | |---|---|---| -| C-APDU | 22 | APDU | +| C-APDU | 22 | Любая команда удалённого управления: hex или собранная редакторами SIM RFM / USIM RFM / RAM-GP (TS 102 226 §5.2.1.0) | | Immediate Action | 81 | Proactive-команда или action indicator | | Error Action | 82 | Proactive-команда при ошибке | | Script Chaining | 83 | Данные для многопакетных скриптов | +#### Строки C-APDU + +Строка C-APDU начинает в режиме **Hex** (вставьте любую команду) и может переключиться на одно-строчный редактор **SIM RFM**, **USIM RFM** или **RAM/GP**, который собирает команду из полей — итоговый APDU оборачивается в TLV `22` и показывается рядом с выбором. GET RESPONSE там не предлагается: расширенный формат не несёт GET RESPONSE (TS 102 226 §5.2.1.1). Кнопка **→ Расширенный скрипт** в представлениях SIM RFM / USIM RFM / RAM-GP добавляет их команды строками C-APDU (строки GET RESPONSE отбрасываются). + +Только REFRESH, DISPLAY TEXT и PLAY TONE допустимы как Immediate Action (TS 102 226 Table 5.5), и только DISPLAY TEXT и PLAY TONE — как Error Action (Table 5.9); поведение карты для других проактивных команд не определено. + #### Сборщик Immediate Action - **Action indicator**: `81` / `82` @@ -145,9 +166,13 @@ CLA = `80` (GlobalPlatform v2.3.1). Удалённое управление со | GET DATA | CA | tag | Чтение объектов данных | | STORE DATA | E2 | 00/40/80/C0 | Запись данных | | SET STATUS | F0 | 80/40/60 | Управление жизненным циклом | +| PUT KEY | D0 | key ver | Замена ключа: P1 = версия ключа, P2 = идентификатор, данные = новый ключевой материал (GP Card Spec §11.8) | +| PUSH | EC | 01 | Просьба приложению открыть BIP-канал / CAT_TP / TCP или отправить пакет идентификации (TS 102 226 §9) | | EXTERNAL AUTHENTICATE | 82 | 00 | Аутентификация SCP | | INTERNAL AUTHENTICATE | 88 | 00 | Challenge-response | +**PUSH** (TS 102 226 §9.2) — это C-APDU (`80 EC 01 P2`), адресованный приложению, поддерживающему BIP и/или CAT_TP (не триггер HTTP-администрирования — тот собирается в конструкторе HTTP OTA выше). P2: `01` открытие BIP-канала (COMPREHENSION-TLV OPEN CHANNEL, необязательно), `02` CAT_TP (порт назначения в transport level `3C` с типом протокола 00, необязательные buffer size `39` / channel data `36`), `03` TCP (bearer `35`, transport level `3C` с типом протокола 02 = TCP-клиент, удалённое соединение, адрес назначения `3E`, NAA/APN `47`), `04` пакет идентификации (channel data `36`; при отсутствии используется ICCID). Проактивную OPEN CHANNEL выдаёт само приложение; успех — `90 00`, отказ — `6F 00` с Result TLV в данных ответа, некорректные данные отклоняются с `6A 80`. + #### Привилегии (INSTALL [for install]) Три байта привилегий по GP Spec Tables 11-7, 11-8, 11-9. diff --git a/frontend/help-ru.html b/frontend/help-ru.html index 106ca7c..fb3215f 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -74,6 +74,8 @@ DISABLE PIN26Отключение PIN ENABLE PIN28Включение PIN UNBLOCK PIN2CРазблокировка PIN с помощью PUK + SEARCH RECORD (SEEK)A2Поиск образца в линейном фиксированном EF; P2 задаёт тип 1/2 и режим (TS 151 011 §9.2.7) + INCREASE32Прибавить значение к последней увеличенной/обновлённой записи циклического EF (значение 3 байта, TS 151 011 §9.2.8) GET RESPONSEC0Получение данных, на которые указывает предшествующий 61XX/9FXX @@ -90,13 +92,17 @@

Для record-команд режим P2: Absolute (04), Next (02) или Previous (03). Если за Case 4 командой сразу следует строка GET RESPONSE, сборщик цепочки автоматически убирает её байт Le (ETSI TS 102 226 §5.1.1). В правой колонке — панель конвертации (IMSI, MSISDN, ICCID, SPN, PLMN, nibble swap). См. §2.5.

+

→ Расширенный скрипт отправляет собранную цепочку в конструктор Expanded Script строками C-APDU (строки GET RESPONSE отбрасываются — расширенный формат их не использует, TS 102 226 §5.2.1.1). Приложение UICC Shared File System RFM (TAR B00000) доступно и в компактном, и в расширенном формате — если TAR не настроен на автоопределение формата, используются разные значения TAR (TS 102 226 §7.0/§5.3).

2.2 USIM RFM

-

CLA = 00 (ETSI TS 102 221). Тот же сборщик цепочки и набор команд, что и SIM. Отличия:

+

CLA = 00 (ETSI TS 102 221). Тот же сборщик цепочки и набор команд, что и SIM, плюс административные команды UICC и формы SEARCH RECORD / INCREASE из TS 102 221:

  • SELECT по умолчанию запрашивает FCP (P2=04) и добавляет Le=00; флажок Silent (P2=0C) выбирает файл без запроса FCP (без Le, без данных ответа).
  • По пути предлагает выбор from MF (P1=08) или from current DF (P1=09); текущий DF сессии ADF RFM — ADF.USIM.
  • Упаковать в Secured packet устанавливает TAR SCP80 в ADF RFM TAR выбранной предустановки (по умолчанию B00001), привязанный к AID ADF (ADF.USIM).
  • +
  • SEARCH RECORD (TS 102 221 §11.1.7) — простые режимы (P2=10/12, P1 = номер записи) и расширенный режим (P2=18) с 2-байтовым search indication; INCREASE (§11.1.8) принимает необязательный SFI и прибавляемое значение.
  • +
  • CREATE FILE / DELETE FILE / RESIZE FILE (ETSI TS 102 222 §6.3/6.4/6.10) принимают шаблон FCP; кнопка Build template собирает структурный каркас (file descriptor, FID, LCSI, размер и разрешающее компактное правило доступа 8C 02 7F 00 — проверьте его для своей карты). RESIZE FILE использует CLA 80 (TS 102 222 Table 1: '8X'/'CX').
  • +
  • SET DATA / RETRIEVE DATA (TS 102 221 §11.3) работают с BER-TLV EF: P2 выбирает первый/следующий/повторный блок (+SFI), поле тега необязательно (5C — список размещённых тегов).
  • Каждый переход SELECT в цепочке запрашивает FCP, если не отмечен как silent.
@@ -113,13 +119,15 @@ ТипТегОписание - C-APDU22APDU + C-APDU22Любая команда удалённого управления: hex APDU или команда, собранная редакторами SIM RFM / USIM RFM / RAM-GP (TS 102 226 §5.2.1.0; GET RESPONSE в этом формате не используется, §5.2.1.1) Immediate Action81Проактивная команда или action indicator Error Action82Условное восстановление при ошибках с action indicator или проактивной командой Script Chaining83Многопакетное выполнение скрипта с флагами First/Intermediary/Last

Сборщик Immediate Action предлагает action indicator (81/82), структурированный сборщик проактивных команд (REFRESH, DISPLAY TEXT, PLAY TONE с авто-генерацией COMPREHENSION-TLV), или ручной hex-ввод.

+

Строка C-APDU начинает в режиме Hex (вставьте любую команду) и может переключиться на одно-строчный редактор SIM RFM, USIM RFM или RAM/GP, который собирает команду из полей — итоговый APDU оборачивается в TLV 22 и показывается рядом с выбором. GET RESPONSE там не предлагается: расширенный формат не несёт GET RESPONSE (TS 102 226 §5.2.1.1). Кнопка → Расширенный скрипт в представлениях SIM RFM / USIM RFM / RAM-GP добавляет их команды строками C-APDU.

+

Только три проактивные команды допустимы как Immediate Action (TS 102 226 Table 5.5): REFRESH, DISPLAY TEXT и PLAY TONE — поведение карты для любой другой проактивной команды не определено.

Error Action TLV (Tag 82)

Восстановление при ошибках по TS 102 226 §5.2.1.3 — одна из четырёх форм:

@@ -131,13 +139,14 @@

Script Chaining TLV (Tag 83)

-

Многопакетное выполнение скрипта с сохранением контекста:

+

Многопакетное выполнение скрипта с сохранением контекста (TS 102 226 §5.2.1.4). TLV имеет вид 83 01 <флаг>, присутствует один раз и только как первый Command TLV; допускается для RFM и RAM:

    -
  • Флаги цепочки: 01 первый скрипт (удалять инфо о цепочке при сбросе), 11 первый скрипт (сохранять инфо о цепочке при сбросе, только RFM), 02 последующий скрипт (будут ещё), 03 последующий скрипт (последний)
  • -
  • Идентификатор скрипта: Корреляционный идентификатор между пакетами (1-4 байта hex, авто-инкремент подсказки)
  • -
  • Дополнительные данные: Расширенная информация цепочки (опционально hex)
  • -
  • Сохранение контекста: UICC сохраняет состояние безопасности/транзакции между пакетами
  • +
  • 01 — первый скрипт, удалить информацию цепочки при сбросе карты
  • +
  • 11 — первый скрипт, сохранить информацию цепочки при сбросе карты (только RFM)
  • +
  • 02 — последующий скрипт, далее будут ещё
  • +
  • 03 — последующий скрипт, последний
+

При цепочке сессия команд выходит за рамки одного шаблона: UICC сохраняет контекст файла/PIN (RFM) или сессию приложения (RAM, например цепочку STORE DATA) до последнего скрипта.

Декодирование ответов (TS 102 226 §5.2.2)

Входящие ответы Proof of Receipt декодируются сервером — формат expanded Remote Application response data (TS 102 226 §5.2.2) или компактный формат. Представление Secured Packet показывает результат после Отправить на карту (см. §3.1): статус PoR (TAR, счётчик, сырой PoR), а статусное слово и данные ответа последней команды подставляются в подвкладку «Парсер ответов» (Remote APDU).

@@ -160,6 +169,8 @@ GET DATACAtagЧтение объектов данных STORE DATAE200/40/80/C0/E0Запись данных SET STATUSF080/40/60Управление жизненным циклом + PUT KEYD0key verЗамена ключа: P1 = версия ключа, P2 = идентификатор ключа, данные = новый ключевой материал (GP Card Spec §11.8) + PUSHEC01Просьба приложению открыть BIP-канал / CAT_TP-соединение / TCP-соединение или отправить пакет идентификации (TS 102 226 §9) EXTERNAL AUTHENTICATE8200Аутентификация SCP INTERNAL AUTHENTICATE8800Challenge-response GET RESPONSEC000Получение данных после 61XX (Le настраивается) @@ -195,6 +206,7 @@

Полные таблицы GET STATUS P1/P2, тегов GET DATA, DELETE P1, STORE DATA P1 и SET STATUS см. в GlobalPlatform v2.3.1 и ETSI TS 102 226 §8.2.1.3.2.

+

PUSH (TS 102 226 §9.2) — это C-APDU (80 EC 01 P2), адресованный приложению, поддерживающему BIP и/или CAT_TP (не триггер HTTP-администрирования — тот собирается в конструкторе HTTP OTA выше). P2: 01 открытие BIP-канала (COMPREHENSION-TLV OPEN CHANNEL, необязательно), 02 CAT_TP-соединение (порт назначения в transport level 3C с типом протокола 00, необязательные buffer size 39 / channel data 36), 03 TCP-соединение (bearer 35, transport level 3C с типом протокола 02 = TCP-клиент, удалённое соединение, адрес назначения 3E, NAA/APN 47), 04 пакет идентификации (channel data 36; при отсутствии используется ICCID). Проактивную команду OPEN CHANNEL выдаёт само приложение; успех — 90 00, отказ — 6F 00 с Result TLV в данных ответа, некорректные данные отклоняются с 6A 80. P1 80 зарезервирован для приложения.

2.5 Конвертация (боковые панели SIM/USIM)

    diff --git a/frontend/help.html b/frontend/help.html index 3884861..655fff1 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -74,6 +74,8 @@ DISABLE PIN26Disable a PIN ENABLE PIN28Enable a PIN UNBLOCK PIN2CUnblock a PIN with PUK + SEARCH RECORD (SEEK)A2Search a linear fixed EF for a pattern; P2 selects type 1/2 and the mode (TS 151 011 §9.2.7) + INCREASE32Add a value to the last increased/updated record of a cyclic EF (3-byte value, TS 151 011 §9.2.8) GET RESPONSEC0Fetch data indicated by a preceding 61XX/9FXX status word @@ -90,12 +92,16 @@

    For record commands, the P2 mode is Absolute (04), Next (02), or Previous (03). When a Case 4 command is immediately followed by a GET RESPONSE row, the chain builder strips its trailing Le byte automatically (ETSI TS 102 226 §5.1.1). A conversion panel is embedded in the right column (IMSI, MSISDN, ICCID, SPN, PLMN, nibble swap). See §2.5.

    +

    → Expanded Script sends the built chain to the Expanded Script builder as C-APDU rows (GET RESPONSE hops are dropped — the expanded format does not use them, TS 102 226 §5.2.1.1). The UICC Shared File System RFM application (TAR B00000) is reachable with either the compact or the expanded format — unless the TAR is configured for automatic format detection, the two use different TAR values (TS 102 226 §7.0/§5.3).

    2.2 USIM RFM

    -

    CLA = 00 (ETSI TS 102 221). Same chain builder and command set as SIM. Differences:

    +

    CLA = 00 (ETSI TS 102 221). Same chain builder and command set as SIM, plus the UICC administrative commands, and the TS 102 221 forms of SEARCH RECORD / INCREASE:

    • SELECT requests FCP by default (P2=04) and appends Le=00; a Silent (P2=0C) checkbox selects without requesting FCP (no Le, no response data).
    • By path offers from MF (P1=08) or from current DF (P1=09); the current DF of an ADF RFM session is ADF.USIM.
    • +
    • SEARCH RECORD (TS 102 221 §11.1.7) offers the simple modes (P2=10/12, P1 = record number) and the enhanced mode (P2=18) with the 2-byte search indication; INCREASE (§11.1.8) takes an optional SFI and the value to add.
    • +
    • CREATE FILE / DELETE FILE / RESIZE FILE (ETSI TS 102 222 §6.3/6.4/6.10) take an FCP template; Build template composes a structural skeleton (file descriptor, FID, LCSI, size, and a permissive compact access rule 8C 02 7F 00 — review it for your card). RESIZE FILE uses CLA 80 (TS 102 222 Table 1: '8X'/'CX').
    • +
    • SET DATA / RETRIEVE DATA (TS 102 221 §11.3) operate on BER-TLV EFs: P2 selects first/next/retransmit block (+SFI), the tag field is optional (5C lists the allocated tags).
    • Pack into Secured packet sets the SCP80 TAR to the selected preset’s ADF RFM TAR (B00001 by default), linked to the ADF’s AID (ADF.USIM).
    • Each SELECT hop in a chain requests FCP unless marked silent.
    @@ -113,13 +119,14 @@ TypeTagDescription - C-APDU22Raw APDU hex + C-APDU22Any remote management command: raw APDU hex or a command built with the SIM RFM / USIM RFM / RAM-GP editors (TS 102 226 §5.2.1.0; GET RESPONSE is not used in this format, §5.2.1.1) Immediate Action81Proactive command or action indicator Error Action82Conditional error recovery with action indicator or proactive command Script Chaining83Multi-packet script execution with First/Intermediary/Last flags -

    The Immediate Action builder offers an action indicator (81/82), a structured proactive command builder (REFRESH, DISPLAY TEXT, PLAY TONE with auto-generated COMPREHENSION-TLV objects), or a freeform hex input.

    +

    A C-APDU row starts in Hex mode (paste any command) and can switch to a one-row SIM RFM, USIM RFM or RAM/GP editor that builds the command from fields — the resulting APDU is wrapped in the 22 TLV and shown next to the picker. GET RESPONSE is not offered there: the expanded format carries no GET RESPONSE (TS 102 226 §5.2.1.1). → Expanded Script in the SIM RFM / USIM RFM / RAM-GP views appends their built commands as C-APDU rows.

    +

    The Immediate Action builder offers an action indicator (81/82), a structured proactive command builder (REFRESH, DISPLAY TEXT, PLAY TONE with auto-generated COMPREHENSION-TLV objects), or a freeform hex input. Only those three proactive commands are allowed as an Immediate Action (TS 102 226 Table 5.5) — the behaviour of the card for any other proactive command is undefined.

    Error Action TLV (Tag 82)

    Error recovery per TS 102 226 §5.2.1.3 — one of four forms:

    @@ -131,13 +138,14 @@

Script Chaining TLV (Tag 83)

-

Multi-packet script execution with context preservation:

+

Multi-packet script execution with context preservation (TS 102 226 §5.2.1.4). The TLV is 83 01 <flag>, present once and only as the first Command TLV; it may be used for RFM and RAM only:

    -
  • Chaining Flags: 01 first script (delete chaining info on reset), 11 first script (keep chaining info across reset, RFM only), 02 subsequent script (more to follow), 03 subsequent script (last)
  • -
  • Script ID: Correlation identifier across packets (1-4 bytes hex, auto-increment hints provided)
  • -
  • Additional Data: Extended chaining information (optional hex)
  • -
  • Context Preservation: UICC keeps security/transaction state open across chained scripts
  • +
  • 01 — first script, delete chaining information upon card reset
  • +
  • 11 — first script, keep chaining information across card reset (RFM only)
  • +
  • 02 — subsequent script, more to follow
  • +
  • 03 — subsequent script, last one
+

With chaining the command session is extended beyond one scripting template: the UICC keeps the file/PIN context (RFM) or the application session (RAM, e.g. a chained STORE DATA sequence) until the last script.

Response decoding (TS 102 226 §5.2.2)

Incoming Proof-of-Receipt responses are decoded by the server — expanded Remote Application response data (TS 102 226 §5.2.2) or the compact format. The Secured Packet view shows the outcome after Send to Card (see §3.1): the PoR status (TAR, counter, raw PoR), with the last command’s status word and response data filled into the Response parser pill under Remote APDU.

@@ -160,6 +168,8 @@ GET DATACAtagRead card data objects STORE DATAE200/40/80/C0/E0Store data (key, certificate, …) SET STATUSF080/40/60Lifecycle state management + PUT KEYD0key verReplace a key: P1 = key version, P2 = key identifier, data = new key material (GP Card Spec §11.8) + PUSHEC01Ask an application to open a BIP channel / CAT_TP link / TCP connection or send an identification packet (TS 102 226 §9) EXTERNAL AUTHENTICATE8200SCP host authentication INTERNAL AUTHENTICATE8800Card challenge-response GET RESPONSEC000Fetch data after a 61XX status (Le configurable) @@ -195,6 +205,7 @@

Refer to GlobalPlatform v2.3.1 and ETSI TS 102 226 §8.2.1.3.2 for the full GET STATUS P1/P2, GET DATA tag, DELETE P1, STORE DATA P1, and SET STATUS tables.

+

PUSH (TS 102 226 §9.2) is a C-APDU (80 EC 01 P2) addressed to an application that supports BIP and/or CAT_TP — not the HTTP administration trigger (that is the HTTP OTA builder above). P2: 01 BIP channel opening (OPEN CHANNEL COMPREHENSION-TLVs, optional), 02 CAT_TP link (destination port in transport level 3C with protocol type 00, optional buffer size 39 / channel data 36), 03 TCP connection (bearer 35, transport level 3C with protocol type 02 = TCP client remote, destination address 3E, NAA/APN 47), 04 identification packet (channel data 36; the ICCID is used when absent). The application issues the proactive OPEN CHANNEL itself; success is 90 00, failure 6F 00 with the Result TLV in the response data, and malformed data is rejected with 6A 80. P1 80 is reserved for application-specific usage.

2.5 Conversion (SIM/USIM sidebars)

    diff --git a/frontend/index.html b/frontend/index.html index 7e70899..3fb5f15 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -74,10 +74,13 @@ + + + + @@ -1437,7 +1451,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.2.0'; +const SIMPLE_VERSION = '3.3.0'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== Tab switching ===== @@ -2301,6 +2315,9 @@ function escHtml(s) { return String(s).replace(/&/g,'&').replace(/-); the kind selects the +// command catalog and the row builders. +function chainKind(chainId) { + if (chainId === 'chain-sim') return 'sim'; + if (chainId === 'chain-usim') return 'usim'; + const m = /^ber-(sim|usim|ram)-\d+$/.exec(chainId || ''); + return m ? m[1] : 'ram'; +} +function chainIsEmbedded(chainId) { return /^ber-/.test(chainId || ''); } +function chainCommands(chainId) { + const kind = chainKind(chainId); + const cmds = kind === 'ram' ? CHAIN_CMDS_RAM : (kind === 'usim' ? CHAIN_CMDS_USIM : CHAIN_CMDS_SIM); + // GET RESPONSE is not used in the Expanded Remote Application data format + // (TS 102 226 5.2.1.1), so the embedded command pickers omit it. + return chainIsEmbedded(chainId) ? cmds.filter(function(c) { return c.value !== 'get-response'; }) : cmds; +} +function chainListen(chainId, fn) { CHAIN_LISTENERS[chainId] = fn; } +function chainNotify(chainId) { const fn = CHAIN_LISTENERS[chainId]; if (fn) fn(); } + function chainInit(id) { if (!_chains[id]) _chains[id] = {rows: []}; } function chainAddRow(id, cmd) { chainInit(id); _chains[id].rows.push({cmd: cmd, fields: {}}); chainRender(id); } function chainDeleteRow(id, idx) { chainInit(id); _chains[id].rows.splice(idx, 1); chainRender(id); } @@ -2347,6 +2393,7 @@ function chainUpdateField(id, idx, field, value) { if (p) p.value = chainBuildHex(id); const b = document.getElementById(id + '-pack-btn'); if (b) b.disabled = !chainBuildHex(id); + chainNotify(id); } function chainRender(chainId) { @@ -2354,23 +2401,24 @@ function chainRender(chainId) { const container = document.getElementById(chainId + '-rows'); const preview = document.getElementById(chainId + '-preview'); const packBtn = document.getElementById(chainId + '-pack-btn'); - if (!container) return; + if (!container) { chainNotify(chainId); return; } + const embedded = chainIsEmbedded(chainId); const rows = _chains[chainId].rows; container.innerHTML = ''; rows.forEach(function(row, idx) { const div = document.createElement('div'); div.className = 'mb-3 p-3 border border-gray-200 dark:border-slate-600 rounded'; - div.innerHTML = chainRenderRow(chainId, idx, row); + div.innerHTML = chainRenderRow(chainId, idx, row, embedded); container.appendChild(div); }); const hex = chainBuildHex(chainId); if (preview) preview.value = hex; if (packBtn) packBtn.disabled = !hex; + chainNotify(chainId); } -function chainRenderRow(chainId, idx, row) { - const isRam = chainId === 'chain-ram'; - const cmds = isRam ? CHAIN_CMDS_RAM : (chainId === 'chain-usim' ? CHAIN_CMDS_USIM : CHAIN_CMDS_SIM); +function chainRenderRow(chainId, idx, row, embedded) { + const cmds = chainCommands(chainId); const cmdOpts = cmds.map(function(c) { return ''; }).join(''); @@ -2379,7 +2427,7 @@ function chainRenderRow(chainId, idx, row) { return '
    ' + '' + '' + - '' + + (embedded ? '' : '') + '
    ' + '
    ' + fieldsHtml + '
    '; } @@ -2387,16 +2435,18 @@ function chainRenderRow(chainId, idx, row) { function chainRowFieldsHtml(chainId, idx, row) { var f = row.fields; function uf(field) { return "chainUpdateField('" + chainId + "'," + idx + ",'" + field + "',this.value)"; } - if (chainId === 'chain-sim' || chainId === 'chain-usim') return chainSimFieldsHtml(chainId, idx, f, uf); - return chainRamFieldsHtml(idx, row, f, uf); + const kind = chainKind(chainId); + if (kind === 'sim' || kind === 'usim') return chainSimFieldsHtml(chainId, idx, f, uf); + return chainRamFieldsHtml(chainId, idx, row, f, uf); } // ===== SIM/USIM field rendering ===== function chainSimFieldsHtml(chainId, idx, f, uf) { var cmd = (_chains[chainId] && _chains[chainId].rows[idx]) ? _chains[chainId].rows[idx].cmd : ''; + var usim = chainKind(chainId) === 'usim'; if (cmd === 'select') { var method = f.method || 'fid'; - var silent = chainId === 'chain-usim' && f.silent; + var silent = usim && f.silent; var html = '
    ' + '' + '
    '; - if (chainId === 'chain-usim') { + if (usim) { html += '
    '; } html += '
    '; @@ -2418,9 +2468,9 @@ function chainSimFieldsHtml(chainId, idx, f, uf) { if (method === 'path') { html += '
    ' + ''; - if (chainId === 'chain-usim') { - html += '
    ' + - '
    '; + if (usim) { + html += '
    ' + + '
    '; } html += '
    '; } @@ -2515,6 +2565,113 @@ function chainSimFieldsHtml(chainId, idx, f, uf) { '
    ' + '
    '; } + if (cmd === 'search-record') { + var srMode = f.srMode || (usim ? '10' : '02'); + if (usim) { + var htmlSr = '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    '; + if (srMode === '18') { + htmlSr += '
    ' + + '
    '; + } + htmlSr += '
    ' + + '
    '; + return htmlSr; + } + return '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    '; + } + if (cmd === 'increase') { + var inc = '
    '; + if (usim) { + inc += '
    ' + + '
    '; + } + inc += '
    ' + + '
    ' + + '
    ' + + '
    '; + return inc; + } + if (cmd === 'create-file' || cmd === 'resize-file') { + var isCreate = cmd === 'create-file'; + var st = f.structure || 'transparent'; + var htmlFcp = '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + (isCreate ? + '
    ' + + '
    ' + : + '
    ' + + '
    ') + + '
    '; + if (isCreate) { + htmlFcp += '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    '; + } + htmlFcp += '
    ' + + '
    ' + + ''; + return htmlFcp; + } + if (cmd === 'delete-file') { + return '
    ' + + '
    '; + } + if (cmd === 'set-data' || cmd === 'retrieve-data') { + var sdMode = f.sdMode || '80'; + var htmlSd = '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    '; + if (cmd === 'retrieve-data') { + htmlSd += '
    ' + + '
    '; + } + return htmlSd; + } if (cmd === 'get-response') { return '
    No parameters — CLA=C0 INS=C0 P1=00 P2=00
    '; } @@ -2522,7 +2679,7 @@ function chainSimFieldsHtml(chainId, idx, f, uf) { } // ===== RAM/GP field rendering ===== -function chainRamFieldsHtml(idx, row, f, uf) { +function chainRamFieldsHtml(chainId, idx, row, f, uf) { var cmd = row.cmd; if (cmd === 'install-load') { return '
    ' + @@ -2542,15 +2699,15 @@ function chainRamFieldsHtml(idx, row, f, uf) { '
    ' + '
    '; // Privileges - html += chainRamPrivHtml(idx, f, uf); + html += chainRamPrivHtml(chainId, idx, f, uf); // Toolkit - html += chainRamToolkitHtml(idx, f, uf); + html += chainRamToolkitHtml(chainId, idx, f, uf); return html; } if (cmd === 'install-make-sel') { return '
    ' + '
    ' + - chainRamPrivHtml(idx, f, uf); + chainRamPrivHtml(chainId, idx, f, uf); } if (cmd === 'install-reg-update') { return '
    ' + @@ -2558,7 +2715,7 @@ function chainRamFieldsHtml(idx, row, f, uf) { '
    ' + '
    ' + '
    ' + - chainRamPrivHtml(idx, f, uf); + chainRamPrivHtml(chainId, idx, f, uf); } if (cmd === 'install-extradition') { return '
    ' + @@ -2601,7 +2758,7 @@ function chainRamFieldsHtml(idx, row, f, uf) { if (cmd === 'get-data') { var tag = f.tag || '5F50'; return '
    ' + - '' + '' + '' + '' + @@ -2672,6 +2829,51 @@ function chainRamFieldsHtml(idx, row, f, uf) { return '
    ' + '
    '; } + if (cmd === 'put-key') { + return '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    '; + } + if (cmd === 'push') { + var pt = f.pushType || '01'; + var htmlPush = '
    ' + + '
    '; + if (pt === '02') { + htmlPush += '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    '; + } else if (pt === '03') { + htmlPush += '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    '; + } else if (pt === '04') { + htmlPush += '
    ' + + '
    '; + } + var extraField = pt === '01' ? 'pushData' : 'pushExtra'; + htmlPush += '
    ' + + '
    '; + return htmlPush; + } if (cmd === 'get-response') { return '
    ' + '
    '; @@ -2680,7 +2882,7 @@ function chainRamFieldsHtml(idx, row, f, uf) { } // ===== RAM/GP privileges sub-form (per-row) ===== -function chainRamPrivHtml(idx, f, uf) { +function chainRamPrivHtml(chainId, idx, f, uf) { var priv = f.priv || '00'; var b1 = parseInt(priv.substring(0, 2), 16) || 0; var b2 = priv.length >= 4 ? parseInt(priv.substring(2, 4), 16) : 0; @@ -2705,29 +2907,29 @@ function chainRamPrivHtml(idx, f, uf) { var byteIdx = c[2]; var val = parseInt(c[1], 16); var checked = (byteIdx === 'b1' && (b1 & val)) || (byteIdx === 'b2' && (b2 & val)) || (byteIdx === 'b3' && (b3 & val)); - html += ''; + html += ''; }); html += '
    '; return html; } -function chainRamUpdatePriv(idx) { +function chainRamUpdatePriv(chainId, idx) { var b1 = 0, b2 = 0, b3 = 0; - document.querySelectorAll('.chain-priv-b1-' + idx + ':checked').forEach(function(cb) { b1 |= parseInt(cb.value, 16); }); - document.querySelectorAll('.chain-priv-b2-' + idx + ':checked').forEach(function(cb) { b2 |= parseInt(cb.value, 16); }); - document.querySelectorAll('.chain-priv-b3-' + idx + ':checked').forEach(function(cb) { b3 |= parseInt(cb.value, 16); }); + document.querySelectorAll('.chain-priv-' + chainId + '-b1-' + idx + ':checked').forEach(function(cb) { b1 |= parseInt(cb.value, 16); }); + document.querySelectorAll('.chain-priv-' + chainId + '-b2-' + idx + ':checked').forEach(function(cb) { b2 |= parseInt(cb.value, 16); }); + document.querySelectorAll('.chain-priv-' + chainId + '-b3-' + idx + ':checked').forEach(function(cb) { b3 |= parseInt(cb.value, 16); }); var hex = b1.toString(16).padStart(2, '0').toUpperCase(); hex += b2.toString(16).padStart(2, '0').toUpperCase(); if (b3) hex += b3.toString(16).padStart(2, '0').toUpperCase(); - chainUpdateField('chain-ram', idx, 'priv', hex); + chainUpdateField(chainId, idx, 'priv', hex); } // ===== RAM/GP toolkit sub-form (per-row) ===== -function chainRamToolkitHtml(idx, f, uf) { +function chainRamToolkitHtml(chainId, idx, f, uf) { var enabled = f.tkEnabled; var html = '
    '; if (!enabled) return html; var tkMode = f.tkMode || 'ea'; @@ -2792,7 +2994,7 @@ function chainRamToolkitHtml(idx, f, uf) { // ===== SIM/USIM hex builder ===== function chainSimBuildRowHex(chainId, idx, row) { var f = row.fields; - var isSim = chainId === 'chain-sim'; + var isSim = chainKind(chainId) === 'sim'; var cla = isSim ? 0xA0 : 0x00; if (row.cmd === 'select') { @@ -2845,6 +3047,70 @@ function chainSimBuildRowHex(chainId, idx, row) { if (row.cmd === 'get-response') { return '00C0000000'; } + function sdP2(f) { + var base = parseInt((f.sdMode || '80').replace(/[^0-9a-fA-F]/g, '') || '80', 16); + var sfi = parseInt((f.sfi || '00').replace(/[^0-9a-fA-F]/g, '') || '00', 16); + return (base | (sfi & 0x1F)) & 0xFF; + } + if (row.cmd === 'search-record') { + var pattern = (f.srPattern || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); + if (!pattern) return ''; + var srm = parseInt(f.srMode || (isSim ? '02' : '10'), 16); + if (isSim) { + // TS 51 011 9.2.7 SEEK: type/mode in P2, pattern in the data field. + var seekLe = (f.le || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); + return buildApdu(0xA0, 0xA2, 0x00, srm, pattern.length / 2, pattern) + seekLe; + } + var ind = ''; + if (srm === 0x18) { + // Enhanced search: 2-byte indication (TS 102 221 table 11.13) first. + ind = (f.srInd || '1800').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); + if (ind.length !== 4) return ''; + } + var srData = ind + pattern; + var rec = parseInt((f.srRec || '00').replace(/[^0-9a-fA-F]/g, '') || '00', 16); + var srLe = (f.le || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); + return buildApdu(cla, 0xA2, rec, srm, srData.length / 2, srData) + srLe; + } + if (row.cmd === 'increase') { + var add = (f.value || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); + if (!add || add.length % 2) return ''; + var incLe = (f.le || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); + if (isSim) { + // TS 51 011 9.2.8: class A0, P1/P2 = 00, P3 = 03 (3-byte value). + return buildApdu(0xA0, 0x32, 0x00, 0x00, add.length / 2, add) + incLe; + } + var incSfi = parseInt((f.sfi || '00').replace(/[^0-9a-fA-F]/g, '') || '00', 16); + return buildApdu(cla, 0x32, incSfi ? (0x80 | (incSfi & 0x1F)) : 0x00, 0x00, add.length / 2, add) + incLe; + } + if (row.cmd === 'create-file') { + var fcp = (f.fcp || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); + if (fcp.length < 4 || fcp.substr(0, 2) !== '62') return ''; + return buildApdu(cla, 0xE0, 0x00, 0x00, fcp.length / 2, fcp); + } + if (row.cmd === 'delete-file') { + var dfid = (f.fid || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); + if (dfid.length !== 4) return ''; + return buildApdu(cla, 0xE4, 0x00, 0x00, 0x02, dfid); + } + if (row.cmd === 'resize-file') { + var rfcp = (f.fcp || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); + if (rfcp.length < 4 || rfcp.substr(0, 2) !== '62') return ''; + // TS 102 222 table 1: RESIZE FILE uses CLA '8X'/'CX' (80 without SM). + return buildApdu(0x80, 0xD4, parseInt(f.mode || '00', 16), 0x00, rfcp.length / 2, rfcp); + } + if (row.cmd === 'set-data') { + var sdData = (f.value || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); + if (!sdData) return ''; + return buildApdu(cla, 0xDB, 0x00, sdP2(f), sdData.length / 2, sdData); + } + if (row.cmd === 'retrieve-data') { + var rdTag = (f.tag || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); + var rdLe = (f.le || '00').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); + var rdP2 = sdP2(f); + if (rdTag) return buildApdu(cla, 0xCB, 0x00, rdP2, rdTag.length / 2, rdTag) + rdLe; + return buildApdu(cla, 0xCB, 0x00, rdP2, null, null) + rdLe; + } var OPS2 = { 'read-record': {ins: 0xB2}, 'read-binary': {ins: 0xB0}, 'update-record': {ins: 0xDC}, 'update-binary': {ins: 0xD6}, @@ -2910,7 +3176,79 @@ function chainSimBuildRowHex(chainId, idx, row) { return buildApdu(cla, op.ins, p1, p2v, p3, data); } +// Compose the FCP template for the CREATE FILE / RESIZE FILE builders +// (TS 102 222 tables 4 and 17). The access rule uses the compact format with +// AM=7F (all EF commands) and SC=00 (no condition) - review it for the card. +function chainBuildFcp(chainId, idx, kind) { + var row = _chains[chainId] && _chains[chainId].rows[idx]; + if (!row) return; + var f = row.fields; + var fid = (f.fid || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); + if (fid.length !== 4) return; + var size = (f.fileSize || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); + if (size.length % 2) size = '0' + size; + if (!size) size = '0000'; + var inner; + if (kind === 'create') { + var st = f.structure || 'transparent'; + var record = st === 'linear_fixed' || st === 'cyclic'; + var fd = ({ transparent: 0x01, linear_fixed: 0x02, cyclic: 0x06, ber_tlv: 0x39 })[st] || 0x01; + if (f.shareable) fd |= 0x40; + var recLen = (f.recLen || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); + if (record) { + if (recLen.length % 2) recLen = '0' + recLen; + if (recLen.length !== 4) return; + if (size === '0000') size = recLen; + } + inner = '82' + (record ? '04' : '02') + fd.toString(16).padStart(2, '0').toUpperCase() + '21' + (record ? recLen : ''); + inner += '83' + '02' + fid; + inner += '8A' + '01' + (f.lcsi || '05').replace(/[^0-9a-fA-F]/g, '').padStart(2, '0').slice(0, 2).toUpperCase(); + inner += '8C' + '02' + '7F00'; + inner += '80' + '02' + size.padStart(4, '0').slice(-4); + if (st === 'ber_tlv') inner += 'A5' + '00'; + } else { + inner = '83' + '02' + fid + '80' + '02' + size.padStart(4, '0').slice(-4); + } + f.fcp = '62' + berLenStr(inner.length / 2) + inner; + chainRender(chainId); +} + // ===== RAM/GP hex builder ===== +// PUSH command data field (TS 102 226 9.2.1-9.2.4): +// 01 BIP channel opening - OPEN CHANNEL COMPREHENSION-TLVs (optional) +// 02 CAT_TP link - transport level 3C (protocol type 00) plus +// optional buffer size 39 / channel data 36 +// 03 TCP connection - bearer 35, transport level 3C (protocol type 02, +// TCP client remote), destination 3E, NAA 47 +// 04 identification packet - channel data 36 (optional) +function chainPushData(f) { + function hx(s) { return (s || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); } + function port(v) { var h = hx(v); return h.length === 4 ? h : ''; } + var pt = f.pushType || '01'; + if (pt === '01') return hx(f.pushData); + if (pt === '04') { + var id4 = hx(f.pushIdent); + return id4 ? '36' + berLenStr(id4.length / 2) + id4 : ''; + } + if (pt === '02') { + var catPort = port(f.pushPort); + if (!catPort) return ''; + var out2 = '3C' + '03' + '00' + catPort; + var sdu = port(f.pushSdu); + if (sdu) out2 += '39' + '02' + sdu; + var id2 = hx(f.pushIdent); + if (id2) out2 += '36' + berLenStr(id2.length / 2) + id2; + return out2 + hx(f.pushExtra); + } + var out3 = hx(f.pushBearer) || '350103'; + out3 += '3C' + '03' + '02' + (port(f.pushPort) || '0000'); + var addr = hx(f.pushAddr); + if (addr) out3 += '3E' + berLenStr(addr.length / 2) + addr; + var apn = _hotaAsciiHex((f.pushApn || '').trim()); + if (apn) out3 += '47' + berLenStr(apn.length / 2) + apn; + return out3 + hx(f.pushExtra); +} + function chainRamBuildRowHex(idx, row) { var f = row.fields; var cmd = row.cmd; @@ -3081,6 +3419,17 @@ function chainRamBuildRowHex(idx, row) { var dl9 = berLenStr(ramData4.length / 2); return buildApdu(0x00, 0x88, 0x00, 0x00, parseInt(dl9, 16), ramData4) + '00'; } + if (cmd === 'put-key') { + var pkData = (f.data || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); + if (!pkData) return ''; + return buildApdu(0x80, 0xD0, parseInt(f.keyVer || '01', 16), parseInt(f.keyId || '01', 16), pkData.length / 2, pkData); + } + if (cmd === 'push') { + var pushData = chainPushData(f); + var pushP2 = parseInt(f.pushType || '01', 16); + if (!pushData) return buildApdu(0x80, 0xEC, 0x01, pushP2, null, null); + return buildApdu(0x80, 0xEC, 0x01, pushP2, pushData.length / 2, pushData); + } if (cmd === 'get-response') { var le = parseInt(f.le, 16) || 0; return buildApdu(0x80, 0xC0, 0x00, 0x00, le, null); @@ -3093,7 +3442,8 @@ function chainBuildRowHex(chainId, idx) { chainInit(chainId); var row = _chains[chainId].rows[idx]; if (!row) return ''; - if (chainId === 'chain-sim' || chainId === 'chain-usim') { + const kind = chainKind(chainId); + if (kind === 'sim' || kind === 'usim') { return chainSimBuildRowHex(chainId, idx, row); } return chainRamBuildRowHex(idx, row); @@ -3184,18 +3534,47 @@ function genConvert() { } // ===== Secured Packet UI ===== -function chainRamApduList() { - // One hex APDU per chain row (the same rows the preview concatenates). +// One APDU per built command, for consumers outside the compact chain: a row +// that expands to several APDUs (SELECT by FID chain) is split, and GET +// RESPONSE rows are dropped because the expanded format does not use them +// (TS 102 226 5.2.1.1). +function chainApduList(chainId) { const list = []; - chainInit('chain-ram'); - const rows = _chains['chain-ram'].rows; + chainInit(chainId); + const rows = _chains[chainId].rows; for (let i = 0; i < rows.length; i++) { - const h = chainBuildRowHex('chain-ram', i); - if (h) list.push(h); + if (rows[i].cmd === 'get-response') continue; + const h = chainBuildRowHex(chainId, i); + if (!h) continue; + if (rows[i].cmd === 'select' && (rows[i].fields.method || 'fid') === 'chain') { + parseCompactApdus(h).forEach(function(n) { + if (n.hex && /^APDU/.test(n.label)) list.push(n.hex); + }); + } else { + list.push(h); + } } return list; } +function chainRamApduList() { + return chainApduList('chain-ram'); +} + +// Import a built chain into the Expanded Script builder as C-APDU rows. +function loadExpandedScript(chainId) { + const apdus = chainApduList(chainId); + if (!apdus.length) return; + cApduSwitchSubtab('ber'); + for (const hex of apdus) { + const row = addBerRow('c-apdu'); + const input = row.querySelector('.ber-hex'); + if (input) input.value = hex; + updateBerRow(row.querySelector('.ber-type')); + } + genBerTlv(); +} + function ramBuildExpanded() { // Show each built command in the TS 102 226 Command Scripting (expanded) // form: AA 22 or AE80 22 0000. @@ -3678,15 +4057,21 @@ const BER_TONES = [ ]; const BER_DEVICES = [['81','UICC'],['82','Terminal'],['83','Network'],['02','Display']]; +// Stable per-row id for the embedded command chains (row indices are +// renumbered when a row is deleted, the uid is not). +let _berUidSeq = 0; + function addBerRow(type) { const c = document.getElementById('ber-rows'); const idx = c.children.length; const row = document.createElement('div'); row.className = 'ber-row border border-gray-300 dark:border-slate-600 rounded p-2 bg-gray-50 dark:bg-slate-800'; row.dataset.berIdx = idx; + row.dataset.berUid = String(++_berUidSeq); row.innerHTML = berRowHtml(idx, type); c.appendChild(row); onBerTypeChange(row.querySelector('.ber-type')); + return row; } function berRowHtml(idx, type) { @@ -3715,7 +4100,17 @@ function onBerTypeChange(sel) { const body = row.querySelector('.ber-body'); const type = sel.value; if (type === 'c-apdu') { - body.innerHTML = ``; + body.innerHTML = `
    + + +
    +
    `; + onBerCmdModeChange(body.querySelector('.ber-cmd-mode')); } else if (type === 'chaining') { body.innerHTML = buildScriptChainingRow(row.dataset.berIdx); } else if (type === 'error') { @@ -3737,6 +4132,52 @@ function onBerTypeChange(sel) { updateBerRow(sel); } +// ===== Expanded Script C-APDU picker (TS 102 226 5.2.1.0/5.2.1.1) ===== +// A C-APDU TLV may hold any remote management command; instead of pasting +// hex, the row can host a one-row "virtual" chain built with the SIM RFM / +// USIM RFM / RAM-GP command editors (GET RESPONSE is excluded - the expanded +// format does not use it). +function berCmdChainId(uid, kind) { return 'ber-' + kind + '-' + uid; } + +function onBerCmdModeChange(sel) { + const row = sel.closest('.ber-row'); + const editor = row.querySelector('.ber-cmd-editor'); + const mode = sel.value; + if (mode === 'hex') { + editor.innerHTML = ''; + updateBerRow(sel); + return; + } + const id = berCmdChainId(row.dataset.berUid, mode); + editor.innerHTML = '
    '; + const cmds = chainCommands(id); + _chains[id] = {rows: [{cmd: cmds[0].value, fields: {}}]}; + chainListen(id, function() { berRefreshCmdRow(row, id); }); + chainRender(id); + updateBerRow(sel); +} + +// Echo the embedded command's APDU next to the picker and refresh the TLV +// preview whenever the one-row chain changes. +function berRefreshCmdRow(row, chainId) { + const echo = row.querySelector('.ber-cmd-apdu'); + const hex = chainBuildRowHex(chainId, 0); + if (echo) echo.textContent = hex ? hex.match(/.{2}/g).join(' ') : ''; + const type = row.querySelector('.ber-type'); + if (type) updateBerRow(type); +} + +// The C-APDU a row contributes: pasted hex or the embedded command chain. +function genBerCmdApdu(row) { + const modeSel = row.querySelector('.ber-cmd-mode'); + const mode = modeSel ? modeSel.value : 'hex'; + if (mode === 'hex') { + const input = row.querySelector('.ber-hex'); + return input ? (input.value || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase() : ''; + } + return chainBuildRowHex(berCmdChainId(row.dataset.berUid, mode), 0); +} + function buildErrorActionRow() { return `
    Error Action: @@ -3885,9 +4326,9 @@ function genBerRowValue(row) { return genScriptChainingValue(row, tag); } if (type === 'c-apdu') { - const hex = (row.querySelector('.ber-hex').value||'').replace(/[^0-9a-fA-F]/g,''); + const hex = genBerCmdApdu(row); if (!hex) return ''; - return tag + berLenStr(hex.length/2) + hex; + return tag + berLenStr(hex.length / 2) + hex; } const st = row.querySelector('.ber-subtype').value; if (st === 'action') { @@ -13579,6 +14020,7 @@ const LANG_RU = { 'Queued': 'В очереди', 'Expanded format': 'Развёрнутый формат', 'To expanded': 'В развёрнутый', + '→ Expanded Script': '→ Расширенный скрипт', 'Queue in SCP81': 'В очередь SCP81', 'No commands': 'Нет команд', 'APDUs': 'APDU', diff --git a/frontend/sw.js b/frontend/sw.js index 2db1f5b..01da706 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v240'; +const CACHE = 'simple-v241'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/ber.test.js b/frontend/tests/ber.test.js index e6972ab..de64d4a 100644 --- a/frontend/tests/ber.test.js +++ b/frontend/tests/ber.test.js @@ -103,7 +103,7 @@ function buildPcHtml(extraFields) { } const FNS = ['berLenStr', 'gsm7TextToSeptets', 'gsm7Encode', 'genBerPcValue', 'genErrorActionValue', - 'genScriptChainingValue']; + 'genScriptChainingValue', 'chainKind', 'chainIsEmbedded', 'chainCommands', 'berCmdChainId', 'genBerCmdApdu', 'genBerRowValue']; let code = ''; for (const f of FNS) code += extractFunc(html, f) + '\n'; const berTagsMatch = html.match(/const BER_TAGS = \{[^}]*\};/); @@ -116,6 +116,16 @@ for (const m of [berTagsMatch, berQualMatch, berDevMatch, berTonesMatch, gsm7Alp if (!m) throw new Error('constant not found'); code = m[0] + '\n' + code; } +for (const c of ['CHAIN_CMDS_SIM', 'CHAIN_CMDS_USIM', 'CHAIN_CMDS_RAM']) { + const m = html.match(new RegExp('const ' + c + ' = \\[[\\s\\S]*?\\n\\];')); + if (!m) throw new Error('catalog not found: ' + c); + code += m[0].replace('const ', 'var ') + '\n'; +} +code += html.match(/const _chains = \{\};/)[0].replace('const ', 'var ') + '\n'; +code += html.match(/const CHAIN_LISTENERS = \{\};/)[0].replace('const ', 'var ') + '\n'; +// The embedded command chain is built by the shared command builders, which +// are covered by ts102226.test.js; here only the C-APDU row wiring matters. +code += 'function chainBuildRowHex(chainId) { return chainId === \'ber-ram-7\' ? \'80D0010101AA\' : \'\'; }\n'; code += '\nvar __berConsts = {BER_TAGS, BER_QUAL, BER_DEVICES, BER_TONES, GSM7_ALPHABET, GSM7_EXT_MAP};'; eval(code); const {BER_TAGS, BER_QUAL, BER_DEVICES, BER_TONES, GSM7_ALPHABET, GSM7_EXT_MAP} = __berConsts; @@ -277,4 +287,37 @@ test('genScriptChainingValue no position emits empty', () => { keep.name = 'chaining-0-keep'; row.children.push(first, interm, last, keep, el('chaining-script-id'), el('chaining-additional')); assert.strictEqual(genScriptChainingValue(row, '83'), ''); +}); + +// ===== Expanded Script C-APDU rows (TS 102 226 5.2.1.0/5.2.1.1) ===== + +function cmdRow(mode, value) { + const row = new StubEl(); + row.dataset = { berUid: '7' }; + const modeEl = el('ber-cmd-mode', mode); + const hexEl = el('ber-hex', value || ''); + const typeEl = el('ber-type', 'c-apdu'); + row.children.push(typeEl, modeEl, hexEl); + row.querySelector = sel => ({ '.ber-type': typeEl, '.ber-cmd-mode': modeEl, '.ber-hex': hexEl })[sel] || null; + return row; +} + +test('genBerCmdApdu reads pasted hex or the embedded command chain', () => { + assert.strictEqual(genBerCmdApdu(cmdRow('hex', '80 ca ff21 00')), '80CAFF2100'); + assert.strictEqual(genBerCmdApdu(cmdRow('ram', 'ignored')), '80D0010101AA'); + assert.strictEqual(genBerCmdApdu(cmdRow('hex', '')), ''); +}); + +test('genBerRowValue wraps a C-APDU row in the 22 command TLV', () => { + assert.strictEqual(genBerRowValue(cmdRow('ram', '')), '220680D0010101AA'); + assert.strictEqual(genBerRowValue(cmdRow('hex', '80C AFF2100')), '220580CAFF2100'); + assert.strictEqual(genBerRowValue(cmdRow('hex', '')), ''); +}); + +test('the embedded pickers offer the RFM/RAM command sets without GET RESPONSE', () => { + assert.ok(chainCommands('ber-sim-1').some(c => c.value === 'select')); + assert.ok(chainCommands('ber-usim-1').some(c => c.value === 'create-file')); + assert.ok(chainCommands('ber-ram-1').some(c => c.value === 'push')); + assert.ok(!chainCommands('ber-ram-1').some(c => c.value === 'get-response')); + assert.ok(chainCommands('chain-ram').some(c => c.value === 'get-response')); }); \ No newline at end of file diff --git a/frontend/tests/sim.test.js b/frontend/tests/sim.test.js index adca5b1..8384967 100644 --- a/frontend/tests/sim.test.js +++ b/frontend/tests/sim.test.js @@ -22,7 +22,7 @@ function extractFunc(src, name) { } // Extract chain builder functions and dependencies -const FNS = ['buildApdu', 'buildSelect', 'escHtml', 'chainInit', 'chainSimBuildRowHex']; +const FNS = ['buildApdu', 'buildSelect', 'escHtml', 'chainInit', 'chainKind', 'chainSimBuildRowHex']; let code = ''; for (const f of FNS) { code += extractFunc(html, f) + '\n'; diff --git a/frontend/tests/ts102226.test.js b/frontend/tests/ts102226.test.js new file mode 100644 index 0000000..3992807 --- /dev/null +++ b/frontend/tests/ts102226.test.js @@ -0,0 +1,247 @@ +const { test } = require('node:test'); +const assert = require('node:assert'); +const fs = require('node:fs'); +const path = require('node:path'); + +const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8'); + +function extractFunc(src, name) { + const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{'); + const m = re.exec(src); + if (!m) throw new Error('function ' + name + ' not found'); + let i = m.index + m[0].length - 1; + let depth = 0; + for (; i < src.length; i++) { + if (src[i] === '{') depth++; + else if (src[i] === '}') { + depth--; + if (depth === 0) break; + } + } + return src.slice(m.index, i + 1); +} + +// Command builders and the expanded-script wiring they feed. +// Reference vectors are computed from the pinned specs: +// TS 102 221 11.1.7/11.1.8 (SEARCH RECORD, INCREASE), 11.3 (SET/RETRIEVE DATA) +// TS 102 222 tables 2/7/16/17 (CREATE/DELETE/RESIZE FILE, FCP templates) +// TS 151 011 9.2.7/9.2.8 (SEEK, INCREASE for 2G SIM) +// TS 102 226 9.2.1-9.2.4 (PUSH), 5.2.1 (Command TLVs) +// GlobalPlatform Card Spec 11.8 (PUT KEY) +const FNS = ['berLenStr', 'buildApdu', 'buildSelect', 'escHtml', 'esc', 'chainInit', + 'chainKind', 'chainIsEmbedded', 'chainCommands', 'chainBuildRowHex', + 'chainSimBuildRowHex', 'chainRamBuildRowHex', 'chainPushData', '_hotaAsciiHex', + 'chainApduList', 'chainBuildFcp']; +let code = ''; +for (const f of FNS) code += extractFunc(html, f) + '\n'; +for (const c of ['CHAIN_CMDS_SIM', 'CHAIN_CMDS_USIM', 'CHAIN_CMDS_RAM']) { + const m = html.match(new RegExp('const ' + c + ' = \\[[\\s\\S]*?\\n\\];')); + if (!m) throw new Error('catalog not found: ' + c); + code += m[0].replace('const ', 'var ') + '\n'; +} +code += html.match(/const _chains = \{\};/)[0].replace('const ', 'var ') + '\n'; +code += html.match(/const CHAIN_LISTENERS = \{\};/)[0].replace('const ', 'var ') + '\n'; +// The real chain renderers are DOM-bound; the pure builders under test only +// need chainRender as a no-op, and chainApduList's splitter is exercised with +// a stub (the splitter itself is covered by apdu_parse.test.js). +code += 'function chainRender() {}\n'; +code += 'var __split = [];\nfunction parseCompactApdus() { return __split; }\n'; +eval(code); + +function row(cmd, fields) { + return { cmd: cmd, fields: fields || {} }; +} + +test('the command catalogs cover the TS 102 226 table 7.1/8.1/9.1 gaps', () => { + const sim = CHAIN_CMDS_SIM.map(c => c.value); + const usim = CHAIN_CMDS_USIM.map(c => c.value); + const ram = CHAIN_CMDS_RAM.map(c => c.value); + ['search-record', 'increase'].forEach(c => assert.ok(sim.includes(c), 'SIM ' + c)); + ['search-record', 'increase', 'create-file', 'delete-file', 'resize-file', 'set-data', 'retrieve-data'] + .forEach(c => assert.ok(usim.includes(c), 'USIM ' + c)); + ['put-key', 'push'].forEach(c => assert.ok(ram.includes(c), 'RAM ' + c)); +}); + +test('chainKind maps the stock and embedded chains; embedded ones hide GET RESPONSE', () => { + assert.strictEqual(chainKind('chain-sim'), 'sim'); + assert.strictEqual(chainKind('chain-usim'), 'usim'); + assert.strictEqual(chainKind('chain-ram'), 'ram'); + assert.strictEqual(chainKind('ber-sim-7'), 'sim'); + assert.strictEqual(chainKind('ber-usim-12'), 'usim'); + assert.strictEqual(chainKind('ber-ram-1'), 'ram'); + assert.ok(chainIsEmbedded('ber-sim-3')); + assert.ok(!chainIsEmbedded('chain-sim')); + // GET RESPONSE is not used in the expanded format (TS 102 226 5.2.1.1). + assert.ok(chainCommands('chain-sim').some(c => c.value === 'get-response')); + assert.ok(!chainCommands('ber-sim-3').some(c => c.value === 'get-response')); + assert.ok(!chainCommands('ber-ram-3').some(c => c.value === 'get-response')); +}); + +test('USIM SEARCH RECORD: simple and enhanced (TS 102 221 11.1.7)', () => { + _chains['chain-usim'] = { rows: [] }; + // Simple forward from record 2 (P2=10), pattern ABCD, Le=00. + assert.strictEqual( + chainSimBuildRowHex('chain-usim', 0, row('search-record', { srMode: '10', srRec: '02', srPattern: 'ABCD', le: '00' })), + '00A2021002ABCD00'); + // Enhanced (P2=18): 2-byte indication + search string. + assert.strictEqual( + chainSimBuildRowHex('chain-usim', 0, row('search-record', { srMode: '18', srRec: '00', srInd: '1800', srPattern: 'AB', le: '00' })), + '00A20018031800AB00'); + // Backward from record 1 (P2=12). + assert.strictEqual( + chainSimBuildRowHex('chain-usim', 0, row('search-record', { srMode: '12', srRec: '01', srPattern: 'AA', le: '00' })), + '00A2011201AA00'); + // Missing pattern: no APDU. + assert.strictEqual(chainSimBuildRowHex('chain-usim', 0, row('search-record', { srPattern: '' })), ''); +}); + +test('SIM SEEK: type/mode in P2, pattern in the data field (TS 151 011 9.2.7)', () => { + _chains['chain-sim'] = { rows: [] }; + // Type 1, from next forward. + assert.strictEqual( + chainSimBuildRowHex('chain-sim', 0, row('search-record', { srMode: '02', srPattern: '112233', le: '' })), + 'A0A2000203112233'); + // Type 2, from the beginning: Le appended. + assert.strictEqual( + chainSimBuildRowHex('chain-sim', 0, row('search-record', { srMode: '10', srPattern: '11', le: '00' })), + 'A0A20010011100'); +}); + +test('INCREASE: TS 102 221 (USIM, optional SFI) and TS 51 011 (SIM)', () => { + _chains['chain-sim'] = { rows: [] }; + _chains['chain-usim'] = { rows: [] }; + assert.strictEqual( + chainSimBuildRowHex('chain-usim', 0, row('increase', { value: '000001', le: '00' })), + '003200000300000100'); + assert.strictEqual( + chainSimBuildRowHex('chain-usim', 0, row('increase', { sfi: '05', value: '000001', le: '00' })), + '003285000300000100'); + // TS 51 011 9.2.8: class A0, P1/P2 = 00, P3 = 03. + assert.strictEqual( + chainSimBuildRowHex('chain-sim', 0, row('increase', { value: '000001' })), + 'A032000003000001'); + assert.strictEqual(chainSimBuildRowHex('chain-usim', 0, row('increase', { value: '' })), ''); +}); + +test('CREATE/DELETE/RESIZE FILE (TS 102 222 tables 2, 7, 16)', () => { + _chains['chain-usim'] = { rows: [] }; + const fcp = '62138202002183026F078A01058C027F0080020010'; + assert.strictEqual( + chainSimBuildRowHex('chain-usim', 0, row('create-file', { fcp: fcp })), + '00E0000015' + fcp); + assert.strictEqual( + chainSimBuildRowHex('chain-usim', 0, row('create-file', { fcp: '83026F07' })), + ''); + assert.strictEqual( + chainSimBuildRowHex('chain-usim', 0, row('delete-file', { fid: '6F07' })), + '00E40000026F07'); + // RESIZE FILE uses CLA 80 (TS 102 222 table 1: '8X'/'CX'). + const rfcp = '620883026F0780020010'; + assert.strictEqual( + chainSimBuildRowHex('chain-usim', 0, row('resize-file', { mode: '00', fcp: rfcp })), + '80D400000A' + rfcp); + assert.strictEqual( + chainSimBuildRowHex('chain-usim', 0, row('resize-file', { mode: '01', fcp: rfcp })), + '80D401000A' + rfcp); +}); + +test('SET DATA / RETRIEVE DATA block coding (TS 102 221 table 11.35)', () => { + _chains['chain-usim'] = { rows: [] }; + assert.strictEqual( + chainSimBuildRowHex('chain-usim', 0, row('set-data', { sdMode: '80', sfi: '00', value: '80017F' })), + '00DB00800380017F'); + // Next block with SFI 5. + assert.strictEqual( + chainSimBuildRowHex('chain-usim', 0, row('set-data', { sdMode: '00', sfi: '05', value: '80017F' })), + '00DB00050380017F'); + // First block, tag 5C, Le 00. + assert.strictEqual( + chainSimBuildRowHex('chain-usim', 0, row('retrieve-data', { sdMode: '80', sfi: '00', tag: '5C', le: '00' })), + '00CB0080015C00'); + // No tag: case 2. + assert.strictEqual( + chainSimBuildRowHex('chain-usim', 0, row('retrieve-data', { sdMode: '80', sfi: '00', tag: '', le: '00' })), + '00CB008000'); +}); + +test('PUT KEY (GlobalPlatform Card Spec 11.8)', () => { + _chains['chain-ram'] = { rows: [] }; + assert.strictEqual( + chainRamBuildRowHex(0, row('put-key', { keyVer: '01', keyId: '02', data: '1122334455667788' })), + '80D00102081122334455667788'); + assert.strictEqual(chainRamBuildRowHex(0, row('put-key', { data: '' })), ''); +}); + +test('PUSH: BIP channel opening and identification packet (TS 102 226 9.2.1/9.2.4)', () => { + _chains['chain-ram'] = { rows: [] }; + // No TLVs: the application uses its defaults, so the APDU is case 1. + assert.strictEqual(chainRamBuildRowHex(0, row('push', { pushType: '01', pushData: '' })), '80EC0101'); + assert.strictEqual( + chainRamBuildRowHex(0, row('push', { pushType: '01', pushData: '350103' })), + '80EC010103350103'); + assert.strictEqual( + chainRamBuildRowHex(0, row('push', { pushType: '04', pushIdent: '0102' })), + '80EC01040436020102'); + assert.strictEqual(chainRamBuildRowHex(0, row('push', { pushType: '04', pushIdent: '' })), '80EC0104'); +}); + +test('PUSH: CAT_TP link and TCP connection data fields (TS 102 226 9.2.2/9.2.3)', () => { + // CAT_TP: transport level 3C (protocol type 00) + optional 39/36. + assert.strictEqual(chainPushData({ pushType: '02', pushPort: '1F90' }), '3C03001F90'); + assert.strictEqual( + chainPushData({ pushType: '02', pushPort: '1F90', pushSdu: '0200', pushIdent: 'ABCD' }), + '3C03001F90390202003602ABCD'); + assert.strictEqual(chainPushData({ pushType: '02', pushPort: '' }), ''); + // TCP: bearer 35, transport 3C (protocol type 02), destination 3E, NAA 47. + assert.strictEqual( + chainPushData({ pushType: '03', pushPort: '0050', pushAddr: '210A000001', pushApn: 'internet' }), + '3501033C030200503E05210A0000014708696E7465726E6574'); + assert.strictEqual( + chainPushData({ pushType: '03', pushPort: '0050', pushBearer: '350104' }), + '3501043C03020050'); + _chains['chain-ram'] = { rows: [] }; + assert.strictEqual( + chainRamBuildRowHex(0, row('push', { pushType: '02', pushPort: '1F90' })), + '80EC0102053C03001F90'); +}); + +test('CREATE FILE FCP template skeleton (TS 102 222 table 4)', () => { + _chains['chain-usim'] = { rows: [] }; + const r1 = row('create-file', { fid: '6F07', structure: 'transparent', fileSize: '0010', lcsi: '05' }); + _chains['chain-usim'].rows.push(r1); + chainBuildFcp('chain-usim', 0, 'create'); + assert.strictEqual(r1.fields.fcp, '62138202012183026F078A01058C027F0080020010'); + const r2 = row('create-file', { fid: '6F3A', structure: 'linear_fixed', recLen: '006E', fileSize: '0226', lcsi: '05' }); + _chains['chain-usim'].rows.push(r2); + chainBuildFcp('chain-usim', 1, 'create'); + assert.strictEqual(r2.fields.fcp, '621582040221006E83026F3A8A01058C027F0080020226'); + // Record files default the file size to one record. + const r3 = row('create-file', { fid: '6F3A', structure: 'cyclic', recLen: '0010', fileSize: '' }); + _chains['chain-usim'].rows.push(r3); + chainBuildFcp('chain-usim', 2, 'create'); + assert.ok(r3.fields.fcp.endsWith('8002' + '0010')); + // RESIZE template: FID + file size only (table 17). + const r4 = row('resize-file', { fid: '6F07', fileSize: '0010' }); + _chains['chain-usim'].rows.push(r4); + chainBuildFcp('chain-usim', 3, 'resize'); + assert.strictEqual(r4.fields.fcp, '620883026F0780020010'); +}); + +test('chainApduList drops GET RESPONSE and splits multi-APDU rows', () => { + _chains['ber-usim-1'] = { rows: [ + row('read-binary', { offset: '0000', le: '00' }), + row('get-response', {}), + row('select', { method: 'chain', chain: '3F00,2FE2' }), + ] }; + // Single APDUs pass through; the SELECT chain row is split by the parser. + __split = [{ label: 'APDU', hex: '00A40000023F00' }, { label: 'APDU', hex: '00A40000022FE2' }]; + const list = chainApduList('ber-usim-1'); + assert.deepStrictEqual(list, ['00B0000000', '00A40000023F00', '00A40000022FE2']); +}); + +test('an embedded chain contributes a C-APDU TLV to the script', () => { + // genBerRowValue is covered in ber.test.js; here the chain-side contract: + // one row per C-APDU, built with the same builders as the stock chains. + _chains['ber-ram-9'] = { rows: [row('put-key', { keyVer: '01', keyId: '01', data: 'AA' })] }; + assert.strictEqual(chainBuildRowHex('ber-ram-9', 0), '80D0010101AA'); +}); diff --git a/pyproject.toml b/pyproject.toml index 5b683a0..c4543d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.2.0" +version = "3.3.0" description = "HTTP REST server wrapping pysim for the SIMple PWA" requires-python = ">=3.8" # pysim is a git-only dependency installed explicitly by setup.bat/setup.sh. diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index bff1d19..af01a10 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -29,7 +29,7 @@ from osmocom.tlv import BER_TLV_IE from osmocom.utils import rpad -VERSION = '3.2.0' +VERSION = '3.3.0' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE