feat: remote-script command palette — RFM/RAM commands in Expanded Script, TS 102 226 §9 PUSH (v3.3.0)
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-<kind>-<uid>) 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.
This commit is contained in:
@@ -64,6 +64,9 @@ CLA = `A0` (GSM 11.11 / ISO 7816-4).
|
|||||||
| DEACTIVATE FILE | 04 | Deactivate a file |
|
| DEACTIVATE FILE | 04 | Deactivate a file |
|
||||||
| VERIFY PIN | 20 | Verify PIN1 or PIN2 |
|
| VERIFY PIN | 20 | Verify PIN1 or PIN2 |
|
||||||
| CHANGE PIN | 24 | Change 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
|
#### 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).
|
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
|
#### References
|
||||||
|
|
||||||
- ETSI TS 102 221: UICC-Terminal Interface; Physical and Logical Characteristics
|
- ETSI TS 102 221: UICC-Terminal Interface; Physical and Logical Characteristics
|
||||||
@@ -110,11 +125,17 @@ Two encoding variants:
|
|||||||
|
|
||||||
| Type | Tag | Description |
|
| 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 |
|
| Immediate Action | 81 | Proactive command or action indicator |
|
||||||
| Error Action | 82 | Proactive command on error |
|
| Error Action | 82 | Proactive command on error |
|
||||||
| Script Chaining | 83 | Chaining data for multi-packet scripts |
|
| 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
|
#### Immediate Action builder
|
||||||
|
|
||||||
When the type is set to Immediate Action, the tool provides a structured builder for:
|
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 |
|
| GET DATA | CA | tag | Read card data objects |
|
||||||
| STORE DATA | E2 | 00/40/80/C0 | Store data (key, certificate, etc.) |
|
| STORE DATA | E2 | 00/40/80/C0 | Store data (key, certificate, etc.) |
|
||||||
| SET STATUS | F0 | 80/40/60 | Lifecycle state management |
|
| 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 |
|
| EXTERNAL AUTHENTICATE | 82 | 00 | SCP host authentication |
|
||||||
| INTERNAL AUTHENTICATE | 88 | 00 | Card challenge-response |
|
| 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
|
#### 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):
|
Tag `C7` in the INSTALL data field. Built from 3 privilege bytes (GP spec Tables 11-7, 11-8, 11-9):
|
||||||
|
|||||||
+26
-1
@@ -64,6 +64,9 @@ CLA = `A0` (GSM 11.11 / ISO 7816-4).
|
|||||||
| DEACTIVATE FILE | 04 | Деактивация файла |
|
| DEACTIVATE FILE | 04 | Деактивация файла |
|
||||||
| VERIFY PIN | 20 | Проверка PIN1 или PIN2 |
|
| VERIFY PIN | 20 | Проверка PIN1 или PIN2 |
|
||||||
| CHANGE PIN | 24 | Смена 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
|
#### Методы 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.
|
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
|
- 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 |
|
| Immediate Action | 81 | Proactive-команда или action indicator |
|
||||||
| Error Action | 82 | Proactive-команда при ошибке |
|
| Error Action | 82 | Proactive-команда при ошибке |
|
||||||
| Script Chaining | 83 | Данные для многопакетных скриптов |
|
| 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
|
#### Сборщик Immediate Action
|
||||||
|
|
||||||
- **Action indicator**: `81` / `82`
|
- **Action indicator**: `81` / `82`
|
||||||
@@ -145,9 +166,13 @@ CLA = `80` (GlobalPlatform v2.3.1). Удалённое управление со
|
|||||||
| GET DATA | CA | tag | Чтение объектов данных |
|
| GET DATA | CA | tag | Чтение объектов данных |
|
||||||
| STORE DATA | E2 | 00/40/80/C0 | Запись данных |
|
| STORE DATA | E2 | 00/40/80/C0 | Запись данных |
|
||||||
| SET STATUS | F0 | 80/40/60 | Управление жизненным циклом |
|
| 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 |
|
| EXTERNAL AUTHENTICATE | 82 | 00 | Аутентификация SCP |
|
||||||
| INTERNAL AUTHENTICATE | 88 | 00 | Challenge-response |
|
| 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])
|
#### Привилегии (INSTALL [for install])
|
||||||
|
|
||||||
Три байта привилегий по GP Spec Tables 11-7, 11-8, 11-9.
|
Три байта привилегий по GP Spec Tables 11-7, 11-8, 11-9.
|
||||||
|
|||||||
+19
-7
@@ -74,6 +74,8 @@
|
|||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">DISABLE PIN</td><td class="py-1 px-2 font-mono">26</td><td class="py-1 px-2">Отключение PIN</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">DISABLE PIN</td><td class="py-1 px-2 font-mono">26</td><td class="py-1 px-2">Отключение PIN</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">ENABLE PIN</td><td class="py-1 px-2 font-mono">28</td><td class="py-1 px-2">Включение PIN</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">ENABLE PIN</td><td class="py-1 px-2 font-mono">28</td><td class="py-1 px-2">Включение PIN</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">UNBLOCK PIN</td><td class="py-1 px-2 font-mono">2C</td><td class="py-1 px-2">Разблокировка PIN с помощью PUK</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">UNBLOCK PIN</td><td class="py-1 px-2 font-mono">2C</td><td class="py-1 px-2">Разблокировка PIN с помощью PUK</td></tr>
|
||||||
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">SEARCH RECORD (SEEK)</td><td class="py-1 px-2 font-mono">A2</td><td class="py-1 px-2">Поиск образца в линейном фиксированном EF; P2 задаёт тип 1/2 и режим (TS 151 011 §9.2.7)</td></tr>
|
||||||
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INCREASE</td><td class="py-1 px-2 font-mono">32</td><td class="py-1 px-2">Прибавить значение к последней увеличенной/обновлённой записи циклического EF (значение 3 байта, TS 151 011 §9.2.8)</td></tr>
|
||||||
<tr><td class="py-1 px-2 font-mono">GET RESPONSE</td><td class="py-1 px-2 font-mono">C0</td><td class="py-1 px-2">Получение данных, на которые указывает предшествующий <code class="font-mono text-sm">61XX</code>/<code class="font-mono text-sm">9FXX</code></td></tr>
|
<tr><td class="py-1 px-2 font-mono">GET RESPONSE</td><td class="py-1 px-2 font-mono">C0</td><td class="py-1 px-2">Получение данных, на которые указывает предшествующий <code class="font-mono text-sm">61XX</code>/<code class="font-mono text-sm">9FXX</code></td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -90,13 +92,17 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p class="text-sm mb-3">Для record-команд режим P2: <strong>Absolute (04)</strong>, <strong>Next (02)</strong> или <strong>Previous (03)</strong>. Если за Case 4 командой сразу следует строка GET RESPONSE, сборщик цепочки автоматически убирает её байт Le (ETSI TS 102 226 §5.1.1). В правой колонке — панель конвертации (IMSI, MSISDN, ICCID, SPN, PLMN, nibble swap). См. <a href="#conversion" class="text-blue-600 dark:text-blue-400 hover:underline">§2.5</a>.</p>
|
<p class="text-sm mb-3">Для record-команд режим P2: <strong>Absolute (04)</strong>, <strong>Next (02)</strong> или <strong>Previous (03)</strong>. Если за Case 4 командой сразу следует строка GET RESPONSE, сборщик цепочки автоматически убирает её байт Le (ETSI TS 102 226 §5.1.1). В правой колонке — панель конвертации (IMSI, MSISDN, ICCID, SPN, PLMN, nibble swap). См. <a href="#conversion" class="text-blue-600 dark:text-blue-400 hover:underline">§2.5</a>.</p>
|
||||||
|
<p class="text-sm mb-3"><strong>→ Расширенный скрипт</strong> отправляет собранную цепочку в конструктор Expanded Script строками C-APDU (строки GET RESPONSE отбрасываются — расширенный формат их не использует, TS 102 226 §5.2.1.1). Приложение UICC Shared File System RFM (TAR <code class="font-mono text-sm">B00000</code>) доступно и в компактном, и в расширенном формате — если TAR не настроен на автоопределение формата, используются разные значения TAR (TS 102 226 §7.0/§5.3).</p>
|
||||||
|
|
||||||
<h3 id="usim-rfm" class="text-lg font-medium mb-2">2.2 USIM RFM</h3>
|
<h3 id="usim-rfm" class="text-lg font-medium mb-2">2.2 USIM RFM</h3>
|
||||||
<p class="mb-2">CLA = <code class="font-mono text-sm">00</code> (ETSI TS 102 221). Тот же сборщик цепочки и набор команд, что и SIM. Отличия:</p>
|
<p class="mb-2">CLA = <code class="font-mono text-sm">00</code> (ETSI TS 102 221). Тот же сборщик цепочки и набор команд, что и SIM, плюс административные команды UICC и формы SEARCH RECORD / INCREASE из TS 102 221:</p>
|
||||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||||
<li><strong>SELECT</strong> по умолчанию запрашивает FCP (P2=<code class="font-mono text-sm">04</code>) и добавляет Le=<code class="font-mono text-sm">00</code>; флажок <strong>Silent (P2=0C)</strong> выбирает файл без запроса FCP (без Le, без данных ответа).</li>
|
<li><strong>SELECT</strong> по умолчанию запрашивает FCP (P2=<code class="font-mono text-sm">04</code>) и добавляет Le=<code class="font-mono text-sm">00</code>; флажок <strong>Silent (P2=0C)</strong> выбирает файл без запроса FCP (без Le, без данных ответа).</li>
|
||||||
<li><strong>По пути</strong> предлагает выбор <strong>from MF</strong> (P1=<code class="font-mono text-sm">08</code>) или <strong>from current DF</strong> (P1=<code class="font-mono text-sm">09</code>); текущий DF сессии ADF RFM — ADF.USIM.</li>
|
<li><strong>По пути</strong> предлагает выбор <strong>from MF</strong> (P1=<code class="font-mono text-sm">08</code>) или <strong>from current DF</strong> (P1=<code class="font-mono text-sm">09</code>); текущий DF сессии ADF RFM — ADF.USIM.</li>
|
||||||
<li><strong>Упаковать в Secured packet</strong> устанавливает TAR SCP80 в <strong>ADF RFM TAR</strong> выбранной предустановки (по умолчанию <code class="font-mono text-sm">B00001</code>), привязанный к AID ADF (ADF.USIM).</li>
|
<li><strong>Упаковать в Secured packet</strong> устанавливает TAR SCP80 в <strong>ADF RFM TAR</strong> выбранной предустановки (по умолчанию <code class="font-mono text-sm">B00001</code>), привязанный к AID ADF (ADF.USIM).</li>
|
||||||
|
<li><strong>SEARCH RECORD</strong> (TS 102 221 §11.1.7) — простые режимы (P2=<code class="font-mono text-sm">10</code>/<code class="font-mono text-sm">12</code>, P1 = номер записи) и расширенный режим (P2=<code class="font-mono text-sm">18</code>) с 2-байтовым search indication; <strong>INCREASE</strong> (§11.1.8) принимает необязательный SFI и прибавляемое значение.</li>
|
||||||
|
<li><strong>CREATE FILE</strong> / <strong>DELETE FILE</strong> / <strong>RESIZE FILE</strong> (ETSI TS 102 222 §6.3/6.4/6.10) принимают шаблон FCP; кнопка <strong>Build template</strong> собирает структурный каркас (file descriptor, FID, LCSI, размер и разрешающее компактное правило доступа <code class="font-mono text-sm">8C 02 7F 00</code> — проверьте его для своей карты). RESIZE FILE использует CLA <code class="font-mono text-sm">80</code> (TS 102 222 Table 1: <code class="font-mono text-sm">'8X'</code>/<code class="font-mono text-sm">'CX'</code>).</li>
|
||||||
|
<li><strong>SET DATA</strong> / <strong>RETRIEVE DATA</strong> (TS 102 221 §11.3) работают с BER-TLV EF: P2 выбирает первый/следующий/повторный блок (+SFI), поле тега необязательно (<code class="font-mono text-sm">5C</code> — список размещённых тегов).</li>
|
||||||
<li>Каждый переход SELECT в цепочке запрашивает FCP, если не отмечен как silent.</li>
|
<li>Каждый переход SELECT в цепочке запрашивает FCP, если не отмечен как silent.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@@ -113,13 +119,15 @@
|
|||||||
<th class="text-left py-1 px-2">Тип</th><th class="text-left py-1 px-2">Тег</th><th class="text-left py-1 px-2">Описание</th>
|
<th class="text-left py-1 px-2">Тип</th><th class="text-left py-1 px-2">Тег</th><th class="text-left py-1 px-2">Описание</th>
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">C-APDU</td><td class="py-1 px-2 font-mono">22</td><td class="py-1 px-2">APDU</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">C-APDU</td><td class="py-1 px-2 font-mono">22</td><td class="py-1 px-2">Любая команда удалённого управления: hex APDU или команда, собранная редакторами SIM RFM / USIM RFM / RAM-GP (TS 102 226 §5.2.1.0; GET RESPONSE в этом формате не используется, §5.2.1.1)</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Immediate Action</td><td class="py-1 px-2 font-mono">81</td><td class="py-1 px-2">Проактивная команда или action indicator</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Immediate Action</td><td class="py-1 px-2 font-mono">81</td><td class="py-1 px-2">Проактивная команда или action indicator</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Error Action</td><td class="py-1 px-2 font-mono">82</td><td class="py-1 px-2">Условное восстановление при ошибках с action indicator или проактивной командой</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Error Action</td><td class="py-1 px-2 font-mono">82</td><td class="py-1 px-2">Условное восстановление при ошибках с action indicator или проактивной командой</td></tr>
|
||||||
<tr><td class="py-1 px-2">Script Chaining</td><td class="py-1 px-2 font-mono">83</td><td class="py-1 px-2">Многопакетное выполнение скрипта с флагами First/Intermediary/Last</td></tr>
|
<tr><td class="py-1 px-2">Script Chaining</td><td class="py-1 px-2 font-mono">83</td><td class="py-1 px-2">Многопакетное выполнение скрипта с флагами First/Intermediary/Last</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p class="text-sm mb-3">Сборщик Immediate Action предлагает action indicator (<code class="font-mono text-sm">81</code>/<code class="font-mono text-sm">82</code>), структурированный сборщик проактивных команд (REFRESH, DISPLAY TEXT, PLAY TONE с авто-генерацией COMPREHENSION-TLV), или ручной hex-ввод.</p>
|
<p class="text-sm mb-3">Сборщик Immediate Action предлагает action indicator (<code class="font-mono text-sm">81</code>/<code class="font-mono text-sm">82</code>), структурированный сборщик проактивных команд (REFRESH, DISPLAY TEXT, PLAY TONE с авто-генерацией COMPREHENSION-TLV), или ручной hex-ввод.</p>
|
||||||
|
<p class="text-sm mb-3">Строка <strong>C-APDU</strong> начинает в режиме <strong>Hex</strong> (вставьте любую команду) и может переключиться на одно-строчный редактор <strong>SIM RFM</strong>, <strong>USIM RFM</strong> или <strong>RAM/GP</strong>, который собирает команду из полей — итоговый APDU оборачивается в TLV <code class="font-mono text-sm">22</code> и показывается рядом с выбором. GET RESPONSE там не предлагается: расширенный формат не несёт GET RESPONSE (TS 102 226 §5.2.1.1). Кнопка <strong>→ Расширенный скрипт</strong> в представлениях SIM RFM / USIM RFM / RAM-GP добавляет их команды строками C-APDU.</p>
|
||||||
|
<p class="text-sm mb-3">Только три проактивные команды допустимы как Immediate Action (TS 102 226 Table 5.5): REFRESH, DISPLAY TEXT и PLAY TONE — поведение карты для любой другой проактивной команды не определено.</p>
|
||||||
|
|
||||||
<h4 id="ber-error-action" class="font-medium mb-2 text-base">Error Action TLV (Tag 82)</h4>
|
<h4 id="ber-error-action" class="font-medium mb-2 text-base">Error Action TLV (Tag 82)</h4>
|
||||||
<p class="text-sm mb-2">Восстановление при ошибках по TS 102 226 §5.2.1.3 — одна из четырёх форм:</p>
|
<p class="text-sm mb-2">Восстановление при ошибках по TS 102 226 §5.2.1.3 — одна из четырёх форм:</p>
|
||||||
@@ -131,13 +139,14 @@
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h4 id="ber-script-chaining" class="font-medium mb-2 text-base">Script Chaining TLV (Tag 83)</h4>
|
<h4 id="ber-script-chaining" class="font-medium mb-2 text-base">Script Chaining TLV (Tag 83)</h4>
|
||||||
<p class="text-sm mb-2">Многопакетное выполнение скрипта с сохранением контекста:</p>
|
<p class="text-sm mb-2">Многопакетное выполнение скрипта с сохранением контекста (TS 102 226 §5.2.1.4). TLV имеет вид <code class="font-mono text-sm">83 01 <флаг></code>, присутствует один раз и только как первый Command TLV; допускается для RFM и RAM:</p>
|
||||||
<ul class="text-sm list-disc pl-5 mb-2">
|
<ul class="text-sm list-disc pl-5 mb-2">
|
||||||
<li><strong>Флаги цепочки:</strong> <code class="font-mono text-sm">01</code> первый скрипт (удалять инфо о цепочке при сбросе), <code class="font-mono text-sm">11</code> первый скрипт (сохранять инфо о цепочке при сбросе, только RFM), <code class="font-mono text-sm">02</code> последующий скрипт (будут ещё), <code class="font-mono text-sm">03</code> последующий скрипт (последний)</li>
|
<li><code class="font-mono text-sm">01</code> — первый скрипт, удалить информацию цепочки при сбросе карты</li>
|
||||||
<li><strong>Идентификатор скрипта:</strong> Корреляционный идентификатор между пакетами (1-4 байта hex, авто-инкремент подсказки)</li>
|
<li><code class="font-mono text-sm">11</code> — первый скрипт, сохранить информацию цепочки при сбросе карты (только RFM)</li>
|
||||||
<li><strong>Дополнительные данные:</strong> Расширенная информация цепочки (опционально hex)</li>
|
<li><code class="font-mono text-sm">02</code> — последующий скрипт, далее будут ещё</li>
|
||||||
<li><strong>Сохранение контекста:</strong> UICC сохраняет состояние безопасности/транзакции между пакетами</li>
|
<li><code class="font-mono text-sm">03</code> — последующий скрипт, последний</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<p class="text-sm mb-3">При цепочке сессия команд выходит за рамки одного шаблона: UICC сохраняет контекст файла/PIN (RFM) или сессию приложения (RAM, например цепочку STORE DATA) до последнего скрипта.</p>
|
||||||
|
|
||||||
<h4 id="expanded-response" class="font-medium mb-2 text-base">Декодирование ответов (TS 102 226 §5.2.2)</h4>
|
<h4 id="expanded-response" class="font-medium mb-2 text-base">Декодирование ответов (TS 102 226 §5.2.2)</h4>
|
||||||
<p class="text-sm mb-2">Входящие ответы Proof of Receipt декодируются сервером — формат expanded Remote Application response data (TS 102 226 §5.2.2) или компактный формат. Представление Secured Packet показывает результат после <strong>Отправить на карту</strong> (см. <a href="#secured-packet" class="text-blue-600 dark:text-blue-400 hover:underline">§3.1</a>): статус PoR (TAR, счётчик, сырой PoR), а статусное слово и данные ответа последней команды подставляются в подвкладку <strong>«Парсер ответов»</strong> (Remote APDU).</p>
|
<p class="text-sm mb-2">Входящие ответы Proof of Receipt декодируются сервером — формат expanded Remote Application response data (TS 102 226 §5.2.2) или компактный формат. Представление Secured Packet показывает результат после <strong>Отправить на карту</strong> (см. <a href="#secured-packet" class="text-blue-600 dark:text-blue-400 hover:underline">§3.1</a>): статус PoR (TAR, счётчик, сырой PoR), а статусное слово и данные ответа последней команды подставляются в подвкладку <strong>«Парсер ответов»</strong> (Remote APDU).</p>
|
||||||
@@ -160,6 +169,8 @@
|
|||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">GET DATA</td><td class="py-1 px-2 font-mono">CA</td><td class="py-1 px-2 font-mono">tag</td><td class="py-1 px-2">Чтение объектов данных</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">GET DATA</td><td class="py-1 px-2 font-mono">CA</td><td class="py-1 px-2 font-mono">tag</td><td class="py-1 px-2">Чтение объектов данных</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">STORE DATA</td><td class="py-1 px-2 font-mono">E2</td><td class="py-1 px-2 font-mono">00/40/80/C0/E0</td><td class="py-1 px-2">Запись данных</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">STORE DATA</td><td class="py-1 px-2 font-mono">E2</td><td class="py-1 px-2 font-mono">00/40/80/C0/E0</td><td class="py-1 px-2">Запись данных</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">SET STATUS</td><td class="py-1 px-2 font-mono">F0</td><td class="py-1 px-2 font-mono">80/40/60</td><td class="py-1 px-2">Управление жизненным циклом</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">SET STATUS</td><td class="py-1 px-2 font-mono">F0</td><td class="py-1 px-2 font-mono">80/40/60</td><td class="py-1 px-2">Управление жизненным циклом</td></tr>
|
||||||
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">PUT KEY</td><td class="py-1 px-2 font-mono">D0</td><td class="py-1 px-2 font-mono">key ver</td><td class="py-1 px-2">Замена ключа: P1 = версия ключа, P2 = идентификатор ключа, данные = новый ключевой материал (GP Card Spec §11.8)</td></tr>
|
||||||
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">PUSH</td><td class="py-1 px-2 font-mono">EC</td><td class="py-1 px-2 font-mono">01</td><td class="py-1 px-2">Просьба приложению открыть BIP-канал / CAT_TP-соединение / TCP-соединение или отправить пакет идентификации (TS 102 226 §9)</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">EXTERNAL AUTHENTICATE</td><td class="py-1 px-2 font-mono">82</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Аутентификация SCP</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">EXTERNAL AUTHENTICATE</td><td class="py-1 px-2 font-mono">82</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Аутентификация SCP</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INTERNAL AUTHENTICATE</td><td class="py-1 px-2 font-mono">88</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Challenge-response</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INTERNAL AUTHENTICATE</td><td class="py-1 px-2 font-mono">88</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Challenge-response</td></tr>
|
||||||
<tr><td class="py-1 px-2 font-mono">GET RESPONSE</td><td class="py-1 px-2 font-mono">C0</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Получение данных после <code class="font-mono text-sm">61XX</code> (Le настраивается)</td></tr>
|
<tr><td class="py-1 px-2 font-mono">GET RESPONSE</td><td class="py-1 px-2 font-mono">C0</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Получение данных после <code class="font-mono text-sm">61XX</code> (Le настраивается)</td></tr>
|
||||||
@@ -195,6 +206,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p class="text-sm mb-3">Полные таблицы 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.</p>
|
<p class="text-sm mb-3">Полные таблицы 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.</p>
|
||||||
|
<p class="text-sm mb-3"><strong>PUSH</strong> (TS 102 226 §9.2) — это C-APDU (<code class="font-mono text-sm">80 EC 01 P2</code>), адресованный приложению, поддерживающему BIP и/или CAT_TP (не триггер HTTP-администрирования — тот собирается в конструкторе HTTP OTA выше). P2: <code class="font-mono text-sm">01</code> открытие BIP-канала (COMPREHENSION-TLV OPEN CHANNEL, необязательно), <code class="font-mono text-sm">02</code> CAT_TP-соединение (порт назначения в transport level <code class="font-mono text-sm">3C</code> с типом протокола 00, необязательные buffer size <code class="font-mono text-sm">39</code> / channel data <code class="font-mono text-sm">36</code>), <code class="font-mono text-sm">03</code> TCP-соединение (bearer <code class="font-mono text-sm">35</code>, transport level <code class="font-mono text-sm">3C</code> с типом протокола 02 = TCP-клиент, удалённое соединение, адрес назначения <code class="font-mono text-sm">3E</code>, NAA/APN <code class="font-mono text-sm">47</code>), <code class="font-mono text-sm">04</code> пакет идентификации (channel data <code class="font-mono text-sm">36</code>; при отсутствии используется ICCID). Проактивную команду OPEN CHANNEL выдаёт само приложение; успех — <code class="font-mono text-sm">90 00</code>, отказ — <code class="font-mono text-sm">6F 00</code> с Result TLV в данных ответа, некорректные данные отклоняются с <code class="font-mono text-sm">6A 80</code>. P1 <code class="font-mono text-sm">80</code> зарезервирован для приложения.</p>
|
||||||
|
|
||||||
<h3 id="conversion" class="text-lg font-medium mb-2">2.5 Конвертация (боковые панели SIM/USIM)</h3>
|
<h3 id="conversion" class="text-lg font-medium mb-2">2.5 Конвертация (боковые панели SIM/USIM)</h3>
|
||||||
<ul class="list-disc list-inside text-sm space-y-1">
|
<ul class="list-disc list-inside text-sm space-y-1">
|
||||||
|
|||||||
+19
-8
@@ -74,6 +74,8 @@
|
|||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">DISABLE PIN</td><td class="py-1 px-2 font-mono">26</td><td class="py-1 px-2">Disable a PIN</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">DISABLE PIN</td><td class="py-1 px-2 font-mono">26</td><td class="py-1 px-2">Disable a PIN</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">ENABLE PIN</td><td class="py-1 px-2 font-mono">28</td><td class="py-1 px-2">Enable a PIN</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">ENABLE PIN</td><td class="py-1 px-2 font-mono">28</td><td class="py-1 px-2">Enable a PIN</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">UNBLOCK PIN</td><td class="py-1 px-2 font-mono">2C</td><td class="py-1 px-2">Unblock a PIN with PUK</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">UNBLOCK PIN</td><td class="py-1 px-2 font-mono">2C</td><td class="py-1 px-2">Unblock a PIN with PUK</td></tr>
|
||||||
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">SEARCH RECORD (SEEK)</td><td class="py-1 px-2 font-mono">A2</td><td class="py-1 px-2">Search a linear fixed EF for a pattern; P2 selects type 1/2 and the mode (TS 151 011 §9.2.7)</td></tr>
|
||||||
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INCREASE</td><td class="py-1 px-2 font-mono">32</td><td class="py-1 px-2">Add a value to the last increased/updated record of a cyclic EF (3-byte value, TS 151 011 §9.2.8)</td></tr>
|
||||||
<tr><td class="py-1 px-2 font-mono">GET RESPONSE</td><td class="py-1 px-2 font-mono">C0</td><td class="py-1 px-2">Fetch data indicated by a preceding <code class="font-mono text-sm">61XX</code>/<code class="font-mono text-sm">9FXX</code> status word</td></tr>
|
<tr><td class="py-1 px-2 font-mono">GET RESPONSE</td><td class="py-1 px-2 font-mono">C0</td><td class="py-1 px-2">Fetch data indicated by a preceding <code class="font-mono text-sm">61XX</code>/<code class="font-mono text-sm">9FXX</code> status word</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -90,12 +92,16 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p class="text-sm mb-3">For record commands, the P2 mode is <strong>Absolute (04)</strong>, <strong>Next (02)</strong>, or <strong>Previous (03)</strong>. 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 <a href="#conversion" class="text-blue-600 dark:text-blue-400 hover:underline">§2.5</a>.</p>
|
<p class="text-sm mb-3">For record commands, the P2 mode is <strong>Absolute (04)</strong>, <strong>Next (02)</strong>, or <strong>Previous (03)</strong>. 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 <a href="#conversion" class="text-blue-600 dark:text-blue-400 hover:underline">§2.5</a>.</p>
|
||||||
|
<p class="text-sm mb-3"><strong>→ Expanded Script</strong> 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 <code class="font-mono text-sm">B00000</code>) 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).</p>
|
||||||
|
|
||||||
<h3 id="usim-rfm" class="text-lg font-medium mb-2">2.2 USIM RFM</h3>
|
<h3 id="usim-rfm" class="text-lg font-medium mb-2">2.2 USIM RFM</h3>
|
||||||
<p class="mb-2">CLA = <code class="font-mono text-sm">00</code> (ETSI TS 102 221). Same chain builder and command set as SIM. Differences:</p>
|
<p class="mb-2">CLA = <code class="font-mono text-sm">00</code> (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:</p>
|
||||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||||
<li><strong>SELECT</strong> requests FCP by default (P2=<code class="font-mono text-sm">04</code>) and appends Le=<code class="font-mono text-sm">00</code>; a <strong>Silent (P2=0C)</strong> checkbox selects without requesting FCP (no Le, no response data).</li>
|
<li><strong>SELECT</strong> requests FCP by default (P2=<code class="font-mono text-sm">04</code>) and appends Le=<code class="font-mono text-sm">00</code>; a <strong>Silent (P2=0C)</strong> checkbox selects without requesting FCP (no Le, no response data).</li>
|
||||||
<li><strong>By path</strong> offers <strong>from MF</strong> (P1=<code class="font-mono text-sm">08</code>) or <strong>from current DF</strong> (P1=<code class="font-mono text-sm">09</code>); the current DF of an ADF RFM session is ADF.USIM.</li>
|
<li><strong>By path</strong> offers <strong>from MF</strong> (P1=<code class="font-mono text-sm">08</code>) or <strong>from current DF</strong> (P1=<code class="font-mono text-sm">09</code>); the current DF of an ADF RFM session is ADF.USIM.</li>
|
||||||
|
<li><strong>SEARCH RECORD</strong> (TS 102 221 §11.1.7) offers the simple modes (P2=<code class="font-mono text-sm">10</code>/<code class="font-mono text-sm">12</code>, P1 = record number) and the enhanced mode (P2=<code class="font-mono text-sm">18</code>) with the 2-byte search indication; <strong>INCREASE</strong> (§11.1.8) takes an optional SFI and the value to add.</li>
|
||||||
|
<li><strong>CREATE FILE</strong> / <strong>DELETE FILE</strong> / <strong>RESIZE FILE</strong> (ETSI TS 102 222 §6.3/6.4/6.10) take an FCP template; <strong>Build template</strong> composes a structural skeleton (file descriptor, FID, LCSI, size, and a permissive compact access rule <code class="font-mono text-sm">8C 02 7F 00</code> — review it for your card). RESIZE FILE uses CLA <code class="font-mono text-sm">80</code> (TS 102 222 Table 1: <code class="font-mono text-sm">'8X'</code>/<code class="font-mono text-sm">'CX'</code>).</li>
|
||||||
|
<li><strong>SET DATA</strong> / <strong>RETRIEVE DATA</strong> (TS 102 221 §11.3) operate on BER-TLV EFs: P2 selects first/next/retransmit block (+SFI), the tag field is optional (<code class="font-mono text-sm">5C</code> lists the allocated tags).</li>
|
||||||
<li><strong>Pack into Secured packet</strong> sets the SCP80 TAR to the selected preset’s <strong>ADF RFM TAR</strong> (<code class="font-mono text-sm">B00001</code> by default), linked to the ADF’s AID (ADF.USIM).</li>
|
<li><strong>Pack into Secured packet</strong> sets the SCP80 TAR to the selected preset’s <strong>ADF RFM TAR</strong> (<code class="font-mono text-sm">B00001</code> by default), linked to the ADF’s AID (ADF.USIM).</li>
|
||||||
<li>Each SELECT hop in a chain requests FCP unless marked silent.</li>
|
<li>Each SELECT hop in a chain requests FCP unless marked silent.</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -113,13 +119,14 @@
|
|||||||
<th class="text-left py-1 px-2">Type</th><th class="text-left py-1 px-2">Tag</th><th class="text-left py-1 px-2">Description</th>
|
<th class="text-left py-1 px-2">Type</th><th class="text-left py-1 px-2">Tag</th><th class="text-left py-1 px-2">Description</th>
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">C-APDU</td><td class="py-1 px-2 font-mono">22</td><td class="py-1 px-2">Raw APDU hex</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">C-APDU</td><td class="py-1 px-2 font-mono">22</td><td class="py-1 px-2">Any 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)</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Immediate Action</td><td class="py-1 px-2 font-mono">81</td><td class="py-1 px-2">Proactive command or action indicator</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Immediate Action</td><td class="py-1 px-2 font-mono">81</td><td class="py-1 px-2">Proactive command or action indicator</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Error Action</td><td class="py-1 px-2 font-mono">82</td><td class="py-1 px-2">Conditional error recovery with action indicator or proactive command</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Error Action</td><td class="py-1 px-2 font-mono">82</td><td class="py-1 px-2">Conditional error recovery with action indicator or proactive command</td></tr>
|
||||||
<tr><td class="py-1 px-2">Script Chaining</td><td class="py-1 px-2 font-mono">83</td><td class="py-1 px-2">Multi-packet script execution with First/Intermediary/Last flags</td></tr>
|
<tr><td class="py-1 px-2">Script Chaining</td><td class="py-1 px-2 font-mono">83</td><td class="py-1 px-2">Multi-packet script execution with First/Intermediary/Last flags</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p class="text-sm mb-3">The Immediate Action builder offers an action indicator (<code class="font-mono text-sm">81</code>/<code class="font-mono text-sm">82</code>), a structured proactive command builder (REFRESH, DISPLAY TEXT, PLAY TONE with auto-generated COMPREHENSION-TLV objects), or a freeform hex input.</p>
|
<p class="text-sm mb-3">A <strong>C-APDU</strong> row starts in <strong>Hex</strong> mode (paste any command) and can switch to a one-row <strong>SIM RFM</strong>, <strong>USIM RFM</strong> or <strong>RAM/GP</strong> editor that builds the command from fields — the resulting APDU is wrapped in the <code class="font-mono text-sm">22</code> 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). <strong>→ Expanded Script</strong> in the SIM RFM / USIM RFM / RAM-GP views appends their built commands as C-APDU rows.</p>
|
||||||
|
<p class="text-sm mb-3">The Immediate Action builder offers an action indicator (<code class="font-mono text-sm">81</code>/<code class="font-mono text-sm">82</code>), 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.</p>
|
||||||
|
|
||||||
<h4 id="ber-error-action" class="font-medium mb-2 text-base">Error Action TLV (Tag 82)</h4>
|
<h4 id="ber-error-action" class="font-medium mb-2 text-base">Error Action TLV (Tag 82)</h4>
|
||||||
<p class="text-sm mb-2">Error recovery per TS 102 226 §5.2.1.3 — one of four forms:</p>
|
<p class="text-sm mb-2">Error recovery per TS 102 226 §5.2.1.3 — one of four forms:</p>
|
||||||
@@ -131,13 +138,14 @@
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h4 id="ber-script-chaining" class="font-medium mb-2 text-base">Script Chaining TLV (Tag 83)</h4>
|
<h4 id="ber-script-chaining" class="font-medium mb-2 text-base">Script Chaining TLV (Tag 83)</h4>
|
||||||
<p class="text-sm mb-2">Multi-packet script execution with context preservation:</p>
|
<p class="text-sm mb-2">Multi-packet script execution with context preservation (TS 102 226 §5.2.1.4). The TLV is <code class="font-mono text-sm">83 01 <flag></code>, present once and only as the first Command TLV; it may be used for RFM and RAM only:</p>
|
||||||
<ul class="text-sm list-disc pl-5 mb-2">
|
<ul class="text-sm list-disc pl-5 mb-2">
|
||||||
<li><strong>Chaining Flags:</strong> <code class="font-mono text-sm">01</code> first script (delete chaining info on reset), <code class="font-mono text-sm">11</code> first script (keep chaining info across reset, RFM only), <code class="font-mono text-sm">02</code> subsequent script (more to follow), <code class="font-mono text-sm">03</code> subsequent script (last)</li>
|
<li><code class="font-mono text-sm">01</code> — first script, delete chaining information upon card reset</li>
|
||||||
<li><strong>Script ID:</strong> Correlation identifier across packets (1-4 bytes hex, auto-increment hints provided)</li>
|
<li><code class="font-mono text-sm">11</code> — first script, keep chaining information across card reset (RFM only)</li>
|
||||||
<li><strong>Additional Data:</strong> Extended chaining information (optional hex)</li>
|
<li><code class="font-mono text-sm">02</code> — subsequent script, more to follow</li>
|
||||||
<li><strong>Context Preservation:</strong> UICC keeps security/transaction state open across chained scripts</li>
|
<li><code class="font-mono text-sm">03</code> — subsequent script, last one</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<p class="text-sm mb-3">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.</p>
|
||||||
|
|
||||||
<h4 id="expanded-response" class="font-medium mb-2 text-base">Response decoding (TS 102 226 §5.2.2)</h4>
|
<h4 id="expanded-response" class="font-medium mb-2 text-base">Response decoding (TS 102 226 §5.2.2)</h4>
|
||||||
<p class="text-sm mb-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 <strong>Send to Card</strong> (see <a href="#secured-packet" class="text-blue-600 dark:text-blue-400 hover:underline">§3.1</a>): the PoR status (TAR, counter, raw PoR), with the last command’s status word and response data filled into the <strong>Response parser</strong> pill under Remote APDU.</p>
|
<p class="text-sm mb-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 <strong>Send to Card</strong> (see <a href="#secured-packet" class="text-blue-600 dark:text-blue-400 hover:underline">§3.1</a>): the PoR status (TAR, counter, raw PoR), with the last command’s status word and response data filled into the <strong>Response parser</strong> pill under Remote APDU.</p>
|
||||||
@@ -160,6 +168,8 @@
|
|||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">GET DATA</td><td class="py-1 px-2 font-mono">CA</td><td class="py-1 px-2 font-mono">tag</td><td class="py-1 px-2">Read card data objects</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">GET DATA</td><td class="py-1 px-2 font-mono">CA</td><td class="py-1 px-2 font-mono">tag</td><td class="py-1 px-2">Read card data objects</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">STORE DATA</td><td class="py-1 px-2 font-mono">E2</td><td class="py-1 px-2 font-mono">00/40/80/C0/E0</td><td class="py-1 px-2">Store data (key, certificate, …)</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">STORE DATA</td><td class="py-1 px-2 font-mono">E2</td><td class="py-1 px-2 font-mono">00/40/80/C0/E0</td><td class="py-1 px-2">Store data (key, certificate, …)</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">SET STATUS</td><td class="py-1 px-2 font-mono">F0</td><td class="py-1 px-2 font-mono">80/40/60</td><td class="py-1 px-2">Lifecycle state management</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">SET STATUS</td><td class="py-1 px-2 font-mono">F0</td><td class="py-1 px-2 font-mono">80/40/60</td><td class="py-1 px-2">Lifecycle state management</td></tr>
|
||||||
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">PUT KEY</td><td class="py-1 px-2 font-mono">D0</td><td class="py-1 px-2 font-mono">key ver</td><td class="py-1 px-2">Replace a key: P1 = key version, P2 = key identifier, data = new key material (GP Card Spec §11.8)</td></tr>
|
||||||
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">PUSH</td><td class="py-1 px-2 font-mono">EC</td><td class="py-1 px-2 font-mono">01</td><td class="py-1 px-2">Ask an application to open a BIP channel / CAT_TP link / TCP connection or send an identification packet (TS 102 226 §9)</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">EXTERNAL AUTHENTICATE</td><td class="py-1 px-2 font-mono">82</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">SCP host authentication</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">EXTERNAL AUTHENTICATE</td><td class="py-1 px-2 font-mono">82</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">SCP host authentication</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INTERNAL AUTHENTICATE</td><td class="py-1 px-2 font-mono">88</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Card challenge-response</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INTERNAL AUTHENTICATE</td><td class="py-1 px-2 font-mono">88</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Card challenge-response</td></tr>
|
||||||
<tr><td class="py-1 px-2 font-mono">GET RESPONSE</td><td class="py-1 px-2 font-mono">C0</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Fetch data after a <code class="font-mono text-sm">61XX</code> status (Le configurable)</td></tr>
|
<tr><td class="py-1 px-2 font-mono">GET RESPONSE</td><td class="py-1 px-2 font-mono">C0</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Fetch data after a <code class="font-mono text-sm">61XX</code> status (Le configurable)</td></tr>
|
||||||
@@ -195,6 +205,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p class="text-sm mb-3">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.</p>
|
<p class="text-sm mb-3">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.</p>
|
||||||
|
<p class="text-sm mb-3"><strong>PUSH</strong> (TS 102 226 §9.2) is a C-APDU (<code class="font-mono text-sm">80 EC 01 P2</code>) addressed to an application that supports BIP and/or CAT_TP — not the HTTP administration trigger (that is the HTTP OTA builder above). P2: <code class="font-mono text-sm">01</code> BIP channel opening (OPEN CHANNEL COMPREHENSION-TLVs, optional), <code class="font-mono text-sm">02</code> CAT_TP link (destination port in transport level <code class="font-mono text-sm">3C</code> with protocol type 00, optional buffer size <code class="font-mono text-sm">39</code> / channel data <code class="font-mono text-sm">36</code>), <code class="font-mono text-sm">03</code> TCP connection (bearer <code class="font-mono text-sm">35</code>, transport level <code class="font-mono text-sm">3C</code> with protocol type 02 = TCP client remote, destination address <code class="font-mono text-sm">3E</code>, NAA/APN <code class="font-mono text-sm">47</code>), <code class="font-mono text-sm">04</code> identification packet (channel data <code class="font-mono text-sm">36</code>; the ICCID is used when absent). The application issues the proactive OPEN CHANNEL itself; success is <code class="font-mono text-sm">90 00</code>, failure <code class="font-mono text-sm">6F 00</code> with the Result TLV in the response data, and malformed data is rejected with <code class="font-mono text-sm">6A 80</code>. P1 <code class="font-mono text-sm">80</code> is reserved for application-specific usage.</p>
|
||||||
|
|
||||||
<h3 id="conversion" class="text-lg font-medium mb-2">2.5 Conversion (SIM/USIM sidebars)</h3>
|
<h3 id="conversion" class="text-lg font-medium mb-2">2.5 Conversion (SIM/USIM sidebars)</h3>
|
||||||
<ul class="list-disc list-inside text-sm space-y-1">
|
<ul class="list-disc list-inside text-sm space-y-1">
|
||||||
|
|||||||
+486
-44
@@ -74,10 +74,13 @@
|
|||||||
<button onclick="chainAddRow('chain-sim','disable')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ DISABLE PIN</button>
|
<button onclick="chainAddRow('chain-sim','disable')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ DISABLE PIN</button>
|
||||||
<button onclick="chainAddRow('chain-sim','enable')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ ENABLE PIN</button>
|
<button onclick="chainAddRow('chain-sim','enable')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ ENABLE PIN</button>
|
||||||
<button onclick="chainAddRow('chain-sim','unblock')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ UNBLOCK PIN</button>
|
<button onclick="chainAddRow('chain-sim','unblock')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ UNBLOCK PIN</button>
|
||||||
|
<button onclick="chainAddRow('chain-sim','search-record')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ SEARCH RECORD</button>
|
||||||
|
<button onclick="chainAddRow('chain-sim','increase')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ INCREASE</button>
|
||||||
<button onclick="chainAddRow('chain-sim','get-response')" class="text-xs px-2 py-1 bg-emerald-600 text-white rounded hover:bg-emerald-700">+ GET RESPONSE</button>
|
<button onclick="chainAddRow('chain-sim','get-response')" class="text-xs px-2 py-1 bg-emerald-600 text-white rounded hover:bg-emerald-700">+ GET RESPONSE</button>
|
||||||
</div>
|
</div>
|
||||||
<textarea id="chain-sim-preview" rows="3" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 bg-gray-100 dark:bg-slate-800" placeholder="Chain preview — add commands above"></textarea>
|
<textarea id="chain-sim-preview" rows="3" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 bg-gray-100 dark:bg-slate-800" placeholder="Chain preview — add commands above"></textarea>
|
||||||
<button onclick="packToSp('chain-sim-preview')" id="chain-sim-pack-btn" disabled class="mb-3 px-5 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Pack into Secured packet">Pack into Secured packet</button>
|
<button onclick="packToSp('chain-sim-preview')" id="chain-sim-pack-btn" disabled class="mb-3 px-5 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Pack into Secured packet">Pack into Secured packet</button>
|
||||||
|
<button onclick="loadExpandedScript('chain-sim')" id="chain-sim-expanded-btn" class="mb-3 px-5 py-2.5 bg-violet-600 text-white text-sm font-medium rounded hover:bg-violet-700" data-l10n="→ Expanded Script">→ Expanded Script</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="c-apdu-sub-usim" class="hidden">
|
<div id="c-apdu-sub-usim" class="hidden">
|
||||||
@@ -95,10 +98,18 @@
|
|||||||
<button onclick="chainAddRow('chain-usim','disable')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ DISABLE PIN</button>
|
<button onclick="chainAddRow('chain-usim','disable')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ DISABLE PIN</button>
|
||||||
<button onclick="chainAddRow('chain-usim','enable')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ ENABLE PIN</button>
|
<button onclick="chainAddRow('chain-usim','enable')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ ENABLE PIN</button>
|
||||||
<button onclick="chainAddRow('chain-usim','unblock')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ UNBLOCK PIN</button>
|
<button onclick="chainAddRow('chain-usim','unblock')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ UNBLOCK PIN</button>
|
||||||
|
<button onclick="chainAddRow('chain-usim','search-record')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ SEARCH RECORD</button>
|
||||||
|
<button onclick="chainAddRow('chain-usim','increase')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ INCREASE</button>
|
||||||
|
<button onclick="chainAddRow('chain-usim','create-file')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ CREATE FILE</button>
|
||||||
|
<button onclick="chainAddRow('chain-usim','delete-file')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ DELETE FILE</button>
|
||||||
|
<button onclick="chainAddRow('chain-usim','resize-file')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ RESIZE FILE</button>
|
||||||
|
<button onclick="chainAddRow('chain-usim','set-data')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ SET DATA</button>
|
||||||
|
<button onclick="chainAddRow('chain-usim','retrieve-data')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ RETRIEVE DATA</button>
|
||||||
<button onclick="chainAddRow('chain-usim','get-response')" class="text-xs px-2 py-1 bg-emerald-600 text-white rounded hover:bg-emerald-700">+ GET RESPONSE</button>
|
<button onclick="chainAddRow('chain-usim','get-response')" class="text-xs px-2 py-1 bg-emerald-600 text-white rounded hover:bg-emerald-700">+ GET RESPONSE</button>
|
||||||
</div>
|
</div>
|
||||||
<textarea id="chain-usim-preview" rows="3" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 bg-gray-100 dark:bg-slate-800" placeholder="Chain preview — add commands above"></textarea>
|
<textarea id="chain-usim-preview" rows="3" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 bg-gray-100 dark:bg-slate-800" placeholder="Chain preview — add commands above"></textarea>
|
||||||
<button onclick="packToSp('chain-usim-preview')" id="chain-usim-pack-btn" disabled class="mb-3 px-5 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Pack into Secured packet">Pack into Secured packet</button>
|
<button onclick="packToSp('chain-usim-preview')" id="chain-usim-pack-btn" disabled class="mb-3 px-5 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Pack into Secured packet">Pack into Secured packet</button>
|
||||||
|
<button onclick="loadExpandedScript('chain-usim')" id="chain-usim-expanded-btn" class="mb-3 px-5 py-2.5 bg-violet-600 text-white text-sm font-medium rounded hover:bg-violet-700" data-l10n="→ Expanded Script">→ Expanded Script</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="c-apdu-sub-ber" class="hidden">
|
<div id="c-apdu-sub-ber" class="hidden">
|
||||||
@@ -140,6 +151,8 @@
|
|||||||
<button onclick="chainAddRow('chain-ram','get-data')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ GET DATA</button>
|
<button onclick="chainAddRow('chain-ram','get-data')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ GET DATA</button>
|
||||||
<button onclick="chainAddRow('chain-ram','store-data')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ STORE DATA</button>
|
<button onclick="chainAddRow('chain-ram','store-data')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ STORE DATA</button>
|
||||||
<button onclick="chainAddRow('chain-ram','set-status')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ SET STATUS</button>
|
<button onclick="chainAddRow('chain-ram','set-status')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ SET STATUS</button>
|
||||||
|
<button onclick="chainAddRow('chain-ram','put-key')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ PUT KEY</button>
|
||||||
|
<button onclick="chainAddRow('chain-ram','push')" class="text-xs px-2 py-1 bg-violet-600 text-white rounded hover:bg-violet-700">+ PUSH</button>
|
||||||
<button onclick="chainAddRow('chain-ram','ext-auth')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ EXTERNAL AUTH</button>
|
<button onclick="chainAddRow('chain-ram','ext-auth')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ EXTERNAL AUTH</button>
|
||||||
<button onclick="chainAddRow('chain-ram','int-auth')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ INTERNAL AUTH</button>
|
<button onclick="chainAddRow('chain-ram','int-auth')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ INTERNAL AUTH</button>
|
||||||
<button onclick="chainAddRow('chain-ram','get-response')" class="text-xs px-2 py-1 bg-emerald-600 text-white rounded hover:bg-emerald-700">+ GET RESPONSE</button>
|
<button onclick="chainAddRow('chain-ram','get-response')" class="text-xs px-2 py-1 bg-emerald-600 text-white rounded hover:bg-emerald-700">+ GET RESPONSE</button>
|
||||||
@@ -152,6 +165,7 @@
|
|||||||
<option value="AA">AA (definite)</option>
|
<option value="AA">AA (definite)</option>
|
||||||
</select>
|
</select>
|
||||||
<button onclick="ramBuildExpanded()" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700" data-l10n="To expanded">To expanded</button>
|
<button onclick="ramBuildExpanded()" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700" data-l10n="To expanded">To expanded</button>
|
||||||
|
<button onclick="loadExpandedScript('chain-ram')" class="text-xs px-2 py-1 bg-violet-600 text-white rounded hover:bg-violet-700" data-l10n="→ Expanded Script">→ Expanded Script</button>
|
||||||
<button data-needs="server" onclick="ramQueueScp81()" class="text-xs px-2 py-1 bg-violet-600 text-white rounded hover:bg-violet-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Queue in SCP81">Queue in SCP81</button>
|
<button data-needs="server" onclick="ramQueueScp81()" class="text-xs px-2 py-1 bg-violet-600 text-white rounded hover:bg-violet-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Queue in SCP81">Queue in SCP81</button>
|
||||||
<span id="chain-ram-queue-msg" class="text-xs"></span>
|
<span id="chain-ram-queue-msg" class="text-xs"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1437,7 +1451,7 @@
|
|||||||
// ===== Version =====
|
// ===== Version =====
|
||||||
// Single source of truth for the PWA version: shown in the header and used
|
// Single source of truth for the PWA version: shown in the header and used
|
||||||
// by the server version check in pysimConnect().
|
// 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;
|
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||||
|
|
||||||
// ===== Tab switching =====
|
// ===== Tab switching =====
|
||||||
@@ -2301,6 +2315,9 @@ function escHtml(s) { return String(s).replace(/&/g,'&').replace(/</g,'<'
|
|||||||
// Chain preview = concatenation of all row hexes, with Le stripping when
|
// Chain preview = concatenation of all row hexes, with Le stripping when
|
||||||
// a Case 4 command is followed by GET RESPONSE (ETSI TS 102 226 compact).
|
// a Case 4 command is followed by GET RESPONSE (ETSI TS 102 226 compact).
|
||||||
const _chains = {};
|
const _chains = {};
|
||||||
|
// Per-chain refresh callbacks (used by the Expanded Script C-APDU rows, whose
|
||||||
|
// editor is a virtual one-row chain without the stock preview elements).
|
||||||
|
const CHAIN_LISTENERS = {};
|
||||||
|
|
||||||
const CHAIN_CMDS_SIM = [
|
const CHAIN_CMDS_SIM = [
|
||||||
{value:'select',label:'SELECT'},{value:'read-record',label:'READ RECORD'},
|
{value:'select',label:'SELECT'},{value:'read-record',label:'READ RECORD'},
|
||||||
@@ -2309,7 +2326,9 @@ const CHAIN_CMDS_SIM = [
|
|||||||
{value:'activate-file',label:'ACTIVATE FILE'},{value:'deactivate-file',label:'DEACTIVATE FILE'},
|
{value:'activate-file',label:'ACTIVATE FILE'},{value:'deactivate-file',label:'DEACTIVATE FILE'},
|
||||||
{value:'verify',label:'VERIFY PIN'},{value:'change',label:'CHANGE PIN'},
|
{value:'verify',label:'VERIFY PIN'},{value:'change',label:'CHANGE PIN'},
|
||||||
{value:'disable',label:'DISABLE PIN'},{value:'enable',label:'ENABLE PIN'},
|
{value:'disable',label:'DISABLE PIN'},{value:'enable',label:'ENABLE PIN'},
|
||||||
{value:'unblock',label:'UNBLOCK PIN'},{value:'get-response',label:'GET RESPONSE'},
|
{value:'unblock',label:'UNBLOCK PIN'},
|
||||||
|
{value:'search-record',label:'SEARCH RECORD (SEEK)'},{value:'increase',label:'INCREASE'},
|
||||||
|
{value:'get-response',label:'GET RESPONSE'},
|
||||||
];
|
];
|
||||||
const CHAIN_CMDS_USIM = [
|
const CHAIN_CMDS_USIM = [
|
||||||
{value:'select',label:'SELECT'},{value:'read-record',label:'READ RECORD'},
|
{value:'select',label:'SELECT'},{value:'read-record',label:'READ RECORD'},
|
||||||
@@ -2318,7 +2337,12 @@ const CHAIN_CMDS_USIM = [
|
|||||||
{value:'activate-file',label:'ACTIVATE FILE'},{value:'deactivate-file',label:'DEACTIVATE FILE'},
|
{value:'activate-file',label:'ACTIVATE FILE'},{value:'deactivate-file',label:'DEACTIVATE FILE'},
|
||||||
{value:'verify',label:'VERIFY PIN'},{value:'change',label:'CHANGE PIN'},
|
{value:'verify',label:'VERIFY PIN'},{value:'change',label:'CHANGE PIN'},
|
||||||
{value:'disable',label:'DISABLE PIN'},{value:'enable',label:'ENABLE PIN'},
|
{value:'disable',label:'DISABLE PIN'},{value:'enable',label:'ENABLE PIN'},
|
||||||
{value:'unblock',label:'UNBLOCK PIN'},{value:'get-response',label:'GET RESPONSE'},
|
{value:'unblock',label:'UNBLOCK PIN'},
|
||||||
|
{value:'search-record',label:'SEARCH RECORD'},{value:'increase',label:'INCREASE'},
|
||||||
|
{value:'create-file',label:'CREATE FILE'},{value:'delete-file',label:'DELETE FILE'},
|
||||||
|
{value:'resize-file',label:'RESIZE FILE'},{value:'set-data',label:'SET DATA'},
|
||||||
|
{value:'retrieve-data',label:'RETRIEVE DATA'},
|
||||||
|
{value:'get-response',label:'GET RESPONSE'},
|
||||||
];
|
];
|
||||||
const CHAIN_CMDS_RAM = [
|
const CHAIN_CMDS_RAM = [
|
||||||
{value:'install-load',label:'INSTALL [for load]'},
|
{value:'install-load',label:'INSTALL [for load]'},
|
||||||
@@ -2329,11 +2353,33 @@ const CHAIN_CMDS_RAM = [
|
|||||||
{value:'load',label:'LOAD'},{value:'delete',label:'DELETE'},
|
{value:'load',label:'LOAD'},{value:'delete',label:'DELETE'},
|
||||||
{value:'get-status',label:'GET STATUS'},{value:'get-data',label:'GET DATA'},
|
{value:'get-status',label:'GET STATUS'},{value:'get-data',label:'GET DATA'},
|
||||||
{value:'store-data',label:'STORE DATA'},{value:'set-status',label:'SET STATUS'},
|
{value:'store-data',label:'STORE DATA'},{value:'set-status',label:'SET STATUS'},
|
||||||
|
{value:'put-key',label:'PUT KEY'},{value:'push',label:'PUSH (TS 102 226 §9)'},
|
||||||
{value:'ext-auth',label:'EXTERNAL AUTHENTICATE'},
|
{value:'ext-auth',label:'EXTERNAL AUTHENTICATE'},
|
||||||
{value:'int-auth',label:'INTERNAL AUTHENTICATE'},
|
{value:'int-auth',label:'INTERNAL AUTHENTICATE'},
|
||||||
{value:'get-response',label:'GET RESPONSE'},
|
{value:'get-response',label:'GET RESPONSE'},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// ===== Chain container kinds =====
|
||||||
|
// The SIM / USIM / RAM builders plus the one-row "virtual" chains embedded in
|
||||||
|
// the Expanded Script C-APDU rows (id ber-<kind>-<row>); 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 chainInit(id) { if (!_chains[id]) _chains[id] = {rows: []}; }
|
||||||
function chainAddRow(id, cmd) { chainInit(id); _chains[id].rows.push({cmd: cmd, fields: {}}); chainRender(id); }
|
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); }
|
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);
|
if (p) p.value = chainBuildHex(id);
|
||||||
const b = document.getElementById(id + '-pack-btn');
|
const b = document.getElementById(id + '-pack-btn');
|
||||||
if (b) b.disabled = !chainBuildHex(id);
|
if (b) b.disabled = !chainBuildHex(id);
|
||||||
|
chainNotify(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function chainRender(chainId) {
|
function chainRender(chainId) {
|
||||||
@@ -2354,23 +2401,24 @@ function chainRender(chainId) {
|
|||||||
const container = document.getElementById(chainId + '-rows');
|
const container = document.getElementById(chainId + '-rows');
|
||||||
const preview = document.getElementById(chainId + '-preview');
|
const preview = document.getElementById(chainId + '-preview');
|
||||||
const packBtn = document.getElementById(chainId + '-pack-btn');
|
const packBtn = document.getElementById(chainId + '-pack-btn');
|
||||||
if (!container) return;
|
if (!container) { chainNotify(chainId); return; }
|
||||||
|
const embedded = chainIsEmbedded(chainId);
|
||||||
const rows = _chains[chainId].rows;
|
const rows = _chains[chainId].rows;
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
rows.forEach(function(row, idx) {
|
rows.forEach(function(row, idx) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'mb-3 p-3 border border-gray-200 dark:border-slate-600 rounded';
|
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);
|
container.appendChild(div);
|
||||||
});
|
});
|
||||||
const hex = chainBuildHex(chainId);
|
const hex = chainBuildHex(chainId);
|
||||||
if (preview) preview.value = hex;
|
if (preview) preview.value = hex;
|
||||||
if (packBtn) packBtn.disabled = !hex;
|
if (packBtn) packBtn.disabled = !hex;
|
||||||
|
chainNotify(chainId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function chainRenderRow(chainId, idx, row) {
|
function chainRenderRow(chainId, idx, row, embedded) {
|
||||||
const isRam = chainId === 'chain-ram';
|
const cmds = chainCommands(chainId);
|
||||||
const cmds = isRam ? CHAIN_CMDS_RAM : (chainId === 'chain-usim' ? CHAIN_CMDS_USIM : CHAIN_CMDS_SIM);
|
|
||||||
const cmdOpts = cmds.map(function(c) {
|
const cmdOpts = cmds.map(function(c) {
|
||||||
return '<option value="' + c.value + '"' + (c.value === row.cmd ? ' selected' : '') + '>' + c.label + '</option>';
|
return '<option value="' + c.value + '"' + (c.value === row.cmd ? ' selected' : '') + '>' + c.label + '</option>';
|
||||||
}).join('');
|
}).join('');
|
||||||
@@ -2379,7 +2427,7 @@ function chainRenderRow(chainId, idx, row) {
|
|||||||
return '<div class="flex items-center gap-2 mb-2">' +
|
return '<div class="flex items-center gap-2 mb-2">' +
|
||||||
'<select onchange="chainUpdateCmd(\'' + chainId + '\',' + idx + ',this.value)" class="flex-shrink-0 w-52 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1 dark:bg-slate-800">' + cmdOpts + '</select>' +
|
'<select onchange="chainUpdateCmd(\'' + chainId + '\',' + idx + ',this.value)" class="flex-shrink-0 w-52 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1 dark:bg-slate-800">' + cmdOpts + '</select>' +
|
||||||
'<input id="' + chainId + '-row-' + idx + '-hex" readonly value="' + escHtml(hex) + '" class="flex-1 font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 bg-gray-50 dark:bg-slate-900">' +
|
'<input id="' + chainId + '-row-' + idx + '-hex" readonly value="' + escHtml(hex) + '" class="flex-1 font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 bg-gray-50 dark:bg-slate-900">' +
|
||||||
'<button onclick="chainDeleteRow(\'' + chainId + '\',' + idx + ')" class="text-red-500 hover:text-red-700 text-lg font-bold px-1" title="Delete">✕</button>' +
|
(embedded ? '' : '<button onclick="chainDeleteRow(\'' + chainId + '\',' + idx + ')" class="text-red-500 hover:text-red-700 text-lg font-bold px-1" title="Delete">✕</button>') +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="pl-4">' + fieldsHtml + '</div>';
|
'<div class="pl-4">' + fieldsHtml + '</div>';
|
||||||
}
|
}
|
||||||
@@ -2387,16 +2435,18 @@ function chainRenderRow(chainId, idx, row) {
|
|||||||
function chainRowFieldsHtml(chainId, idx, row) {
|
function chainRowFieldsHtml(chainId, idx, row) {
|
||||||
var f = row.fields;
|
var f = row.fields;
|
||||||
function uf(field) { return "chainUpdateField('" + chainId + "'," + idx + ",'" + field + "',this.value)"; }
|
function uf(field) { return "chainUpdateField('" + chainId + "'," + idx + ",'" + field + "',this.value)"; }
|
||||||
if (chainId === 'chain-sim' || chainId === 'chain-usim') return chainSimFieldsHtml(chainId, idx, f, uf);
|
const kind = chainKind(chainId);
|
||||||
return chainRamFieldsHtml(idx, row, f, uf);
|
if (kind === 'sim' || kind === 'usim') return chainSimFieldsHtml(chainId, idx, f, uf);
|
||||||
|
return chainRamFieldsHtml(chainId, idx, row, f, uf);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== SIM/USIM field rendering =====
|
// ===== SIM/USIM field rendering =====
|
||||||
function chainSimFieldsHtml(chainId, idx, f, uf) {
|
function chainSimFieldsHtml(chainId, idx, f, uf) {
|
||||||
var cmd = (_chains[chainId] && _chains[chainId].rows[idx]) ? _chains[chainId].rows[idx].cmd : '';
|
var cmd = (_chains[chainId] && _chains[chainId].rows[idx]) ? _chains[chainId].rows[idx].cmd : '';
|
||||||
|
var usim = chainKind(chainId) === 'usim';
|
||||||
if (cmd === 'select') {
|
if (cmd === 'select') {
|
||||||
var method = f.method || 'fid';
|
var method = f.method || 'fid';
|
||||||
var silent = chainId === 'chain-usim' && f.silent;
|
var silent = usim && f.silent;
|
||||||
var html = '<div class="grid grid-cols-2 gap-2 mb-2"><div>' +
|
var html = '<div class="grid grid-cols-2 gap-2 mb-2"><div>' +
|
||||||
'<label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Method</label>' +
|
'<label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Method</label>' +
|
||||||
'<select onchange="' + uf('method') + ';chainRender(\'' + chainId + '\')" class="w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1 dark:bg-slate-800">' +
|
'<select onchange="' + uf('method') + ';chainRender(\'' + chainId + '\')" class="w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1 dark:bg-slate-800">' +
|
||||||
@@ -2405,9 +2455,9 @@ function chainSimFieldsHtml(chainId, idx, f, uf) {
|
|||||||
'<option value="dfname"' + (method === 'dfname' ? ' selected' : '') + '>By AID</option>' +
|
'<option value="dfname"' + (method === 'dfname' ? ' selected' : '') + '>By AID</option>' +
|
||||||
'<option value="chain"' + (method === 'chain' ? ' selected' : '') + '>Chain</option>' +
|
'<option value="chain"' + (method === 'chain' ? ' selected' : '') + '>Chain</option>' +
|
||||||
'</select></div>';
|
'</select></div>';
|
||||||
if (chainId === 'chain-usim') {
|
if (usim) {
|
||||||
html += '<div class="flex items-end pb-0.5"><label class="flex items-center gap-1.5 text-xs">' +
|
html += '<div class="flex items-end pb-0.5"><label class="flex items-center gap-1.5 text-xs">' +
|
||||||
'<input type="checkbox"' + (silent ? ' checked' : '') + ' onchange="chainUpdateField(\'chain-usim\',' + idx + ',\'silent\',this.checked);chainRender(\'chain-usim\')" class="rounded">' +
|
'<input type="checkbox"' + (silent ? ' checked' : '') + ' onchange="chainUpdateField(\'' + chainId + '\',' + idx + ',\'silent\',this.checked);chainRender(\'' + chainId + '\')" class="rounded">' +
|
||||||
'Silent (P2=0C)</label></div>';
|
'Silent (P2=0C)</label></div>';
|
||||||
}
|
}
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
@@ -2418,9 +2468,9 @@ function chainSimFieldsHtml(chainId, idx, f, uf) {
|
|||||||
if (method === 'path') {
|
if (method === 'path') {
|
||||||
html += '<div class="mb-2"><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Path (hex)</label>' +
|
html += '<div class="mb-2"><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Path (hex)</label>' +
|
||||||
'<input value="' + escHtml(f.path || '') + '" oninput="' + uf('path') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="7FFF6FC5">';
|
'<input value="' + escHtml(f.path || '') + '" oninput="' + uf('path') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="7FFF6FC5">';
|
||||||
if (chainId === 'chain-usim') {
|
if (usim) {
|
||||||
html += '<div class="flex gap-3 mt-1 text-xs"><label class="flex items-center gap-1"><input type="radio" name="ch-usim-' + idx + '-base" value="mf"' + ((f.base || 'mf') === 'mf' ? ' checked' : '') + ' onchange="chainUpdateField(\'chain-usim\',' + idx + ',\'base\',\'mf\')"> from MF</label>' +
|
html += '<div class="flex gap-3 mt-1 text-xs"><label class="flex items-center gap-1"><input type="radio" name="ch-' + chainId + '-' + idx + '-base" value="mf"' + ((f.base || 'mf') === 'mf' ? ' checked' : '') + ' onchange="chainUpdateField(\'' + chainId + '\',' + idx + ',\'base\',\'mf\')"> from MF</label>' +
|
||||||
'<label class="flex items-center gap-1"><input type="radio" name="ch-usim-' + idx + '-base" value="df"' + (f.base === 'df' ? ' checked' : '') + ' onchange="chainUpdateField(\'chain-usim\',' + idx + ',\'base\',\'df\')"> from current DF (ADF.USIM)</label></div>';
|
'<label class="flex items-center gap-1"><input type="radio" name="ch-' + chainId + '-' + idx + '-base" value="df"' + (f.base === 'df' ? ' checked' : '') + ' onchange="chainUpdateField(\'' + chainId + '\',' + idx + ',\'base\',\'df\')"> from current DF (ADF.USIM)</label></div>';
|
||||||
}
|
}
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
}
|
}
|
||||||
@@ -2515,6 +2565,113 @@ function chainSimFieldsHtml(chainId, idx, f, uf) {
|
|||||||
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">New PIN</label>' +
|
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">New PIN</label>' +
|
||||||
'<input value="' + escHtml(f.pinNew || '') + '" oninput="' + uf('pinNew') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="5678" maxlength="8"></div></div>';
|
'<input value="' + escHtml(f.pinNew || '') + '" oninput="' + uf('pinNew') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="5678" maxlength="8"></div></div>';
|
||||||
}
|
}
|
||||||
|
if (cmd === 'search-record') {
|
||||||
|
var srMode = f.srMode || (usim ? '10' : '02');
|
||||||
|
if (usim) {
|
||||||
|
var htmlSr = '<div class="grid grid-cols-3 gap-2 mb-2">' +
|
||||||
|
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Mode (P2)</label>' +
|
||||||
|
'<select onchange="' + uf('srMode') + '" class="w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1 dark:bg-slate-800">' +
|
||||||
|
'<option value="10"' + (srMode === '10' ? ' selected' : '') + '>10 — forward from P1</option>' +
|
||||||
|
'<option value="12"' + (srMode === '12' ? ' selected' : '') + '>12 — backward from P1</option>' +
|
||||||
|
'<option value="18"' + (srMode === '18' ? ' selected' : '') + '>18 — enhanced search</option>' +
|
||||||
|
'<option value="1C"' + (srMode === '1C' ? ' selected' : '') + '>1C — proprietary search</option></select></div>' +
|
||||||
|
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Record (P1, 00 = current)</label>' +
|
||||||
|
'<input value="' + escHtml(f.srRec || '00') + '" oninput="' + uf('srRec') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="2"></div>' +
|
||||||
|
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Le</label>' +
|
||||||
|
'<input value="' + escHtml(f.le || '00') + '" oninput="' + uf('le') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="2" placeholder="00"></div></div>';
|
||||||
|
if (srMode === '18') {
|
||||||
|
htmlSr += '<div class="mb-2"><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Search indication (2 bytes, table 11.13)</label>' +
|
||||||
|
'<input value="' + escHtml(f.srInd || '1800') + '" oninput="' + uf('srInd') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="4"></div>';
|
||||||
|
}
|
||||||
|
htmlSr += '<div class="mb-2"><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Search string (hex)</label>' +
|
||||||
|
'<input value="' + escHtml(f.srPattern || '') + '" oninput="' + uf('srPattern') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800"></div>';
|
||||||
|
return htmlSr;
|
||||||
|
}
|
||||||
|
return '<div class="grid grid-cols-3 gap-2">' +
|
||||||
|
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Type/mode (P2)</label>' +
|
||||||
|
'<select onchange="' + uf('srMode') + '" class="w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1 dark:bg-slate-800">' +
|
||||||
|
'<option value="00"' + (srMode === '00' ? ' selected' : '') + '>00 — type 1, from start</option>' +
|
||||||
|
'<option value="01"' + (srMode === '01' ? ' selected' : '') + '>01 — type 1, backward</option>' +
|
||||||
|
'<option value="02"' + (srMode === '02' ? ' selected' : '') + '>02 — type 1, next forward</option>' +
|
||||||
|
'<option value="03"' + (srMode === '03' ? ' selected' : '') + '>03 — type 1, previous</option>' +
|
||||||
|
'<option value="10"' + (srMode === '10' ? ' selected' : '') + '>10 — type 2, from start</option>' +
|
||||||
|
'<option value="11"' + (srMode === '11' ? ' selected' : '') + '>11 — type 2, backward</option>' +
|
||||||
|
'<option value="12"' + (srMode === '12' ? ' selected' : '') + '>12 — type 2, next forward</option>' +
|
||||||
|
'<option value="13"' + (srMode === '13' ? ' selected' : '') + '>13 — type 2, previous</option></select></div>' +
|
||||||
|
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Pattern (1-16 bytes hex)</label>' +
|
||||||
|
'<input value="' + escHtml(f.srPattern || '') + '" oninput="' + uf('srPattern') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800"></div>' +
|
||||||
|
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Le (empty for type 1)</label>' +
|
||||||
|
'<input value="' + escHtml(f.le || '') + '" oninput="' + uf('le') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="2"></div></div>';
|
||||||
|
}
|
||||||
|
if (cmd === 'increase') {
|
||||||
|
var inc = '<div class="grid grid-cols-3 gap-2">';
|
||||||
|
if (usim) {
|
||||||
|
inc += '<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">SFI (00 = current EF)</label>' +
|
||||||
|
'<input value="' + escHtml(f.sfi || '00') + '" oninput="' + uf('sfi') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="2"></div>';
|
||||||
|
}
|
||||||
|
inc += '<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Value to add (hex)</label>' +
|
||||||
|
'<input value="' + escHtml(f.value || (usim ? '' : '000001')) + '" oninput="' + uf('value') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800"></div>' +
|
||||||
|
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Le' + (usim ? '' : ' (empty = case 3)') + '</label>' +
|
||||||
|
'<input value="' + escHtml(f.le || (usim ? '00' : '')) + '" oninput="' + uf('le') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="2"></div></div>';
|
||||||
|
return inc;
|
||||||
|
}
|
||||||
|
if (cmd === 'create-file' || cmd === 'resize-file') {
|
||||||
|
var isCreate = cmd === 'create-file';
|
||||||
|
var st = f.structure || 'transparent';
|
||||||
|
var htmlFcp = '<div class="grid grid-cols-3 gap-2 mb-2">' +
|
||||||
|
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">FID (2 bytes)</label>' +
|
||||||
|
'<input value="' + escHtml(f.fid || '') + '" oninput="' + uf('fid') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="4" placeholder="6F00"></div>' +
|
||||||
|
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">File size (hex bytes)</label>' +
|
||||||
|
'<input value="' + escHtml(f.fileSize || '') + '" oninput="' + uf('fileSize') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="4" placeholder="0010"></div>' +
|
||||||
|
(isCreate ?
|
||||||
|
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Structure</label>' +
|
||||||
|
'<select onchange="' + uf('structure') + '" class="w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1 dark:bg-slate-800">' +
|
||||||
|
'<option value="transparent"' + (st === 'transparent' ? ' selected' : '') + '>Transparent EF</option>' +
|
||||||
|
'<option value="linear_fixed"' + (st === 'linear_fixed' ? ' selected' : '') + '>Linear fixed EF</option>' +
|
||||||
|
'<option value="cyclic"' + (st === 'cyclic' ? ' selected' : '') + '>Cyclic EF</option>' +
|
||||||
|
'<option value="ber_tlv"' + (st === 'ber_tlv' ? ' selected' : '') + '>BER-TLV EF</option></select></div>'
|
||||||
|
:
|
||||||
|
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Mode (P1)</label>' +
|
||||||
|
'<select onchange="' + uf('mode') + '" class="w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1 dark:bg-slate-800">' +
|
||||||
|
'<option value="00"' + ((f.mode || '00') === '00' ? ' selected' : '') + '>00 — mode 0</option>' +
|
||||||
|
'<option value="01"' + (f.mode === '01' ? ' selected' : '') + '>01 — mode 1</option></select></div>') +
|
||||||
|
'</div>';
|
||||||
|
if (isCreate) {
|
||||||
|
htmlFcp += '<div class="grid grid-cols-3 gap-2 mb-2">' +
|
||||||
|
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Record length (hex, record files)</label>' +
|
||||||
|
'<input value="' + escHtml(f.recLen || '') + '" oninput="' + uf('recLen') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="4" placeholder="006E"></div>' +
|
||||||
|
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">LCSI</label>' +
|
||||||
|
'<input value="' + escHtml(f.lcsi || '05') + '" oninput="' + uf('lcsi') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="2"></div>' +
|
||||||
|
'<div class="flex items-end pb-1"><label class="flex items-center gap-1 text-xs"><input type="checkbox"' + (f.shareable ? ' checked' : '') +
|
||||||
|
' onchange="chainUpdateField(\'' + chainId + '\',' + idx + ',\'shareable\',this.checked)" class="rounded"> Shareable</label></div></div>';
|
||||||
|
}
|
||||||
|
htmlFcp += '<div class="mb-2"><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">FCP template (hex)</label>' +
|
||||||
|
'<textarea oninput="' + uf('fcp') + '" rows="2" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800">' + escHtml(f.fcp || '') + '</textarea></div>' +
|
||||||
|
'<button type="button" onclick="chainBuildFcp(\'' + chainId + '\',' + idx + ',\'' + (isCreate ? 'create' : 'resize') + '\')" class="text-xs px-2 py-1 bg-slate-600 text-white rounded hover:bg-slate-700">Build template</button>';
|
||||||
|
return htmlFcp;
|
||||||
|
}
|
||||||
|
if (cmd === 'delete-file') {
|
||||||
|
return '<div class="mb-2"><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">FID (2 bytes)</label>' +
|
||||||
|
'<input value="' + escHtml(f.fid || '') + '" oninput="' + uf('fid') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="4" placeholder="6F00"></div>';
|
||||||
|
}
|
||||||
|
if (cmd === 'set-data' || cmd === 'retrieve-data') {
|
||||||
|
var sdMode = f.sdMode || '80';
|
||||||
|
var htmlSd = '<div class="grid grid-cols-3 gap-2 mb-2">' +
|
||||||
|
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">P2 (block)</label>' +
|
||||||
|
'<select onchange="' + uf('sdMode') + '" class="w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1 dark:bg-slate-800">' +
|
||||||
|
'<option value="80"' + (sdMode === '80' ? ' selected' : '') + '>80 — first block</option>' +
|
||||||
|
'<option value="00"' + (sdMode === '00' ? ' selected' : '') + '>00 — next block</option>' +
|
||||||
|
'<option value="40"' + (sdMode === '40' ? ' selected' : '') + '>40 — retransmit previous</option></select></div>' +
|
||||||
|
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">SFI (00 = current EF)</label>' +
|
||||||
|
'<input value="' + escHtml(f.sfi || '00') + '" oninput="' + uf('sfi') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="2"></div>' +
|
||||||
|
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' + (cmd === 'set-data' ? 'Data (BER-TLV hex)' : 'Tag (hex, 5C = tag list)') + '</label>' +
|
||||||
|
'<input value="' + escHtml(cmd === 'set-data' ? (f.value || '') : (f.tag || '')) + '" oninput="' + uf(cmd === 'set-data' ? 'value' : 'tag') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800"></div></div>';
|
||||||
|
if (cmd === 'retrieve-data') {
|
||||||
|
htmlSd += '<div class="mb-2"><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Le</label>' +
|
||||||
|
'<input value="' + escHtml(f.le || '00') + '" oninput="' + uf('le') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="2"></div>';
|
||||||
|
}
|
||||||
|
return htmlSd;
|
||||||
|
}
|
||||||
if (cmd === 'get-response') {
|
if (cmd === 'get-response') {
|
||||||
return '<div class="text-xs text-gray-500 dark:text-slate-400 italic">No parameters — CLA=C0 INS=C0 P1=00 P2=00</div>';
|
return '<div class="text-xs text-gray-500 dark:text-slate-400 italic">No parameters — CLA=C0 INS=C0 P1=00 P2=00</div>';
|
||||||
}
|
}
|
||||||
@@ -2522,7 +2679,7 @@ function chainSimFieldsHtml(chainId, idx, f, uf) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===== RAM/GP field rendering =====
|
// ===== RAM/GP field rendering =====
|
||||||
function chainRamFieldsHtml(idx, row, f, uf) {
|
function chainRamFieldsHtml(chainId, idx, row, f, uf) {
|
||||||
var cmd = row.cmd;
|
var cmd = row.cmd;
|
||||||
if (cmd === 'install-load') {
|
if (cmd === 'install-load') {
|
||||||
return '<div class="grid grid-cols-2 gap-2 mb-2">' +
|
return '<div class="grid grid-cols-2 gap-2 mb-2">' +
|
||||||
@@ -2542,15 +2699,15 @@ function chainRamFieldsHtml(idx, row, f, uf) {
|
|||||||
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Executable Module AID / Applet Class AID</label>' +
|
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Executable Module AID / Applet Class AID</label>' +
|
||||||
'<input value="' + escHtml(f.modAid || '') + '" oninput="' + uf('modAid') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="A000000151000000"></div></div>';
|
'<input value="' + escHtml(f.modAid || '') + '" oninput="' + uf('modAid') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="A000000151000000"></div></div>';
|
||||||
// Privileges
|
// Privileges
|
||||||
html += chainRamPrivHtml(idx, f, uf);
|
html += chainRamPrivHtml(chainId, idx, f, uf);
|
||||||
// Toolkit
|
// Toolkit
|
||||||
html += chainRamToolkitHtml(idx, f, uf);
|
html += chainRamToolkitHtml(chainId, idx, f, uf);
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
if (cmd === 'install-make-sel') {
|
if (cmd === 'install-make-sel') {
|
||||||
return '<div class="mb-2"><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Application / Instance AID</label>' +
|
return '<div class="mb-2"><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Application / Instance AID</label>' +
|
||||||
'<input value="' + escHtml(f.aid || '') + '" oninput="' + uf('aid') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="A000000151000000"></div>' +
|
'<input value="' + escHtml(f.aid || '') + '" oninput="' + uf('aid') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="A000000151000000"></div>' +
|
||||||
chainRamPrivHtml(idx, f, uf);
|
chainRamPrivHtml(chainId, idx, f, uf);
|
||||||
}
|
}
|
||||||
if (cmd === 'install-reg-update') {
|
if (cmd === 'install-reg-update') {
|
||||||
return '<div class="grid grid-cols-2 gap-2 mb-2">' +
|
return '<div class="grid grid-cols-2 gap-2 mb-2">' +
|
||||||
@@ -2558,7 +2715,7 @@ function chainRamFieldsHtml(idx, row, f, uf) {
|
|||||||
'<input value="' + escHtml(f.sdAid || '') + '" oninput="' + uf('sdAid') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="A000000151000000"></div>' +
|
'<input value="' + escHtml(f.sdAid || '') + '" oninput="' + uf('sdAid') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="A000000151000000"></div>' +
|
||||||
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Application / Instance AID</label>' +
|
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Application / Instance AID</label>' +
|
||||||
'<input value="' + escHtml(f.aid || '') + '" oninput="' + uf('aid') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="A000000151000000"></div></div>' +
|
'<input value="' + escHtml(f.aid || '') + '" oninput="' + uf('aid') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="A000000151000000"></div></div>' +
|
||||||
chainRamPrivHtml(idx, f, uf);
|
chainRamPrivHtml(chainId, idx, f, uf);
|
||||||
}
|
}
|
||||||
if (cmd === 'install-extradition') {
|
if (cmd === 'install-extradition') {
|
||||||
return '<div class="grid grid-cols-2 gap-2">' +
|
return '<div class="grid grid-cols-2 gap-2">' +
|
||||||
@@ -2601,7 +2758,7 @@ function chainRamFieldsHtml(idx, row, f, uf) {
|
|||||||
if (cmd === 'get-data') {
|
if (cmd === 'get-data') {
|
||||||
var tag = f.tag || '5F50';
|
var tag = f.tag || '5F50';
|
||||||
return '<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">GET DATA tag</label>' +
|
return '<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">GET DATA tag</label>' +
|
||||||
'<select onchange="chainUpdateField(\'chain-ram\',' + idx + ',\'tag\',this.value)" class="w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1 dark:bg-slate-800 mb-1">' +
|
'<select onchange="chainUpdateField(\'' + chainId + '\',' + idx + ',\'tag\',this.value)" class="w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1 dark:bg-slate-800 mb-1">' +
|
||||||
'<option value=""' + (!tag ? ' selected' : '') + '>— select preset —</option>' +
|
'<option value=""' + (!tag ? ' selected' : '') + '>— select preset —</option>' +
|
||||||
'<option value="42"' + (tag === '42' ? ' selected' : '') + '>42 — IIN</option>' +
|
'<option value="42"' + (tag === '42' ? ' selected' : '') + '>42 — IIN</option>' +
|
||||||
'<option value="45"' + (tag === '45' ? ' selected' : '') + '>45 — CIN</option>' +
|
'<option value="45"' + (tag === '45' ? ' selected' : '') + '>45 — CIN</option>' +
|
||||||
@@ -2672,6 +2829,51 @@ function chainRamFieldsHtml(idx, row, f, uf) {
|
|||||||
return '<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Data (hex) — challenge</label>' +
|
return '<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Data (hex) — challenge</label>' +
|
||||||
'<input value="' + escHtml(f.data || '') + '" oninput="' + uf('data') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="hex"></div>';
|
'<input value="' + escHtml(f.data || '') + '" oninput="' + uf('data') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="hex"></div>';
|
||||||
}
|
}
|
||||||
|
if (cmd === 'put-key') {
|
||||||
|
return '<div class="grid grid-cols-2 gap-2 mb-2">' +
|
||||||
|
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Key version (P1, hex)</label>' +
|
||||||
|
'<input value="' + escHtml(f.keyVer || '01') + '" oninput="' + uf('keyVer') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="2"></div>' +
|
||||||
|
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Key identifier (P2, hex)</label>' +
|
||||||
|
'<input value="' + escHtml(f.keyId || '01') + '" oninput="' + uf('keyId') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="2"></div></div>' +
|
||||||
|
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Key data (hex, GP Card Spec 11.8 — new key under the KEK / target key, optional KCV)</label>' +
|
||||||
|
'<textarea oninput="' + uf('data') + '" rows="2" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800">' + escHtml(f.data || '') + '</textarea></div>';
|
||||||
|
}
|
||||||
|
if (cmd === 'push') {
|
||||||
|
var pt = f.pushType || '01';
|
||||||
|
var htmlPush = '<div class="mb-2"><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Request (P2)</label>' +
|
||||||
|
'<select onchange="' + uf('pushType') + ';chainRender(\'' + chainId + '\')" class="w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1 dark:bg-slate-800">' +
|
||||||
|
'<option value="01"' + (pt === '01' ? ' selected' : '') + '>01 — BIP channel opening (TS 102 226 9.2.1)</option>' +
|
||||||
|
'<option value="02"' + (pt === '02' ? ' selected' : '') + '>02 — CAT_TP link establishment (9.2.2)</option>' +
|
||||||
|
'<option value="03"' + (pt === '03' ? ' selected' : '') + '>03 — TCP connection (9.2.3)</option>' +
|
||||||
|
'<option value="04"' + (pt === '04' ? ' selected' : '') + '>04 — identification packet (9.2.4)</option></select></div>';
|
||||||
|
if (pt === '02') {
|
||||||
|
htmlPush += '<div class="grid grid-cols-3 gap-2 mb-2">' +
|
||||||
|
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Destination port (hex)</label>' +
|
||||||
|
'<input value="' + escHtml(f.pushPort || '') + '" oninput="' + uf('pushPort') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="4" placeholder="1F90"></div>' +
|
||||||
|
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Max SDU (hex, optional)</label>' +
|
||||||
|
'<input value="' + escHtml(f.pushSdu || '') + '" oninput="' + uf('pushSdu') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="4"></div>' +
|
||||||
|
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Identification data (hex, optional)</label>' +
|
||||||
|
'<input value="' + escHtml(f.pushIdent || '') + '" oninput="' + uf('pushIdent') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800"></div></div>';
|
||||||
|
} else if (pt === '03') {
|
||||||
|
htmlPush += '<div class="grid grid-cols-2 gap-2 mb-2">' +
|
||||||
|
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Destination port (hex)</label>' +
|
||||||
|
'<input value="' + escHtml(f.pushPort || '') + '" oninput="' + uf('pushPort') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="4" placeholder="0050"></div>' +
|
||||||
|
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Bearer description (hex, empty = 350103 default bearer)</label>' +
|
||||||
|
'<input value="' + escHtml(f.pushBearer || '') + '" oninput="' + uf('pushBearer') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="350103"></div>' +
|
||||||
|
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Destination address value (hex: 21+IPv4 / 57+IPv6 / F0+FQDN)</label>' +
|
||||||
|
'<input value="' + escHtml(f.pushAddr || '') + '" oninput="' + uf('pushAddr') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="210A000001"></div>' +
|
||||||
|
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Network access name / APN (text)</label>' +
|
||||||
|
'<input value="' + escHtml(f.pushApn || '') + '" oninput="' + uf('pushApn') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800"></div></div>';
|
||||||
|
} else if (pt === '04') {
|
||||||
|
htmlPush += '<div class="mb-2"><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Identification data (hex, optional — ICCID is used when absent)</label>' +
|
||||||
|
'<input value="' + escHtml(f.pushIdent || '') + '" oninput="' + uf('pushIdent') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800"></div>';
|
||||||
|
}
|
||||||
|
var extraField = pt === '01' ? 'pushData' : 'pushExtra';
|
||||||
|
htmlPush += '<div class="mb-2"><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' +
|
||||||
|
(pt === '01' ? 'OPEN CHANNEL COMPREHENSION-TLVs (hex, optional)' : 'Extra TLVs (hex, optional)') + '</label>' +
|
||||||
|
'<textarea oninput="' + uf(extraField) + '" rows="2" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800">' + escHtml(f[extraField] || '') + '</textarea></div>';
|
||||||
|
return htmlPush;
|
||||||
|
}
|
||||||
if (cmd === 'get-response') {
|
if (cmd === 'get-response') {
|
||||||
return '<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Le (max bytes)</label>' +
|
return '<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Le (max bytes)</label>' +
|
||||||
'<input value="' + escHtml(f.le || '00') + '" oninput="' + uf('le') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="00" maxlength="2"></div>';
|
'<input value="' + escHtml(f.le || '00') + '" oninput="' + uf('le') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="00" maxlength="2"></div>';
|
||||||
@@ -2680,7 +2882,7 @@ function chainRamFieldsHtml(idx, row, f, uf) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===== RAM/GP privileges sub-form (per-row) =====
|
// ===== RAM/GP privileges sub-form (per-row) =====
|
||||||
function chainRamPrivHtml(idx, f, uf) {
|
function chainRamPrivHtml(chainId, idx, f, uf) {
|
||||||
var priv = f.priv || '00';
|
var priv = f.priv || '00';
|
||||||
var b1 = parseInt(priv.substring(0, 2), 16) || 0;
|
var b1 = parseInt(priv.substring(0, 2), 16) || 0;
|
||||||
var b2 = priv.length >= 4 ? parseInt(priv.substring(2, 4), 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 byteIdx = c[2];
|
||||||
var val = parseInt(c[1], 16);
|
var val = parseInt(c[1], 16);
|
||||||
var checked = (byteIdx === 'b1' && (b1 & val)) || (byteIdx === 'b2' && (b2 & val)) || (byteIdx === 'b3' && (b3 & val));
|
var checked = (byteIdx === 'b1' && (b1 & val)) || (byteIdx === 'b2' && (b2 & val)) || (byteIdx === 'b3' && (b3 & val));
|
||||||
html += '<label class="flex items-center gap-1"><input type="checkbox" class="chain-priv-' + byteIdx + '-' + idx + '"' +
|
html += '<label class="flex items-center gap-1"><input type="checkbox" class="chain-priv-' + chainId + '-' + byteIdx + '-' + idx + '"' +
|
||||||
(checked ? ' checked' : '') + ' onchange="chainRamUpdatePriv(' + idx + ')"> ' + c[0] + '</label>';
|
(checked ? ' checked' : '') + ' onchange="chainRamUpdatePriv(\'' + chainId + '\',' + idx + ')"> ' + c[0] + '</label>';
|
||||||
});
|
});
|
||||||
html += '</div></div>';
|
html += '</div></div>';
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
function chainRamUpdatePriv(idx) {
|
function chainRamUpdatePriv(chainId, idx) {
|
||||||
var b1 = 0, b2 = 0, b3 = 0;
|
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-' + chainId + '-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-' + chainId + '-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 + '-b3-' + idx + ':checked').forEach(function(cb) { b3 |= parseInt(cb.value, 16); });
|
||||||
var hex = b1.toString(16).padStart(2, '0').toUpperCase();
|
var hex = b1.toString(16).padStart(2, '0').toUpperCase();
|
||||||
hex += b2.toString(16).padStart(2, '0').toUpperCase();
|
hex += b2.toString(16).padStart(2, '0').toUpperCase();
|
||||||
if (b3) hex += b3.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) =====
|
// ===== RAM/GP toolkit sub-form (per-row) =====
|
||||||
function chainRamToolkitHtml(idx, f, uf) {
|
function chainRamToolkitHtml(chainId, idx, f, uf) {
|
||||||
var enabled = f.tkEnabled;
|
var enabled = f.tkEnabled;
|
||||||
var html = '<div class="mb-2"><label class="flex items-center gap-2 text-xs font-medium text-gray-700 dark:text-slate-300">' +
|
var html = '<div class="mb-2"><label class="flex items-center gap-2 text-xs font-medium text-gray-700 dark:text-slate-300">' +
|
||||||
'<input type="checkbox"' + (enabled ? ' checked' : '') + ' onchange="chainUpdateField(\'chain-ram\',' + idx + ',\'tkEnabled\',this.checked);chainRender(\'chain-ram\')" class="rounded">' +
|
'<input type="checkbox"' + (enabled ? ' checked' : '') + ' onchange="chainUpdateField(\'' + chainId + '\',' + idx + ',\'tkEnabled\',this.checked);chainRender(\'' + chainId + '\')" class="rounded">' +
|
||||||
'SIM/UICC Toolkit parameters</label></div>';
|
'SIM/UICC Toolkit parameters</label></div>';
|
||||||
if (!enabled) return html;
|
if (!enabled) return html;
|
||||||
var tkMode = f.tkMode || 'ea';
|
var tkMode = f.tkMode || 'ea';
|
||||||
@@ -2792,7 +2994,7 @@ function chainRamToolkitHtml(idx, f, uf) {
|
|||||||
// ===== SIM/USIM hex builder =====
|
// ===== SIM/USIM hex builder =====
|
||||||
function chainSimBuildRowHex(chainId, idx, row) {
|
function chainSimBuildRowHex(chainId, idx, row) {
|
||||||
var f = row.fields;
|
var f = row.fields;
|
||||||
var isSim = chainId === 'chain-sim';
|
var isSim = chainKind(chainId) === 'sim';
|
||||||
var cla = isSim ? 0xA0 : 0x00;
|
var cla = isSim ? 0xA0 : 0x00;
|
||||||
|
|
||||||
if (row.cmd === 'select') {
|
if (row.cmd === 'select') {
|
||||||
@@ -2845,6 +3047,70 @@ function chainSimBuildRowHex(chainId, idx, row) {
|
|||||||
if (row.cmd === 'get-response') {
|
if (row.cmd === 'get-response') {
|
||||||
return '00C0000000';
|
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 = {
|
var OPS2 = {
|
||||||
'read-record': {ins: 0xB2}, 'read-binary': {ins: 0xB0},
|
'read-record': {ins: 0xB2}, 'read-binary': {ins: 0xB0},
|
||||||
'update-record': {ins: 0xDC}, 'update-binary': {ins: 0xD6},
|
'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);
|
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 =====
|
// ===== 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) {
|
function chainRamBuildRowHex(idx, row) {
|
||||||
var f = row.fields;
|
var f = row.fields;
|
||||||
var cmd = row.cmd;
|
var cmd = row.cmd;
|
||||||
@@ -3081,6 +3419,17 @@ function chainRamBuildRowHex(idx, row) {
|
|||||||
var dl9 = berLenStr(ramData4.length / 2);
|
var dl9 = berLenStr(ramData4.length / 2);
|
||||||
return buildApdu(0x00, 0x88, 0x00, 0x00, parseInt(dl9, 16), ramData4) + '00';
|
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') {
|
if (cmd === 'get-response') {
|
||||||
var le = parseInt(f.le, 16) || 0;
|
var le = parseInt(f.le, 16) || 0;
|
||||||
return buildApdu(0x80, 0xC0, 0x00, 0x00, le, null);
|
return buildApdu(0x80, 0xC0, 0x00, 0x00, le, null);
|
||||||
@@ -3093,7 +3442,8 @@ function chainBuildRowHex(chainId, idx) {
|
|||||||
chainInit(chainId);
|
chainInit(chainId);
|
||||||
var row = _chains[chainId].rows[idx];
|
var row = _chains[chainId].rows[idx];
|
||||||
if (!row) return '';
|
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 chainSimBuildRowHex(chainId, idx, row);
|
||||||
}
|
}
|
||||||
return chainRamBuildRowHex(idx, row);
|
return chainRamBuildRowHex(idx, row);
|
||||||
@@ -3184,18 +3534,47 @@ function genConvert() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===== Secured Packet UI =====
|
// ===== Secured Packet UI =====
|
||||||
function chainRamApduList() {
|
// One APDU per built command, for consumers outside the compact chain: a row
|
||||||
// One hex APDU per chain row (the same rows the preview concatenates).
|
// 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 = [];
|
const list = [];
|
||||||
chainInit('chain-ram');
|
chainInit(chainId);
|
||||||
const rows = _chains['chain-ram'].rows;
|
const rows = _chains[chainId].rows;
|
||||||
for (let i = 0; i < rows.length; i++) {
|
for (let i = 0; i < rows.length; i++) {
|
||||||
const h = chainBuildRowHex('chain-ram', i);
|
if (rows[i].cmd === 'get-response') continue;
|
||||||
if (h) list.push(h);
|
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;
|
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() {
|
function ramBuildExpanded() {
|
||||||
// Show each built command in the TS 102 226 Command Scripting (expanded)
|
// Show each built command in the TS 102 226 Command Scripting (expanded)
|
||||||
// form: AA <len> 22 <len> <apdu> or AE80 22 <len> <apdu> 0000.
|
// form: AA <len> 22 <len> <apdu> or AE80 22 <len> <apdu> 0000.
|
||||||
@@ -3678,15 +4057,21 @@ const BER_TONES = [
|
|||||||
];
|
];
|
||||||
const BER_DEVICES = [['81','UICC'],['82','Terminal'],['83','Network'],['02','Display']];
|
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) {
|
function addBerRow(type) {
|
||||||
const c = document.getElementById('ber-rows');
|
const c = document.getElementById('ber-rows');
|
||||||
const idx = c.children.length;
|
const idx = c.children.length;
|
||||||
const row = document.createElement('div');
|
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.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.berIdx = idx;
|
||||||
|
row.dataset.berUid = String(++_berUidSeq);
|
||||||
row.innerHTML = berRowHtml(idx, type);
|
row.innerHTML = berRowHtml(idx, type);
|
||||||
c.appendChild(row);
|
c.appendChild(row);
|
||||||
onBerTypeChange(row.querySelector('.ber-type'));
|
onBerTypeChange(row.querySelector('.ber-type'));
|
||||||
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
function berRowHtml(idx, type) {
|
function berRowHtml(idx, type) {
|
||||||
@@ -3715,7 +4100,17 @@ function onBerTypeChange(sel) {
|
|||||||
const body = row.querySelector('.ber-body');
|
const body = row.querySelector('.ber-body');
|
||||||
const type = sel.value;
|
const type = sel.value;
|
||||||
if (type === 'c-apdu') {
|
if (type === 'c-apdu') {
|
||||||
body.innerHTML = `<input class="ber-hex w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-1 py-1 dark:bg-slate-700 dark:text-slate-300" placeholder="hex" oninput="updateBerRow(this)">`;
|
body.innerHTML = `<div class="flex gap-1 mb-1 flex-wrap items-center">
|
||||||
|
<select class="ber-cmd-mode text-xs border border-gray-300 dark:border-slate-600 rounded px-1 py-1 dark:bg-slate-700 dark:text-slate-300" onchange="onBerCmdModeChange(this)">
|
||||||
|
<option value="hex">Hex</option>
|
||||||
|
<option value="sim">SIM RFM</option>
|
||||||
|
<option value="usim">USIM RFM</option>
|
||||||
|
<option value="ram">RAM/GP</option>
|
||||||
|
</select>
|
||||||
|
<span class="ber-cmd-apdu text-xs font-mono text-gray-500 dark:text-slate-400"></span>
|
||||||
|
</div>
|
||||||
|
<div class="ber-cmd-editor"></div>`;
|
||||||
|
onBerCmdModeChange(body.querySelector('.ber-cmd-mode'));
|
||||||
} else if (type === 'chaining') {
|
} else if (type === 'chaining') {
|
||||||
body.innerHTML = buildScriptChainingRow(row.dataset.berIdx);
|
body.innerHTML = buildScriptChainingRow(row.dataset.berIdx);
|
||||||
} else if (type === 'error') {
|
} else if (type === 'error') {
|
||||||
@@ -3737,6 +4132,52 @@ function onBerTypeChange(sel) {
|
|||||||
updateBerRow(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 = '<input class="ber-hex w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-1 py-1 dark:bg-slate-700 dark:text-slate-300" placeholder="hex" oninput="updateBerRow(this)">';
|
||||||
|
updateBerRow(sel);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = berCmdChainId(row.dataset.berUid, mode);
|
||||||
|
editor.innerHTML = '<div id="' + id + '-rows"></div>';
|
||||||
|
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() {
|
function buildErrorActionRow() {
|
||||||
return `<div class="flex gap-2 items-center">
|
return `<div class="flex gap-2 items-center">
|
||||||
<span class="text-xs text-gray-600 dark:text-slate-400">Error Action:</span>
|
<span class="text-xs text-gray-600 dark:text-slate-400">Error Action:</span>
|
||||||
@@ -3885,9 +4326,9 @@ function genBerRowValue(row) {
|
|||||||
return genScriptChainingValue(row, tag);
|
return genScriptChainingValue(row, tag);
|
||||||
}
|
}
|
||||||
if (type === 'c-apdu') {
|
if (type === 'c-apdu') {
|
||||||
const hex = (row.querySelector('.ber-hex').value||'').replace(/[^0-9a-fA-F]/g,'');
|
const hex = genBerCmdApdu(row);
|
||||||
if (!hex) return '';
|
if (!hex) return '';
|
||||||
return tag + berLenStr(hex.length/2) + hex;
|
return tag + berLenStr(hex.length / 2) + hex;
|
||||||
}
|
}
|
||||||
const st = row.querySelector('.ber-subtype').value;
|
const st = row.querySelector('.ber-subtype').value;
|
||||||
if (st === 'action') {
|
if (st === 'action') {
|
||||||
@@ -13579,6 +14020,7 @@ const LANG_RU = {
|
|||||||
'Queued': 'В очереди',
|
'Queued': 'В очереди',
|
||||||
'Expanded format': 'Развёрнутый формат',
|
'Expanded format': 'Развёрнутый формат',
|
||||||
'To expanded': 'В развёрнутый',
|
'To expanded': 'В развёрнутый',
|
||||||
|
'→ Expanded Script': '→ Расширенный скрипт',
|
||||||
'Queue in SCP81': 'В очередь SCP81',
|
'Queue in SCP81': 'В очередь SCP81',
|
||||||
'No commands': 'Нет команд',
|
'No commands': 'Нет команд',
|
||||||
'APDUs': 'APDU',
|
'APDUs': 'APDU',
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simple-v240';
|
const CACHE = 'simple-v241';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ function buildPcHtml(extraFields) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FNS = ['berLenStr', 'gsm7TextToSeptets', 'gsm7Encode', 'genBerPcValue', 'genErrorActionValue',
|
const FNS = ['berLenStr', 'gsm7TextToSeptets', 'gsm7Encode', 'genBerPcValue', 'genErrorActionValue',
|
||||||
'genScriptChainingValue'];
|
'genScriptChainingValue', 'chainKind', 'chainIsEmbedded', 'chainCommands', 'berCmdChainId', 'genBerCmdApdu', 'genBerRowValue'];
|
||||||
let code = '';
|
let code = '';
|
||||||
for (const f of FNS) code += extractFunc(html, f) + '\n';
|
for (const f of FNS) code += extractFunc(html, f) + '\n';
|
||||||
const berTagsMatch = html.match(/const BER_TAGS = \{[^}]*\};/);
|
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');
|
if (!m) throw new Error('constant not found');
|
||||||
code = m[0] + '\n' + code;
|
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};';
|
code += '\nvar __berConsts = {BER_TAGS, BER_QUAL, BER_DEVICES, BER_TONES, GSM7_ALPHABET, GSM7_EXT_MAP};';
|
||||||
eval(code);
|
eval(code);
|
||||||
const {BER_TAGS, BER_QUAL, BER_DEVICES, BER_TONES, GSM7_ALPHABET, GSM7_EXT_MAP} = __berConsts;
|
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';
|
keep.name = 'chaining-0-keep';
|
||||||
row.children.push(first, interm, last, keep, el('chaining-script-id'), el('chaining-additional'));
|
row.children.push(first, interm, last, keep, el('chaining-script-id'), el('chaining-additional'));
|
||||||
assert.strictEqual(genScriptChainingValue(row, '83'), '');
|
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'));
|
||||||
});
|
});
|
||||||
@@ -22,7 +22,7 @@ function extractFunc(src, name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract chain builder functions and dependencies
|
// Extract chain builder functions and dependencies
|
||||||
const FNS = ['buildApdu', 'buildSelect', 'escHtml', 'chainInit', 'chainSimBuildRowHex'];
|
const FNS = ['buildApdu', 'buildSelect', 'escHtml', 'chainInit', 'chainKind', 'chainSimBuildRowHex'];
|
||||||
let code = '';
|
let code = '';
|
||||||
for (const f of FNS) {
|
for (const f of FNS) {
|
||||||
code += extractFunc(html, f) + '\n';
|
code += extractFunc(html, f) + '\n';
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pysim-simple-server"
|
name = "pysim-simple-server"
|
||||||
version = "3.2.0"
|
version = "3.3.0"
|
||||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ from osmocom.tlv import BER_TLV_IE
|
|||||||
from osmocom.utils import rpad
|
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
|
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user