cards: per-target TARs (ISD/UICC/USIM) and an ADM field in presets (v2.6.0)
- the SCP80 fieldset gains ISD TAR (000000), UICC TAR (B00000) and USIM TAR (B00001), prefilled with the spec defaults; empty stored values mean the default for that target (cardsTarValue) - ADM field between "From card" and "Add", stored in the preset for later use by the file manager (placeholder ADM (optional), spaces stripped) - packing a chain selects the preset TAR for its target: RAM/GP -> ISD, SIM RFM -> UICC, USIM RFM -> USIM (spec default without a preset); _spTarKey keeps the target when the card preset is picked afterwards, RAM card selection forces the ISD TAR - USIM RFM 'from current DF' labelled (ADF.USIM); the list TAR column shows all three values; help EN/RU updated - SW cache simple-v199; new cards_form.test.js + html markup guards (440 frontend / 281 Python tests green)
This commit is contained in:
+10
-6
@@ -55,7 +55,7 @@
|
||||
|
||||
<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">+ SELECT</code> (или другую командную кнопку), чтобы добавить строку, заполните её поля — предпросмотр цепочки (над кнопкой упаковки) обновится автоматически. Добавьте строку <strong>GET RESPONSE</strong>, чтобы получить данные после SELECT. Кнопка <strong>Упаковать в Secured packet</strong> упаковывает всю цепочку в пакет SCP80.</p>
|
||||
<p class="mb-2">Команды собираются в виде <strong>цепочки</strong>: нажмите кнопку <code class="font-mono text-sm">+ SELECT</code> (или другую командную кнопку), чтобы добавить строку, заполните её поля — предпросмотр цепочки (над кнопкой упаковки) обновится автоматически. Добавьте строку <strong>GET RESPONSE</strong>, чтобы получить данные после SELECT. Кнопка <strong>Упаковать в Secured packet</strong> упаковывает всю цепочку в пакет SCP80 и устанавливает TAR SCP80 в <strong>UICC TAR</strong> выбранной предустановки (по умолчанию <code class="font-mono text-sm">B00000</code>). Текущий DF сессии SIM RFM — MF, поэтому пути SELECT адресуют файлы от MF.</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>
|
||||
@@ -95,7 +95,8 @@
|
||||
<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><strong>По пути</strong> предлагает выбор <strong>from MF</strong> (P1=<code class="font-mono text-sm">08</code>) или <strong>from current DF</strong> (P1=<code class="font-mono text-sm">09</code>); текущий DF сессии USIM RFM — ADF.USIM.</li>
|
||||
<li><strong>Упаковать в Secured packet</strong> устанавливает TAR SCP80 в <strong>USIM TAR</strong> выбранной предустановки (по умолчанию <code class="font-mono text-sm">B00001</code>).</li>
|
||||
<li>Каждый переход SELECT в цепочке запрашивает FCP, если не отмечен как silent.</li>
|
||||
</ul>
|
||||
|
||||
@@ -275,7 +276,7 @@
|
||||
<p class="text-sm mb-3">Кнопка <strong>Проверить в pySim</strong> сверяет собранный пакет с эталонной реализацией <code class="font-mono text-sm">OtaDialectSms.encode_cmd</code>. Кнопка <strong>Отправить на карту</strong> доставляет пакет через ENVELOPE SMS-PP-DOWNLOAD (при подключении к серверу). Полученный Proof of Receipt декодируется и показывается строкой статуса PoR (статус, TAR, счётчик, сырой PoR); статусное слово и данные ответа последней команды подставляются в подвкладку <strong>«Парсер ответов»</strong> (Remote APDU), а успешный PoR увеличивает счётчик повторов и очищает пакет.</p>
|
||||
|
||||
<h3 id="ram" class="text-lg font-medium mb-2">3.2 RAM</h3>
|
||||
<p class="mb-2">Выполняет операции удалённого управления приложениями (Remote Application Management) как защищённые пакеты SCP80 через SMS-PP-DOWNLOAD ENVELOPE. Карта должна поддерживать SCP03 (AES или 3DES). Предустановка карты со вкладки <strong>Карты</strong> обеспечивает SPI, ключи, TAR и счётчик.</p>
|
||||
<p class="mb-2">Выполняет операции удалённого управления приложениями (Remote Application Management) как защищённые пакеты SCP80 через SMS-PP-DOWNLOAD ENVELOPE. Карта должна поддерживать SCP03 (AES или 3DES). Предустановка карты со вкладки <strong>Карты</strong> обеспечивает SPI, ключи, TAR и счётчик (RAM использует <strong>ISD TAR</strong> предустановки, по умолчанию <code class="font-mono text-sm">000000</code>).</p>
|
||||
|
||||
<h4 id="ram-operations" class="font-medium mb-1">Операции</h4>
|
||||
<table class="w-full text-sm mb-3 border-collapse">
|
||||
@@ -298,22 +299,25 @@
|
||||
|
||||
<section class="mb-10">
|
||||
<h2 id="cards" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">4. Вкладка «Карты»</h2>
|
||||
<p class="mb-2">Хранит предустановки карт локально в браузере (<code class="font-mono text-sm">localStorage</code>), чтобы представление Secured Packet могло автоматически подставлять ключи и параметры, а слушатель SCP81 HTTP OTA — находить PSK-ключи. «Карты» — верхнеуровневая вкладка. При подключении карты читается её EF.ICCID, и предустановка с тем же ICCID автоматически выбирается в обоих представлениях SCP80. Поля формы сгруппированы в два блока с рамкой: <strong>SCP80 (GSM 03.48, ETSI TS 102 225)</strong> (KIc/KID, SPI1/SPI2, TAR, счётчик и ключи) и <strong>SCP81 (HTTP OTA)</strong> (пара PSK identity/ключ с пояснением о выборе ключа).</p>
|
||||
<p class="mb-2">Хранит предустановки карт локально в браузере (<code class="font-mono text-sm">localStorage</code>), чтобы представление Secured Packet могло автоматически подставлять ключи и параметры, а слушатель SCP81 HTTP OTA — находить PSK-ключи. «Карты» — верхнеуровневая вкладка. При подключении карты читается её EF.ICCID, и предустановка с тем же ICCID автоматически выбирается в обоих представлениях SCP80. Поля формы сгруппированы в два блока с рамкой: <strong>SCP80 (GSM 03.48, ETSI TS 102 225)</strong> (KIc/KID, SPI1/SPI2, TAR-ы ISD/UICC/USIM, счётчик и ключи) и <strong>SCP81 (HTTP OTA)</strong> (пара PSK identity/ключ с пояснением о выборе ключа). Необязательное поле <strong>ADM</strong> находится в верхней строке и сохраняется для последующего использования файловым менеджером.</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">Опциональный идентификатор карты. Кнопка <strong>С карты</strong> подставляет EF.ICCID подключённой карты (активна, только когда ICCID читается); при сохранении ICCID, уже присутствующий в списке, отклоняется (сравнение без пробелов и с учётом сырой hex-формы, конфликтующий пресет называется)</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">ADM</td><td class="py-1 px-2">Необязательный администраторский PIN (hex или до 8 ASCII-цифр), сохраняется в предустановке для последующего использования файловым менеджером; представлениями SCP80/SCP81 не используется</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">ISD TAR</td><td class="py-1 px-2">Toolkit Application Reference для операций ISD/RAM (представление SCP80 RAM, цепочки RAM/GP); по умолчанию по спецификации <code class="font-mono text-sm">000000</code>, подставляется при пустом значении</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">UICC TAR</td><td class="py-1 px-2">TAR для SIM RFM (TS 31.116); по умолчанию <code class="font-mono text-sm">B00000</code> — сессия SIM RFM выбирает файлы от MF</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">USIM TAR</td><td class="py-1 px-2">TAR для USIM RFM (TS 31.115); по умолчанию <code class="font-mono text-sm">B00001</code> — текущий DF сессии USIM RFM — ADF.USIM</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 байт); увеличивается и сохраняется после каждой успешной отправки SCP80</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><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>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">PSK identity</td><td class="py-1 px-2">SCP81 HTTP OTA: идентификатор, который карта присылает в TLS-рукопожатии (например, <code class="font-mono text-sm">89390100000129506903</code>)</td></tr>
|
||||
<tr><td class="py-1 px-2">PSK key</td><td class="py-1 px-2">SCP81 HTTP OTA: 32 hex-символа (16 байт) для PSK TLS-слушателя; ключ выбирается по идентификатору, который предъявляет карта</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-sm mb-3">Столбец <strong>SCP81</strong> показывает, задана ли в предустановке рабочая пара PSK: <strong>✓</strong> (зелёный) — идентификатор и ключ заданы; <strong>⚠</strong> (жёлтый) — задано только одно из двух, и слушатель такую предустановку игнорирует; <strong>—</strong> — PSK нет. Идентификатор и ключ PSK задаются вместе. Кнопка <strong>Редактировать</strong> загружает предустановку в форму (кнопка Add становится <strong>Сохранить</strong>; <strong>Отмена</strong> очищает форму), поэтому поля можно менять без повторного ввода карты; <strong>Удалить</strong> удаляет строку. Обмен предустановками: <strong>Экспорт в файл</strong> для выгрузки и <strong>Импорт из файла</strong> для загрузки. Выбранная предустановка автоматически заполняет форму Secured Packet; изменения сразу передаются работающему слушателю SCP81.</p>
|
||||
<p class="text-sm mb-3">Столбец <strong>SCP81</strong> показывает, задана ли в предустановке рабочая пара PSK: <strong>✓</strong> (зелёный) — идентификатор и ключ заданы; <strong>⚠</strong> (жёлтый) — задано только одно из двух, и слушатель такую предустановку игнорирует; <strong>—</strong> — PSK нет. Идентификатор и ключ PSK задаются вместе. Кнопка <strong>Редактировать</strong> загружает предустановку в форму (кнопка Add становится <strong>Сохранить</strong>; <strong>Отмена</strong> очищает форму), поэтому поля можно менять без повторного ввода карты; <strong>Удалить</strong> удаляет строку. Обмен предустановками: <strong>Экспорт в файл</strong> для выгрузки и <strong>Импорт из файла</strong> для загрузки. Выбранная предустановка автоматически заполняет форму Secured Packet и устанавливает TAR для операции (RAM/GP → ISD TAR, SIM RFM → UICC TAR, USIM RFM → USIM TAR); изменения сразу передаются работающему слушателю SCP81.</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-10">
|
||||
|
||||
+10
-6
@@ -55,7 +55,7 @@
|
||||
|
||||
<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 command button (e.g. <code class="font-mono text-sm">+ SELECT</code>) 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>
|
||||
<p class="mb-2">Commands are assembled as a <strong>chain</strong>: press a command button (e.g. <code class="font-mono text-sm">+ SELECT</code>) 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; packing sets the SCP80 TAR to the selected preset’s <strong>UICC TAR</strong> (<code class="font-mono text-sm">B00000</code> by default). A SIM RFM session’s current DF is MF, so SELECT paths address files from MF.</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>
|
||||
@@ -95,7 +95,8 @@
|
||||
<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><strong>By path</strong> offers <strong>from MF</strong> (P1=<code class="font-mono text-sm">08</code>) or <strong>from current DF</strong> (P1=<code class="font-mono text-sm">09</code>); the current DF of a USIM RFM session is ADF.USIM.</li>
|
||||
<li><strong>Pack into Secured packet</strong> sets the SCP80 TAR to the selected preset’s <strong>USIM TAR</strong> (<code class="font-mono text-sm">B00001</code> by default).</li>
|
||||
<li>Each SELECT hop in a chain requests FCP unless marked silent.</li>
|
||||
</ul>
|
||||
|
||||
@@ -275,7 +276,7 @@
|
||||
<p class="text-sm mb-3">A “Verify vs pySim” button cross-checks the assembled packet against pySim’s reference <code class="font-mono text-sm">OtaDialectSms.encode_cmd</code>. A “Send to Card” button delivers it via SMS-PP-DOWNLOAD ENVELOPE (when connected to the server). The returned Proof of Receipt is decoded and shown as a PoR status line (status, TAR, counter, raw PoR); the last command’s status word and response data are filled into the <strong>Response parser</strong> tab, and a successful PoR advances the replay counter and clears the packet.</p>
|
||||
|
||||
<h3 id="ram" class="text-lg font-medium mb-2">3.2 RAM</h3>
|
||||
<p class="mb-2">Delivers Remote Application Management operations as SCP80 secured packets via SMS-PP-DOWNLOAD ENVELOPE. The card must support SCP03 (AES or 3DES). A saved card preset from the <strong>Cards</strong> tab provides the SPI, keys, TAR, and counter.</p>
|
||||
<p class="mb-2">Delivers Remote Application Management operations as SCP80 secured packets via SMS-PP-DOWNLOAD ENVELOPE. The card must support SCP03 (AES or 3DES). A saved card preset from the <strong>Cards</strong> tab provides the SPI, keys, TAR, and counter (RAM uses the preset's <strong>ISD TAR</strong>, <code class="font-mono text-sm">000000</code> by default).</p>
|
||||
|
||||
<h4 id="ram-operations" class="font-medium mb-1">Operations</h4>
|
||||
<table class="w-full text-sm mb-3 border-collapse">
|
||||
@@ -298,22 +299,25 @@
|
||||
|
||||
<section class="mb-10">
|
||||
<h2 id="cards" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">4. Cards tab</h2>
|
||||
<p class="mb-2">Stores card presets locally in the browser (<code class="font-mono text-sm">localStorage</code>) so the Secured Packet view can auto-fill keys and parameters, and so the SCP81 HTTP OTA listener can look up PSK keys. Cards is a top-level tab. When the card is equipped its EF.ICCID is read and the preset with the same ICCID is selected automatically in both SCP80 views. The form groups the fields into two bordered blocks: <strong>SCP80 (GSM 03.48, ETSI TS 102 225)</strong> (KIc/KID, SPI1/SPI2, TAR, counter and keys) and <strong>SCP81 (HTTP OTA)</strong> (the PSK identity/key pair with the key-selection note).</p>
|
||||
<p class="mb-2">Stores card presets locally in the browser (<code class="font-mono text-sm">localStorage</code>) so the Secured Packet view can auto-fill keys and parameters, and so the SCP81 HTTP OTA listener can look up PSK keys. Cards is a top-level tab. When the card is equipped its EF.ICCID is read and the preset with the same ICCID is selected automatically in both SCP80 views. The form groups the fields into two bordered blocks: <strong>SCP80 (GSM 03.48, ETSI TS 102 225)</strong> (KIc/KID, SPI1/SPI2, the ISD/UICC/USIM TARs, counter and keys) and <strong>SCP81 (HTTP OTA)</strong> (the PSK identity/key pair with the key-selection note). The optional <strong>ADM</strong> field sits in the top row and is stored for later use by the file manager.</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">Field</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">Name</td><td class="py-1 px-2">Human-readable label (required)</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">Optional card identifier. <strong>From card</strong> fills it with the equipped card's EF.ICCID (enabled only while a readable ICCID is known); saving refuses an ICCID that already exists in the list (compared ignoring spaces and the raw-hex form, and the conflicting preset is named)</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">ADM</td><td class="py-1 px-2">Optional administrator PIN (hex, or up to 8 ASCII digits), stored in the preset for later use by the file manager; not used by the SCP80/SCP81 views</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">Key and algorithm indicators (e.g. 15 = index 1, 3DES-CBC2; x2 = AES), required together with the keys</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">ISD TAR</td><td class="py-1 px-2">Toolkit Application Reference for ISD/RAM operations (SCP80 RAM view, RAM/GP chains); spec default <code class="font-mono text-sm">000000</code>, prefilled when empty</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">UICC TAR</td><td class="py-1 px-2">TAR for SIM RFM (TS 31.116); spec default <code class="font-mono text-sm">B00000</code> — a SIM RFM session selects from MF</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">USIM TAR</td><td class="py-1 px-2">TAR for USIM RFM (TS 31.115); spec default <code class="font-mono text-sm">B00001</code> — a USIM RFM session's current DF is ADF.USIM</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">Replay counter (5 bytes); advanced and saved after each successful SCP80 send</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">KIc key / KID key</td><td class="py-1 px-2">16/24/32 hex chars (8/16/24-byte 3DES) or 32/48/64 hex chars (16/24/32-byte AES) keys</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">PSK identity</td><td class="py-1 px-2">SCP81 HTTP OTA: the identity the card sends in the TLS handshake (e.g. <code class="font-mono text-sm">89390100000129506903</code>)</td></tr>
|
||||
<tr><td class="py-1 px-2">PSK key</td><td class="py-1 px-2">SCP81 HTTP OTA: 32 hex chars (16 bytes) for the PSK TLS listener, picked by the identity the card presents</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-sm mb-3">The <strong>SCP81</strong> column shows whether the preset supplies a usable PSK pair: <strong>✓</strong> (green) — identity and key set; <strong>⚠</strong> (amber) — only one of the two, so the listener ignores the preset; <strong>—</strong> — no PSK. PSK identity and key must be set together. <strong>Edit</strong> loads a preset into the form (the Add button becomes <strong>Save</strong>; <strong>Cancel</strong> clears it) so fields can be changed without re-entering the card; <strong>Remove</strong> deletes the row. Presets can be shared with <strong>Export to file</strong> and restored with <strong>Import from file</strong>. The selected card preset auto-fills the Secured Packet form; edits are pushed into a running SCP81 listener automatically.</p>
|
||||
<p class="text-sm mb-3">The <strong>SCP81</strong> column shows whether the preset supplies a usable PSK pair: <strong>✓</strong> (green) — identity and key set; <strong>⚠</strong> (amber) — only one of the two, so the listener ignores the preset; <strong>—</strong> — no PSK. PSK identity and key must be set together. <strong>Edit</strong> loads a preset into the form (the Add button becomes <strong>Save</strong>; <strong>Cancel</strong> clears it) so fields can be changed without re-entering the card; <strong>Remove</strong> deletes the row. Presets can be shared with <strong>Export to file</strong> and restored with <strong>Import from file</strong>. The selected card preset auto-fills the Secured Packet form and sets the TAR for the operation (RAM/GP → ISD TAR, SIM RFM → UICC TAR, USIM RFM → USIM TAR); edits are pushed into a running SCP81 listener automatically.</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-10">
|
||||
|
||||
+63
-10
@@ -1220,6 +1220,7 @@
|
||||
<input id="cards-name" class="flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="Name (e.g. Foobar SIM)">
|
||||
<input id="cards-iccid" class="flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 font-mono" placeholder="ICCID (optional)">
|
||||
<button id="cards-iccid-from-card" data-needs="card-iccid" onclick="cardsIccidFromCard()" class="px-3 py-1.5 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 whitespace-nowrap disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="From card">From card</button>
|
||||
<input id="cards-adm" class="w-40 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 font-mono" placeholder="ADM (optional)">
|
||||
<button id="cards-add-btn" onclick="cardsAdd()" class="px-4 py-2.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700 whitespace-nowrap" data-l10n="Add">Add</button>
|
||||
<button id="cards-cancel-btn" onclick="cardsCancelEdit()" class="hidden px-4 py-2.5 bg-gray-500 text-white text-sm font-medium rounded hover:bg-gray-600 whitespace-nowrap" data-l10n="Cancel">Cancel</button>
|
||||
</div>
|
||||
@@ -1230,7 +1231,9 @@
|
||||
<input id="cards-kid" class="w-12 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono text-center" placeholder="KID" maxlength="2">
|
||||
<input id="cards-spi1" class="w-12 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono text-center" placeholder="SPI1" maxlength="2">
|
||||
<input id="cards-spi2" class="w-12 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono text-center" placeholder="SPI2" maxlength="2">
|
||||
<input id="cards-tar" class="w-20 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono" placeholder="ISD TAR" maxlength="6">
|
||||
<input id="cards-tar" class="w-20 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono" placeholder="ISD TAR" maxlength="6" value="000000">
|
||||
<input id="cards-uicc-tar" class="w-20 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono" placeholder="UICC TAR" maxlength="6" value="B00000">
|
||||
<input id="cards-usim-tar" class="w-20 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono" placeholder="USIM TAR" maxlength="6" value="B00001">
|
||||
<input id="cards-cntr" class="w-24 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono" placeholder="Counter" maxlength="10">
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@@ -1255,7 +1258,7 @@
|
||||
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700">KID</th>
|
||||
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700">SPI1</th>
|
||||
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700">SPI2</th>
|
||||
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700">TAR</th>
|
||||
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700">TAR (ISD/UICC/USIM)</th>
|
||||
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="Cntr">Cntr</th>
|
||||
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="SCP81">SCP81</th>
|
||||
<th class="border-b border-gray-200 dark:border-slate-700"></th>
|
||||
@@ -1384,7 +1387,7 @@
|
||||
// ===== Version =====
|
||||
// Single source of truth for the PWA version: shown in the header and used
|
||||
// by the server version check in pysimConnect().
|
||||
const SIMPLE_VERSION = '2.5.2';
|
||||
const SIMPLE_VERSION = '2.6.0';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== Tab switching =====
|
||||
@@ -2045,7 +2048,7 @@ function chainSimFieldsHtml(chainId, idx, f, uf) {
|
||||
'<input value="' + escHtml(f.path || '') + '" oninput="' + uf('path') + '" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="7FFF6FC5">';
|
||||
if (chainId === 'chain-usim') {
|
||||
html += '<div class="flex gap-3 mt-1 text-xs"><label class="flex items-center gap-1"><input type="radio" name="ch-usim-' + idx + '-base" value="mf"' + ((f.base || 'mf') === 'mf' ? ' checked' : '') + ' onchange="chainUpdateField(\'chain-usim\',' + idx + ',\'base\',\'mf\')"> from MF</label>' +
|
||||
'<label class="flex items-center gap-1"><input type="radio" name="ch-usim-' + idx + '-base" value="df"' + (f.base === 'df' ? ' checked' : '') + ' onchange="chainUpdateField(\'chain-usim\',' + idx + ',\'base\',\'df\')"> from current DF</label></div>';
|
||||
'<label class="flex items-center gap-1"><input type="radio" name="ch-usim-' + idx + '-base" value="df"' + (f.base === 'df' ? ' checked' : '') + ' onchange="chainUpdateField(\'chain-usim\',' + idx + ',\'base\',\'df\')"> from current DF (ADF.USIM)</label></div>';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
@@ -2855,12 +2858,36 @@ async function ramQueueScp81() {
|
||||
}
|
||||
}
|
||||
|
||||
// Which TAR of the selected preset feeds the SCP80 form: RAM/GP -> ISD,
|
||||
// SIM RFM -> UICC, USIM RFM -> USIM (spec defaults when the value is empty).
|
||||
let _spTarKey = 'tar';
|
||||
|
||||
function spTarKeyForPack(sourceId) {
|
||||
return {
|
||||
'chain-ram-preview': 'tar',
|
||||
'chain-sim-preview': 'uiccTar',
|
||||
'chain-usim-preview': 'usimTar',
|
||||
}[sourceId] || null;
|
||||
}
|
||||
|
||||
function spPresetTar(key) {
|
||||
const sel = document.getElementById('sp-card-sel');
|
||||
const idx = sel ? parseInt(sel.value, 10) : NaN;
|
||||
const c = (!isNaN(idx) && cards[idx]) ? cards[idx] : null;
|
||||
return cardsTarValue(key, c ? c[key] : '');
|
||||
}
|
||||
|
||||
function packToSp(sourceId) {
|
||||
const val = document.getElementById(sourceId).value;
|
||||
if (!val) return;
|
||||
switchTab('scp80');
|
||||
scp80SwitchSubtab('sp');
|
||||
document.getElementById('sp-apdu').value = val;
|
||||
const key = spTarKeyForPack(sourceId);
|
||||
if (key) {
|
||||
_spTarKey = key;
|
||||
document.getElementById('sp-tar').value = spPresetTar(key);
|
||||
}
|
||||
}
|
||||
function spCntrAdjust(delta) {
|
||||
const el = document.getElementById('sp-cntr');
|
||||
@@ -5318,6 +5345,7 @@ function ramApplyCard(idx) {
|
||||
// getRamSpParams() picks up the right SPI/KIc/KID/TAR/CNTR/keys
|
||||
const i = parseInt(idx, 10);
|
||||
if (!isNaN(i) && cards[i]) _ramCardIdx = i;
|
||||
_spTarKey = 'tar'; // RAM/GP operations target the ISD
|
||||
cardsApply(idx);
|
||||
}
|
||||
|
||||
@@ -6087,6 +6115,13 @@ function cardsPskName(identity) {
|
||||
return c ? c.name : '';
|
||||
}
|
||||
|
||||
// Spec-default TARs (GP ISD 000000, TS 31.116 UICC B00000, TS 31.115 USIM
|
||||
// B00001): an empty preset value means the default for that target.
|
||||
function cardsTarValue(key, value) {
|
||||
const defaults = { tar: '000000', uiccTar: 'B00000', usimTar: 'B00001' };
|
||||
return value || defaults[key] || '';
|
||||
}
|
||||
|
||||
function cardsFormValues() {
|
||||
const val = id => {
|
||||
const el = document.getElementById('cards-' + id);
|
||||
@@ -6095,11 +6130,14 @@ function cardsFormValues() {
|
||||
return {
|
||||
name: val('name'),
|
||||
iccid: val('iccid'),
|
||||
adm: val('adm').replace(/\s/g, '').toUpperCase(),
|
||||
kic: val('kic'),
|
||||
kid: val('kid'),
|
||||
spi1: val('spi1'),
|
||||
spi2: val('spi2'),
|
||||
tar: val('tar'),
|
||||
uiccTar: val('uicc-tar'),
|
||||
usimTar: val('usim-tar'),
|
||||
cntr: val('cntr'),
|
||||
kicKey: val('kic-key'),
|
||||
kidKey: val('kid-key'),
|
||||
@@ -6116,15 +6154,24 @@ function cardsSetFormMode() {
|
||||
}
|
||||
|
||||
function cardsClearForm() {
|
||||
['name','iccid','kic','kid','spi1','spi2','tar','cntr','kic-key','kid-key',
|
||||
['name','iccid','adm','kic','kid','spi1','spi2','cntr','kic-key','kid-key',
|
||||
'psk-id','psk-key'].forEach(id => {
|
||||
const el = document.getElementById('cards-' + id);
|
||||
if (el) el.value = '';
|
||||
});
|
||||
// TARs keep their per-target spec defaults instead of being emptied
|
||||
[['tar','tar'],['uicc-tar','uiccTar'],['usim-tar','usimTar']].forEach(([id, key]) => {
|
||||
const el = document.getElementById('cards-' + id);
|
||||
if (el) el.value = cardsTarValue(key, '');
|
||||
});
|
||||
}
|
||||
|
||||
function cardsAdd() {
|
||||
const v = cardsFormValues();
|
||||
// empty TAR fields fall back to the per-target spec defaults
|
||||
v.tar = cardsTarValue('tar', v.tar);
|
||||
v.uiccTar = cardsTarValue('uiccTar', v.uiccTar);
|
||||
v.usimTar = cardsTarValue('usimTar', v.usimTar);
|
||||
if (!v.name) { alert(t('Name is required')); return; }
|
||||
if (!v.kic || !v.kid) { alert('KIc and KID are required'); return; }
|
||||
if (!v.kicKey || !v.kidKey) { alert('KIc key and KID key are required'); return; }
|
||||
@@ -6162,8 +6209,11 @@ function cardsEdit(i) {
|
||||
const el = document.getElementById('cards-' + id);
|
||||
if (el) el.value = value == null ? '' : value;
|
||||
};
|
||||
set('name', c.name); set('iccid', c.iccid); set('kic', c.kic); set('kid', c.kid);
|
||||
set('spi1', c.spi1); set('spi2', c.spi2); set('tar', c.tar); set('cntr', c.cntr);
|
||||
set('name', c.name); set('iccid', c.iccid); set('adm', c.adm); set('kic', c.kic); set('kid', c.kid);
|
||||
set('spi1', c.spi1); set('spi2', c.spi2); set('cntr', c.cntr);
|
||||
set('tar', cardsTarValue('tar', c.tar));
|
||||
set('uicc-tar', cardsTarValue('uiccTar', c.uiccTar));
|
||||
set('usim-tar', cardsTarValue('usimTar', c.usimTar));
|
||||
set('kic-key', c.kicKey); set('kid-key', c.kidKey);
|
||||
set('psk-id', c.pskIdentity); set('psk-key', c.pskKey);
|
||||
cardsSetFormMode();
|
||||
@@ -6213,7 +6263,7 @@ function cardsRender() {
|
||||
html += '<td class="py-1 px-2 font-mono text-xs text-center">' + esc(c.kid) + '</td>';
|
||||
html += '<td class="py-1 px-2 font-mono text-xs text-center">' + esc(c.spi1) + '</td>';
|
||||
html += '<td class="py-1 px-2 font-mono text-xs text-center">' + esc(c.spi2) + '</td>';
|
||||
html += '<td class="py-1 px-2 font-mono text-xs">' + esc(c.tar || '') + '</td>';
|
||||
html += '<td class="py-1 px-2 font-mono text-xs" title="ISD / UICC / USIM">' + esc(cardsTarValue('tar', c.tar)) + ' ' + esc(cardsTarValue('uiccTar', c.uiccTar)) + ' ' + esc(cardsTarValue('usimTar', c.usimTar)) + '</td>';
|
||||
html += '<td class="py-1 px-2 font-mono text-xs">' + esc(c.cntr) + '</td>';
|
||||
html += '<td class="py-1 px-2 text-center">' + ota + '</td>';
|
||||
html += '<td class="py-1 px-2 whitespace-nowrap"><button onclick="cardsEdit(' + i + ')" class="px-2 py-0.5 text-xs rounded bg-blue-600 text-white hover:bg-blue-700">' + t('Edit') + '</button> '
|
||||
@@ -6306,7 +6356,7 @@ function cardsApply(idx) {
|
||||
document.getElementById('sp-kid-idx').value = ((kidByte >> 4) & 0x0F).toString(16).toUpperCase();
|
||||
document.getElementById('sp-kid-alg').value = (kidByte & 0x0F).toString(16).padStart(2, '0');
|
||||
updateSpKid();
|
||||
document.getElementById('sp-tar').value = c.tar || 'B00001';
|
||||
document.getElementById('sp-tar').value = cardsTarValue(_spTarKey, c[_spTarKey]);
|
||||
document.getElementById('sp-cntr').value = c.cntr;
|
||||
document.getElementById('sp-kic-key').value = c.kicKey;
|
||||
document.getElementById('sp-kid-key').value = c.kidKey;
|
||||
@@ -6349,11 +6399,14 @@ function cardsImport(text) {
|
||||
cards.push({
|
||||
name: entry.name,
|
||||
iccid: entry.iccid,
|
||||
adm: (entry.adm || '').replace(/\s/g, '').toUpperCase(),
|
||||
kic: entry.kic || '15',
|
||||
kid: entry.kid || '15',
|
||||
spi1: entry.spi1 || '16',
|
||||
spi2: entry.spi2 || '01',
|
||||
tar: entry.tar || '',
|
||||
tar: cardsTarValue('tar', entry.tar),
|
||||
uiccTar: cardsTarValue('uiccTar', entry.uiccTar),
|
||||
usimTar: cardsTarValue('usimTar', entry.usimTar),
|
||||
cntr: entry.cntr || '0000000001',
|
||||
kicKey: entry.kicKey || '',
|
||||
kidKey: entry.kidKey || '',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v198';
|
||||
const CACHE = 'simple-v199';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
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);
|
||||
}
|
||||
|
||||
let code = '';
|
||||
for (const fn of ['cardsTarValue', 'cardsFormValues', 'cardsClearForm', 'cardsEdit',
|
||||
'cardsAdd', 'cardsImport', 'cardsApply', 'ramApplyCard',
|
||||
'spTarKeyForPack', 'spPresetTar', 'packToSp']) {
|
||||
code += extractFunc(html, fn) + '\n';
|
||||
}
|
||||
eval(code);
|
||||
|
||||
const CARD_IDS = ['cards-name','cards-iccid','cards-adm','cards-kic','cards-kid',
|
||||
'cards-spi1','cards-spi2','cards-tar','cards-uicc-tar','cards-usim-tar',
|
||||
'cards-cntr','cards-kic-key','cards-kid-key','cards-psk-id','cards-psk-key',
|
||||
'cards-add-btn','cards-cancel-btn'];
|
||||
const SP_IDS = ['sp-card-sel','sp-tar','sp-spi1','sp-spi2','sp-spi2-sm','sp-spi2-hex',
|
||||
'sp-kic-idx','sp-kic-alg','sp-kic-hex','sp-kid-idx','sp-kid-alg','sp-kid-hex',
|
||||
'sp-cntr','sp-kic-key','sp-kid-key','sp-apdu','sp-result','sp-spi1-hex'];
|
||||
const CHAIN_IDS = ['chain-sim-preview','chain-usim-preview','chain-ram-preview',
|
||||
'ber-result','hota-preview'];
|
||||
|
||||
function fakeEl() {
|
||||
return {
|
||||
value: '',
|
||||
textContent: '',
|
||||
innerHTML: '',
|
||||
focus() {},
|
||||
classList: { add() {}, remove() {}, toggle() {}, contains: () => false },
|
||||
setAttribute() {}, removeAttribute() {},
|
||||
};
|
||||
}
|
||||
|
||||
function setup() {
|
||||
const els = {};
|
||||
for (const id of CARD_IDS.concat(SP_IDS, CHAIN_IDS)) els[id] = fakeEl();
|
||||
globalThis.document = { getElementById: id => els[id] || null, querySelectorAll: () => [] };
|
||||
globalThis.cards = [];
|
||||
globalThis._cardsEditIdx = null;
|
||||
globalThis._spTarKey = 'tar';
|
||||
globalThis.t = s => s;
|
||||
globalThis.alert = () => {};
|
||||
globalThis.cardsSave = () => {};
|
||||
globalThis.cardsRender = () => {};
|
||||
globalThis.cardsRebuildSelect = () => {};
|
||||
globalThis.scp81PushPskMap = () => {};
|
||||
globalThis.cardsSetFormMode = () => {};
|
||||
globalThis.cardsCancelEdit = () => {};
|
||||
globalThis.cardsFindDuplicateIccid = () => -1;
|
||||
globalThis.ioStatus = () => {};
|
||||
globalThis.switchTab = () => {};
|
||||
globalThis.scp80SwitchSubtab = () => {};
|
||||
globalThis.updateSpKic = () => {};
|
||||
globalThis.updateSpKid = () => {};
|
||||
globalThis.spInvalidate = () => {};
|
||||
globalThis.updateSp = () => {};
|
||||
globalThis.genSp = () => {};
|
||||
return els;
|
||||
}
|
||||
|
||||
test('cardsTarValue falls back to the per-target spec defaults', () => {
|
||||
assert.strictEqual(cardsTarValue('tar', '123456'), '123456');
|
||||
assert.strictEqual(cardsTarValue('tar', ''), '000000');
|
||||
assert.strictEqual(cardsTarValue('uiccTar', ''), 'B00000');
|
||||
assert.strictEqual(cardsTarValue('usimTar', null), 'B00001');
|
||||
assert.strictEqual(cardsTarValue('nope', ''), '');
|
||||
});
|
||||
|
||||
test('clearing the card form keeps the TAR spec defaults', () => {
|
||||
const els = setup();
|
||||
els['cards-tar'].value = 'AABBCC';
|
||||
els['cards-uicc-tar'].value = 'AABBCC';
|
||||
els['cards-usim-tar'].value = 'AABBCC';
|
||||
els['cards-adm'].value = 'DEAD';
|
||||
els['cards-name'].value = 'X';
|
||||
cardsClearForm();
|
||||
assert.strictEqual(els['cards-tar'].value, '000000');
|
||||
assert.strictEqual(els['cards-uicc-tar'].value, 'B00000');
|
||||
assert.strictEqual(els['cards-usim-tar'].value, 'B00001');
|
||||
assert.strictEqual(els['cards-adm'].value, '');
|
||||
assert.strictEqual(els['cards-name'].value, '');
|
||||
});
|
||||
|
||||
test('editing prefills empty stored TARs with the spec defaults', () => {
|
||||
const els = setup();
|
||||
globalThis.cards = [
|
||||
{ name: 'Legacy', kic: '15', kid: '15', tar: '' },
|
||||
{ name: 'Full', kic: '15', kid: '15', tar: '000001', uiccTar: 'B0000C', usimTar: 'B0000D', adm: '0011' },
|
||||
];
|
||||
cardsEdit(0);
|
||||
assert.strictEqual(els['cards-tar'].value, '000000');
|
||||
assert.strictEqual(els['cards-uicc-tar'].value, 'B00000');
|
||||
assert.strictEqual(els['cards-usim-tar'].value, 'B00001');
|
||||
assert.strictEqual(els['cards-adm'].value, '');
|
||||
cardsEdit(1);
|
||||
assert.strictEqual(els['cards-tar'].value, '000001');
|
||||
assert.strictEqual(els['cards-uicc-tar'].value, 'B0000C');
|
||||
assert.strictEqual(els['cards-usim-tar'].value, 'B0000D');
|
||||
assert.strictEqual(els['cards-adm'].value, '0011');
|
||||
});
|
||||
|
||||
test('the form values carry the ADM and per-target TAR fields', () => {
|
||||
const els = setup();
|
||||
els['cards-tar'].value = ' 000001 ';
|
||||
els['cards-uicc-tar'].value = 'B00002';
|
||||
els['cards-usim-tar'].value = 'B00003';
|
||||
els['cards-adm'].value = ' 00 11 22 33 ';
|
||||
const v = cardsFormValues();
|
||||
assert.strictEqual(v.tar, '000001');
|
||||
assert.strictEqual(v.uiccTar, 'B00002');
|
||||
assert.strictEqual(v.usimTar, 'B00003');
|
||||
assert.strictEqual(v.adm, '00112233');
|
||||
});
|
||||
|
||||
test('saving a preset stores spec defaults for empty TARs', () => {
|
||||
const els = setup();
|
||||
els['cards-name'].value = 'New card';
|
||||
els['cards-kic'].value = '15';
|
||||
els['cards-kid'].value = '15';
|
||||
els['cards-kic-key'].value = 'AA';
|
||||
els['cards-kid-key'].value = 'BB';
|
||||
els['cards-tar'].value = '';
|
||||
els['cards-uicc-tar'].value = '';
|
||||
els['cards-usim-tar'].value = '';
|
||||
els['cards-adm'].value = 'aa bb';
|
||||
cardsAdd();
|
||||
assert.strictEqual(cards.length, 1);
|
||||
assert.strictEqual(cards[0].tar, '000000');
|
||||
assert.strictEqual(cards[0].uiccTar, 'B00000');
|
||||
assert.strictEqual(cards[0].usimTar, 'B00001');
|
||||
assert.strictEqual(cards[0].adm, 'AABB');
|
||||
assert.strictEqual(cards[0].spi1, '16');
|
||||
});
|
||||
|
||||
test('import fills missing TARs with spec defaults and normalizes ADM', () => {
|
||||
setup();
|
||||
cardsImport(JSON.stringify([
|
||||
{ name: 'Imported', kic: '15', kid: '15', adm: ' aa bb ' },
|
||||
{ name: 'Explicit', tar: '000009', uiccTar: 'B0000C', usimTar: 'B0000D', adm: 'cc' },
|
||||
]));
|
||||
assert.strictEqual(cards.length, 2);
|
||||
assert.strictEqual(cards[0].tar, '000000');
|
||||
assert.strictEqual(cards[0].uiccTar, 'B00000');
|
||||
assert.strictEqual(cards[0].usimTar, 'B00001');
|
||||
assert.strictEqual(cards[0].adm, 'AABB');
|
||||
assert.strictEqual(cards[1].tar, '000009');
|
||||
assert.strictEqual(cards[1].uiccTar, 'B0000C');
|
||||
assert.strictEqual(cards[1].usimTar, 'B0000D');
|
||||
assert.strictEqual(cards[1].adm, 'CC');
|
||||
});
|
||||
|
||||
test('applying a preset uses the TAR of the current operation', () => {
|
||||
const els = setup();
|
||||
globalThis.cards = [{
|
||||
name: 'C', kic: '15', kid: '15', spi1: '16', spi2: '01',
|
||||
tar: '000000', uiccTar: 'B0000C', usimTar: 'B0000D',
|
||||
cntr: '0000000001', kicKey: 'AA', kidKey: 'BB',
|
||||
}];
|
||||
globalThis._spTarKey = 'tar';
|
||||
cardsApply(0);
|
||||
assert.strictEqual(els['sp-tar'].value, '000000');
|
||||
globalThis._spTarKey = 'uiccTar';
|
||||
cardsApply(0);
|
||||
assert.strictEqual(els['sp-tar'].value, 'B0000C');
|
||||
globalThis._spTarKey = 'usimTar';
|
||||
cardsApply(0);
|
||||
assert.strictEqual(els['sp-tar'].value, 'B0000D');
|
||||
// empty stored TAR -> spec default
|
||||
globalThis.cards = [{ name: 'D', kic: '15', kid: '15', tar: '' }];
|
||||
globalThis._spTarKey = 'tar';
|
||||
cardsApply(0);
|
||||
assert.strictEqual(els['sp-tar'].value, '000000');
|
||||
});
|
||||
|
||||
test('RAM card selection forces the ISD TAR', () => {
|
||||
const els = setup();
|
||||
globalThis.cards = [{ name: 'C', kic: '15', kid: '15', tar: '000000', uiccTar: 'B0000C', usimTar: 'B0000D' }];
|
||||
globalThis._spTarKey = 'usimTar';
|
||||
ramApplyCard('0');
|
||||
assert.strictEqual(globalThis._spTarKey, 'tar');
|
||||
assert.strictEqual(els['sp-tar'].value, '000000');
|
||||
});
|
||||
|
||||
test('spTarKeyForPack maps each chain to its preset TAR field', () => {
|
||||
assert.strictEqual(spTarKeyForPack('chain-ram-preview'), 'tar');
|
||||
assert.strictEqual(spTarKeyForPack('chain-sim-preview'), 'uiccTar');
|
||||
assert.strictEqual(spTarKeyForPack('chain-usim-preview'), 'usimTar');
|
||||
assert.strictEqual(spTarKeyForPack('ber-result'), null);
|
||||
assert.strictEqual(spTarKeyForPack('hota-preview'), null);
|
||||
});
|
||||
|
||||
test('spPresetTar reads the selected preset and falls back to the spec defaults', () => {
|
||||
const els = setup();
|
||||
globalThis.cards = [{ name: 'A', tar: '000000', uiccTar: 'B0000A' }];
|
||||
els['sp-card-sel'].value = '0';
|
||||
assert.strictEqual(spPresetTar('uiccTar'), 'B0000A');
|
||||
assert.strictEqual(spPresetTar('usimTar'), 'B00001');
|
||||
els['sp-card-sel'].value = '';
|
||||
assert.strictEqual(spPresetTar('uiccTar'), 'B00000');
|
||||
});
|
||||
|
||||
test('packing a chain selects the TAR of its target', () => {
|
||||
const els = setup();
|
||||
globalThis.cards = [{ name: 'C', tar: '000000', uiccTar: 'B0000C', usimTar: 'B0000D' }];
|
||||
els['sp-card-sel'].value = '0';
|
||||
els['chain-sim-preview'].value = 'A0A4000002';
|
||||
packToSp('chain-sim-preview');
|
||||
assert.strictEqual(globalThis._spTarKey, 'uiccTar');
|
||||
assert.strictEqual(els['sp-tar'].value, 'B0000C');
|
||||
assert.strictEqual(els['sp-apdu'].value, 'A0A4000002');
|
||||
els['chain-usim-preview'].value = '00A4000002';
|
||||
packToSp('chain-usim-preview');
|
||||
assert.strictEqual(globalThis._spTarKey, 'usimTar');
|
||||
assert.strictEqual(els['sp-tar'].value, 'B0000D');
|
||||
els['chain-ram-preview'].value = '80E29000';
|
||||
packToSp('chain-ram-preview');
|
||||
assert.strictEqual(globalThis._spTarKey, 'tar');
|
||||
assert.strictEqual(els['sp-tar'].value, '000000');
|
||||
// BER / HTTP OTA packs leave the TAR alone
|
||||
els['sp-tar'].value = 'ABCDEF';
|
||||
els['ber-result'].value = 'AA0100';
|
||||
packToSp('ber-result');
|
||||
assert.strictEqual(els['sp-tar'].value, 'ABCDEF');
|
||||
assert.strictEqual(els['sp-apdu'].value, 'AA0100');
|
||||
});
|
||||
|
||||
test('packing without a selected preset uses the spec default TAR', () => {
|
||||
const els = setup();
|
||||
els['chain-sim-preview'].value = 'A0A4000002';
|
||||
packToSp('chain-sim-preview');
|
||||
assert.strictEqual(els['sp-tar'].value, 'B00000');
|
||||
els['chain-usim-preview'].value = '00A4000002';
|
||||
packToSp('chain-usim-preview');
|
||||
assert.strictEqual(els['sp-tar'].value, 'B00001');
|
||||
els['chain-ram-preview'].value = '80E29000';
|
||||
packToSp('chain-ram-preview');
|
||||
assert.strictEqual(els['sp-tar'].value, '000000');
|
||||
});
|
||||
@@ -33,6 +33,9 @@ test('the card form groups the SCP80 and SCP81 fields into labelled fieldsets',
|
||||
assert.ok(scp81, 'SCP81 card fieldset not found');
|
||||
assert.match(scp80, /id="cards-kic"/);
|
||||
assert.match(scp80, /id="cards-kid-key"/);
|
||||
assert.match(scp80, /id="cards-tar"[^>]*value="000000"/);
|
||||
assert.match(scp80, /id="cards-uicc-tar"[^>]*value="B00000"/);
|
||||
assert.match(scp80, /id="cards-usim-tar"[^>]*value="B00001"/);
|
||||
assert.match(scp81, /id="cards-psk-id"/);
|
||||
assert.match(scp81, /id="cards-psk-key"/);
|
||||
// the PSK explanation lives inside the SCP81 group, not outside it
|
||||
@@ -41,6 +44,17 @@ test('the card form groups the SCP80 and SCP81 fields into labelled fieldsets',
|
||||
assert.ok(!fieldsets.some(f => f.includes('id="cards-iccid"')));
|
||||
});
|
||||
|
||||
test('the ADM field sits between From card and Add, outside the card fieldsets', () => {
|
||||
const fieldsets = [...html.matchAll(/<fieldset[\s\S]*?<\/fieldset>/g)].map(m => m[0]);
|
||||
assert.ok(!fieldsets.some(f => f.includes('id="cards-adm"')));
|
||||
assert.match(html, /id="cards-adm"[^>]*placeholder="ADM \(optional\)"/);
|
||||
const fromIdx = html.indexOf('id="cards-iccid-from-card"');
|
||||
const admIdx = html.indexOf('id="cards-adm"');
|
||||
const addIdx = html.indexOf('id="cards-add-btn"');
|
||||
assert.ok(fromIdx >= 0 && fromIdx < admIdx && admIdx < addIdx,
|
||||
'ADM input must sit between the From card and Add buttons');
|
||||
});
|
||||
|
||||
test('labelled containers use fieldsets with embedded legends', () => {
|
||||
const legendCls = '<legend class="px-1 text-xs font-medium text-gray-500 dark:text-slate-400"';
|
||||
for (const label of ['STK menu', 'STATUS and Polling', 'TERMINAL PROFILE',
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-simple-server"
|
||||
version = "2.5.2"
|
||||
version = "2.6.0"
|
||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||
requires-python = ">=3.8"
|
||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||
|
||||
@@ -24,7 +24,7 @@ from osmocom.construct import GsmOrUcs2Adapter
|
||||
from osmocom.tlv import BER_TLV_IE
|
||||
|
||||
|
||||
VERSION = '2.5.2'
|
||||
VERSION = '2.6.0'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
|
||||
Reference in New Issue
Block a user