Compare commits
5 Commits
f7b826b243
...
9d7fa11871
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d7fa11871 | |||
| 7c1d623ca2 | |||
| 7388f39bd6 | |||
| 201b0df278 | |||
| 1db6c12d52 |
+66
-43
@@ -42,16 +42,17 @@
|
||||
|
||||
<section class="mb-10">
|
||||
<h2 id="c-apdu" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">2. Вкладка C-APDU</h2>
|
||||
<p class="mb-3">Построение командных APDU (C-APDU). Четыре подвкладки охватывают разные поколения карт и наборы команд.</p>
|
||||
<p class="mb-3">Построение командных APDU (C-APDU). Пять подвкладок охватывают разные поколения карт и наборы команд: <strong>SIM RFM</strong>, <strong>USIM RFM</strong>, <strong>Expanded Script</strong>, <strong>RAM/GP</strong> и <strong>C-APDU Parser</strong>.</p>
|
||||
|
||||
<h3 id="sim-rfm" class="text-lg font-medium mb-2">2.1 SIM RFM</h3>
|
||||
<p class="mb-2">CLA = <code class="font-mono text-sm">A0</code> (GSM 11.11 / TS 151 011, ISO 7816-4). Удалённое управление файлами классических SIM-карт.</p>
|
||||
<p class="mb-2">Команды собираются в виде <strong>цепочки</strong>: нажмите кнопку <code class="font-mono text-sm">+ Command</code>, чтобы добавить строку, заполните её поля — предпросмотр цепочки (над кнопкой упаковки) обновится автоматически. Добавьте строку <strong>GET RESPONSE</strong>, чтобы получить данные после SELECT. Кнопка <strong>Pack into Secured packet</strong> упаковывает всю цепочку в пакет SCP80.</p>
|
||||
<table class="w-full text-sm mb-3 border-collapse">
|
||||
<thead><tr class="border-b border-gray-300 dark:border-slate-700">
|
||||
<th class="text-left py-1 px-2">Команда</th><th class="text-left py-1 px-2">INS</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 font-mono">SELECT</td><td class="py-1 px-2 font-mono">A4</td><td class="py-1 px-2">Выбор EF/DF по FID, пути, DF name или цепочке</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">SELECT</td><td class="py-1 px-2 font-mono">A4</td><td class="py-1 px-2">Выбор EF/DF по FID, пути, AID или цепочке</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">UPDATE RECORD</td><td class="py-1 px-2 font-mono">DC</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">UPDATE BINARY</td><td class="py-1 px-2 font-mono">D6</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">READ RECORD</td><td class="py-1 px-2 font-mono">B2</td><td class="py-1 px-2">Чтение записи</td></tr>
|
||||
@@ -60,7 +61,11 @@
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">ACTIVATE FILE</td><td class="py-1 px-2 font-mono">44</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">DEACTIVATE FILE</td><td class="py-1 px-2 font-mono">04</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">VERIFY PIN</td><td class="py-1 px-2 font-mono">20</td><td class="py-1 px-2">Проверка PIN1 или PIN2</td></tr>
|
||||
<tr><td class="py-1 px-2 font-mono">CHANGE PIN</td><td class="py-1 px-2 font-mono">24</td><td class="py-1 px-2">Смена PIN1 или PIN2</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CHANGE PIN</td><td class="py-1 px-2 font-mono">24</td><td class="py-1 px-2">Смена PIN1 или PIN2</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">DISABLE PIN</td><td class="py-1 px-2 font-mono">26</td><td class="py-1 px-2">Отключение PIN</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">ENABLE PIN</td><td class="py-1 px-2 font-mono">28</td><td class="py-1 px-2">Включение PIN</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">UNBLOCK PIN</td><td class="py-1 px-2 font-mono">2C</td><td class="py-1 px-2">Разблокировка PIN с помощью PUK</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>
|
||||
<h4 id="sim-select-methods" class="font-medium mb-1">Методы SELECT</h4>
|
||||
@@ -72,20 +77,18 @@
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">По FID</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">FID (4 hex)</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">По полному пути от MF</td><td class="py-1 px-2 font-mono">08</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Полный путь от MF</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">По DF name / AID</td><td class="py-1 px-2 font-mono">04</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">AID</td></tr>
|
||||
<tr><td class="py-1 px-2">Цепочка ADF RFM</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">FID через запятую</td></tr>
|
||||
<tr><td class="py-1 px-2">Цепочка</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">FID через запятую; токен <code class="font-mono text-sm">C0</code> (или <code class="font-mono text-sm">C0:NN</code>) вставляет GET RESPONSE</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4 id="sim-options" class="font-medium mb-1">Опции</h4>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>Start with SELECT</strong> — добавить SELECT перед операцией.</li>
|
||||
<li><strong>Selection mode (P2)</strong> — для record-команд: Absolute (04), Next (06), Previous (02).</li>
|
||||
<li><strong>Record size</strong> — дополнить/обрезать данные до указанного размера.</li>
|
||||
<li><strong>Allow P1/P2 editing</strong> — ручное редактирование P1/P2.</li>
|
||||
</ul>
|
||||
<p class="text-sm mb-3">В правой колонке — панель конвертации (IMSI, MSISDN, ICCID, SPN, PLMN, nibble swap). См. <a href="#conversion" class="text-blue-600 dark:text-blue-400 hover:underline">§2.5</a>.</p>
|
||||
<p class="text-sm mb-3">Для record-команд режим P2: <strong>Absolute (04)</strong>, <strong>Next (02)</strong> или <strong>Previous (03)</strong>. Если за Case 4 командой сразу следует строка GET RESPONSE, сборщик цепочки автоматически убирает её байт Le (ETSI TS 102 226 §5.1.1). В правой колонке — панель конвертации (IMSI, MSISDN, ICCID, SPN, PLMN, nibble swap). См. <a href="#conversion" class="text-blue-600 dark:text-blue-400 hover:underline">§2.5</a>.</p>
|
||||
|
||||
<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, но SELECT использует P1=<code class="font-mono text-sm">09</code>, P2=<code class="font-mono text-sm">0C</code> (выбор по FID из текущего каталога).</p>
|
||||
<p class="mb-2">CLA = <code class="font-mono text-sm">00</code> (ETSI TS 102 221). Тот же сборщик цепочки и набор команд, что и SIM. Отличия:</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>).</li>
|
||||
<li>Каждый переход SELECT в цепочке запрашивает FCP, если не отмечен как silent.</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="ber-tlv" class="text-lg font-medium mb-2">2.3 Expanded Script</h3>
|
||||
<p class="mb-2">Построение формата Expanded Remote Application data по ETSI TS 102 226 §5.2.1.</p>
|
||||
@@ -137,7 +140,7 @@
|
||||
</ul>
|
||||
|
||||
<h3 id="ram-gp" class="text-lg font-medium mb-2">2.4 RAM/GP</h3>
|
||||
<p class="mb-2">CLA = <code class="font-mono text-sm">80</code> (GlobalPlatform Card Specification v2.3.1). Команды удалённого управления приложениями.</p>
|
||||
<p class="mb-2">CLA = <code class="font-mono text-sm">80</code> (GlobalPlatform Card Specification v2.3.1). Команды удалённого управления приложениями. Строятся тем же сборщиком цепочки, что и SIM/USIM.</p>
|
||||
<table class="w-full text-sm mb-3 border-collapse">
|
||||
<thead><tr class="border-b border-gray-300 dark:border-slate-700">
|
||||
<th class="text-left py-1 px-2">Команда</th><th class="text-left py-1 px-2">INS</th><th class="text-left py-1 px-2">P1</th><th class="text-left py-1 px-2">Описание</th>
|
||||
@@ -145,21 +148,22 @@
|
||||
<tbody>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INSTALL [for load]</td><td class="py-1 px-2 font-mono">E6</td><td class="py-1 px-2 font-mono">02</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">INSTALL [for install]</td><td class="py-1 px-2 font-mono">E6</td><td class="py-1 px-2 font-mono">0C</td><td class="py-1 px-2">Установка приложения или SD</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INSTALL [make selectable]</td><td class="py-1 px-2 font-mono">E6</td><td class="py-1 px-2 font-mono">10</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">INSTALL [registry update]</td><td class="py-1 px-2 font-mono">E6</td><td class="py-1 px-2 font-mono">01</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">INSTALL [extradition]</td><td class="py-1 px-2 font-mono">E6</td><td class="py-1 px-2 font-mono">04</td><td class="py-1 px-2">Перемещение между SD</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">LOAD</td><td class="py-1 px-2 font-mono">E8</td><td class="py-1 px-2 font-mono">00</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">DELETE</td><td class="py-1 px-2 font-mono">E4</td><td class="py-1 px-2 font-mono">00/80</td><td class="py-1 px-2">Удаление приложения или SD</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INSTALL [make selectable]</td><td class="py-1 px-2 font-mono">E6</td><td class="py-1 px-2 font-mono">08</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">INSTALL [registry update]</td><td class="py-1 px-2 font-mono">E6</td><td class="py-1 px-2 font-mono">40</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">INSTALL [extradition]</td><td class="py-1 px-2 font-mono">E6</td><td class="py-1 px-2 font-mono">10</td><td class="py-1 px-2">Перемещение между SD</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">LOAD</td><td class="py-1 px-2 font-mono">E8</td><td class="py-1 px-2 font-mono">80</td><td class="py-1 px-2">Загрузка блока кода (P1=80 последний блок, номер блока в P2)</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">DELETE</td><td class="py-1 px-2 font-mono">E4</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Удаление приложения или SD (P2=00 только AID / 80 AID + связанные объекты)</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">GET STATUS</td><td class="py-1 px-2 font-mono">F2</td><td class="py-1 px-2 font-mono">80/40/20/10</td><td class="py-1 px-2">Статус карты</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">GET DATA</td><td class="py-1 px-2 font-mono">CA</td><td class="py-1 px-2 font-mono">tag</td><td class="py-1 px-2">Чтение объектов данных</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">STORE DATA</td><td class="py-1 px-2 font-mono">E2</td><td class="py-1 px-2 font-mono">00/40/80/C0</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">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><td class="py-1 px-2 font-mono">INTERNAL AUTHENTICATE</td><td class="py-1 px-2 font-mono">88</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Challenge-response</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INTERNAL AUTHENTICATE</td><td class="py-1 px-2 font-mono">88</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Challenge-response</td></tr>
|
||||
<tr><td class="py-1 px-2 font-mono">GET RESPONSE</td><td class="py-1 px-2 font-mono">C0</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Получение данных после <code class="font-mono text-sm">61XX</code> (Le настраивается)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4 id="ram-privileges" class="font-medium mb-1">Привилегии (INSTALL [for install])</h4>
|
||||
<p class="text-sm mb-2">Тег <code class="font-mono text-sm">C7</code> в поле данных INSTALL, три байта привилегий (таблицы 11-7/8/9 спецификации GP):</p>
|
||||
<p class="text-sm mb-2">Три байта привилегий (таблицы 11-7/8/9 спецификации GP), кодируются как length-value поле внутри данных INSTALL:</p>
|
||||
<table class="w-full text-sm mb-3 border-collapse">
|
||||
<thead><tr class="border-b border-gray-300 dark:border-slate-700">
|
||||
<th class="text-left py-1 px-2">Бит</th><th class="text-left py-1 px-2">Байт 1</th><th class="text-left py-1 px-2">Байт 2</th><th class="text-left py-1 px-2">Байт 3</th>
|
||||
@@ -199,6 +203,9 @@
|
||||
<li><strong>Nibble swap</strong> — поменять пары полубайтов hex-строки чётной длины.</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="c-apdu-parser" class="text-lg font-medium mb-2">2.6 C-APDU Parser</h3>
|
||||
<p class="text-sm mb-3">Вставка raw APDU hex и отображение сворачиваемого дерева. Автоматически определяет контейнер: <strong>Expanded Script</strong> (начало <code class="font-mono text-sm">AA</code> или <code class="font-mono text-sm">AE80</code>, декодируется по ETSI TS 102 226 §5.2.1) или <strong>Compact C-APDU chain</strong> (последовательность C-APDU ISO 7816). Каждый узел показывает метку, hex и краткое описание; родительские узлы раскрываются в подэлементы.
|
||||
|
||||
|
||||
<section class="mb-10">
|
||||
<h2 id="scp80" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">3. Вкладка SCP80</h2>
|
||||
@@ -275,25 +282,18 @@
|
||||
|
||||
<section class="mb-10">
|
||||
<h2 id="response-parser" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">4. Вкладка Response parser</h2>
|
||||
<p class="mb-3">Второй переключатель вкладки <strong>SCP80</strong>. Хранит предустановки карт локально в браузере (<code class="font-mono text-sm">localStorage</code>), чтобы представление Secured Packet могло автоматически подставлять ключи и параметры.</p>
|
||||
<table class="w-full text-sm mb-3 border-collapse">
|
||||
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><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">Name</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">ICCID</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">KIc / KID</td><td class="py-1 px-2">Индикаторы ключа и алгоритма (например, 15 = индекс 1, 3DES-CBC2; x2 = AES)</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">SPI1 / SPI2</td><td class="py-1 px-2">Security Parameter Indicators</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">TAR</td><td class="py-1 px-2">Toolkit Application Reference</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Counter</td><td class="py-1 px-2">Счётчик повторов (5 байт)</td></tr>
|
||||
<tr><td class="py-1 px-2">KIc key / KID key</td><td class="py-1 px-2">16/24/32 hex-символа (ключи 8/16/24 байта 3DES) или 32/48/64 hex-символа (ключи 16/24/32 байта AES)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-sm mb-3"><strong>Export as JSON</strong> / <strong>Import JSON from clipboard</strong> для обмена предустановками. Выбранная предустановка автоматически заполняет форму Secured Packet.</p>
|
||||
<p class="mb-3">Декодирует raw-ответ команды: выберите отправленную команду, введите SW (например, <code class="font-mono text-sm">9000</code>) и hex данных ответа, затем нажмите <strong>Decode</strong>.</p>
|
||||
<ul class="list-disc list-inside text-sm space-y-1">
|
||||
<li><strong>Команда</strong> — группа SIM/USIM (SELECT, STATUS, READ/UPDATE, PIN-операции, CAT-команды типа TERMINAL PROFILE/ENVELOPE/FETCH/TERMINAL RESPONSE, MANAGE CHANNEL, …) или группа RAM/GP (INSTALL, LOAD, DELETE, GET/STORE DATA, auth, SCP-команды).</li>
|
||||
<li><strong>Декодирование SW</strong> — статусные слова разрешаются по generic-, UICC- (TS 102 221) и GlobalPlatform-таблицам, контекст определяется автоматически.</li>
|
||||
<li><strong>Декодирование привилегий</strong> — ответы GET DATA / INSTALL декодируют байты привилегий в читаемые флаги.</li>
|
||||
<li><strong>Данные ответа</strong> — raw hex отображается и интерпретируется согласно команде (например, FCP-шаблоны SELECT).</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<section class="mb-10">
|
||||
<h2 id="card-reader" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">5. Вкладка Card reader (pySim)</h2>
|
||||
<p class="mb-3">Подключение к локальному <a href="https://github.com/anttro/otaman" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> для работы с картой. Подвкладки: <strong>File manager</strong>, <strong>Custom files</strong>, <strong>pySim command line</strong>, <strong>Raw APDU</strong> и <strong>Proactive UICC</strong>.</p>
|
||||
<p class="mb-3">Подключение к локальному <a href="https://github.com/anttro/otaman" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> для работы с картой. Подвкладки: <strong>File manager</strong>, <strong>Custom files</strong>, <strong>Profiler</strong>, <strong>pySim command line</strong>, <strong>Raw APDU</strong> и <strong>Proactive UICC</strong>.</p>
|
||||
|
||||
<h3 id="file-manager" class="text-lg font-medium mb-2">5.1 File manager</h3>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
@@ -312,9 +312,12 @@
|
||||
<p class="text-sm mb-3">Отправка произвольного APDU и просмотр ответа.</p>
|
||||
|
||||
<h3 id="proactive-uicc" class="text-lg font-medium mb-2">5.5 Proactive UICC</h3>
|
||||
<p class="text-sm mb-3">Работа с сессией Card Application Toolkit: подписанные события, журнал проактивных команд, словарь данных PROVIDE LOCAL INFORMATION и опрос STATUS.</p>
|
||||
<p class="text-sm mb-3">Работа с сессией Card Application Toolkit: меню STK, подписанные события, журнал проактивных команд, словарь данных PROVIDE LOCAL INFORMATION и опрос STATUS.</p>
|
||||
|
||||
<h4 id="subscribed-events" class="font-medium mb-1">5.5.1 Подписанные события (SET UP EVENT LIST)</h4>
|
||||
<h4 id="stk-menu" class="font-medium mb-1">5.5.1 Меню STK</h4>
|
||||
<p class="text-sm mb-3">Если карта выдала команду SET UP MENU, вверху этого представления появляется блок «STK menu» с изумрудной кнопкой <strong>STK: <название></strong>, открывающей оверлей меню (браузер STK-меню карты). Если карта не задала меню, вместо кнопки показывается «No menu set by the card». Состояние меню обновляется при каждом открытии представления.</p>
|
||||
|
||||
<h4 id="subscribed-events" class="font-medium mb-1">5.5.2 Подписанные события (SET UP EVENT LIST)</h4>
|
||||
<p class="text-sm mb-2">События, которые отслеживает карта. У каждого события есть кнопка <strong>Send</strong>, открывающая форму, специфичную для типа события:</p>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>События без данных</strong> (User Activity, Idle Screen, Data Available, …) — уведомление в один клик</li>
|
||||
@@ -324,10 +327,10 @@
|
||||
</ul>
|
||||
<p class="text-sm mb-3">Отправка события использует <code class="font-mono text-sm">ENVELOPE(Event Download)</code> по TS 102 223 / TS 131 111.</p>
|
||||
|
||||
<h4 id="proactive-log" class="font-medium mb-1">5.5.2 Журнал проактивных команд</h4>
|
||||
<h4 id="proactive-log" class="font-medium mb-1">5.5.3 Журнал проактивных команд</h4>
|
||||
<p class="text-sm mb-2">Хронологический список извлечённых проактивных команд. Каждая строка показывает время, код типа, имя и декодированный квалификатор (для команд, у которых он есть). Для команд с данными ответа показывается строка <code class="font-mono text-sm">Response:</code> с байтами TERMINAL RESPONSE (без служебных TLV); ответы PROVIDE LOCAL INFORMATION декодируются через словарь данных PLI.</p>
|
||||
|
||||
<h4 id="pli-dict" class="font-medium mb-1">5.5.3 Словарь данных PROVIDE LOCAL INFORMATION</h4>
|
||||
<h4 id="pli-dict" class="font-medium mb-1">5.5.4 Словарь данных PROVIDE LOCAL INFORMATION</h4>
|
||||
<p class="text-sm mb-2">Редактируемые hex-значения для всех 22 квалификаторов PLI (TS 102 223 §8.6 + TS 131 111). У десяти квалификаторов есть встроенные формы декодирования/кодирования:</p>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>00</strong> Location Info (MCC, MNC, LAC/TAC, Cell ID)</li>
|
||||
@@ -337,10 +340,30 @@
|
||||
</ul>
|
||||
<p class="text-sm mb-3">Значения хранятся на сервере до перезапуска. Когда карта выдаёт PLI, сервер вставляет значения словаря в TERMINAL RESPONSE.</p>
|
||||
|
||||
<h4 id="status-polling" class="font-medium mb-1">5.5.4 Опрос STATUS</h4>
|
||||
<h4 id="status-polling" class="font-medium mb-1">5.5.5 Опрос STATUS</h4>
|
||||
<p class="text-sm mb-3">Кнопка <strong>Send STATUS</strong> отправляет STATUS (F2) вручную. Переключатель <strong>Polling</strong> включает фоновый опрос: после настраиваемого интервала бездействия (аргумент сервера <code class="font-mono text-sm">--poll-interval</code>, 1–255 с, по умолчанию 30 с) сервер отправляет STATUS и обрабатывает любую ожидающую проактивную команду. При извлечении карты опрос останавливается, а состояние карты сбрасывается.</p>
|
||||
|
||||
<h3 id="usage-scenarios" class="text-lg font-medium mb-2">5.6 Сценарии использования</h3>
|
||||
<h3 id="profiler" class="text-lg font-medium mb-2">5.6 Profiler</h3>
|
||||
<p class="text-sm mb-2">Проверяет соответствие карты именованному <strong>профилю</strong> — упорядоченному набору правил, описывающих ожидаемую файловую систему и (опционально) содержимое файлов. Профили хранятся в <code class="font-mono text-sm">localStorage</code>.</p>
|
||||
<h4 class="font-medium mb-1">Список профилей</h4>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>New profile</strong> — создаёт пустой набор правил, запросив имя.</li>
|
||||
<li><strong>Profile from card</strong> — сканирует подключённую карту и создаёт по одному правилу на каждый существующий файл (см. ниже), затем открывает редактор.</li>
|
||||
<li><strong>Import profile</strong> — загружает набор правил из JSON-файла (имя хранится внутри JSON).</li>
|
||||
<li>В каждой строке профиля показаны имя и время создания, а также действия <strong>Edit</strong>, <strong>Check</strong>, <strong>Export</strong> (скачать JSON) и <strong>Delete</strong>.</li>
|
||||
</ul>
|
||||
<h4 class="font-medium mb-1">Правила файловой системы</h4>
|
||||
<p class="text-sm mb-2">Правила выполняются последовательно. Правило файловой системы задаётся:</p>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>Путь</strong> — начинается с <code class="font-mono text-sm">MF</code> (например, <code class="font-mono text-sm">MF/7F10/6F3A</code>) или с AID ADF (например, <code class="font-mono text-sm">A0000000871002/6F07</code>).</li>
|
||||
<li><strong>Атрибуты файла</strong> — тип файла, размер, длина записи и число записей из FCI-шаблона (любой можно оставить незаданным).</li>
|
||||
<li><strong>Содержимое</strong> (опционально) — <strong>Exact</strong> (точное совпадение hex) или <strong>Mask</strong>, где <code class="font-mono text-sm">?</code> — шаблон на один полубайт (маска без <code class="font-mono text-sm">?</code> — совпадение префикса, например <code class="font-mono text-sm">0891</code> для MCC/MNC из IMSI). Для record-файлов хранится список по записям.</li>
|
||||
</ul>
|
||||
<p class="text-sm mb-3"><strong>Check</strong> выполняет каждое правило на подключённой карте и показывает строку прогресса и отчёт прохождения (существование, каждый атрибут FCI и совпадение содержимого).</p>
|
||||
<h4 class="font-medium mb-1">Опции сканирования «Profile from card»</h4>
|
||||
<p class="text-sm mb-2">Диалог сканирования запрашивает имя профиля и предлагает список <strong>«Ignore contents of»</strong> (все отмечены по умолчанию) часто перезаписываемых файлов, содержимое которых пропускается: <code class="font-mono text-sm">EF.LOCI</code>, <code class="font-mono text-sm">EF.PSLOCI</code>, <code class="font-mono text-sm">EF.EPSLOCI</code>, <code class="font-mono text-sm">EF.5GS3GPPLOCI</code>, <code class="font-mono text-sm">EF.Keys</code>, <code class="font-mono text-sm">EF.KeysPS</code>, <code class="font-mono text-sm">EF.SMS</code>, <code class="font-mono text-sm">EF.Kc</code>, <code class="font-mono text-sm">EF.KcGPRS</code>, <code class="font-mono text-sm">EF.LOCIGPRS</code>, <code class="font-mono text-sm">EF.CBMID</code>, <code class="font-mono text-sm">EF.SMSS</code>. Правила создаются только для файлов, которые реально существуют на карте (возвращён FCI-шаблон); отсутствующие файлы пропускаются. Пользовательские файлы из подвкладки <strong>Custom files</strong> включаются с той же проверкой существования.</p>
|
||||
|
||||
<h3 id="usage-scenarios" class="text-lg font-medium mb-2">5.7 Сценарии использования</h3>
|
||||
|
||||
<h4 id="scenario-a" class="font-medium mb-1">Сценарий A — Работа с файлами, не входящими в модель pySim (Custom files)</h4>
|
||||
<ol class="list-decimal list-inside text-sm space-y-1 mb-3">
|
||||
|
||||
+59
-29
@@ -42,16 +42,17 @@
|
||||
|
||||
<section class="mb-10">
|
||||
<h2 id="c-apdu" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">2. C-APDU tab</h2>
|
||||
<p class="mb-3">Builds command APDUs (C-APDUs). Four sub-tabs cover different card generations and command sets.</p>
|
||||
<p class="mb-3">Builds command APDUs (C-APDUs). Five sub-tabs cover different card generations and command sets: <strong>SIM RFM</strong>, <strong>USIM RFM</strong>, <strong>Expanded Script</strong>, <strong>RAM/GP</strong>, and <strong>C-APDU Parser</strong>.</p>
|
||||
|
||||
<h3 id="sim-rfm" class="text-lg font-medium mb-2">2.1 SIM RFM</h3>
|
||||
<p class="mb-2">CLA = <code class="font-mono text-sm">A0</code> (GSM 11.11 / TS 151 011, ISO 7816-4). Remote File Management for classic SIM cards.</p>
|
||||
<p class="mb-2">Commands are assembled as a <strong>chain</strong>: press a <code class="font-mono text-sm">+ Command</code> button to append a row, fill that row’s fields, and the chain preview (above the pack button) updates automatically. Add a <strong>GET RESPONSE</strong> row to fetch data following a SELECT. Press <strong>Pack into Secured packet</strong> to wrap the whole chain into an SCP80 packet.</p>
|
||||
<table class="w-full text-sm mb-3 border-collapse">
|
||||
<thead><tr class="border-b border-gray-300 dark:border-slate-700">
|
||||
<th class="text-left py-1 px-2">Command</th><th class="text-left py-1 px-2">INS</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 font-mono">SELECT</td><td class="py-1 px-2 font-mono">A4</td><td class="py-1 px-2">Select EF/DF by FID, path, DF name or chain</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">SELECT</td><td class="py-1 px-2 font-mono">A4</td><td class="py-1 px-2">Select EF/DF by FID, path, AID or chain</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">UPDATE RECORD</td><td class="py-1 px-2 font-mono">DC</td><td class="py-1 px-2">Update a record in a record-oriented EF</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">UPDATE BINARY</td><td class="py-1 px-2 font-mono">D6</td><td class="py-1 px-2">Update binary content at an offset</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">READ RECORD</td><td class="py-1 px-2 font-mono">B2</td><td class="py-1 px-2">Read a record</td></tr>
|
||||
@@ -60,7 +61,11 @@
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">ACTIVATE FILE</td><td class="py-1 px-2 font-mono">44</td><td class="py-1 px-2">Activate a file</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">DEACTIVATE FILE</td><td class="py-1 px-2 font-mono">04</td><td class="py-1 px-2">Deactivate a file</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">VERIFY PIN</td><td class="py-1 px-2 font-mono">20</td><td class="py-1 px-2">Verify PIN1 or PIN2</td></tr>
|
||||
<tr><td class="py-1 px-2 font-mono">CHANGE PIN</td><td class="py-1 px-2 font-mono">24</td><td class="py-1 px-2">Change PIN1 or PIN2</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CHANGE PIN</td><td class="py-1 px-2 font-mono">24</td><td class="py-1 px-2">Change PIN1 or PIN2</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">DISABLE PIN</td><td class="py-1 px-2 font-mono">26</td><td class="py-1 px-2">Disable a PIN</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">ENABLE PIN</td><td class="py-1 px-2 font-mono">28</td><td class="py-1 px-2">Enable a PIN</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">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><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>
|
||||
<h4 id="sim-select-methods" class="font-medium mb-1">SELECT methods</h4>
|
||||
@@ -72,20 +77,18 @@
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">By FID</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">2-byte FID (4 hex)</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">By full path from MF</td><td class="py-1 px-2 font-mono">08</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Full path hex from MF</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">By DF name / AID</td><td class="py-1 px-2 font-mono">04</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">AID (application ID)</td></tr>
|
||||
<tr><td class="py-1 px-2">ADF RFM chain</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Comma-separated FIDs</td></tr>
|
||||
<tr><td class="py-1 px-2">Chain</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Comma-separated FIDs; a <code class="font-mono text-sm">C0</code> (or <code class="font-mono text-sm">C0:NN</code>) token inserts a GET RESPONSE hop</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4 id="sim-options" class="font-medium mb-1">Options</h4>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>Start with SELECT</strong> — prepend a SELECT before the operation; unchecked sends standalone.</li>
|
||||
<li><strong>Selection mode (P2)</strong> — record commands: Absolute (04), Next (06), Previous (02).</li>
|
||||
<li><strong>Record size</strong> — pad/truncate data to the specified byte count.</li>
|
||||
<li><strong>Allow P1/P2 editing</strong> — manual override of P1/P2 bytes.</li>
|
||||
</ul>
|
||||
<p class="text-sm mb-3">A conversion panel is embedded in the right column (IMSI, MSISDN, ICCID, SPN, PLMN, nibble swap). See <a href="#conversion" class="text-blue-600 dark:text-blue-400 hover:underline">§2.5</a>.</p>
|
||||
<p class="text-sm mb-3">For record commands, the P2 mode is <strong>Absolute (04)</strong>, <strong>Next (02)</strong>, or <strong>Previous (03)</strong>. When a Case 4 command is immediately followed by a GET RESPONSE row, the chain builder strips its trailing Le byte automatically (ETSI TS 102 226 §5.1.1). A conversion panel is embedded in the right column (IMSI, MSISDN, ICCID, SPN, PLMN, nibble swap). See <a href="#conversion" class="text-blue-600 dark:text-blue-400 hover:underline">§2.5</a>.</p>
|
||||
|
||||
<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 commands as SIM, but SELECT uses P1=<code class="font-mono text-sm">09</code>, P2=<code class="font-mono text-sm">0C</code> (select by FID from the current directory).</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. Differences:</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>).</li>
|
||||
<li>Each SELECT hop in a chain requests FCP unless marked silent.</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="ber-tlv" class="text-lg font-medium mb-2">2.3 Expanded Script</h3>
|
||||
<p class="mb-2">Builds the Expanded Remote Application data format per ETSI TS 102 226 §5.2.1.</p>
|
||||
@@ -137,7 +140,7 @@
|
||||
</ul>
|
||||
|
||||
<h3 id="ram-gp" class="text-lg font-medium mb-2">2.4 RAM/GP</h3>
|
||||
<p class="mb-2">CLA = <code class="font-mono text-sm">80</code> (GlobalPlatform Card Specification v2.3.1). Remote Application Management commands for card content management.</p>
|
||||
<p class="mb-2">CLA = <code class="font-mono text-sm">80</code> (GlobalPlatform Card Specification v2.3.1). Remote Application Management commands for card content management. Built with the same chain builder as SIM/USIM: add rows, fill fields, and the chain preview updates automatically.</p>
|
||||
<table class="w-full text-sm mb-3 border-collapse">
|
||||
<thead><tr class="border-b border-gray-300 dark:border-slate-700">
|
||||
<th class="text-left py-1 px-2">Command</th><th class="text-left py-1 px-2">INS</th><th class="text-left py-1 px-2">P1</th><th class="text-left py-1 px-2">Description</th>
|
||||
@@ -145,21 +148,22 @@
|
||||
<tbody>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INSTALL [for load]</td><td class="py-1 px-2 font-mono">E6</td><td class="py-1 px-2 font-mono">02</td><td class="py-1 px-2">Register a load file for loading</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INSTALL [for install]</td><td class="py-1 px-2 font-mono">E6</td><td class="py-1 px-2 font-mono">0C</td><td class="py-1 px-2">Install an application or SD</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INSTALL [make selectable]</td><td class="py-1 px-2 font-mono">E6</td><td class="py-1 px-2 font-mono">10</td><td class="py-1 px-2">Make an application selectable</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INSTALL [registry update]</td><td class="py-1 px-2 font-mono">E6</td><td class="py-1 px-2 font-mono">01</td><td class="py-1 px-2">Update registry entries</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INSTALL [extradition]</td><td class="py-1 px-2 font-mono">E6</td><td class="py-1 px-2 font-mono">04</td><td class="py-1 px-2">Extradition between SDs</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">LOAD</td><td class="py-1 px-2 font-mono">E8</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Load executable code blocks</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">DELETE</td><td class="py-1 px-2 font-mono">E4</td><td class="py-1 px-2 font-mono">00/80</td><td class="py-1 px-2">Delete application or SD</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INSTALL [make selectable]</td><td class="py-1 px-2 font-mono">E6</td><td class="py-1 px-2 font-mono">08</td><td class="py-1 px-2">Make an application selectable</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INSTALL [registry update]</td><td class="py-1 px-2 font-mono">E6</td><td class="py-1 px-2 font-mono">40</td><td class="py-1 px-2">Update registry entries</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INSTALL [extradition]</td><td class="py-1 px-2 font-mono">E6</td><td class="py-1 px-2 font-mono">10</td><td class="py-1 px-2">Extradition between SDs</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">LOAD</td><td class="py-1 px-2 font-mono">E8</td><td class="py-1 px-2 font-mono">80</td><td class="py-1 px-2">Load executable code block (P1=80 last block, block number in P2)</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">DELETE</td><td class="py-1 px-2 font-mono">E4</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Delete application or SD (P2=00 AID only / 80 AID + related objects)</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">GET STATUS</td><td class="py-1 px-2 font-mono">F2</td><td class="py-1 px-2 font-mono">80/40/20/10</td><td class="py-1 px-2">Get card status</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">GET DATA</td><td class="py-1 px-2 font-mono">CA</td><td class="py-1 px-2 font-mono">tag</td><td class="py-1 px-2">Read card data objects</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">STORE DATA</td><td class="py-1 px-2 font-mono">E2</td><td class="py-1 px-2 font-mono">00/40/80/C0</td><td class="py-1 px-2">Store data (key, certificate, …)</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">STORE DATA</td><td class="py-1 px-2 font-mono">E2</td><td class="py-1 px-2 font-mono">00/40/80/C0/E0</td><td class="py-1 px-2">Store data (key, certificate, …)</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">SET STATUS</td><td class="py-1 px-2 font-mono">F0</td><td class="py-1 px-2 font-mono">80/40/60</td><td class="py-1 px-2">Lifecycle state management</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">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><td class="py-1 px-2 font-mono">INTERNAL AUTHENTICATE</td><td class="py-1 px-2 font-mono">88</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Card challenge-response</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">INTERNAL AUTHENTICATE</td><td class="py-1 px-2 font-mono">88</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Card challenge-response</td></tr>
|
||||
<tr><td class="py-1 px-2 font-mono">GET RESPONSE</td><td class="py-1 px-2 font-mono">C0</td><td class="py-1 px-2 font-mono">00</td><td class="py-1 px-2">Fetch data after a <code class="font-mono text-sm">61XX</code> status (Le configurable)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4 id="ram-privileges" class="font-medium mb-1">Privileges (INSTALL [for install])</h4>
|
||||
<p class="text-sm mb-2">Tag <code class="font-mono text-sm">C7</code> in the INSTALL data field, built from 3 privilege bytes (GP spec Tables 11-7/8/9):</p>
|
||||
<p class="text-sm mb-2">Three privilege bytes (GP spec Tables 11-7/8/9), encoded as a length-value field inside the INSTALL data:</p>
|
||||
<table class="w-full text-sm mb-3 border-collapse">
|
||||
<thead><tr class="border-b border-gray-300 dark:border-slate-700">
|
||||
<th class="text-left py-1 px-2">Bit</th><th class="text-left py-1 px-2">Byte 1</th><th class="text-left py-1 px-2">Byte 2</th><th class="text-left py-1 px-2">Byte 3</th>
|
||||
@@ -199,6 +203,9 @@
|
||||
<li><strong>Nibble swap</strong> — swap nibble pairs of an even-length hex string.</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="c-apdu-parser" class="text-lg font-medium mb-2">2.6 C-APDU Parser</h3>
|
||||
<p class="text-sm mb-3">Pastes raw APDU hex and renders a collapsible tree. It auto-detects the container: an <strong>Expanded Script</strong> (leading <code class="font-mono text-sm">AA</code> or <code class="font-mono text-sm">AE80</code>, decoded per ETSI TS 102 226 §5.2.1) or a <strong>Compact C-APDU chain</strong> (a sequence of ISO 7816 C-APDUs). Each node shows its label, hex and a short description; parent nodes expand to reveal their sub-elements.
|
||||
|
||||
|
||||
<section class="mb-10">
|
||||
<h2 id="scp80" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">3. SCP80 tab</h2>
|
||||
@@ -286,7 +293,7 @@
|
||||
|
||||
<section class="mb-10">
|
||||
<h2 id="card-reader" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">5. Card reader (pySim) tab</h2>
|
||||
<p class="mb-3">Connects to a local <a href="https://github.com/anttro/otaman" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> for live card operations. Sub-tabs: <strong>File manager</strong>, <strong>Custom files</strong>, <strong>pySim command line</strong>, <strong>Raw APDU</strong>, and <strong>Proactive UICC</strong>.</p>
|
||||
<p class="mb-3">Connects to a local <a href="https://github.com/anttro/otaman" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> for live card operations. Sub-tabs: <strong>File manager</strong>, <strong>Custom files</strong>, <strong>Profiler</strong>, <strong>pySim command line</strong>, <strong>Raw APDU</strong>, and <strong>Proactive UICC</strong>.</p>
|
||||
|
||||
<h3 id="file-manager" class="text-lg font-medium mb-2">5.1 File manager</h3>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
@@ -305,9 +312,12 @@
|
||||
<p class="text-sm mb-3">Send an arbitrary APDU and view the raw response.</p>
|
||||
|
||||
<h3 id="proactive-uicc" class="text-lg font-medium mb-2">5.5 Proactive UICC</h3>
|
||||
<p class="text-sm mb-3">Interacts with the Card Application Toolkit session: subscribed events, the proactive command log, the PROVIDE LOCAL INFORMATION data dictionary, and STATUS polling.</p>
|
||||
<p class="text-sm mb-3">Interacts with the Card Application Toolkit session: the STK menu, subscribed events, the proactive command log, the PROVIDE LOCAL INFORMATION data dictionary, and STATUS polling.</p>
|
||||
|
||||
<h4 id="subscribed-events" class="font-medium mb-1">5.5.1 Subscribed events (SET UP EVENT LIST)</h4>
|
||||
<h4 id="stk-menu" class="font-medium mb-1">5.5.1 STK menu</h4>
|
||||
<p class="text-sm mb-3">When the card has issued a SET UP MENU command, a “STK menu” block appears at the top of this view with an emerald <strong>STK: <title></strong> button that opens the menu overlay (same as the card’s STK menu browser). If the card has not set up a menu, the block shows “No menu set by the card” instead. The menu state is refreshed each time the view is opened.</p>
|
||||
|
||||
<h4 id="subscribed-events" class="font-medium mb-1">5.5.2 Subscribed events (SET UP EVENT LIST)</h4>
|
||||
<p class="text-sm mb-2">The events the card monitors. Each event has a <strong>Send</strong> button that opens a form specific to the event type:</p>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>No-data events</strong> (User Activity, Idle Screen, Data Available, …) — one-click notification</li>
|
||||
@@ -317,10 +327,10 @@
|
||||
</ul>
|
||||
<p class="text-sm mb-3">Sending an event uses <code class="font-mono text-sm">ENVELOPE(Event Download)</code> per TS 102 223 / TS 131 111.</p>
|
||||
|
||||
<h4 id="proactive-log" class="font-medium mb-1">5.5.2 Proactive command log</h4>
|
||||
<h4 id="proactive-log" class="font-medium mb-1">5.5.3 Proactive command log</h4>
|
||||
<p class="text-sm mb-2">Chronological list of fetched proactive commands. Each row shows the elapsed time, type code, name, and a decoded qualifier (for commands that have one). Commands with response data show a <code class="font-mono text-sm">Response:</code> line with the TERMINAL RESPONSE bytes (boilerplate TLVs stripped); PROVIDE LOCAL INFORMATION responses are decoded using the PLI data dictionary decoders.</p>
|
||||
|
||||
<h4 id="pli-dict" class="font-medium mb-1">5.5.3 PROVIDE LOCAL INFORMATION data dictionary</h4>
|
||||
<h4 id="pli-dict" class="font-medium mb-1">5.5.4 PROVIDE LOCAL INFORMATION data dictionary</h4>
|
||||
<p class="text-sm mb-2">Editable hex values for all 22 PLI qualifiers (TS 102 223 §8.6 + TS 131 111). Ten qualifiers have inline decode/encode forms:</p>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>00</strong> Location Info (MCC, MNC, LAC/TAC, Cell ID)</li>
|
||||
@@ -330,10 +340,30 @@
|
||||
</ul>
|
||||
<p class="text-sm mb-3">Values persist server-side until restart. When the card issues PLI, the server injects the dictionary values into the TERMINAL RESPONSE.</p>
|
||||
|
||||
<h4 id="status-polling" class="font-medium mb-1">5.5.4 STATUS polling</h4>
|
||||
<h4 id="status-polling" class="font-medium mb-1">5.5.5 STATUS polling</h4>
|
||||
<p class="text-sm mb-3">A <strong>Send STATUS</strong> button issues a manual STATUS (F2). A <strong>Polling</strong> toggle enables background polling: after a configurable idle interval (server CLI <code class="font-mono text-sm">--poll-interval</code>, 1–255 s, default 30 s) the server sends STATUS and handles any pending proactive command. Polling stops and card state resets if the card is removed.</p>
|
||||
|
||||
<h3 id="usage-scenarios" class="text-lg font-medium mb-2">5.6 Usage scenarios</h3>
|
||||
<h3 id="profiler" class="text-lg font-medium mb-2">5.6 Profiler</h3>
|
||||
<p class="text-sm mb-2">Verifies that a card matches a named <strong>profile</strong> — an ordered set of rules describing the expected file system and (optionally) file contents. Profiles are stored in <code class="font-mono text-sm">localStorage</code>.</p>
|
||||
<h4 class="font-medium mb-1">Profile list</h4>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>New profile</strong> — creates an empty ruleset after prompting for a name.</li>
|
||||
<li><strong>Profile from card</strong> — scans the equipped card and generates one rule per existing file (see below), then opens the editor.</li>
|
||||
<li><strong>Import profile</strong> — loads a ruleset from a JSON file (the name is stored inside the JSON).</li>
|
||||
<li>Each profile row shows its name and creation time, with <strong>Edit</strong>, <strong>Check</strong>, <strong>Export</strong> (download JSON), and <strong>Delete</strong> actions.</li>
|
||||
</ul>
|
||||
<h4 class="font-medium mb-1">Filesystem rules</h4>
|
||||
<p class="text-sm mb-2">Rules run sequentially. A filesystem rule is defined by:</p>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>Path</strong> — starts with <code class="font-mono text-sm">MF</code> (e.g. <code class="font-mono text-sm">MF/7F10/6F3A</code>) or an ADF AID (e.g. <code class="font-mono text-sm">A0000000871002/6F07</code>).</li>
|
||||
<li><strong>File attributes</strong> — file type, size, record length and record count, taken from the FCI template (any may be left unset).</li>
|
||||
<li><strong>Contents</strong> (optional) — <strong>Exact</strong> hex equality, or <strong>Mask</strong> where <code class="font-mono text-sm">?</code> is a per-nibble wildcard (a mask with no <code class="font-mono text-sm">?</code> is a prefix match, e.g. <code class="font-mono text-sm">0891</code> for the IMSI MCC/MNC). Record files store a per-record list.</li>
|
||||
</ul>
|
||||
<p class="text-sm mb-3"><strong>Check</strong> runs every rule against the equipped card and shows a live progress line plus a pass/fail report (existence, each FCI attribute, and the content match).</p>
|
||||
<h4 class="font-medium mb-1">“Profile from card” scan options</h4>
|
||||
<p class="text-sm mb-2">The scan dialog asks for a profile name and offers an <strong>“Ignore contents of”</strong> checklist (all checked by default) of frequently-overwritten files whose contents are skipped: <code class="font-mono text-sm">EF.LOCI</code>, <code class="font-mono text-sm">EF.PSLOCI</code>, <code class="font-mono text-sm">EF.EPSLOCI</code>, <code class="font-mono text-sm">EF.5GS3GPPLOCI</code>, <code class="font-mono text-sm">EF.Keys</code>, <code class="font-mono text-sm">EF.KeysPS</code>, <code class="font-mono text-sm">EF.SMS</code>, <code class="font-mono text-sm">EF.Kc</code>, <code class="font-mono text-sm">EF.KcGPRS</code>, <code class="font-mono text-sm">EF.LOCIGPRS</code>, <code class="font-mono text-sm">EF.CBMID</code>, <code class="font-mono text-sm">EF.SMSS</code>. Rules are created only for files that actually exist on the card (an FCI template is returned); missing files are skipped. Custom files from the <strong>Custom files</strong> sub-tab are included under the same existence check.</p>
|
||||
|
||||
<h3 id="usage-scenarios" class="text-lg font-medium mb-2">5.7 Usage scenarios</h3>
|
||||
|
||||
<h4 id="scenario-a" class="font-medium mb-1">Scenario A — Working with files not in pySim’s model (Custom files)</h4>
|
||||
<ol class="list-decimal list-inside text-sm space-y-1 mb-3">
|
||||
|
||||
+653
-13
@@ -18,7 +18,7 @@
|
||||
<div class="max-w-7xl mx-auto px-6 py-2">
|
||||
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v1.9.24</span></h1>
|
||||
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v1.9.26</span></h1>
|
||||
<div class="flex items-center gap-4">
|
||||
<button id="install-btn" class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700" style="display:none">INSTALL PWA [for offline use]</button>
|
||||
<a href="https://github.com/anttro/otaman" target="_blank" class="text-xs text-gray-400 hover:text-gray-600 dark:text-slate-500 dark:hover:text-slate-300">github</a>
|
||||
@@ -648,10 +648,10 @@
|
||||
<div class="flex gap-1 mb-2">
|
||||
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-blue-600 text-white" data-pysim-sub="files" data-l10n="File manager">File manager</button>
|
||||
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="custom" data-l10n="Custom files">Custom files</button>
|
||||
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="profiler" data-l10n="Profiler">Profiler</button>
|
||||
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="cmd" data-l10n="pySim command line">pySim command line</button>
|
||||
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="apdu" data-l10n="Raw APDU">Raw APDU</button>
|
||||
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="proactive">Proactive UICC</button>
|
||||
<button id="stk-menu-btn" onclick="stkMenuOpen()" class="pysim-subtab px-3 py-1 text-sm rounded-full bg-emerald-600 text-white hover:bg-emerald-700" style="display:none">STK: <span id="stk-menu-title"></span></button>
|
||||
</div>
|
||||
|
||||
<div id="pysim-sub-files">
|
||||
@@ -697,6 +697,36 @@
|
||||
<textarea id="pysim-cf-io" class="hidden mt-2 w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1.5 bg-gray-100 dark:bg-slate-800" rows="6"></textarea>
|
||||
</div>
|
||||
|
||||
<div id="pysim-sub-profiler" class="hidden">
|
||||
<div id="profiler-list">
|
||||
<div class="flex flex-wrap gap-2 mb-3">
|
||||
<button onclick="profilerNew()" class="px-2.5 py-1 text-sm rounded bg-blue-600 text-white hover:bg-blue-700" data-l10n="New profile">New profile</button>
|
||||
<button onclick="profilerFromCard()" class="px-2.5 py-1 text-sm rounded bg-blue-600 text-white hover:bg-blue-700" data-l10n="Profile from card">Profile from card</button>
|
||||
<button onclick="document.getElementById('profiler-import-file').click()" class="px-2.5 py-1 text-sm rounded bg-gray-600 text-white hover:bg-gray-700" data-l10n="Import profile">Import profile</button>
|
||||
<input type="file" id="profiler-import-file" accept=".json,application/json" class="hidden" onchange="profilerImportFile(this)">
|
||||
</div>
|
||||
<div id="profiler-list-body"></div>
|
||||
</div>
|
||||
<div id="profiler-editor" class="hidden">
|
||||
<div class="flex flex-wrap gap-2 items-center mb-3">
|
||||
<button onclick="profilerBackToList()" class="px-2.5 py-1 text-sm rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300" data-l10n="Back to list">Back to list</button>
|
||||
<input id="profiler-name" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="Profile name">
|
||||
<button onclick="profilerAddRule()" class="px-2.5 py-1 text-sm rounded bg-blue-600 text-white hover:bg-blue-700" data-l10n="Add rule">Add rule</button>
|
||||
<button onclick="profilerSaveProfile()" class="px-2.5 py-1 text-sm rounded bg-emerald-600 text-white hover:bg-emerald-700" data-l10n="Save">Save</button>
|
||||
</div>
|
||||
<div id="profiler-rules"></div>
|
||||
</div>
|
||||
<div id="profiler-results" class="hidden">
|
||||
<div class="flex flex-wrap gap-2 items-center mb-3">
|
||||
<button onclick="profilerBackToList()" class="px-2.5 py-1 text-sm rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300" data-l10n="Back to list">Back to list</button>
|
||||
<span id="profiler-results-title" class="text-sm font-semibold"></span>
|
||||
</div>
|
||||
<div id="profiler-progress" class="mb-2 text-xs font-mono text-gray-600 dark:text-slate-400"></div>
|
||||
<div id="profiler-summary" class="mb-2 text-sm"></div>
|
||||
<div id="profiler-report"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="pysim-sub-cmd" class="hidden">
|
||||
<div class="flex gap-2 items-center mb-2">
|
||||
<div class="relative flex-1">
|
||||
@@ -719,7 +749,12 @@
|
||||
<div id="pysim-sub-proactive" class="hidden">
|
||||
<div class="flex gap-6">
|
||||
<div>
|
||||
<div class="mb-1 text-sm text-gray-500 dark:text-slate-400" data-l10n="STATUS and Polling">STATUS and Polling</div>
|
||||
<div class="mb-1 text-sm text-gray-500 dark:text-slate-400" data-l10n="STK menu">STK menu</div>
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<button id="stk-menu-btn" onclick="stkMenuOpen()" class="px-2.5 py-1 text-sm rounded bg-emerald-600 text-white hover:bg-emerald-700" style="display:none">STK: <span id="stk-menu-title"></span></button>
|
||||
<span id="stk-menu-none" class="text-xs text-gray-500 dark:text-slate-400" style="display:none" data-l10n="No menu set by the card">No menu set by the card</span>
|
||||
</div>
|
||||
<div class="mb-1 text-sm text-gray-500 dark:text-slate-400" data-l10n="STATUS and Polling">STATUS and Polling</div>
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<button id="pli-status-btn" onclick="pysimStatusPoll()" class="px-2.5 py-1 text-sm rounded bg-gray-600 text-white hover:bg-gray-700" data-l10n="Send STATUS">Send STATUS</button>
|
||||
<span class="text-xs text-gray-500 dark:text-slate-400">|</span>
|
||||
@@ -758,6 +793,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="profiler-scan-modal" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center hidden">
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg p-6 max-w-sm w-full mx-4 max-h-[85vh] overflow-auto">
|
||||
<h3 class="text-lg font-semibold mb-4 text-gray-800 dark:text-slate-200" data-l10n="Profile from card">Profile from card</h3>
|
||||
<label class="block mb-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="Profile name">Profile name</label>
|
||||
<input id="profiler-scan-name" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2 dark:bg-slate-800 mb-3" placeholder="Profile name">
|
||||
<div class="mb-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="Ignore contents of:">Ignore contents of:</div>
|
||||
<div id="profiler-scan-ignore" class="space-y-1 mb-3 max-h-56 overflow-auto"></div>
|
||||
<div id="profiler-scan-error" class="text-xs text-red-500 hidden mb-2"></div>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<button onclick="profilerScanCancel()" class="px-3 py-1.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-100 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300" data-l10n="Cancel">Cancel</button>
|
||||
<button id="profiler-scan-btn" onclick="profilerScanStart()" class="px-4 py-1.5 text-xs rounded bg-blue-600 text-white hover:bg-blue-700" data-l10n="Scan">Scan</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="stk-menu-panel" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center hidden">
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg p-4 max-w-xs w-full max-h-[80vh] flex flex-col">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
@@ -1598,7 +1648,7 @@ function chainSimBuildRowHex(chainId, idx, row) {
|
||||
var fid = (f.fid || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
|
||||
if (fid.length !== 4) return '';
|
||||
var apdu = buildApdu(cla, 0xA4, 0x00, p2, 0x02, fid);
|
||||
if (!isSim) apdu += '00';
|
||||
if (!isSim && !silent) apdu += '00';
|
||||
return apdu;
|
||||
}
|
||||
if (method === 'path') {
|
||||
@@ -1606,14 +1656,14 @@ function chainSimBuildRowHex(chainId, idx, row) {
|
||||
if (path.length < 2 || path.length % 2 !== 0) return '';
|
||||
var p1 = (!isSim && f.base === 'df') ? 0x09 : 0x08;
|
||||
var apdu2 = buildApdu(cla, 0xA4, p1, p2, path.length / 2, path);
|
||||
if (!isSim) apdu2 += '00';
|
||||
if (!isSim && !silent) apdu2 += '00';
|
||||
return apdu2;
|
||||
}
|
||||
if (method === 'dfname') {
|
||||
var aid = (f.dfname || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
|
||||
if (aid.length < 2) return '';
|
||||
var apdu3 = buildApdu(cla, 0xA4, 0x04, p2, aid.length / 2, aid);
|
||||
if (!isSim) apdu3 += '00';
|
||||
if (!isSim && !silent) apdu3 += '00';
|
||||
return apdu3;
|
||||
}
|
||||
if (method === 'chain') {
|
||||
@@ -1630,7 +1680,7 @@ function chainSimBuildRowHex(chainId, idx, row) {
|
||||
var fid2 = tok.replace(/[^0-9a-fA-F]/g, '').toUpperCase();
|
||||
if (fid2.length !== 4) continue;
|
||||
var s = buildSelect(cla, 0x00, p2, fid2);
|
||||
if (!isSim) s += '00';
|
||||
if (!isSim && !silent) s += '00';
|
||||
out += s;
|
||||
}
|
||||
return out;
|
||||
@@ -4849,15 +4899,16 @@ let stkMenuStack = [];
|
||||
|
||||
async function stkCheckMenu() {
|
||||
try {
|
||||
console.log('stkCheckMenu called');
|
||||
const data = await pysimFetch('/api/menu');
|
||||
console.log('menu data:', data);
|
||||
const btn = document.getElementById('stk-menu-btn');
|
||||
const none = document.getElementById('stk-menu-none');
|
||||
if (data && data.title && data.items && data.items.length) {
|
||||
document.getElementById('stk-menu-title').textContent = data.title;
|
||||
btn.style.display = '';
|
||||
none.style.display = 'none';
|
||||
} else {
|
||||
btn.style.display = 'none';
|
||||
none.style.display = '';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('stkCheckMenu error:', e);
|
||||
@@ -4866,7 +4917,6 @@ async function stkCheckMenu() {
|
||||
|
||||
function stkSetPillsDisabled(disabled) {
|
||||
document.querySelectorAll('.pysim-subtab').forEach(b => {
|
||||
if (b.id === 'stk-menu-btn') return;
|
||||
b.disabled = disabled;
|
||||
b.classList.toggle('opacity-50', disabled);
|
||||
});
|
||||
@@ -5174,12 +5224,13 @@ function pysimSwitchSubtab(name) {
|
||||
btn.classList.toggle('text-gray-700', !isActive);
|
||||
btn.classList.toggle('dark:text-slate-300', !isActive);
|
||||
});
|
||||
['cmd', 'apdu', 'files', 'custom', 'proactive'].forEach(s => {
|
||||
['cmd', 'apdu', 'files', 'custom', 'proactive', 'profiler'].forEach(s => {
|
||||
document.getElementById('pysim-sub-' + s).classList.toggle('hidden', s !== name);
|
||||
});
|
||||
if (name === 'custom') pysimCustomRender();
|
||||
if (name === 'proactive') { pysimEventsRender(); pysimProactiveLogRender(); pysimPliRender(); pysimPollStatusInit(); }
|
||||
pysimHelpAnchor = {cmd:'pysim-cmdline', apdu:'raw-apdu', files:'file-manager', custom:'custom-files', proactive:'proactive-uicc'}[name] || 'card-reader';
|
||||
if (name === 'proactive') { stkCheckMenu(); pysimEventsRender(); pysimProactiveLogRender(); pysimPliRender(); pysimPollStatusInit(); }
|
||||
if (name === 'profiler') profilerSetView('list');
|
||||
pysimHelpAnchor = {cmd:'pysim-cmdline', apdu:'raw-apdu', files:'file-manager', custom:'custom-files', proactive:'proactive-uicc', profiler:'card-reader'}[name] || 'card-reader';
|
||||
setHelpAnchor(pysimHelpAnchor);
|
||||
}
|
||||
|
||||
@@ -6597,6 +6648,560 @@ function pysimCustomImportField() {
|
||||
// Init custom files
|
||||
pysimCustomLoad();
|
||||
|
||||
// ===== Profiler =====
|
||||
let profiles = [];
|
||||
let profilerView = 'list';
|
||||
let profilerEditId = null;
|
||||
let profilerDraft = null;
|
||||
let profilerResults = null;
|
||||
|
||||
// Files whose contents are ignored (contents check omitted) when scanning.
|
||||
// Keyed by FID; display name is the pySim name.
|
||||
const PROFILER_IGNORE_FILES = [
|
||||
{ fid: '6F7E', name: 'EF.LOCI' },
|
||||
{ fid: '6F73', name: 'EF.PSLOCI' },
|
||||
{ fid: '6FE3', name: 'EF.EPSLOCI' },
|
||||
{ fid: '4F01', name: 'EF.5GS3GPPLOCI' },
|
||||
{ fid: '6F08', name: 'EF.Keys' },
|
||||
{ fid: '6F09', name: 'EF.KeysPS' },
|
||||
{ fid: '6F3C', name: 'EF.SMS' },
|
||||
{ fid: '6F20', name: 'EF.Kc' },
|
||||
{ fid: '4F52', name: 'EF.KcGPRS' },
|
||||
{ fid: '6F53', name: 'EF.LOCIGPRS' },
|
||||
{ fid: '6F48', name: 'EF.CBMID' },
|
||||
{ fid: '6F43', name: 'EF.SMSS' },
|
||||
];
|
||||
|
||||
function profilerLoad() {
|
||||
try {
|
||||
const d = localStorage.getItem('otaman_profiles');
|
||||
profiles = d ? JSON.parse(d) : [];
|
||||
} catch (e) { profiles = []; }
|
||||
}
|
||||
|
||||
function profilerSave() {
|
||||
localStorage.setItem('otaman_profiles', JSON.stringify(profiles));
|
||||
}
|
||||
|
||||
function profilerNewId() {
|
||||
return Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
|
||||
}
|
||||
|
||||
function profilerNormHex(s) {
|
||||
return (s || '').toUpperCase().replace(/[^0-9A-F?]/g, '');
|
||||
}
|
||||
|
||||
// Match an expected hex (may contain '?' nibble wildcards) against actual hex.
|
||||
function profilerMatch(mode, expected, actual) {
|
||||
const e = profilerNormHex(expected);
|
||||
const a = profilerNormHex(actual);
|
||||
if (!e) return false;
|
||||
if (mode === 'mask') {
|
||||
if (e.indexOf('?') < 0) {
|
||||
return a.indexOf(e) === 0;
|
||||
}
|
||||
if (e.length !== a.length) return false;
|
||||
for (let i = 0; i < e.length; i++) {
|
||||
if (e[i] !== '?' && e[i] !== a[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return e === a;
|
||||
}
|
||||
|
||||
function profilerValidateProfile(obj) {
|
||||
if (!obj || typeof obj !== 'object') return 'Not an object';
|
||||
if (!obj.name) return 'Missing name';
|
||||
if (!Array.isArray(obj.rules)) return 'Missing rules array';
|
||||
for (const r of obj.rules) {
|
||||
if (!r || typeof r !== 'object') return 'Invalid rule';
|
||||
if (r.type !== 'file') return 'Unsupported rule type: ' + r.type;
|
||||
if (!r.path) return 'Rule missing path';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function profilerRenderList() {
|
||||
const el = document.getElementById('profiler-list-body');
|
||||
if (!profiles.length) {
|
||||
el.innerHTML = '<span class="text-gray-400">' + t('No profiles defined.') + '</span>';
|
||||
return;
|
||||
}
|
||||
let html = '';
|
||||
for (let i = 0; i < profiles.length; i++) {
|
||||
const p = profiles[i];
|
||||
html += '<div class="flex items-center gap-2 py-1.5 border-b border-gray-200 dark:border-slate-700">';
|
||||
html += '<b>' + esc(p.name) + '</b>';
|
||||
html += '<span class="text-xs text-gray-400 dark:text-slate-500">' + esc(new Date(p.created).toLocaleString()) + '</span>';
|
||||
html += '<span class="text-xs text-gray-400 dark:text-slate-500">(' + p.rules.length + ' ' + esc(t('rules')) + ')</span>';
|
||||
html += '<span class="ml-auto flex gap-1">';
|
||||
html += '<button onclick="profilerEdit(' + i + ')" class="px-2 py-0.5 text-xs rounded bg-blue-600 text-white hover:bg-blue-700">' + esc(t('Edit')) + '</button>';
|
||||
html += '<button onclick="profilerCheck(' + i + ')" class="px-2 py-0.5 text-xs rounded bg-emerald-600 text-white hover:bg-emerald-700">' + esc(t('Check')) + '</button>';
|
||||
html += '<button onclick="profilerExport(' + i + ')" class="px-2 py-0.5 text-xs rounded bg-gray-600 text-white hover:bg-gray-700">' + esc(t('Export')) + '</button>';
|
||||
html += '<button onclick="profilerDelete(' + i + ')" class="px-2 py-0.5 text-xs rounded bg-red-600 text-white hover:bg-red-700">' + esc(t('Delete')) + '</button>';
|
||||
html += '</span></div>';
|
||||
}
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
function profilerSetView(view) {
|
||||
profilerView = view;
|
||||
document.getElementById('profiler-list').classList.toggle('hidden', view !== 'list');
|
||||
document.getElementById('profiler-editor').classList.toggle('hidden', view !== 'editor');
|
||||
document.getElementById('profiler-results').classList.toggle('hidden', view !== 'results');
|
||||
if (view === 'list') profilerRenderList();
|
||||
}
|
||||
|
||||
function profilerNew() {
|
||||
const name = prompt(t('Profile name') + ':', '');
|
||||
if (!name) return;
|
||||
profiles.push({ id: profilerNewId(), name: name, created: new Date().toISOString(), rules: [] });
|
||||
profilerSave();
|
||||
profilerEdit(profiles.length - 1);
|
||||
}
|
||||
|
||||
function profilerFromCard() {
|
||||
document.getElementById('profiler-scan-name').value = '';
|
||||
const ignore = document.getElementById('profiler-scan-ignore');
|
||||
let html = '';
|
||||
for (const f of PROFILER_IGNORE_FILES) {
|
||||
html += '<label class="flex items-center gap-2 text-sm">' +
|
||||
'<input type="checkbox" data-ignore-fid="' + f.fid + '" checked>' +
|
||||
'<span>' + esc(t('Ignore contents of')) + ' ' + esc(f.name) + '</span></label>';
|
||||
}
|
||||
ignore.innerHTML = html;
|
||||
document.getElementById('profiler-scan-error').classList.add('hidden');
|
||||
document.getElementById('profiler-scan-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function profilerScanCancel() {
|
||||
document.getElementById('profiler-scan-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
async function profilerScanStart() {
|
||||
const name = document.getElementById('profiler-scan-name').value.trim();
|
||||
const errEl = document.getElementById('profiler-scan-error');
|
||||
if (!name) {
|
||||
errEl.textContent = t('Please enter a profile name');
|
||||
errEl.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
const ignoreFids = new Set();
|
||||
document.querySelectorAll('#profiler-scan-ignore input[data-ignore-fid]').forEach(cb => {
|
||||
if (cb.checked) ignoreFids.add(cb.getAttribute('data-ignore-fid').toUpperCase());
|
||||
});
|
||||
const btn = document.getElementById('profiler-scan-btn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = t('Scanning...');
|
||||
try {
|
||||
const rules = await profilerScanCard(ignoreFids);
|
||||
const profile = { id: profilerNewId(), name: name, created: new Date().toISOString(), rules: rules };
|
||||
profiles.push(profile);
|
||||
profilerSave();
|
||||
profilerScanCancel();
|
||||
profilerEdit(profiles.length - 1);
|
||||
} catch (e) {
|
||||
errEl.textContent = e.message;
|
||||
errEl.classList.remove('hidden');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = t('Scan');
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively walk the card filesystem and build rules for files that exist.
|
||||
async function profilerScanCard(ignoreFids) {
|
||||
const rules = [];
|
||||
const seen = new Set();
|
||||
|
||||
// dir: { name, fid, parentSel (to select this dir), pathPrefix (rule-path segs for its children) }
|
||||
async function walkDir(dir) {
|
||||
let body = { name: dir.name, fid: dir.fid };
|
||||
if (dir.parentSel) body.parent_sel = dir.parentSel;
|
||||
let data;
|
||||
try {
|
||||
data = await pysimFetch('/api/tree', body);
|
||||
} catch (e) { return; }
|
||||
if (!data || data.exists === false) return;
|
||||
for (const c of (data.children || [])) {
|
||||
// parent selector for c, mirroring getParentSel()
|
||||
const parentSel = (dir.name === 'MF') ? 'MF'
|
||||
: (dir.name && dir.name.startsWith('ADF.')) ? dir.name
|
||||
: (dir.fid ? dir.fid : dir.name);
|
||||
if (c.isDir) {
|
||||
// ADF roots use the AID, not the generic ADF fid
|
||||
const childPrefix = c.aid ? [c.aid.toUpperCase()]
|
||||
: dir.pathPrefix.concat(c.fid ? c.fid.toUpperCase() : c.name);
|
||||
await walkDir({ name: c.name, fid: c.fid, parentSel: parentSel, pathPrefix: childPrefix });
|
||||
} else {
|
||||
const childPath = dir.pathPrefix.concat(c.fid ? c.fid.toUpperCase() : c.name).join('/');
|
||||
if (seen.has(childPath)) continue;
|
||||
seen.add(childPath);
|
||||
const rule = await profilerBuildFileRule(childPath, c, ignoreFids);
|
||||
if (rule) rules.push(rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MF-rooted walk
|
||||
await walkDir({ name: 'MF', fid: null, parentSel: null, pathPrefix: ['MF'] });
|
||||
|
||||
// Sweep custom files not already covered (map leading MF fid 3F00 -> MF)
|
||||
for (const cf of pysimCustomFiles) {
|
||||
const parts = cf.path.split('/').filter(Boolean);
|
||||
if (!parts.length) continue;
|
||||
let segs;
|
||||
if (parts[0].toUpperCase() === '3F00') segs = ['MF'].concat(parts.slice(1));
|
||||
else segs = parts;
|
||||
const path = segs.join('/');
|
||||
if (seen.has(path)) continue;
|
||||
seen.add(path);
|
||||
const rule = await profilerBuildFileRule(path, { fid: cf.fid, name: cf.name }, ignoreFids);
|
||||
if (rule) rules.push(rule);
|
||||
}
|
||||
return rules;
|
||||
}
|
||||
|
||||
async function profilerBuildFileRule(path, c, ignoreFids) {
|
||||
let sel;
|
||||
try {
|
||||
sel = await pysimFetch('/api/select', { path: path });
|
||||
} catch (e) { return null; }
|
||||
if (!sel || sel.exists !== true) return null;
|
||||
const rule = {
|
||||
type: 'file',
|
||||
path: path,
|
||||
fileType: sel.file_type || null,
|
||||
fileSize: (sel.file_size === null || sel.file_size === undefined) ? null : sel.file_size,
|
||||
recordLen: (sel.record_len === null || sel.record_len === undefined) ? null : sel.record_len,
|
||||
numRecords: (sel.num_of_rec === null || sel.num_of_rec === undefined) ? null : sel.num_of_rec,
|
||||
content: null,
|
||||
};
|
||||
const fid = (c.fid || sel.fid || '').toUpperCase();
|
||||
if (ignoreFids.has(fid)) return rule;
|
||||
try {
|
||||
const rd = await pysimFetch('/api/read', { path: path, mode: 'raw' });
|
||||
if (rd && rd.success) {
|
||||
if (rd.records) {
|
||||
rule.content = {
|
||||
mode: 'exact', kind: 'record',
|
||||
records: rd.records.map(r => ({ num: r.num, data: r.data })),
|
||||
};
|
||||
} else if (rd.data) {
|
||||
rule.content = { mode: 'exact', kind: 'transparent', expected: rd.data };
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
return rule;
|
||||
}
|
||||
|
||||
function profilerImportFile(input) {
|
||||
const file = input.files && input.files[0];
|
||||
input.value = '';
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
try {
|
||||
const obj = JSON.parse(reader.result);
|
||||
const err = profilerValidateProfile(obj);
|
||||
if (err) throw new Error(err);
|
||||
obj.id = profilerNewId();
|
||||
obj.created = obj.created || new Date().toISOString();
|
||||
profiles.push(obj);
|
||||
profilerSave();
|
||||
profilerRenderList();
|
||||
} catch (e) {
|
||||
alert('Import error: ' + e.message);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
function profilerExport(i) {
|
||||
const p = profiles[i];
|
||||
downloadJson((p.name || 'profile').replace(/[^a-zA-Z0-9_-]+/g, '_') + '.json', p);
|
||||
}
|
||||
|
||||
function profilerDelete(i) {
|
||||
const p = profiles[i];
|
||||
if (!confirm(t('Delete profile') + ' "' + p.name + '"?') ) return;
|
||||
profiles.splice(i, 1);
|
||||
profilerSave();
|
||||
profilerRenderList();
|
||||
}
|
||||
|
||||
function profilerEdit(i) {
|
||||
const p = profiles[i];
|
||||
profilerEditId = p.id;
|
||||
profilerDraft = JSON.parse(JSON.stringify(p));
|
||||
profilerSetView('editor');
|
||||
document.getElementById('profiler-name').value = profilerDraft.name;
|
||||
profilerRenderEditor();
|
||||
}
|
||||
|
||||
function profilerBackToList() {
|
||||
profilerDraft = null;
|
||||
profilerEditId = null;
|
||||
profilerSetView('list');
|
||||
}
|
||||
|
||||
function profilerAddRule() {
|
||||
if (!profilerDraft) return;
|
||||
profilerDraft.rules.push({ type: 'file', path: '', fileType: null, fileSize: null, recordLen: null, numRecords: null, content: null });
|
||||
profilerRenderEditor();
|
||||
}
|
||||
|
||||
function profilerDeleteRule(i) {
|
||||
if (!profilerDraft) return;
|
||||
profilerDraft.rules.splice(i, 1);
|
||||
profilerRenderEditor();
|
||||
}
|
||||
|
||||
function profilerRenderEditor() {
|
||||
if (!profilerDraft) return;
|
||||
const el = document.getElementById('profiler-rules');
|
||||
if (!profilerDraft.rules.length) {
|
||||
el.innerHTML = '<span class="text-gray-400">' + t('No rules defined.') + '</span>';
|
||||
return;
|
||||
}
|
||||
let html = '';
|
||||
for (let i = 0; i < profilerDraft.rules.length; i++) {
|
||||
const r = profilerDraft.rules[i];
|
||||
html += profilerRenderRule(i, r);
|
||||
}
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
function profilerRenderRule(i, r) {
|
||||
const fts = { transparent: 'transparent', linear_fixed: 'linear_fixed', cyclic: 'cyclic', ber_tlv: 'ber_tlv', df: 'df' };
|
||||
let ftOpts = '<option value=""' + (!r.fileType ? ' selected' : '') + '>—</option>';
|
||||
for (const k in fts) {
|
||||
ftOpts += '<option value="' + k + '"' + (r.fileType === k ? ' selected' : '') + '>' + fts[k] + '</option>';
|
||||
}
|
||||
const cm = r.content ? r.content.mode : 'exact';
|
||||
let html = '<div class="mb-2 p-3 border border-gray-200 dark:border-slate-600 rounded">';
|
||||
html += '<div class="flex items-center gap-2 mb-2">';
|
||||
html += '<span class="text-xs font-semibold text-gray-500 dark:text-slate-400">#' + (i + 1) + '</span>';
|
||||
html += '<button onclick="profilerDeleteRule(' + i + ')" class="ml-auto px-2 py-0.5 text-xs rounded bg-red-600 text-white hover:bg-red-700">' + esc(t('Delete')) + '</button>';
|
||||
html += '</div>';
|
||||
html += '<div class="grid grid-cols-1 gap-2 mb-2">';
|
||||
html += '<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' + esc(t('Path')) + '</label>' +
|
||||
'<input value="' + escHtml(r.path || '') + '" oninput="profilerUpdateRule(' + i + ',\'path\',this.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" placeholder="MF/7F10/6F3A"></div>';
|
||||
html += '</div>';
|
||||
html += '<div class="grid grid-cols-2 md:grid-cols-4 gap-2 mb-2">';
|
||||
html += '<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' + esc(t('File type')) + '</label>' +
|
||||
'<select onchange="profilerUpdateRule(' + i + ',\'fileType\',this.value||null)" class="w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1 dark:bg-slate-800">' + ftOpts + '</select></div>';
|
||||
html += '<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' + esc(t('Size')) + '</label>' +
|
||||
'<input type="number" value="' + (r.fileSize === null ? '' : r.fileSize) + '" oninput="profilerUpdateRule(' + i + ',\'fileSize\',this.value===\'\'?null:parseInt(this.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" placeholder="—"></div>';
|
||||
html += '<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' + esc(t('Record length')) + '</label>' +
|
||||
'<input type="number" value="' + (r.recordLen === null ? '' : r.recordLen) + '" oninput="profilerUpdateRule(' + i + ',\'recordLen\',this.value===\'\'?null:parseInt(this.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" placeholder="—"></div>';
|
||||
html += '<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' + esc(t('Record count')) + '</label>' +
|
||||
'<input type="number" value="' + (r.numRecords === null ? '' : r.numRecords) + '" oninput="profilerUpdateRule(' + i + ',\'numRecords\',this.value===\'\'?null:parseInt(this.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" placeholder="—"></div>';
|
||||
html += '</div>';
|
||||
html += profilerRenderContentEditor(i, r);
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function profilerRenderContentEditor(i, r) {
|
||||
const c = r.content;
|
||||
const mode = c ? c.mode : 'exact';
|
||||
let html = '<div class="mb-1 text-xs text-gray-600 dark:text-slate-400">' + esc(t('Contents')) + '</div>';
|
||||
html += '<div class="flex items-center gap-3 mb-1">';
|
||||
html += '<label class="text-xs flex items-center gap-1"><input type="radio" name="profiler-cm-' + i + '" value="none"' + (!c ? ' checked' : '') + ' onchange="profilerSetContent(' + i + ',null)"> ' + esc(t('None')) + '</label>';
|
||||
html += '<label class="text-xs flex items-center gap-1"><input type="radio" name="profiler-cm-' + i + '" value="exact"' + (c && mode === 'exact' ? ' checked' : '') + ' onchange="profilerSetContent(' + i + ',\'exact\')"> ' + esc(t('Exact')) + '</label>';
|
||||
html += '<label class="text-xs flex items-center gap-1"><input type="radio" name="profiler-cm-' + i + '" value="mask"' + (c && mode === 'mask' ? ' checked' : '') + ' onchange="profilerSetContent(' + i + ',\'mask\')"> ' + esc(t('Mask')) + '</label>';
|
||||
html += '</div>';
|
||||
if (c) {
|
||||
if (c.kind === 'record') {
|
||||
html += '<div class="space-y-1">';
|
||||
for (let ri = 0; ri < c.records.length; ri++) {
|
||||
const rec = c.records[ri];
|
||||
html += '<div class="flex items-center gap-2"><span class="text-xs text-gray-500 w-8">' + rec.num + '</span>' +
|
||||
'<input value="' + escHtml(rec.data) + '" oninput="profilerUpdateRecord(' + i + ',' + ri + ',this.value)" class="flex-1 font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800"></div>';
|
||||
}
|
||||
html += '</div>';
|
||||
} else {
|
||||
html += '<textarea oninput="profilerUpdateRuleContent(' + i + ',this.value)" 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(c.expected || '') + '</textarea>';
|
||||
}
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
function profilerUpdateRule(i, field, value) {
|
||||
if (!profilerDraft) return;
|
||||
profilerDraft.rules[i][field] = value;
|
||||
}
|
||||
|
||||
function profilerSetContent(i, mode) {
|
||||
if (!profilerDraft) return;
|
||||
const r = profilerDraft.rules[i];
|
||||
if (!mode) {
|
||||
r.content = null;
|
||||
} else if (r.content) {
|
||||
r.content.mode = mode;
|
||||
} else {
|
||||
r.content = { mode: mode, kind: 'transparent', expected: '' };
|
||||
}
|
||||
profilerRenderEditor();
|
||||
}
|
||||
|
||||
function profilerUpdateRuleContent(i, value) {
|
||||
if (!profilerDraft) return;
|
||||
const c = profilerDraft.rules[i].content;
|
||||
if (c) c.expected = value;
|
||||
}
|
||||
|
||||
function profilerUpdateRecord(i, ri, value) {
|
||||
if (!profilerDraft) return;
|
||||
const c = profilerDraft.rules[i].content;
|
||||
if (c && c.records) c.records[ri].data = value;
|
||||
}
|
||||
|
||||
function profilerSaveProfile() {
|
||||
if (!profilerDraft) return;
|
||||
profilerDraft.name = document.getElementById('profiler-name').value.trim();
|
||||
const idx = profiles.findIndex(p => p.id === profilerEditId);
|
||||
if (idx >= 0) profiles[idx] = profilerDraft;
|
||||
profilerSave();
|
||||
profilerSetView('list');
|
||||
}
|
||||
|
||||
async function profilerCheck(i) {
|
||||
const p = profiles[i];
|
||||
profilerSetView('results');
|
||||
document.getElementById('profiler-results-title').textContent = p.name;
|
||||
const prog = document.getElementById('profiler-progress');
|
||||
const summary = document.getElementById('profiler-summary');
|
||||
const report = document.getElementById('profiler-report');
|
||||
report.innerHTML = '';
|
||||
summary.innerHTML = '';
|
||||
const results = [];
|
||||
let passed = 0, failed = 0, errors = 0;
|
||||
for (let ri = 0; ri < p.rules.length; ri++) {
|
||||
const r = p.rules[ri];
|
||||
prog.textContent = t('Checking') + ' ' + (ri + 1) + '/' + p.rules.length + ' — ' + r.path;
|
||||
const res = await profilerRunRule(r);
|
||||
results.push(res);
|
||||
if (res.status === 'pass') passed++;
|
||||
else if (res.status === 'fail') failed++;
|
||||
else errors++;
|
||||
}
|
||||
prog.textContent = '';
|
||||
summary.innerHTML = '<span class="text-emerald-600 font-semibold">' + passed + ' ' + t('passed') + '</span>' +
|
||||
' · <span class="text-red-600 font-semibold">' + failed + ' ' + t('failed') + '</span>' +
|
||||
' · <span class="text-yellow-600 font-semibold">' + errors + ' ' + t('errors') + '</span>';
|
||||
report.innerHTML = profilerRenderReport(results);
|
||||
}
|
||||
|
||||
async function profilerRunRule(r) {
|
||||
const res = { path: r.path, name: null, status: 'pass', checks: [] };
|
||||
let sel;
|
||||
try {
|
||||
sel = await pysimFetch('/api/select', { path: r.path });
|
||||
} catch (e) {
|
||||
res.status = 'error';
|
||||
res.error = e.message;
|
||||
return res;
|
||||
}
|
||||
if (sel && sel.name) res.name = sel.name;
|
||||
if (!sel || sel.exists !== true) {
|
||||
res.status = 'fail';
|
||||
res.checks.push({ label: 'exists', expected: true, actual: false, ok: false });
|
||||
return res;
|
||||
}
|
||||
res.checks.push({ label: 'exists', expected: true, actual: true, ok: true });
|
||||
if (r.fileType) {
|
||||
const ok = sel.file_type === r.fileType;
|
||||
if (!ok) res.status = 'fail';
|
||||
res.checks.push({ label: 'fileType', expected: r.fileType, actual: sel.file_type, ok: ok });
|
||||
}
|
||||
if (r.fileSize !== null && r.fileSize !== undefined) {
|
||||
const ok = sel.file_size === r.fileSize;
|
||||
if (!ok) res.status = 'fail';
|
||||
res.checks.push({ label: 'fileSize', expected: r.fileSize, actual: sel.file_size, ok: ok });
|
||||
}
|
||||
if (r.recordLen !== null && r.recordLen !== undefined) {
|
||||
const ok = sel.record_len === r.recordLen;
|
||||
if (!ok) res.status = 'fail';
|
||||
res.checks.push({ label: 'recordLen', expected: r.recordLen, actual: sel.record_len, ok: ok });
|
||||
}
|
||||
if (r.numRecords !== null && r.numRecords !== undefined) {
|
||||
const ok = sel.num_of_rec === r.numRecords;
|
||||
if (!ok) res.status = 'fail';
|
||||
res.checks.push({ label: 'numRecords', expected: r.numRecords, actual: sel.num_of_rec, ok: ok });
|
||||
}
|
||||
if (r.content) {
|
||||
let rd;
|
||||
try {
|
||||
rd = await pysimFetch('/api/read', { path: r.path, mode: 'raw' });
|
||||
} catch (e) {
|
||||
res.status = 'error';
|
||||
res.error = e.message;
|
||||
return res;
|
||||
}
|
||||
if (!rd || !rd.success) {
|
||||
res.status = 'fail';
|
||||
res.checks.push({ label: 'content', expected: 'readable', actual: 'read failed (' + (rd && rd.sw ? rd.sw : '?') + ')', ok: false });
|
||||
} else if (r.content.kind === 'record') {
|
||||
const exp = r.content.records || [];
|
||||
const act = rd.records || [];
|
||||
if (exp.length !== act.length) {
|
||||
res.status = 'fail';
|
||||
res.checks.push({ label: 'content.records', expected: exp.length + ' records', actual: act.length + ' records', ok: false });
|
||||
} else {
|
||||
for (let j = 0; j < exp.length; j++) {
|
||||
const ok = exp[j].num === act[j].num && profilerMatch(r.content.mode, exp[j].data, act[j].data);
|
||||
if (!ok) res.status = 'fail';
|
||||
res.checks.push({ label: 'content.rec' + exp[j].num, expected: exp[j].data, actual: act[j].data, ok: ok });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const ok = profilerMatch(r.content.mode, r.content.expected, rd.data);
|
||||
if (!ok) res.status = 'fail';
|
||||
res.checks.push({ label: 'content', expected: r.content.expected, actual: rd.data, ok: ok });
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function profilerCustomNameForPath(path) {
|
||||
if (!path) return null;
|
||||
const norm = path.toUpperCase();
|
||||
for (const c of pysimCustomFiles) {
|
||||
const parts = c.path.split('/').filter(Boolean);
|
||||
if (!parts.length) continue;
|
||||
let segs;
|
||||
if (parts[0].toUpperCase() === '3F00') segs = ['MF'].concat(parts.slice(1));
|
||||
else segs = parts;
|
||||
if (segs.join('/').toUpperCase() === norm) return c.name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function profilerRenderReport(results) {
|
||||
let html = '';
|
||||
for (const r of results) {
|
||||
const color = r.status === 'pass' ? 'text-emerald-600' : (r.status === 'fail' ? 'text-red-600' : 'text-yellow-600');
|
||||
const icon = r.status === 'pass' ? '✓' : (r.status === 'fail' ? '✗' : '⚠');
|
||||
const name = profilerCustomNameForPath(r.path) || r.name;
|
||||
const title = name
|
||||
? '<span>' + esc(name) + '</span> <span class="text-gray-400 dark:text-slate-500">(' + esc(r.path) + ')</span>'
|
||||
: '<span>' + esc(r.path) + '</span>';
|
||||
html += '<div class="mb-2 p-2 border border-gray-200 dark:border-slate-600 rounded">';
|
||||
html += '<div class="flex items-center gap-2"><span class="' + color + ' font-bold">' + icon + '</span><span class="font-mono text-sm">' + title + '</span></div>';
|
||||
if (r.error) html += '<div class="text-xs text-yellow-600 mt-1">' + esc(r.error) + '</div>';
|
||||
for (const c of r.checks) {
|
||||
if (c.ok) continue;
|
||||
html += '<div class="text-xs text-red-600 mt-1">' + esc(c.label) + ': ' + esc(t('expected')) + ' <b>' + esc(String(c.expected)) + '</b>, ' + esc(t('actual')) + ' <b>' + esc(String(c.actual)) + '</b></div>';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
profilerLoad();
|
||||
|
||||
// Init sub-tab pills
|
||||
document.querySelectorAll('.pysim-subtab').forEach(btn => {
|
||||
btn.addEventListener('click', () => pysimSwitchSubtab(btn.dataset.pysimSub));
|
||||
@@ -6786,11 +7391,46 @@ const LANG_RU = {
|
||||
'Send STATUS': 'Отправить STATUS',
|
||||
'help': 'справка',
|
||||
'STATUS and Polling': 'STATUS и опрос',
|
||||
'STK menu': 'Меню STK',
|
||||
'No menu set by the card': 'Меню не задано картой',
|
||||
'Polling': 'Опрос',
|
||||
'STATUS...': 'STATUS...',
|
||||
'Saving...': 'Сохранение...',
|
||||
'Saved': 'Сохранено',
|
||||
'Save': 'Сохранить',
|
||||
'Profiler': 'Профайлер',
|
||||
'New profile': 'Новый профиль',
|
||||
'Profile from card': 'Профиль с карты',
|
||||
'Import profile': 'Импорт профиля',
|
||||
'Back to list': 'Назад к списку',
|
||||
'Add rule': 'Добавить правило',
|
||||
'Scan': 'Сканировать',
|
||||
'Ignore contents of:': 'Игнорировать содержимое:',
|
||||
'Ignore contents of': 'Игнорировать содержимое',
|
||||
'No profiles defined.': 'Профили не заданы.',
|
||||
'rules': 'правил',
|
||||
'Check': 'Проверить',
|
||||
'Export': 'Экспорт',
|
||||
'Profile name': 'Имя профиля',
|
||||
'Please enter a profile name': 'Введите имя профиля',
|
||||
'Scanning...': 'Сканирование...',
|
||||
'Delete profile': 'Удалить профиль',
|
||||
'No rules defined.': 'Правила не заданы.',
|
||||
'Path': 'Путь',
|
||||
'File type': 'Тип файла',
|
||||
'Size': 'Размер',
|
||||
'Record length': 'Длина записи',
|
||||
'Record count': 'Кол-во записей',
|
||||
'Contents': 'Содержимое',
|
||||
'None': 'Нет',
|
||||
'Exact': 'Точное',
|
||||
'Mask': 'Маска',
|
||||
'Checking': 'Проверка',
|
||||
'passed': 'пройдено',
|
||||
'failed': 'не пройдено',
|
||||
'errors': 'ошибок',
|
||||
'expected': 'ожидалось',
|
||||
'actual': 'фактически',
|
||||
};
|
||||
let currentLang = 'en';
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'otaman-v35';
|
||||
const CACHE = 'otaman-v39';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
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);
|
||||
}
|
||||
|
||||
const FNS = ['profilerNormHex', 'profilerMatch', 'profilerValidateProfile'];
|
||||
let code = '';
|
||||
for (const f of FNS) code += extractFunc(html, f) + '\n';
|
||||
eval(code);
|
||||
|
||||
test('profilerNormHex uppercases and strips non-hex', () => {
|
||||
assert.strictEqual(profilerNormHex('aabbcc'), 'AABBCC');
|
||||
assert.strictEqual(profilerNormHex(' aa bb cc '), 'AABBCC');
|
||||
assert.strictEqual(profilerNormHex('08?91?'), '08?91?');
|
||||
assert.strictEqual(profilerNormHex(''), '');
|
||||
});
|
||||
|
||||
test('exact match requires full equality', () => {
|
||||
assert.ok(profilerMatch('exact', 'AABBCC', 'aabbcc'));
|
||||
assert.ok(!profilerMatch('exact', 'AABB', 'AABBCC'));
|
||||
assert.ok(!profilerMatch('exact', 'AABBCC', 'AABB'));
|
||||
assert.ok(!profilerMatch('exact', '', 'AABB'));
|
||||
});
|
||||
|
||||
test('mask with ? wildcard matches per nibble', () => {
|
||||
assert.ok(profilerMatch('mask', '08?91?', '081910'));
|
||||
assert.ok(profilerMatch('mask', '08?91?', '08A91B'));
|
||||
assert.ok(!profilerMatch('mask', '08?91?', '091910'));
|
||||
assert.ok(!profilerMatch('mask', '08?91?', '0891')); // length mismatch
|
||||
});
|
||||
|
||||
test('mask without ? is a prefix match', () => {
|
||||
assert.ok(profilerMatch('mask', '0891', '08911012345678'));
|
||||
assert.ok(profilerMatch('mask', '0891', '0891'));
|
||||
assert.ok(!profilerMatch('mask', '0891', '081910'));
|
||||
assert.ok(!profilerMatch('mask', '', '0891'));
|
||||
});
|
||||
|
||||
test('profile validation', () => {
|
||||
assert.strictEqual(profilerValidateProfile(null), 'Not an object');
|
||||
assert.strictEqual(profilerValidateProfile({ name: 'x' }), 'Missing rules array');
|
||||
assert.strictEqual(profilerValidateProfile({ name: 'x', rules: [{ type: 'ota' }] }), 'Unsupported rule type: ota');
|
||||
assert.strictEqual(profilerValidateProfile({ name: 'x', rules: [{ type: 'file' }] }), 'Rule missing path');
|
||||
assert.strictEqual(profilerValidateProfile({ name: 'x', rules: [{ type: 'file', path: 'MF/7F10/6F3A' }] }), null);
|
||||
});
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-otaman-server"
|
||||
version = "1.9.24"
|
||||
version = "1.9.26"
|
||||
description = "HTTP REST server wrapping pysim for the OTAMan PWA"
|
||||
requires-python = ">=3.8"
|
||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||
|
||||
@@ -18,7 +18,7 @@ from osmocom.construct import GsmOrUcs2Adapter
|
||||
from osmocom.tlv import BER_TLV_IE
|
||||
|
||||
|
||||
VERSION = '1.9.24'
|
||||
VERSION = '1.9.26'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
@@ -158,6 +158,34 @@ def _select_with_parent(lchan, name, parent_sel, app):
|
||||
return fcp
|
||||
|
||||
|
||||
def _select_path(lchan, path, app):
|
||||
"""Select a file described by a full path.
|
||||
|
||||
Path is '/' separated; the first element is either 'MF' (or the MF fid
|
||||
'3F00') or an ADF AID (hex). Remaining elements are FIDs or file names.
|
||||
pySim's lchan.select() cannot select an ADF by its raw AID (selectables are
|
||||
keyed by name/fid only), so ADF roots are resolved through rs.mf.applications.
|
||||
"""
|
||||
parts = [p for p in (path or '').split('/') if p]
|
||||
if not parts:
|
||||
raise RuntimeError('Empty path')
|
||||
rs = app.rs
|
||||
first = parts[0]
|
||||
if first.upper() in ('MF', '3F00'):
|
||||
lchan.select('MF', app)
|
||||
else:
|
||||
aid = first.lower()
|
||||
adf = rs.mf.applications.get(aid)
|
||||
if not adf:
|
||||
adf = next((v for k, v in rs.mf.applications.items() if k.lower() == aid), None)
|
||||
if not adf:
|
||||
raise RuntimeError('ADF not found: %s' % first)
|
||||
lchan.select_file(adf, app)
|
||||
for seg in parts[1:]:
|
||||
lchan.select(seg, app)
|
||||
return lchan.selected_file
|
||||
|
||||
|
||||
def _parse_tree_output(output):
|
||||
lines = (output or '').split('\n')
|
||||
children = []
|
||||
@@ -1591,6 +1619,7 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
return
|
||||
body = self._read_body()
|
||||
self._log_req(body)
|
||||
path = body.get('path')
|
||||
fid = body.get('fid')
|
||||
name = fid if fid else body.get('name', '')
|
||||
parent_sel = body.get('parent_sel')
|
||||
@@ -1601,12 +1630,18 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
return
|
||||
lchan = rs.lchan[0]
|
||||
try:
|
||||
_select_with_parent(lchan, name, parent_sel, app)
|
||||
if path:
|
||||
_select_path(lchan, path, app)
|
||||
else:
|
||||
_select_with_parent(lchan, name, parent_sel, app)
|
||||
cur = lchan.selected_file
|
||||
data = {
|
||||
'name': cur.name if cur else None,
|
||||
'fid': cur.fid.upper() if cur and cur.fid else None,
|
||||
'file_type': _get_file_type(lchan, cur),
|
||||
'file_size': lchan.selected_file_size() if lchan else None,
|
||||
'record_len': lchan.selected_file_record_len() if lchan else None,
|
||||
'num_of_rec': lchan.selected_file_num_of_rec() if lchan else None,
|
||||
'exists': True,
|
||||
}
|
||||
self._send_json(data)
|
||||
@@ -1623,9 +1658,9 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
return
|
||||
body = self._read_body()
|
||||
self._log_req(body)
|
||||
path = body.get('path')
|
||||
fid = body.get('fid')
|
||||
name = fid if fid else body.get('name', '')
|
||||
fid = body.get('fid')
|
||||
parent_sel = body.get('parent_sel')
|
||||
mode = body.get('mode', 'raw')
|
||||
rs = app.rs
|
||||
@@ -1636,7 +1671,10 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
lchan = rs.lchan[0]
|
||||
try:
|
||||
sel = fid if fid else name
|
||||
_select_with_parent(lchan, sel, parent_sel, app)
|
||||
if path:
|
||||
_select_path(lchan, path, app)
|
||||
else:
|
||||
_select_with_parent(lchan, sel, parent_sel, app)
|
||||
ft = _get_file_type(lchan, lchan.selected_file)
|
||||
is_record = ft in ('linear_fixed', 'cyclic')
|
||||
if mode == 'decoded':
|
||||
|
||||
Reference in New Issue
Block a user