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:
+19
-7
@@ -74,6 +74,8 @@
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">DISABLE PIN</td><td class="py-1 px-2 font-mono">26</td><td class="py-1 px-2">Отключение PIN</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">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 §9.2.7)</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INCREASE</td><td class="py-1 px-2 font-mono">32</td><td class="py-1 px-2">Прибавить значение к последней увеличенной/обновлённой записи циклического EF (значение 3 байта, TS 151 011 §9.2.8)</td></tr>
|
||||
<tr><td class="py-1 px-2 font-mono">GET RESPONSE</td><td class="py-1 px-2 font-mono">C0</td><td class="py-1 px-2">Получение данных, на которые указывает предшествующий <code class="font-mono text-sm">61XX</code>/<code class="font-mono text-sm">9FXX</code></td></tr>
|
||||
</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 4 командой сразу следует строка GET RESPONSE, сборщик цепочки автоматически убирает её байт Le (ETSI TS 102 226 §5.1.1). В правой колонке — панель конвертации (IMSI, MSISDN, ICCID, SPN, PLMN, nibble swap). См. <a href="#conversion" class="text-blue-600 dark:text-blue-400 hover:underline">§2.5</a>.</p>
|
||||
<p class="text-sm mb-3"><strong>→ Расширенный скрипт</strong> отправляет собранную цепочку в конструктор Expanded Script строками C-APDU (строки GET RESPONSE отбрасываются — расширенный формат их не использует, TS 102 226 §5.2.1.1). Приложение UICC Shared File System RFM (TAR <code class="font-mono text-sm">B00000</code>) доступно и в компактном, и в расширенном формате — если TAR не настроен на автоопределение формата, используются разные значения TAR (TS 102 226 §7.0/§5.3).</p>
|
||||
|
||||
<h3 id="usim-rfm" class="text-lg font-medium mb-2">2.2 USIM RFM</h3>
|
||||
<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 §11.1.7) — простые режимы (P2=<code class="font-mono text-sm">10</code>/<code class="font-mono text-sm">12</code>, P1 = номер записи) и расширенный режим (P2=<code class="font-mono text-sm">18</code>) с 2-байтовым search indication; <strong>INCREASE</strong> (§11.1.8) принимает необязательный SFI и прибавляемое значение.</li>
|
||||
<li><strong>CREATE FILE</strong> / <strong>DELETE FILE</strong> / <strong>RESIZE FILE</strong> (ETSI TS 102 222 §6.3/6.4/6.10) принимают шаблон FCP; кнопка <strong>Build template</strong> собирает структурный каркас (file descriptor, FID, LCSI, размер и разрешающее компактное правило доступа <code class="font-mono text-sm">8C 02 7F 00</code> — проверьте его для своей карты). RESIZE FILE использует CLA <code class="font-mono text-sm">80</code> (TS 102 222 Table 1: <code class="font-mono text-sm">'8X'</code>/<code class="font-mono text-sm">'CX'</code>).</li>
|
||||
<li><strong>SET DATA</strong> / <strong>RETRIEVE DATA</strong> (TS 102 221 §11.3) работают с BER-TLV EF: P2 выбирает первый/следующий/повторный блок (+SFI), поле тега необязательно (<code class="font-mono text-sm">5C</code> — список размещённых тегов).</li>
|
||||
<li>Каждый переход SELECT в цепочке запрашивает FCP, если не отмечен как silent.</li>
|
||||
</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 §5.2.1.0; GET RESPONSE в этом формате не используется, §5.2.1.1)</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Immediate Action</td><td class="py-1 px-2 font-mono">81</td><td class="py-1 px-2">Проактивная команда или action indicator</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">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 §5.2.1.1). Кнопка <strong>→ Расширенный скрипт</strong> в представлениях SIM RFM / USIM RFM / RAM-GP добавляет их команды строками C-APDU.</p>
|
||||
<p class="text-sm mb-3">Только три проактивные команды допустимы как Immediate Action (TS 102 226 Table 5.5): REFRESH, DISPLAY TEXT и PLAY TONE — поведение карты для любой другой проактивной команды не определено.</p>
|
||||
|
||||
<h4 id="ber-error-action" class="font-medium mb-2 text-base">Error Action TLV (Tag 82)</h4>
|
||||
<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 §5.2.1.4). TLV имеет вид <code class="font-mono text-sm">83 01 <флаг></code>, присутствует один раз и только как первый Command TLV; допускается для RFM и RAM:</p>
|
||||
<ul class="text-sm list-disc pl-5 mb-2">
|
||||
<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> — первый скрипт, удалить информацию цепочки при сбросе карты</li>
|
||||
<li><code class="font-mono text-sm">11</code> — первый скрипт, сохранить информацию цепочки при сбросе карты (только RFM)</li>
|
||||
<li><code class="font-mono text-sm">02</code> — последующий скрипт, далее будут ещё</li>
|
||||
<li><code class="font-mono text-sm">03</code> — последующий скрипт, последний</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">§3.1</a>): статус PoR (TAR, счётчик, сырой PoR), а статусное слово и данные ответа последней команды подставляются в подвкладку <strong>«Парсер ответов»</strong> (Remote APDU).</p>
|
||||
@@ -160,6 +169,8 @@
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">GET DATA</td><td class="py-1 px-2 font-mono">CA</td><td class="py-1 px-2 font-mono">tag</td><td class="py-1 px-2">Чтение объектов данных</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">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 §11.8)</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">PUSH</td><td class="py-1 px-2 font-mono">EC</td><td class="py-1 px-2 font-mono">01</td><td class="py-1 px-2">Просьба приложению открыть BIP-канал / CAT_TP-соединение / TCP-соединение или отправить пакет идентификации (TS 102 226 §9)</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">EXTERNAL AUTHENTICATE</td><td class="py-1 px-2 font-mono">82</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Аутентификация SCP</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">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 §8.2.1.3.2.</p>
|
||||
<p class="text-sm mb-3"><strong>PUSH</strong> (TS 102 226 §9.2) — это C-APDU (<code class="font-mono text-sm">80 EC 01 P2</code>), адресованный приложению, поддерживающему BIP и/или CAT_TP (не триггер HTTP-администрирования — тот собирается в конструкторе HTTP OTA выше). P2: <code class="font-mono text-sm">01</code> открытие BIP-канала (COMPREHENSION-TLV OPEN CHANNEL, необязательно), <code class="font-mono text-sm">02</code> CAT_TP-соединение (порт назначения в transport level <code class="font-mono text-sm">3C</code> с типом протокола 00, необязательные buffer size <code class="font-mono text-sm">39</code> / channel data <code class="font-mono text-sm">36</code>), <code class="font-mono text-sm">03</code> TCP-соединение (bearer <code class="font-mono text-sm">35</code>, transport level <code class="font-mono text-sm">3C</code> с типом протокола 02 = TCP-клиент, удалённое соединение, адрес назначения <code class="font-mono text-sm">3E</code>, NAA/APN <code class="font-mono text-sm">47</code>), <code class="font-mono text-sm">04</code> пакет идентификации (channel data <code class="font-mono text-sm">36</code>; при отсутствии используется ICCID). Проактивную команду OPEN CHANNEL выдаёт само приложение; успех — <code class="font-mono text-sm">90 00</code>, отказ — <code class="font-mono text-sm">6F 00</code> с Result TLV в данных ответа, некорректные данные отклоняются с <code class="font-mono text-sm">6A 80</code>. P1 <code class="font-mono text-sm">80</code> зарезервирован для приложения.</p>
|
||||
|
||||
<h3 id="conversion" class="text-lg font-medium mb-2">2.5 Конвертация (боковые панели SIM/USIM)</h3>
|
||||
<ul class="list-disc list-inside text-sm space-y-1">
|
||||
|
||||
+19
-8
@@ -74,6 +74,8 @@
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">DISABLE PIN</td><td class="py-1 px-2 font-mono">26</td><td class="py-1 px-2">Disable a PIN</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">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 §9.2.7)</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INCREASE</td><td class="py-1 px-2 font-mono">32</td><td class="py-1 px-2">Add a value to the last increased/updated record of a cyclic EF (3-byte value, TS 151 011 §9.2.8)</td></tr>
|
||||
<tr><td class="py-1 px-2 font-mono">GET RESPONSE</td><td class="py-1 px-2 font-mono">C0</td><td class="py-1 px-2">Fetch data indicated by a preceding <code class="font-mono text-sm">61XX</code>/<code class="font-mono text-sm">9FXX</code> status word</td></tr>
|
||||
</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 4 command is immediately followed by a GET RESPONSE row, the chain builder strips its trailing Le byte automatically (ETSI TS 102 226 §5.1.1). A conversion panel is embedded in the right column (IMSI, MSISDN, ICCID, SPN, PLMN, nibble swap). See <a href="#conversion" class="text-blue-600 dark:text-blue-400 hover:underline">§2.5</a>.</p>
|
||||
<p class="text-sm mb-3"><strong>→ Expanded Script</strong> sends the built chain to the Expanded Script builder as C-APDU rows (GET RESPONSE hops are dropped — the expanded format does not use them, TS 102 226 §5.2.1.1). The UICC Shared File System RFM application (TAR <code class="font-mono text-sm">B00000</code>) is reachable with either the compact or the expanded format — unless the TAR is configured for automatic format detection, the two use different TAR values (TS 102 226 §7.0/§5.3).</p>
|
||||
|
||||
<h3 id="usim-rfm" class="text-lg font-medium mb-2">2.2 USIM RFM</h3>
|
||||
<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 §11.1.7) offers the simple modes (P2=<code class="font-mono text-sm">10</code>/<code class="font-mono text-sm">12</code>, P1 = record number) and the enhanced mode (P2=<code class="font-mono text-sm">18</code>) with the 2-byte search indication; <strong>INCREASE</strong> (§11.1.8) takes an optional SFI and the value to add.</li>
|
||||
<li><strong>CREATE FILE</strong> / <strong>DELETE FILE</strong> / <strong>RESIZE FILE</strong> (ETSI TS 102 222 §6.3/6.4/6.10) take an FCP template; <strong>Build template</strong> composes a structural skeleton (file descriptor, FID, LCSI, size, and a permissive compact access rule <code class="font-mono text-sm">8C 02 7F 00</code> — review it for your card). RESIZE FILE uses CLA <code class="font-mono text-sm">80</code> (TS 102 222 Table 1: <code class="font-mono text-sm">'8X'</code>/<code class="font-mono text-sm">'CX'</code>).</li>
|
||||
<li><strong>SET DATA</strong> / <strong>RETRIEVE DATA</strong> (TS 102 221 §11.3) operate on BER-TLV EFs: P2 selects first/next/retransmit block (+SFI), the tag field is optional (<code class="font-mono text-sm">5C</code> lists the allocated tags).</li>
|
||||
<li><strong>Pack into Secured packet</strong> sets the SCP80 TAR to the selected preset’s <strong>ADF RFM TAR</strong> (<code class="font-mono text-sm">B00001</code> by default), linked to the ADF’s AID (ADF.USIM).</li>
|
||||
<li>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 §5.2.1.0; GET RESPONSE is not used in this format, §5.2.1.1)</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Immediate Action</td><td class="py-1 px-2 font-mono">81</td><td class="py-1 px-2">Proactive command or action indicator</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">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 — the resulting APDU is wrapped in the <code class="font-mono text-sm">22</code> TLV and shown next to the picker. GET RESPONSE is not offered there: the expanded format carries no GET RESPONSE (TS 102 226 §5.2.1.1). <strong>→ Expanded Script</strong> in the SIM RFM / USIM RFM / RAM-GP views appends their built commands as C-APDU rows.</p>
|
||||
<p class="text-sm mb-3">The Immediate Action builder offers an action indicator (<code class="font-mono text-sm">81</code>/<code class="font-mono text-sm">82</code>), a structured proactive command builder (REFRESH, DISPLAY TEXT, PLAY TONE with auto-generated COMPREHENSION-TLV objects), or a freeform hex input. Only those three proactive commands are allowed as an Immediate Action (TS 102 226 Table 5.5) — the behaviour of the card for any other proactive command is undefined.</p>
|
||||
|
||||
<h4 id="ber-error-action" class="font-medium mb-2 text-base">Error Action TLV (Tag 82)</h4>
|
||||
<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 §5.2.1.4). The TLV is <code class="font-mono text-sm">83 01 <flag></code>, present once and only as the first Command TLV; it may be used for RFM and RAM only:</p>
|
||||
<ul class="text-sm list-disc pl-5 mb-2">
|
||||
<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> — first script, delete chaining information upon card reset</li>
|
||||
<li><code class="font-mono text-sm">11</code> — first script, keep chaining information across card reset (RFM only)</li>
|
||||
<li><code class="font-mono text-sm">02</code> — subsequent script, more to follow</li>
|
||||
<li><code class="font-mono text-sm">03</code> — 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">§3.1</a>): the PoR status (TAR, counter, raw PoR), with the last command’s status word and response data filled into the <strong>Response parser</strong> pill under Remote APDU.</p>
|
||||
@@ -160,6 +168,8 @@
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">GET DATA</td><td class="py-1 px-2 font-mono">CA</td><td class="py-1 px-2 font-mono">tag</td><td class="py-1 px-2">Read card data objects</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">STORE DATA</td><td class="py-1 px-2 font-mono">E2</td><td class="py-1 px-2 font-mono">00/40/80/C0/E0</td><td class="py-1 px-2">Store data (key, certificate, …)</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">SET STATUS</td><td class="py-1 px-2 font-mono">F0</td><td class="py-1 px-2 font-mono">80/40/60</td><td class="py-1 px-2">Lifecycle state management</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">PUT KEY</td><td class="py-1 px-2 font-mono">D0</td><td class="py-1 px-2 font-mono">key ver</td><td class="py-1 px-2">Replace a key: P1 = key version, P2 = key identifier, data = new key material (GP Card Spec §11.8)</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">PUSH</td><td class="py-1 px-2 font-mono">EC</td><td class="py-1 px-2 font-mono">01</td><td class="py-1 px-2">Ask an application to open a BIP channel / CAT_TP link / TCP connection or send an identification packet (TS 102 226 §9)</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">EXTERNAL AUTHENTICATE</td><td class="py-1 px-2 font-mono">82</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">SCP host authentication</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">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 §8.2.1.3.2 for the full GET STATUS P1/P2, GET DATA tag, DELETE P1, STORE DATA P1, and SET STATUS tables.</p>
|
||||
<p class="text-sm mb-3"><strong>PUSH</strong> (TS 102 226 §9.2) is a C-APDU (<code class="font-mono text-sm">80 EC 01 P2</code>) addressed to an application that supports BIP and/or CAT_TP — not the HTTP administration trigger (that is the HTTP OTA builder above). P2: <code class="font-mono text-sm">01</code> BIP channel opening (OPEN CHANNEL COMPREHENSION-TLVs, optional), <code class="font-mono text-sm">02</code> CAT_TP link (destination port in transport level <code class="font-mono text-sm">3C</code> with protocol type 00, optional buffer size <code class="font-mono text-sm">39</code> / channel data <code class="font-mono text-sm">36</code>), <code class="font-mono text-sm">03</code> TCP connection (bearer <code class="font-mono text-sm">35</code>, transport level <code class="font-mono text-sm">3C</code> with protocol type 02 = TCP client remote, destination address <code class="font-mono text-sm">3E</code>, NAA/APN <code class="font-mono text-sm">47</code>), <code class="font-mono text-sm">04</code> identification packet (channel data <code class="font-mono text-sm">36</code>; the ICCID is used when absent). The application issues the proactive OPEN CHANNEL itself; success is <code class="font-mono text-sm">90 00</code>, failure <code class="font-mono text-sm">6F 00</code> with the Result TLV in the response data, and malformed data is rejected with <code class="font-mono text-sm">6A 80</code>. P1 <code class="font-mono text-sm">80</code> is reserved for application-specific usage.</p>
|
||||
|
||||
<h3 id="conversion" class="text-lg font-medium mb-2">2.5 Conversion (SIM/USIM sidebars)</h3>
|
||||
<ul class="list-disc list-inside text-sm space-y-1">
|
||||
|
||||
+486
-44
@@ -74,10 +74,13 @@
|
||||
<button onclick="chainAddRow('chain-sim','disable')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ DISABLE PIN</button>
|
||||
<button onclick="chainAddRow('chain-sim','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,'&').replace(/</g,'<'
|
||||
// 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 — forward from P1</option>' +
|
||||
'<option value="12"' + (srMode === '12' ? ' selected' : '') + '>12 — backward from P1</option>' +
|
||||
'<option value="18"' + (srMode === '18' ? ' selected' : '') + '>18 — enhanced search</option>' +
|
||||
'<option value="1C"' + (srMode === '1C' ? ' selected' : '') + '>1C — proprietary search</option></select></div>' +
|
||||
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Record (P1, 00 = current)</label>' +
|
||||
'<input value="' + escHtml(f.srRec || '00') + '" oninput="' + uf('srRec') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="2"></div>' +
|
||||
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Le</label>' +
|
||||
'<input value="' + escHtml(f.le || '00') + '" oninput="' + uf('le') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="2" placeholder="00"></div></div>';
|
||||
if (srMode === '18') {
|
||||
htmlSr += '<div class="mb-2"><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Search indication (2 bytes, table 11.13)</label>' +
|
||||
'<input value="' + escHtml(f.srInd || '1800') + '" oninput="' + uf('srInd') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="4"></div>';
|
||||
}
|
||||
htmlSr += '<div class="mb-2"><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Search string (hex)</label>' +
|
||||
'<input value="' + escHtml(f.srPattern || '') + '" oninput="' + uf('srPattern') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800"></div>';
|
||||
return htmlSr;
|
||||
}
|
||||
return '<div class="grid grid-cols-3 gap-2">' +
|
||||
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Type/mode (P2)</label>' +
|
||||
'<select onchange="' + uf('srMode') + '" class="w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1 dark:bg-slate-800">' +
|
||||
'<option value="00"' + (srMode === '00' ? ' selected' : '') + '>00 — type 1, from start</option>' +
|
||||
'<option value="01"' + (srMode === '01' ? ' selected' : '') + '>01 — type 1, backward</option>' +
|
||||
'<option value="02"' + (srMode === '02' ? ' selected' : '') + '>02 — type 1, next forward</option>' +
|
||||
'<option value="03"' + (srMode === '03' ? ' selected' : '') + '>03 — type 1, previous</option>' +
|
||||
'<option value="10"' + (srMode === '10' ? ' selected' : '') + '>10 — type 2, from start</option>' +
|
||||
'<option value="11"' + (srMode === '11' ? ' selected' : '') + '>11 — type 2, backward</option>' +
|
||||
'<option value="12"' + (srMode === '12' ? ' selected' : '') + '>12 — type 2, next forward</option>' +
|
||||
'<option value="13"' + (srMode === '13' ? ' selected' : '') + '>13 — type 2, previous</option></select></div>' +
|
||||
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Pattern (1-16 bytes hex)</label>' +
|
||||
'<input value="' + escHtml(f.srPattern || '') + '" oninput="' + uf('srPattern') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800"></div>' +
|
||||
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Le (empty for type 1)</label>' +
|
||||
'<input value="' + escHtml(f.le || '') + '" oninput="' + uf('le') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="2"></div></div>';
|
||||
}
|
||||
if (cmd === 'increase') {
|
||||
var inc = '<div class="grid grid-cols-3 gap-2">';
|
||||
if (usim) {
|
||||
inc += '<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">SFI (00 = current EF)</label>' +
|
||||
'<input value="' + escHtml(f.sfi || '00') + '" oninput="' + uf('sfi') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="2"></div>';
|
||||
}
|
||||
inc += '<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Value to add (hex)</label>' +
|
||||
'<input value="' + escHtml(f.value || (usim ? '' : '000001')) + '" oninput="' + uf('value') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800"></div>' +
|
||||
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Le' + (usim ? '' : ' (empty = case 3)') + '</label>' +
|
||||
'<input value="' + escHtml(f.le || (usim ? '00' : '')) + '" oninput="' + uf('le') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="2"></div></div>';
|
||||
return inc;
|
||||
}
|
||||
if (cmd === 'create-file' || cmd === 'resize-file') {
|
||||
var isCreate = cmd === 'create-file';
|
||||
var st = f.structure || 'transparent';
|
||||
var htmlFcp = '<div class="grid grid-cols-3 gap-2 mb-2">' +
|
||||
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">FID (2 bytes)</label>' +
|
||||
'<input value="' + escHtml(f.fid || '') + '" oninput="' + uf('fid') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="4" placeholder="6F00"></div>' +
|
||||
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">File size (hex bytes)</label>' +
|
||||
'<input value="' + escHtml(f.fileSize || '') + '" oninput="' + uf('fileSize') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="4" placeholder="0010"></div>' +
|
||||
(isCreate ?
|
||||
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Structure</label>' +
|
||||
'<select onchange="' + uf('structure') + '" class="w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1 dark:bg-slate-800">' +
|
||||
'<option value="transparent"' + (st === 'transparent' ? ' selected' : '') + '>Transparent EF</option>' +
|
||||
'<option value="linear_fixed"' + (st === 'linear_fixed' ? ' selected' : '') + '>Linear fixed EF</option>' +
|
||||
'<option value="cyclic"' + (st === 'cyclic' ? ' selected' : '') + '>Cyclic EF</option>' +
|
||||
'<option value="ber_tlv"' + (st === 'ber_tlv' ? ' selected' : '') + '>BER-TLV EF</option></select></div>'
|
||||
:
|
||||
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Mode (P1)</label>' +
|
||||
'<select onchange="' + uf('mode') + '" class="w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1 dark:bg-slate-800">' +
|
||||
'<option value="00"' + ((f.mode || '00') === '00' ? ' selected' : '') + '>00 — mode 0</option>' +
|
||||
'<option value="01"' + (f.mode === '01' ? ' selected' : '') + '>01 — mode 1</option></select></div>') +
|
||||
'</div>';
|
||||
if (isCreate) {
|
||||
htmlFcp += '<div class="grid grid-cols-3 gap-2 mb-2">' +
|
||||
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Record length (hex, record files)</label>' +
|
||||
'<input value="' + escHtml(f.recLen || '') + '" oninput="' + uf('recLen') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="4" placeholder="006E"></div>' +
|
||||
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">LCSI</label>' +
|
||||
'<input value="' + escHtml(f.lcsi || '05') + '" oninput="' + uf('lcsi') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="2"></div>' +
|
||||
'<div class="flex items-end pb-1"><label class="flex items-center gap-1 text-xs"><input type="checkbox"' + (f.shareable ? ' checked' : '') +
|
||||
' onchange="chainUpdateField(\'' + chainId + '\',' + idx + ',\'shareable\',this.checked)" class="rounded"> Shareable</label></div></div>';
|
||||
}
|
||||
htmlFcp += '<div class="mb-2"><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">FCP template (hex)</label>' +
|
||||
'<textarea oninput="' + uf('fcp') + '" rows="2" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800">' + escHtml(f.fcp || '') + '</textarea></div>' +
|
||||
'<button type="button" onclick="chainBuildFcp(\'' + chainId + '\',' + idx + ',\'' + (isCreate ? 'create' : 'resize') + '\')" class="text-xs px-2 py-1 bg-slate-600 text-white rounded hover:bg-slate-700">Build template</button>';
|
||||
return htmlFcp;
|
||||
}
|
||||
if (cmd === 'delete-file') {
|
||||
return '<div class="mb-2"><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">FID (2 bytes)</label>' +
|
||||
'<input value="' + escHtml(f.fid || '') + '" oninput="' + uf('fid') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="4" placeholder="6F00"></div>';
|
||||
}
|
||||
if (cmd === 'set-data' || cmd === 'retrieve-data') {
|
||||
var sdMode = f.sdMode || '80';
|
||||
var htmlSd = '<div class="grid grid-cols-3 gap-2 mb-2">' +
|
||||
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">P2 (block)</label>' +
|
||||
'<select onchange="' + uf('sdMode') + '" class="w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1 dark:bg-slate-800">' +
|
||||
'<option value="80"' + (sdMode === '80' ? ' selected' : '') + '>80 — first block</option>' +
|
||||
'<option value="00"' + (sdMode === '00' ? ' selected' : '') + '>00 — next block</option>' +
|
||||
'<option value="40"' + (sdMode === '40' ? ' selected' : '') + '>40 — retransmit previous</option></select></div>' +
|
||||
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">SFI (00 = current EF)</label>' +
|
||||
'<input value="' + escHtml(f.sfi || '00') + '" oninput="' + uf('sfi') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="2"></div>' +
|
||||
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' + (cmd === 'set-data' ? 'Data (BER-TLV hex)' : 'Tag (hex, 5C = tag list)') + '</label>' +
|
||||
'<input value="' + escHtml(cmd === 'set-data' ? (f.value || '') : (f.tag || '')) + '" oninput="' + uf(cmd === 'set-data' ? 'value' : 'tag') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800"></div></div>';
|
||||
if (cmd === 'retrieve-data') {
|
||||
htmlSd += '<div class="mb-2"><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Le</label>' +
|
||||
'<input value="' + escHtml(f.le || '00') + '" oninput="' + uf('le') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="2"></div>';
|
||||
}
|
||||
return htmlSd;
|
||||
}
|
||||
if (cmd === 'get-response') {
|
||||
return '<div class="text-xs text-gray-500 dark:text-slate-400 italic">No parameters — CLA=C0 INS=C0 P1=00 P2=00</div>';
|
||||
}
|
||||
@@ -2522,7 +2679,7 @@ function chainSimFieldsHtml(chainId, idx, f, uf) {
|
||||
}
|
||||
|
||||
// ===== RAM/GP field rendering =====
|
||||
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' : '') + '>— select preset —</option>' +
|
||||
'<option value="42"' + (tag === '42' ? ' selected' : '') + '>42 — IIN</option>' +
|
||||
'<option value="45"' + (tag === '45' ? ' selected' : '') + '>45 — CIN</option>' +
|
||||
@@ -2672,6 +2829,51 @@ function chainRamFieldsHtml(idx, row, f, uf) {
|
||||
return '<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Data (hex) — challenge</label>' +
|
||||
'<input value="' + escHtml(f.data || '') + '" oninput="' + uf('data') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="hex"></div>';
|
||||
}
|
||||
if (cmd === 'put-key') {
|
||||
return '<div class="grid grid-cols-2 gap-2 mb-2">' +
|
||||
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Key version (P1, hex)</label>' +
|
||||
'<input value="' + escHtml(f.keyVer || '01') + '" oninput="' + uf('keyVer') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="2"></div>' +
|
||||
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Key identifier (P2, hex)</label>' +
|
||||
'<input value="' + escHtml(f.keyId || '01') + '" oninput="' + uf('keyId') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="2"></div></div>' +
|
||||
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Key data (hex, GP Card Spec 11.8 — new key under the KEK / target key, optional KCV)</label>' +
|
||||
'<textarea oninput="' + uf('data') + '" rows="2" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800">' + escHtml(f.data || '') + '</textarea></div>';
|
||||
}
|
||||
if (cmd === 'push') {
|
||||
var pt = f.pushType || '01';
|
||||
var htmlPush = '<div class="mb-2"><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Request (P2)</label>' +
|
||||
'<select onchange="' + uf('pushType') + ';chainRender(\'' + chainId + '\')" class="w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1 dark:bg-slate-800">' +
|
||||
'<option value="01"' + (pt === '01' ? ' selected' : '') + '>01 — BIP channel opening (TS 102 226 9.2.1)</option>' +
|
||||
'<option value="02"' + (pt === '02' ? ' selected' : '') + '>02 — CAT_TP link establishment (9.2.2)</option>' +
|
||||
'<option value="03"' + (pt === '03' ? ' selected' : '') + '>03 — TCP connection (9.2.3)</option>' +
|
||||
'<option value="04"' + (pt === '04' ? ' selected' : '') + '>04 — identification packet (9.2.4)</option></select></div>';
|
||||
if (pt === '02') {
|
||||
htmlPush += '<div class="grid grid-cols-3 gap-2 mb-2">' +
|
||||
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Destination port (hex)</label>' +
|
||||
'<input value="' + escHtml(f.pushPort || '') + '" oninput="' + uf('pushPort') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="4" placeholder="1F90"></div>' +
|
||||
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Max SDU (hex, optional)</label>' +
|
||||
'<input value="' + escHtml(f.pushSdu || '') + '" oninput="' + uf('pushSdu') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="4"></div>' +
|
||||
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Identification data (hex, optional)</label>' +
|
||||
'<input value="' + escHtml(f.pushIdent || '') + '" oninput="' + uf('pushIdent') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800"></div></div>';
|
||||
} else if (pt === '03') {
|
||||
htmlPush += '<div class="grid grid-cols-2 gap-2 mb-2">' +
|
||||
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Destination port (hex)</label>' +
|
||||
'<input value="' + escHtml(f.pushPort || '') + '" oninput="' + uf('pushPort') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" maxlength="4" placeholder="0050"></div>' +
|
||||
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Bearer description (hex, empty = 350103 default bearer)</label>' +
|
||||
'<input value="' + escHtml(f.pushBearer || '') + '" oninput="' + uf('pushBearer') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="350103"></div>' +
|
||||
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Destination address value (hex: 21+IPv4 / 57+IPv6 / F0+FQDN)</label>' +
|
||||
'<input value="' + escHtml(f.pushAddr || '') + '" oninput="' + uf('pushAddr') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="210A000001"></div>' +
|
||||
'<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Network access name / APN (text)</label>' +
|
||||
'<input value="' + escHtml(f.pushApn || '') + '" oninput="' + uf('pushApn') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800"></div></div>';
|
||||
} else if (pt === '04') {
|
||||
htmlPush += '<div class="mb-2"><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">Identification data (hex, optional — ICCID is used when absent)</label>' +
|
||||
'<input value="' + escHtml(f.pushIdent || '') + '" oninput="' + uf('pushIdent') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800"></div>';
|
||||
}
|
||||
var extraField = pt === '01' ? 'pushData' : 'pushExtra';
|
||||
htmlPush += '<div class="mb-2"><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' +
|
||||
(pt === '01' ? 'OPEN CHANNEL COMPREHENSION-TLVs (hex, optional)' : 'Extra TLVs (hex, optional)') + '</label>' +
|
||||
'<textarea oninput="' + uf(extraField) + '" rows="2" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800">' + escHtml(f[extraField] || '') + '</textarea></div>';
|
||||
return htmlPush;
|
||||
}
|
||||
if (cmd === 'get-response') {
|
||||
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
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v240';
|
||||
const CACHE = 'simple-v241';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -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'));
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
Reference in New Issue
Block a user