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:
2026-09-23 07:34:21 +03:00
parent 43f50d3b72
commit 7aee1818b8
11 changed files with 871 additions and 66 deletions
+19 -7
View File
@@ -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">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">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 &sect;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 &sect;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>
</tbody>
</table>
@@ -90,13 +92,17 @@
</tbody>
</table>
<p class="text-sm mb-3">Для record-команд режим P2: <strong>Absolute (04)</strong>, <strong>Next (02)</strong> или <strong>Previous (03)</strong>. Если за Case&nbsp;4 командой сразу следует строка GET RESPONSE, сборщик цепочки автоматически убирает её байт Le (ETSI TS 102 226 &sect;5.1.1). В правой колонке — панель конвертации (IMSI, MSISDN, ICCID, SPN, PLMN, nibble swap). См. <a href="#conversion" class="text-blue-600 dark:text-blue-400 hover:underline">&sect;2.5</a>.</p>
<p class="text-sm mb-3"><strong>&rarr; Расширенный скрипт</strong> отправляет собранную цепочку в конструктор Expanded Script строками C-APDU (строки GET RESPONSE отбрасываются — расширенный формат их не использует, TS 102 226 &sect;5.2.1.1). Приложение UICC Shared File System RFM (TAR <code class="font-mono text-sm">B00000</code>) доступно и в компактном, и в расширенном формате — если TAR не настроен на автоопределение формата, используются разные значения TAR (TS 102 226 &sect;7.0/&sect;5.3).</p>
<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">
<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>Упаковать в 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 &sect;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> (&sect;11.1.8) принимает необязательный SFI и прибавляемое значение.</li>
<li><strong>CREATE FILE</strong> / <strong>DELETE FILE</strong> / <strong>RESIZE FILE</strong> (ETSI TS 102 222 &sect;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 &sect;11.3) работают с BER-TLV EF: P2 выбирает первый/следующий/повторный блок (+SFI), поле тега необязательно (<code class="font-mono text-sm">5C</code> — список размещённых тегов).</li>
<li>Каждый переход SELECT в цепочке запрашивает FCP, если не отмечен как silent.</li>
</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>
</tr></thead>
<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 &sect;5.2.1.0; GET RESPONSE в этом формате не используется, &sect;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">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>
</tbody>
</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">Строка <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 &sect;5.2.1.1). Кнопка <strong>&rarr; Расширенный скрипт</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>
<p class="text-sm mb-2">Восстановление при ошибках по TS 102 226 §5.2.1.3 — одна из четырёх форм:</p>
@@ -131,13 +139,14 @@
</ul>
<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 &sect;5.2.1.4). TLV имеет вид <code class="font-mono text-sm">83 01 &lt;флаг&gt;</code>, присутствует один раз и только как первый Command TLV; допускается для RFM и RAM:</p>
<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><strong>Идентификатор скрипта:</strong> Корреляционный идентификатор между пакетами (1-4 байта hex, авто-инкремент подсказки)</li>
<li><strong>Дополнительные данные:</strong> Расширенная информация цепочки (опционально hex)</li>
<li><strong>Сохранение контекста:</strong> UICC сохраняет состояние безопасности/транзакции между пакетами</li>
<li><code class="font-mono text-sm">01</code> &mdash; первый скрипт, удалить информацию цепочки при сбросе карты</li>
<li><code class="font-mono text-sm">11</code> &mdash; первый скрипт, сохранить информацию цепочки при сбросе карты (только RFM)</li>
<li><code class="font-mono text-sm">02</code> &mdash; последующий скрипт, далее будут ещё</li>
<li><code class="font-mono text-sm">03</code> &mdash; последующий скрипт, последний</li>
</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>
<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">&sect;3.1</a>): статус PoR (TAR, счётчик, сырой PoR), а статусное слово и данные ответа последней команды подставляются в подвкладку <strong>&laquo;Парсер ответов&raquo;</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">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">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 &sect;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 &sect;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">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>
@@ -195,6 +206,7 @@
</tbody>
</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 &sect;8.2.1.3.2.</p>
<p class="text-sm mb-3"><strong>PUSH</strong> (TS 102 226 &sect;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>
<ul class="list-disc list-inside text-sm space-y-1">
+19 -8
View File
@@ -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">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">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 &sect;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 &sect;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>
</tbody>
</table>
@@ -90,12 +92,16 @@
</tbody>
</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&nbsp;4 command is immediately followed by a GET RESPONSE row, the chain builder strips its trailing Le byte automatically (ETSI TS 102 226 &sect;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">&sect;2.5</a>.</p>
<p class="text-sm mb-3"><strong>&rarr; Expanded Script</strong> sends the built chain to the Expanded Script builder as C-APDU rows (GET RESPONSE hops are dropped &mdash; the expanded format does not use them, TS 102 226 &sect;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 &mdash; unless the TAR is configured for automatic format detection, the two use different TAR values (TS 102 226 &sect;7.0/&sect;5.3).</p>
<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">
<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>SEARCH RECORD</strong> (TS 102 221 &sect;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> (&sect;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 &sect;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> &mdash; 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 &sect;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&rsquo;s <strong>ADF RFM TAR</strong> (<code class="font-mono text-sm">B00001</code> by default), linked to the ADF&rsquo;s AID (ADF.USIM).</li>
<li>Each SELECT hop in a chain requests FCP unless marked silent.</li>
</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>
</tr></thead>
<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 &sect;5.2.1.0; GET RESPONSE is not used in this format, &sect;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">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>
</tbody>
</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 &mdash; 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 &sect;5.2.1.1). <strong>&rarr; 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) &mdash; 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>
<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>
<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 &sect;5.2.1.4). The TLV is <code class="font-mono text-sm">83 01 &lt;flag&gt;</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">
<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><strong>Script ID:</strong> Correlation identifier across packets (1-4 bytes hex, auto-increment hints provided)</li>
<li><strong>Additional Data:</strong> Extended chaining information (optional hex)</li>
<li><strong>Context Preservation:</strong> UICC keeps security/transaction state open across chained scripts</li>
<li><code class="font-mono text-sm">01</code> &mdash; first script, delete chaining information upon card reset</li>
<li><code class="font-mono text-sm">11</code> &mdash; first script, keep chaining information across card reset (RFM only)</li>
<li><code class="font-mono text-sm">02</code> &mdash; subsequent script, more to follow</li>
<li><code class="font-mono text-sm">03</code> &mdash; subsequent script, last one</li>
</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>
<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">&sect;3.1</a>): the PoR status (TAR, counter, raw PoR), with the last command&rsquo;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">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, &hellip;)</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 &sect;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 &sect;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">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>
@@ -195,6 +205,7 @@
</tbody>
</table>
<p class="text-sm mb-3">Refer to GlobalPlatform v2.3.1 and ETSI TS 102 226 &sect;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 &sect;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 &mdash; 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>
<ul class="list-disc list-inside text-sm space-y-1">
+486 -44
View File
@@ -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','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','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>
</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>
<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 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','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','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>
</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>
<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 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','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','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','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>
@@ -152,6 +165,7 @@
<option value="AA">AA (definite)</option>
</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="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>
<span id="chain-ram-queue-msg" class="text-xs"></span>
</div>
@@ -1437,7 +1451,7 @@
// ===== Version =====
// Single source of truth for the PWA version: shown in the header and used
// by the server version check in pysimConnect().
const SIMPLE_VERSION = '3.2.0';
const SIMPLE_VERSION = '3.3.0';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
@@ -2301,6 +2315,9 @@ function escHtml(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;'
// 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).
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 = [
{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:'verify',label:'VERIFY PIN'},{value:'change',label:'CHANGE 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 = [
{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:'verify',label:'VERIFY PIN'},{value:'change',label:'CHANGE 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 = [
{value:'install-load',label:'INSTALL [for load]'},
@@ -2329,11 +2353,33 @@ const CHAIN_CMDS_RAM = [
{value:'load',label:'LOAD'},{value:'delete',label:'DELETE'},
{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:'put-key',label:'PUT KEY'},{value:'push',label:'PUSH (TS 102 226 §9)'},
{value:'ext-auth',label:'EXTERNAL AUTHENTICATE'},
{value:'int-auth',label:'INTERNAL AUTHENTICATE'},
{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 chainAddRow(id, cmd) { chainInit(id); _chains[id].rows.push({cmd: cmd, fields: {}}); chainRender(id); }
function chainDeleteRow(id, idx) { chainInit(id); _chains[id].rows.splice(idx, 1); chainRender(id); }
@@ -2347,6 +2393,7 @@ function chainUpdateField(id, idx, field, value) {
if (p) p.value = chainBuildHex(id);
const b = document.getElementById(id + '-pack-btn');
if (b) b.disabled = !chainBuildHex(id);
chainNotify(id);
}
function chainRender(chainId) {
@@ -2354,23 +2401,24 @@ function chainRender(chainId) {
const container = document.getElementById(chainId + '-rows');
const preview = document.getElementById(chainId + '-preview');
const packBtn = document.getElementById(chainId + '-pack-btn');
if (!container) return;
if (!container) { chainNotify(chainId); return; }
const embedded = chainIsEmbedded(chainId);
const rows = _chains[chainId].rows;
container.innerHTML = '';
rows.forEach(function(row, idx) {
const div = document.createElement('div');
div.className = 'mb-3 p-3 border border-gray-200 dark:border-slate-600 rounded';
div.innerHTML = chainRenderRow(chainId, idx, row);
div.innerHTML = chainRenderRow(chainId, idx, row, embedded);
container.appendChild(div);
});
const hex = chainBuildHex(chainId);
if (preview) preview.value = hex;
if (packBtn) packBtn.disabled = !hex;
chainNotify(chainId);
}
function chainRenderRow(chainId, idx, row) {
const isRam = chainId === 'chain-ram';
const cmds = isRam ? CHAIN_CMDS_RAM : (chainId === 'chain-usim' ? CHAIN_CMDS_USIM : CHAIN_CMDS_SIM);
function chainRenderRow(chainId, idx, row, embedded) {
const cmds = chainCommands(chainId);
const cmdOpts = cmds.map(function(c) {
return '<option value="' + c.value + '"' + (c.value === row.cmd ? ' selected' : '') + '>' + c.label + '</option>';
}).join('');
@@ -2379,7 +2427,7 @@ function chainRenderRow(chainId, idx, row) {
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>' +
'<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 class="pl-4">' + fieldsHtml + '</div>';
}
@@ -2387,16 +2435,18 @@ function chainRenderRow(chainId, idx, row) {
function chainRowFieldsHtml(chainId, idx, row) {
var f = row.fields;
function uf(field) { return "chainUpdateField('" + chainId + "'," + idx + ",'" + field + "',this.value)"; }
if (chainId === 'chain-sim' || chainId === 'chain-usim') return chainSimFieldsHtml(chainId, idx, f, uf);
return chainRamFieldsHtml(idx, row, f, uf);
const kind = chainKind(chainId);
if (kind === 'sim' || kind === 'usim') return chainSimFieldsHtml(chainId, idx, f, uf);
return chainRamFieldsHtml(chainId, idx, row, f, uf);
}
// ===== SIM/USIM field rendering =====
function chainSimFieldsHtml(chainId, idx, f, uf) {
var cmd = (_chains[chainId] && _chains[chainId].rows[idx]) ? _chains[chainId].rows[idx].cmd : '';
var usim = chainKind(chainId) === 'usim';
if (cmd === 'select') {
var method = f.method || 'fid';
var silent = chainId === 'chain-usim' && f.silent;
var silent = usim && f.silent;
var html = '<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>' +
'<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="chain"' + (method === 'chain' ? ' selected' : '') + '>Chain</option>' +
'</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">' +
'<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>';
}
html += '</div>';
@@ -2418,9 +2468,9 @@ function chainSimFieldsHtml(chainId, idx, f, uf) {
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>' +
'<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') {
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>' +
'<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>';
if (usim) {
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-' + chainId + '-' + idx + '-base" value="df"' + (f.base === 'df' ? ' checked' : '') + ' onchange="chainUpdateField(\'' + chainId + '\',' + idx + ',\'base\',\'df\')"> from current DF (ADF.USIM)</label></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>' +
'<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 &mdash; forward from P1</option>' +
'<option value="12"' + (srMode === '12' ? ' selected' : '') + '>12 &mdash; backward from P1</option>' +
'<option value="18"' + (srMode === '18' ? ' selected' : '') + '>18 &mdash; enhanced search</option>' +
'<option value="1C"' + (srMode === '1C' ? ' selected' : '') + '>1C &mdash; 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 &mdash; type 1, from start</option>' +
'<option value="01"' + (srMode === '01' ? ' selected' : '') + '>01 &mdash; type 1, backward</option>' +
'<option value="02"' + (srMode === '02' ? ' selected' : '') + '>02 &mdash; type 1, next forward</option>' +
'<option value="03"' + (srMode === '03' ? ' selected' : '') + '>03 &mdash; type 1, previous</option>' +
'<option value="10"' + (srMode === '10' ? ' selected' : '') + '>10 &mdash; type 2, from start</option>' +
'<option value="11"' + (srMode === '11' ? ' selected' : '') + '>11 &mdash; type 2, backward</option>' +
'<option value="12"' + (srMode === '12' ? ' selected' : '') + '>12 &mdash; type 2, next forward</option>' +
'<option value="13"' + (srMode === '13' ? ' selected' : '') + '>13 &mdash; 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 &mdash; mode 0</option>' +
'<option value="01"' + (f.mode === '01' ? ' selected' : '') + '>01 &mdash; 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 &mdash; first block</option>' +
'<option value="00"' + (sdMode === '00' ? ' selected' : '') + '>00 &mdash; next block</option>' +
'<option value="40"' + (sdMode === '40' ? ' selected' : '') + '>40 &mdash; 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') {
return '<div class="text-xs text-gray-500 dark:text-slate-400 italic">No parameters &mdash; CLA=C0 INS=C0 P1=00 P2=00</div>';
}
@@ -2522,7 +2679,7 @@ function chainSimFieldsHtml(chainId, idx, f, uf) {
}
// ===== RAM/GP field rendering =====
function chainRamFieldsHtml(idx, row, f, uf) {
function chainRamFieldsHtml(chainId, idx, row, f, uf) {
var cmd = row.cmd;
if (cmd === 'install-load') {
return '<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>' +
'<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
html += chainRamPrivHtml(idx, f, uf);
html += chainRamPrivHtml(chainId, idx, f, uf);
// Toolkit
html += chainRamToolkitHtml(idx, f, uf);
html += chainRamToolkitHtml(chainId, idx, f, uf);
return html;
}
if (cmd === 'install-make-sel') {
return '<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>' +
chainRamPrivHtml(idx, f, uf);
chainRamPrivHtml(chainId, idx, f, uf);
}
if (cmd === 'install-reg-update') {
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>' +
'<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>' +
chainRamPrivHtml(idx, f, uf);
chainRamPrivHtml(chainId, idx, f, uf);
}
if (cmd === 'install-extradition') {
return '<div class="grid grid-cols-2 gap-2">' +
@@ -2601,7 +2758,7 @@ function chainRamFieldsHtml(idx, row, f, uf) {
if (cmd === 'get-data') {
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>' +
'<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' : '') + '>&mdash; select preset &mdash;</option>' +
'<option value="42"' + (tag === '42' ? ' selected' : '') + '>42 &mdash; IIN</option>' +
'<option value="45"' + (tag === '45' ? ' selected' : '') + '>45 &mdash; 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) &mdash; 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>';
}
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 &mdash; 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 &mdash; BIP channel opening (TS 102 226 9.2.1)</option>' +
'<option value="02"' + (pt === '02' ? ' selected' : '') + '>02 &mdash; CAT_TP link establishment (9.2.2)</option>' +
'<option value="03"' + (pt === '03' ? ' selected' : '') + '>03 &mdash; TCP connection (9.2.3)</option>' +
'<option value="04"' + (pt === '04' ? ' selected' : '') + '>04 &mdash; 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 &mdash; 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') {
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>';
@@ -2680,7 +2882,7 @@ function chainRamFieldsHtml(idx, row, f, uf) {
}
// ===== RAM/GP privileges sub-form (per-row) =====
function chainRamPrivHtml(idx, f, uf) {
function chainRamPrivHtml(chainId, idx, f, uf) {
var priv = f.priv || '00';
var b1 = parseInt(priv.substring(0, 2), 16) || 0;
var b2 = priv.length >= 4 ? parseInt(priv.substring(2, 4), 16) : 0;
@@ -2705,29 +2907,29 @@ function chainRamPrivHtml(idx, f, uf) {
var byteIdx = c[2];
var val = parseInt(c[1], 16);
var checked = (byteIdx === 'b1' && (b1 & val)) || (byteIdx === 'b2' && (b2 & val)) || (byteIdx === 'b3' && (b3 & val));
html += '<label class="flex items-center gap-1"><input type="checkbox" class="chain-priv-' + byteIdx + '-' + idx + '"' +
(checked ? ' checked' : '') + ' onchange="chainRamUpdatePriv(' + idx + ')"> ' + c[0] + '</label>';
html += '<label class="flex items-center gap-1"><input type="checkbox" class="chain-priv-' + chainId + '-' + byteIdx + '-' + idx + '"' +
(checked ? ' checked' : '') + ' onchange="chainRamUpdatePriv(\'' + chainId + '\',' + idx + ')"> ' + c[0] + '</label>';
});
html += '</div></div>';
return html;
}
function chainRamUpdatePriv(idx) {
function chainRamUpdatePriv(chainId, idx) {
var b1 = 0, b2 = 0, b3 = 0;
document.querySelectorAll('.chain-priv-b1-' + idx + ':checked').forEach(function(cb) { b1 |= parseInt(cb.value, 16); });
document.querySelectorAll('.chain-priv-b2-' + idx + ':checked').forEach(function(cb) { b2 |= parseInt(cb.value, 16); });
document.querySelectorAll('.chain-priv-b3-' + idx + ':checked').forEach(function(cb) { b3 |= parseInt(cb.value, 16); });
document.querySelectorAll('.chain-priv-' + chainId + '-b1-' + idx + ':checked').forEach(function(cb) { b1 |= parseInt(cb.value, 16); });
document.querySelectorAll('.chain-priv-' + chainId + '-b2-' + idx + ':checked').forEach(function(cb) { b2 |= parseInt(cb.value, 16); });
document.querySelectorAll('.chain-priv-' + chainId + '-b3-' + idx + ':checked').forEach(function(cb) { b3 |= parseInt(cb.value, 16); });
var hex = b1.toString(16).padStart(2, '0').toUpperCase();
hex += b2.toString(16).padStart(2, '0').toUpperCase();
if (b3) hex += b3.toString(16).padStart(2, '0').toUpperCase();
chainUpdateField('chain-ram', idx, 'priv', hex);
chainUpdateField(chainId, idx, 'priv', hex);
}
// ===== RAM/GP toolkit sub-form (per-row) =====
function chainRamToolkitHtml(idx, f, uf) {
function chainRamToolkitHtml(chainId, idx, f, uf) {
var enabled = f.tkEnabled;
var html = '<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>';
if (!enabled) return html;
var tkMode = f.tkMode || 'ea';
@@ -2792,7 +2994,7 @@ function chainRamToolkitHtml(idx, f, uf) {
// ===== SIM/USIM hex builder =====
function chainSimBuildRowHex(chainId, idx, row) {
var f = row.fields;
var isSim = chainId === 'chain-sim';
var isSim = chainKind(chainId) === 'sim';
var cla = isSim ? 0xA0 : 0x00;
if (row.cmd === 'select') {
@@ -2845,6 +3047,70 @@ function chainSimBuildRowHex(chainId, idx, row) {
if (row.cmd === 'get-response') {
return '00C0000000';
}
function sdP2(f) {
var base = parseInt((f.sdMode || '80').replace(/[^0-9a-fA-F]/g, '') || '80', 16);
var sfi = parseInt((f.sfi || '00').replace(/[^0-9a-fA-F]/g, '') || '00', 16);
return (base | (sfi & 0x1F)) & 0xFF;
}
if (row.cmd === 'search-record') {
var pattern = (f.srPattern || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
if (!pattern) return '';
var srm = parseInt(f.srMode || (isSim ? '02' : '10'), 16);
if (isSim) {
// TS 51 011 9.2.7 SEEK: type/mode in P2, pattern in the data field.
var seekLe = (f.le || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
return buildApdu(0xA0, 0xA2, 0x00, srm, pattern.length / 2, pattern) + seekLe;
}
var ind = '';
if (srm === 0x18) {
// Enhanced search: 2-byte indication (TS 102 221 table 11.13) first.
ind = (f.srInd || '1800').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
if (ind.length !== 4) return '';
}
var srData = ind + pattern;
var rec = parseInt((f.srRec || '00').replace(/[^0-9a-fA-F]/g, '') || '00', 16);
var srLe = (f.le || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
return buildApdu(cla, 0xA2, rec, srm, srData.length / 2, srData) + srLe;
}
if (row.cmd === 'increase') {
var add = (f.value || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
if (!add || add.length % 2) return '';
var incLe = (f.le || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
if (isSim) {
// TS 51 011 9.2.8: class A0, P1/P2 = 00, P3 = 03 (3-byte value).
return buildApdu(0xA0, 0x32, 0x00, 0x00, add.length / 2, add) + incLe;
}
var incSfi = parseInt((f.sfi || '00').replace(/[^0-9a-fA-F]/g, '') || '00', 16);
return buildApdu(cla, 0x32, incSfi ? (0x80 | (incSfi & 0x1F)) : 0x00, 0x00, add.length / 2, add) + incLe;
}
if (row.cmd === 'create-file') {
var fcp = (f.fcp || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
if (fcp.length < 4 || fcp.substr(0, 2) !== '62') return '';
return buildApdu(cla, 0xE0, 0x00, 0x00, fcp.length / 2, fcp);
}
if (row.cmd === 'delete-file') {
var dfid = (f.fid || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
if (dfid.length !== 4) return '';
return buildApdu(cla, 0xE4, 0x00, 0x00, 0x02, dfid);
}
if (row.cmd === 'resize-file') {
var rfcp = (f.fcp || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
if (rfcp.length < 4 || rfcp.substr(0, 2) !== '62') return '';
// TS 102 222 table 1: RESIZE FILE uses CLA '8X'/'CX' (80 without SM).
return buildApdu(0x80, 0xD4, parseInt(f.mode || '00', 16), 0x00, rfcp.length / 2, rfcp);
}
if (row.cmd === 'set-data') {
var sdData = (f.value || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
if (!sdData) return '';
return buildApdu(cla, 0xDB, 0x00, sdP2(f), sdData.length / 2, sdData);
}
if (row.cmd === 'retrieve-data') {
var rdTag = (f.tag || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
var rdLe = (f.le || '00').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
var rdP2 = sdP2(f);
if (rdTag) return buildApdu(cla, 0xCB, 0x00, rdP2, rdTag.length / 2, rdTag) + rdLe;
return buildApdu(cla, 0xCB, 0x00, rdP2, null, null) + rdLe;
}
var OPS2 = {
'read-record': {ins: 0xB2}, 'read-binary': {ins: 0xB0},
'update-record': {ins: 0xDC}, 'update-binary': {ins: 0xD6},
@@ -2910,7 +3176,79 @@ function chainSimBuildRowHex(chainId, idx, row) {
return buildApdu(cla, op.ins, p1, p2v, p3, data);
}
// Compose the FCP template for the CREATE FILE / RESIZE FILE builders
// (TS 102 222 tables 4 and 17). The access rule uses the compact format with
// AM=7F (all EF commands) and SC=00 (no condition) - review it for the card.
function chainBuildFcp(chainId, idx, kind) {
var row = _chains[chainId] && _chains[chainId].rows[idx];
if (!row) return;
var f = row.fields;
var fid = (f.fid || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
if (fid.length !== 4) return;
var size = (f.fileSize || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
if (size.length % 2) size = '0' + size;
if (!size) size = '0000';
var inner;
if (kind === 'create') {
var st = f.structure || 'transparent';
var record = st === 'linear_fixed' || st === 'cyclic';
var fd = ({ transparent: 0x01, linear_fixed: 0x02, cyclic: 0x06, ber_tlv: 0x39 })[st] || 0x01;
if (f.shareable) fd |= 0x40;
var recLen = (f.recLen || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
if (record) {
if (recLen.length % 2) recLen = '0' + recLen;
if (recLen.length !== 4) return;
if (size === '0000') size = recLen;
}
inner = '82' + (record ? '04' : '02') + fd.toString(16).padStart(2, '0').toUpperCase() + '21' + (record ? recLen : '');
inner += '83' + '02' + fid;
inner += '8A' + '01' + (f.lcsi || '05').replace(/[^0-9a-fA-F]/g, '').padStart(2, '0').slice(0, 2).toUpperCase();
inner += '8C' + '02' + '7F00';
inner += '80' + '02' + size.padStart(4, '0').slice(-4);
if (st === 'ber_tlv') inner += 'A5' + '00';
} else {
inner = '83' + '02' + fid + '80' + '02' + size.padStart(4, '0').slice(-4);
}
f.fcp = '62' + berLenStr(inner.length / 2) + inner;
chainRender(chainId);
}
// ===== RAM/GP hex builder =====
// PUSH command data field (TS 102 226 9.2.1-9.2.4):
// 01 BIP channel opening - OPEN CHANNEL COMPREHENSION-TLVs (optional)
// 02 CAT_TP link - transport level 3C (protocol type 00) plus
// optional buffer size 39 / channel data 36
// 03 TCP connection - bearer 35, transport level 3C (protocol type 02,
// TCP client remote), destination 3E, NAA 47
// 04 identification packet - channel data 36 (optional)
function chainPushData(f) {
function hx(s) { return (s || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); }
function port(v) { var h = hx(v); return h.length === 4 ? h : ''; }
var pt = f.pushType || '01';
if (pt === '01') return hx(f.pushData);
if (pt === '04') {
var id4 = hx(f.pushIdent);
return id4 ? '36' + berLenStr(id4.length / 2) + id4 : '';
}
if (pt === '02') {
var catPort = port(f.pushPort);
if (!catPort) return '';
var out2 = '3C' + '03' + '00' + catPort;
var sdu = port(f.pushSdu);
if (sdu) out2 += '39' + '02' + sdu;
var id2 = hx(f.pushIdent);
if (id2) out2 += '36' + berLenStr(id2.length / 2) + id2;
return out2 + hx(f.pushExtra);
}
var out3 = hx(f.pushBearer) || '350103';
out3 += '3C' + '03' + '02' + (port(f.pushPort) || '0000');
var addr = hx(f.pushAddr);
if (addr) out3 += '3E' + berLenStr(addr.length / 2) + addr;
var apn = _hotaAsciiHex((f.pushApn || '').trim());
if (apn) out3 += '47' + berLenStr(apn.length / 2) + apn;
return out3 + hx(f.pushExtra);
}
function chainRamBuildRowHex(idx, row) {
var f = row.fields;
var cmd = row.cmd;
@@ -3081,6 +3419,17 @@ function chainRamBuildRowHex(idx, row) {
var dl9 = berLenStr(ramData4.length / 2);
return buildApdu(0x00, 0x88, 0x00, 0x00, parseInt(dl9, 16), ramData4) + '00';
}
if (cmd === 'put-key') {
var pkData = (f.data || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
if (!pkData) return '';
return buildApdu(0x80, 0xD0, parseInt(f.keyVer || '01', 16), parseInt(f.keyId || '01', 16), pkData.length / 2, pkData);
}
if (cmd === 'push') {
var pushData = chainPushData(f);
var pushP2 = parseInt(f.pushType || '01', 16);
if (!pushData) return buildApdu(0x80, 0xEC, 0x01, pushP2, null, null);
return buildApdu(0x80, 0xEC, 0x01, pushP2, pushData.length / 2, pushData);
}
if (cmd === 'get-response') {
var le = parseInt(f.le, 16) || 0;
return buildApdu(0x80, 0xC0, 0x00, 0x00, le, null);
@@ -3093,7 +3442,8 @@ function chainBuildRowHex(chainId, idx) {
chainInit(chainId);
var row = _chains[chainId].rows[idx];
if (!row) return '';
if (chainId === 'chain-sim' || chainId === 'chain-usim') {
const kind = chainKind(chainId);
if (kind === 'sim' || kind === 'usim') {
return chainSimBuildRowHex(chainId, idx, row);
}
return chainRamBuildRowHex(idx, row);
@@ -3184,18 +3534,47 @@ function genConvert() {
}
// ===== Secured Packet UI =====
function chainRamApduList() {
// One hex APDU per chain row (the same rows the preview concatenates).
// One APDU per built command, for consumers outside the compact chain: a row
// that expands to several APDUs (SELECT by FID chain) is split, and GET
// RESPONSE rows are dropped because the expanded format does not use them
// (TS 102 226 5.2.1.1).
function chainApduList(chainId) {
const list = [];
chainInit('chain-ram');
const rows = _chains['chain-ram'].rows;
chainInit(chainId);
const rows = _chains[chainId].rows;
for (let i = 0; i < rows.length; i++) {
const h = chainBuildRowHex('chain-ram', i);
if (h) list.push(h);
if (rows[i].cmd === 'get-response') continue;
const h = chainBuildRowHex(chainId, i);
if (!h) continue;
if (rows[i].cmd === 'select' && (rows[i].fields.method || 'fid') === 'chain') {
parseCompactApdus(h).forEach(function(n) {
if (n.hex && /^APDU/.test(n.label)) list.push(n.hex);
});
} else {
list.push(h);
}
}
return list;
}
function chainRamApduList() {
return chainApduList('chain-ram');
}
// Import a built chain into the Expanded Script builder as C-APDU rows.
function loadExpandedScript(chainId) {
const apdus = chainApduList(chainId);
if (!apdus.length) return;
cApduSwitchSubtab('ber');
for (const hex of apdus) {
const row = addBerRow('c-apdu');
const input = row.querySelector('.ber-hex');
if (input) input.value = hex;
updateBerRow(row.querySelector('.ber-type'));
}
genBerTlv();
}
function ramBuildExpanded() {
// Show each built command in the TS 102 226 Command Scripting (expanded)
// form: AA <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']];
// Stable per-row id for the embedded command chains (row indices are
// renumbered when a row is deleted, the uid is not).
let _berUidSeq = 0;
function addBerRow(type) {
const c = document.getElementById('ber-rows');
const idx = c.children.length;
const row = document.createElement('div');
row.className = 'ber-row border border-gray-300 dark:border-slate-600 rounded p-2 bg-gray-50 dark:bg-slate-800';
row.dataset.berIdx = idx;
row.dataset.berUid = String(++_berUidSeq);
row.innerHTML = berRowHtml(idx, type);
c.appendChild(row);
onBerTypeChange(row.querySelector('.ber-type'));
return row;
}
function berRowHtml(idx, type) {
@@ -3715,7 +4100,17 @@ function onBerTypeChange(sel) {
const body = row.querySelector('.ber-body');
const type = sel.value;
if (type === 'c-apdu') {
body.innerHTML = `<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') {
body.innerHTML = buildScriptChainingRow(row.dataset.berIdx);
} else if (type === 'error') {
@@ -3737,6 +4132,52 @@ function onBerTypeChange(sel) {
updateBerRow(sel);
}
// ===== Expanded Script C-APDU picker (TS 102 226 5.2.1.0/5.2.1.1) =====
// A C-APDU TLV may hold any remote management command; instead of pasting
// hex, the row can host a one-row "virtual" chain built with the SIM RFM /
// USIM RFM / RAM-GP command editors (GET RESPONSE is excluded - the expanded
// format does not use it).
function berCmdChainId(uid, kind) { return 'ber-' + kind + '-' + uid; }
function onBerCmdModeChange(sel) {
const row = sel.closest('.ber-row');
const editor = row.querySelector('.ber-cmd-editor');
const mode = sel.value;
if (mode === 'hex') {
editor.innerHTML = '<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() {
return `<div class="flex gap-2 items-center">
<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);
}
if (type === 'c-apdu') {
const hex = (row.querySelector('.ber-hex').value||'').replace(/[^0-9a-fA-F]/g,'');
const hex = genBerCmdApdu(row);
if (!hex) return '';
return tag + berLenStr(hex.length/2) + hex;
return tag + berLenStr(hex.length / 2) + hex;
}
const st = row.querySelector('.ber-subtype').value;
if (st === 'action') {
@@ -13579,6 +14020,7 @@ const LANG_RU = {
'Queued': 'В очереди',
'Expanded format': 'Развёрнутый формат',
'To expanded': 'В развёрнутый',
'→ Expanded Script': '→ Расширенный скрипт',
'Queue in SCP81': 'В очередь SCP81',
'No commands': 'Нет команд',
'APDUs': 'APDU',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v240';
const CACHE = 'simple-v241';
const URLS = [
'index.html',
'help.html',
+44 -1
View File
@@ -103,7 +103,7 @@ function buildPcHtml(extraFields) {
}
const FNS = ['berLenStr', 'gsm7TextToSeptets', 'gsm7Encode', 'genBerPcValue', 'genErrorActionValue',
'genScriptChainingValue'];
'genScriptChainingValue', 'chainKind', 'chainIsEmbedded', 'chainCommands', 'berCmdChainId', 'genBerCmdApdu', 'genBerRowValue'];
let code = '';
for (const f of FNS) code += extractFunc(html, f) + '\n';
const berTagsMatch = html.match(/const BER_TAGS = \{[^}]*\};/);
@@ -116,6 +116,16 @@ for (const m of [berTagsMatch, berQualMatch, berDevMatch, berTonesMatch, gsm7Alp
if (!m) throw new Error('constant not found');
code = m[0] + '\n' + code;
}
for (const c of ['CHAIN_CMDS_SIM', 'CHAIN_CMDS_USIM', 'CHAIN_CMDS_RAM']) {
const m = html.match(new RegExp('const ' + c + ' = \\[[\\s\\S]*?\\n\\];'));
if (!m) throw new Error('catalog not found: ' + c);
code += m[0].replace('const ', 'var ') + '\n';
}
code += html.match(/const _chains = \{\};/)[0].replace('const ', 'var ') + '\n';
code += html.match(/const CHAIN_LISTENERS = \{\};/)[0].replace('const ', 'var ') + '\n';
// The embedded command chain is built by the shared command builders, which
// are covered by ts102226.test.js; here only the C-APDU row wiring matters.
code += 'function chainBuildRowHex(chainId) { return chainId === \'ber-ram-7\' ? \'80D0010101AA\' : \'\'; }\n';
code += '\nvar __berConsts = {BER_TAGS, BER_QUAL, BER_DEVICES, BER_TONES, GSM7_ALPHABET, GSM7_EXT_MAP};';
eval(code);
const {BER_TAGS, BER_QUAL, BER_DEVICES, BER_TONES, GSM7_ALPHABET, GSM7_EXT_MAP} = __berConsts;
@@ -277,4 +287,37 @@ test('genScriptChainingValue no position emits empty', () => {
keep.name = 'chaining-0-keep';
row.children.push(first, interm, last, keep, el('chaining-script-id'), el('chaining-additional'));
assert.strictEqual(genScriptChainingValue(row, '83'), '');
});
// ===== Expanded Script C-APDU rows (TS 102 226 5.2.1.0/5.2.1.1) =====
function cmdRow(mode, value) {
const row = new StubEl();
row.dataset = { berUid: '7' };
const modeEl = el('ber-cmd-mode', mode);
const hexEl = el('ber-hex', value || '');
const typeEl = el('ber-type', 'c-apdu');
row.children.push(typeEl, modeEl, hexEl);
row.querySelector = sel => ({ '.ber-type': typeEl, '.ber-cmd-mode': modeEl, '.ber-hex': hexEl })[sel] || null;
return row;
}
test('genBerCmdApdu reads pasted hex or the embedded command chain', () => {
assert.strictEqual(genBerCmdApdu(cmdRow('hex', '80 ca ff21 00')), '80CAFF2100');
assert.strictEqual(genBerCmdApdu(cmdRow('ram', 'ignored')), '80D0010101AA');
assert.strictEqual(genBerCmdApdu(cmdRow('hex', '')), '');
});
test('genBerRowValue wraps a C-APDU row in the 22 command TLV', () => {
assert.strictEqual(genBerRowValue(cmdRow('ram', '')), '220680D0010101AA');
assert.strictEqual(genBerRowValue(cmdRow('hex', '80C AFF2100')), '220580CAFF2100');
assert.strictEqual(genBerRowValue(cmdRow('hex', '')), '');
});
test('the embedded pickers offer the RFM/RAM command sets without GET RESPONSE', () => {
assert.ok(chainCommands('ber-sim-1').some(c => c.value === 'select'));
assert.ok(chainCommands('ber-usim-1').some(c => c.value === 'create-file'));
assert.ok(chainCommands('ber-ram-1').some(c => c.value === 'push'));
assert.ok(!chainCommands('ber-ram-1').some(c => c.value === 'get-response'));
assert.ok(chainCommands('chain-ram').some(c => c.value === 'get-response'));
});
+1 -1
View File
@@ -22,7 +22,7 @@ function extractFunc(src, name) {
}
// Extract chain builder functions and dependencies
const FNS = ['buildApdu', 'buildSelect', 'escHtml', 'chainInit', 'chainSimBuildRowHex'];
const FNS = ['buildApdu', 'buildSelect', 'escHtml', 'chainInit', 'chainKind', 'chainSimBuildRowHex'];
let code = '';
for (const f of FNS) {
code += extractFunc(html, f) + '\n';
+247
View File
@@ -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');
});