feat: TAR probe becomes its own SCP80 pill with a TAR checklist (v3.8.0)
The probe is not a RAM operation, so it moves out of the RAM view into a third SCP80 pill (Secured packet | RAM | TAR probe, RU "TAR-сканер"), with its own preset and key-version selectors, its own equipped-card mismatch banner and its own result line; the help gains a "3.3 TAR probe" section and the anchor map follows the pill. The TAR field is a checklist now: pre-populated with the standard allocations of TS 101 220 V18.3.0 Annex D, Table D.1 (each row carrying the document it comes from, all checked by default, Select all / Select none), plus custom TARs added through a small form (three hex bytes + an optional description, removed with a x). Custom entries live in localStorage['simple_tar_probe'] on a best-effort basis - losing them is harmless, the standard list is always there. Only the checked TARs are sent; nothing checked is refused with a message. A Python test guards the PWA's list against the server's TAR_PROBE_TARS (the endpoint keeps its own default for API callers). RAM leftovers removed (the tar-probe operation entry, the #ram-tar-rows block, the ramExecute dispatch, ramTarProbe); getRamSpParams() takes the view's selector id; tarRender/tarApplyCard/tarKeysetChanged/tarKeysetGuard mirror the RAM view; spKeysetSync and spPresetWarningRender cover the new selectors. sw cache simple-v338. Tests: 740 Node (the checklist rows, the custom round-trip, the checked-list read, the renderer, the pill wiring) and 619 Python (the list drift guard).
This commit is contained in:
@@ -323,7 +323,6 @@
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Обзор карты (все данные GP)</td><td class="py-1 px-2">Запрос GET STATUS для ISD, приложений, ELF и модулей ELF, а также GET DATA FF21 (расширенные ресурсы карты: число установленных приложений и свободная энергонезависимая/энергозависимая память). Компактный листинг содержит только AID, состояние жизненного цикла и привилегии (версии в устаревшем формате нет); если карта отвергает его (<code class="font-mono text-sm">P2.b2=0</code>, устаревший по GP), запросы повторяются в расширенном формате <code class="font-mono text-sm">E3</code>, а флажок <strong>Запрашивать расширенные данные реестра (связи load-файла и SD)</strong> дополнительно запрашивает их и объединяет расширенные поля: версию исполняемого load-файла, AID load-файла, AID Security Domain, списки модулей, пункты меню STK апплета (SCP Registry Data, TS 102 226 8.2.1.6) и любые другие объекты реестра, которые вернула карта (показываются как есть, без потерь) — он стоит несколько дополнительных защищённых пакетов на каждый обзор и <strong>включён по умолчанию</strong> (запоминается в браузере; снимите флажок, чтобы пропустить). Карты различаются в том, что принимают, поэтому расширенный запрос пробуется в трёх формах — эталонный список тегов (с <code class="font-mono text-sm">EA</code> по TS 102 226), тот же список без <code class="font-mono text-sm">EA</code> и вовсе без списка тегов (форма GlobalPlatformPro) — а обзор сообщает, какая форма сработала и где выполнен откат к компактному списку (версии пакетов тогда недоступны). Результаты отображаются в обзоре (строка успеха минимальна, счётчики — в заголовках разделов) с кнопками <strong>Удалить</strong> для каждого элемента и определённым форматом команд RAM (подсказка при наведении объясняет compact и expanded).</td></tr>
|
||||
<tr><td class="py-1 px-2">Установка пакета (.cap файл)</td><td class="py-1 px-2">Отправка <code class="font-mono text-sm">.cap</code> файла на карту через сервер: INSTALL[for load] → LOAD ×N → INSTALL[for install (+make selectable)]. Load-файл делится на LOAD APDU размера <strong>размер блока LOAD</strong> (1–240 байт полезной нагрузки, по умолчанию 240); каждый защищённый пакет доставляется одним SMS или, если он больше, конкатенированной SMS-PP загрузкой до 5 сегментов, так что большой <code>.cap</code> просто занимает несколько SMS. Сразу после выбора файл проверяется и в форме показывается <strong>оценка требований к памяти</strong>: <strong>требование NVRAM</strong> (образ кода + постоянные данные, сумма в стиле C6+C8 из GP Card Spec) и оценка RAM. Там же показаны <strong>библиотеки, с которыми слинкован CAP</strong> (его компонент Import: имя библиотеки, AID и минимальная версия <code class="font-mono text-sm">≥ x.y</code> — имена стандартных библиотек и семейства подставляются, а где известно, даётся подсказка о версии Java Card), идентификаторы пакета и апплетов и разбивка по компонентам (размеры и доли в порядке load-файла) — всё сгруппировано по разделам <strong>Package</strong>, <strong>Requires</strong>, <strong>Memory</strong> и <strong>Components</strong>. Строка <strong>Platform</strong> показывает минимальный уровень Java Card, который должна предоставить карта (выводится из импорта <code class="font-mono text-sm">javacard.framework</code>; флаг <code class="font-mono text-sm">int</code> в заголовке дополнительно требует поддержки int — Java Card 2.2+), и <strong>релиз ETSI</strong>, из которого пришли импорты UICC API (версия <code class="font-mono text-sm">uicc.toolkit</code> → REL-N по корпусу <code class="font-mono text-sm">sim-tools</code> в рабочем каталоге; основание показано в скобках): JCRE проверяет каждый импорт по своим пакетам при верификации load-файла (JC VM spec 4.5.2), поэтому карта со старой платформой отклоняет LOAD сразу после завершения компонента <strong>Import</strong> (в строке шага виден remote SW карты и компонент, который завершил блок, например <em>завершает Import</em>) — меньший размер блока LOAD помогает только если блок превышает буфер карты. Повреждённый файл не проходит анализ, и <strong>Execute</strong> остаётся недоступной до успешного анализа. Оценка носит справочный характер и никогда не используется при установке: NVRAM — это образ кода плюс постоянные данные (сумма в стиле C6+C8 по GP Table 11-48; управление памятью карты, округление и запись в реестре не учитываются, а образ статических полей входит в обе части); рекомендуемый <strong>C6</strong> следует за load-файлом, а рекомендуемые квоты — это десятичные значения в байтах (шестнадцатеричное значение в скобках — это кодирование параметров установки C7/C8: C6 — энергонезависимый код, C7 — энергозависимая RAM, C8 — энергонезависимые данные).</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Проверка совместимости CAP</td><td class="py-1 px-2">Отвечает на один вопрос, ничего не устанавливая: <strong>может ли эта карта удовлетворить список импортов CAP?</strong> Сервер выполняет проверку формата, INSTALL[for load] и блоки LOAD только до блока, завершающего компонент <strong>Import</strong> — именно там JCRE проверяет список импортов — и останавливается: без флага последнего блока и без INSTALL[for install], поэтому ничего не фиксируется и удалять после этого ничего не нужно. Список <strong>«Версии импортов для проверки»</strong> предзаполняется из анализа CAP: <code class="font-mono text-sm">AID=version</code> переписывает запрошенную версию импорта, <code class="font-mono text-sm">+AID=version</code> добавляет синтетический импорт (запрос возможностей карты: LOAD проходит, только если карта экспортирует этот пакет версии ≥ указанной), а <strong>«Все 0.0»</strong> снимает все требования. Использование в два шага: нажмите «Все 0.0» — если проверка проходит, причиной был список импортов; затем возвращайте по одной строке, чтобы найти пакет и версию, которых не хватает карте. Вердикт явно указывает, что проверяется только <strong>этап LOAD/импортов</strong>: этап связывания (наличие реально вызываемых API) проверяет настоящая установка на INSTALL[for install]. Запуск и результат показываются в окне (как при установке): строки шагов, вердикт и блок деталей по каждому AID — при отказе он называет раздел CAP, который покрывает неудачный блок (диапазон байтов load-файла и компоненты, и завершает ли этот блок <strong>Import</strong>), SW команды с его значением, список импортов ровно в том виде, в каком он проверялся (версии CAP, правки пробы, добавленные синтетические записи, строки без совпадений) и что означает состояние пробы; при успехе — список импортов, который прошёл проверку.</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Проба TAR (зарегистрированные OTA-приложения)</td><td class="py-1 px-2">Отправляет <strong>по одному защищённому пакету на каждый TAR</strong> — стандартные значения из TS 101 220 Annex D, Table D.1, если список TAR не задан — с безобидной C-APDU (по умолчанию <code class="font-mono text-sm">SELECT MF</code>), набором ключей предустановки и SPI <code class="font-mono text-sm">16/01</code> (проверка счётчика + PoR), и классифицирует ответ карты: <strong>зарегистрировано</strong> (ENVELOPE 9000 + PoR со SW/данными приложения), <strong>принято, нет PoR</strong> (пакет молча отброшен), <strong>отклонено</strong> (обычно <code class="font-mono text-sm">6200</code>). Полезно для картирования OTA-приложений карты. Каждый принятый пакет расходует счётчик, который сохраняется в набор ключей — проба отклоняется, если в SPI1 нет проверки счётчика.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -340,6 +339,12 @@
|
||||
|
||||
</section>
|
||||
<section class="mb-10">
|
||||
<h3 id="tar-probe" class="text-lg font-medium mb-2">3.3 TAR-сканер</h3>
|
||||
<p class="mb-2">Третья пилюля вкладки SCP80 показывает, <strong>какие OTA-приложения зарегистрированы на карте</strong>: отправляется <strong>по одному защищённому пакету на каждый выбранный TAR</strong> — стандартные значения из TS 101 220 V18.3.0 Annex D, Table D.1, каждая строка несёт документ-источник — с безобидной C-APDU (по умолчанию <code class="font-mono text-sm">SELECT MF</code>), предустановкой и набором ключей этого представления, SPI <code class="font-mono text-sm">16/01</code> (проверка счётчика + PoR).</p>
|
||||
<p class="mb-2">Список предзаполнен стандартными TAR (все отмечены; кнопки <strong>Выбрать все</strong> / <strong>Снять все</strong> переключают быстро), свои TAR добавляются кнопкой <strong>Добавить TAR</strong> (три hex-байта и необязательное описание). Пользовательские записи хранятся в <code class="font-mono text-sm">localStorage</code> по возможности — их потеря безвредна.</p>
|
||||
<p class="mb-2">Каждый TAR классифицируется по ответу карты: <strong>зарегистрировано</strong> (ENVELOPE 9000 + PoR со SW и данными приложения), <strong>принято, нет ответа</strong> (PoR без R-APDU), <strong>принято, нет PoR</strong> (пакет молча отброшен) и <strong>отклонено</strong> (ENVELOPE не 9000, обычно <code class="font-mono text-sm">6200</code> для TAR без зарегистрированного приложения). Таблица результата появляется в окне операции; строка под кнопкой Execute показывает <code class="font-mono text-sm">зарегистрировано N / M</code>.</p>
|
||||
<p class="mb-2">Каждый принятый картой пакет <strong>расходует счётчик</strong>, который сохраняется в набор ключей предустановки — проба отклоняется, если в SPI1 нет проверки счётчика или в предустановке нет указанной версии ключа. На живой карте: <code class="font-mono text-sm">000000</code>, <code class="font-mono text-sm">B00000</code> и <code class="font-mono text-sm">B00001</code> зарегистрированы (приложения компактного формата), расширенный формат, USAT, мультиплексирование и CASD отклонены с <code class="font-mono text-sm">6200</code>.</p>
|
||||
|
||||
<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">~/.pysim-simple-server/card_presets.json</code>; переопределяется <code class="font-mono text-sm">--card-presets</code>), чтобы представление Secured Packet могло автоматически подставлять ключи и параметры, слушатель SCP81 HTTP OTA — находить PSK-ключи, а счётчики SCP80, израсходованные операцией, сохранялись самой операцией — закрытая вкладка или второе окно браузера больше не могут потерять инкремент. Предустановка содержит <strong>один набор ключей на версию ключа</strong> (ниббл <code class="font-mono text-sm">b8..b5</code> KIc/KID, TS 102 225 §5.1.2/A.2): KIc и KID набора должны содержать одну версию (01–0F; A.2 отвергает несовпадение), и у каждой версии ключа на карте свой счётчик (Annex A.1), поэтому счётчики ведутся по наборам. Список загружается при подключении PWA и экспортируется/импортируется как JSON (импорт принимает и предустановки старых сборок — плоские и с наборами ключей). «Карты» — верхнеуровневая вкладка. При подключении карты читается её EF.ICCID, и предустановка с тем же ICCID автоматически выбирается в обоих представлениях SCP80; если подключённая карта не совпадает <strong>ни с одной</strong> предустановкой — или выбранная предустановка принадлежит другой карте — оба представления SCP80/RAM показывают жёлтое предупреждение над выбором предустановки. Поля формы сгруппированы в два блока с рамкой: <strong>SCP80 (GSM 03.48, ETSI TS 102 225)</strong> (KIc/KID, SPI1/SPI2, TAR-ы ISD/UICC RFM/ADF RFM, счётчик и ключи) и <strong>SCP81 (HTTP OTA)</strong> (пара PSK identity/ключ с пояснением о выборе ключа). Необязательное поле <strong>ADM</strong> находится в верхней строке и сохраняется для последующего использования файловым менеджером. В шапке отображаются серые маркеры <strong>SCP80</strong>/<strong>SCP81</strong> и значок ключа на бейдже ADM, когда в предустановке с ICCID подключённой карты эти поля заполнены.</p>
|
||||
<table class="w-full text-sm mb-3 border-collapse">
|
||||
|
||||
+6
-1
@@ -321,7 +321,6 @@
|
||||
<tbody>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Explore Card (all GP data)</td><td class="py-1 px-2">Queries GET STATUS for ISD, Applications, ELFs, and ELF Modules, plus GET DATA FF21 (Extended Card Resources: the number of installed applications and the free non-volatile/volatile memory). The compact listing carries the AID, life cycle and privileges (the legacy format has no version); if a card rejects it (<code class="font-mono text-sm">P2.b2=0</code>, deprecated by GP) the queries are retried in the expanded <code class="font-mono text-sm">E3</code> form, and the <strong>Fetch expanded registry details (load-file / SD associations)</strong> checkbox also requests it as an extra pass and merges the expanded-only fields: the Executable Load File version, Load File AID, Security Domain AID, module lists, the applet STK menu entries (SCP Registry Data, TS 102 226 8.2.1.6) and any other registry data objects the card returns (shown raw, never dropped) — it costs a few extra secured packets per Explore and is <strong>on by default</strong> (remembered per browser; untick to skip). Cards differ in what they accept, so the expanded query is tried in three forms — the reference tag list (with the TS 102 226 <code class="font-mono text-sm">EA</code>), the same list without <code class="font-mono text-sm">EA</code>, and no tag list at all (the GlobalPlatformPro form) — and the explorer reports which form worked and where the run fell back to the compact listing (package versions are then unavailable). Results appear in an explorer view (the success line stays minimal; the counts are in the section headings) with per-item <strong>Delete</strong> buttons and the detected RAM command format (hover for what compact vs expanded means).</td></tr>
|
||||
<tr><td class="py-1 px-2">Install Package (.cap file)</td><td class="py-1 px-2">Sends a <code class="font-mono text-sm">.cap</code> file to the card via the server: INSTALL[for load] → LOAD ×N → INSTALL[for install (+make selectable)]. The load file is split into LOAD APDUs of the <strong>LOAD block size</strong> (1–240 bytes of payload, 240 by default); each secured packet is delivered as a single SMS or, when larger, as a concatenated SMS-PP download of up to 5 segments, so a large <code class="font-mono text-sm">.cap</code> simply takes several SMS. As soon as the file is selected it is validated and its <strong>memory requirements are estimated</strong> in the form: the <strong>NVRAM requirement</strong> (the code image + persistent data, the GlobalPlatform Card Spec C6+C8 style total) and the RAM estimate. The same box reports the <strong>libraries the CAP is linked against</strong> (its Import component: library name, AID and the minimum version <code class="font-mono text-sm">≥ x.y</code>, with standard names and families resolved and a Java Card SDK hint where known), the package/applet identity and the component breakdown (sizes and shares in load-file order) — grouped under the <strong>Package</strong>, <strong>Requires</strong>, <strong>Memory</strong> and <strong>Components</strong> headings. The <strong>Platform</strong> line names the minimum Java Card level the card must provide (derived from the <code class="font-mono text-sm">javacard.framework</code> import; the header <code class="font-mono text-sm">int</code> flag additionally requires int support, Java Card 2.2+) and the <strong>ETSI release</strong> the UICC API imports come from (<code class="font-mono text-sm">uicc.toolkit</code> version → REL-N, per the workspace <code class="font-mono text-sm">sim-tools</code> corpus; shown with its basis in brackets): the JCRE checks every import against its own packages when it verifies the load file (JC VM spec 4.5.2), so a card whose platform is older rejects the LOAD as soon as the <strong>Import</strong> component is complete (the step line carries the card's remote SW and names the component the block completed, e.g. <em>completes Import</em>) — a smaller LOAD block only helps when the block exceeds the card's buffer. A corrupt or wrong-format file fails the analysis, and <strong>Execute</strong> stays disabled until a successful analysis. The estimate is informational only and is never used for installation: NVRAM is the code image plus the persistent data (the GlobalPlatform C6+C8 style total, Table 11-48 — card memory management, rounding and the registry entry are not included, and the static field image is counted in both parts); the suggested <strong>C6</strong> follows the load file, and the suggested quotas are decimal byte counts (the hex in parentheses is how the C7/C8 install parameters encode them: C6 = non-volatile code, C7 = volatile RAM, C8 = non-volatile data).</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Probe TARs (registered OTA applications)</td><td class="py-1 px-2">Sends <strong>one secured packet per TAR</strong> — the standard allocations of TS 101 220 Annex D, Table D.1, unless a TAR list is given — each carrying a harmless C-APDU (default <code class="font-mono text-sm">SELECT MF</code>) with the preset's keyset and SPI <code class="font-mono text-sm">16/01</code> (counter check + PoR), and classifies the card's answer: <strong>registered</strong> (ENVELOPE 9000 + a PoR with the application's own status word/data), <strong>accepted, no PoR</strong> (the packet was silently dropped), <strong>refused</strong> (typically <code class="font-mono text-sm">6200</code>). Useful to map which OTA applications a card carries. Every accepted packet consumes a counter, which is saved into the keyset — the probe is refused when the SPI1 has no counter check.</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">CAP compatibility test</td><td class="py-1 px-2">Answers one question without installing anything: <strong>can this card satisfy the CAP's import list?</strong> The server runs the format check, INSTALL[for load] and the LOAD blocks only up to the block that completes the <strong>Import</strong> component — the point where the JCRE verifies the import list — then stops: no last-block flag and no INSTALL[for install], so nothing is committed and nothing has to be deleted afterwards. The <strong>Import versions to test</strong> list is pre-filled from the CAP analysis: <code class="font-mono text-sm">AID=version</code> rewrites an import's requested version, <code class="font-mono text-sm">+AID=version</code> appends a synthetic import (a card-capability query: the LOAD passes only if the card exports that package at ≥ that version), and <strong>Set all 0.0</strong> drops every requirement. Two-step use: press Set all 0.0 — if the test passes, the import list was the blocker; then restore one line at a time to find the package and version the card lacks. The verdict states it covers the <strong>LOAD/import gate only</strong>: the link gate (whether the applet's actual API calls exist) is what a real install checks at INSTALL[for install]. The run and its result appear in a popup (like the install): the step lines, the verdict and a per-AID detail block — on a rejection it names the CAP section the failing block covers (load-file byte range and components, and whether that block completes <strong>Import</strong>), the remote status word with its meaning, the import list exactly as tested (CAP versions, probe rewrites, appended synthetic entries, unmatched lines) and what the probe state implies; on success it lists the import list that passed.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -339,6 +338,12 @@
|
||||
|
||||
</section>
|
||||
<section class="mb-10">
|
||||
<h3 id="tar-probe" class="text-lg font-medium mb-2">3.3 TAR probe</h3>
|
||||
<p class="mb-2">The third SCP80 pill probes <strong>which OTA applications the card has registered</strong>: it sends <strong>one secured packet per selected TAR</strong> — the standard allocations of TS 101 220 V18.3.0 Annex D, Table D.1, each row carrying the document it comes from — with a harmless C-APDU (default <code class="font-mono text-sm">SELECT MF</code>) using the view's preset and keyset, SPI <code class="font-mono text-sm">16/01</code> (counter check + PoR).</p>
|
||||
<p class="mb-2">The list is pre-filled with the standard TARs (all checked; <strong>Select all</strong> / <strong>Select none</strong> switch them quickly) and custom TARs can be added with <strong>Add TAR</strong> (three hex bytes plus an optional description). Custom entries are kept in <code class="font-mono text-sm">localStorage</code> on a best-effort basis — losing them is harmless.</p>
|
||||
<p class="mb-2">Every TAR is classified from the card's answer: <strong>registered</strong> (ENVELOPE 9000 + a PoR carrying the application's own status word and data), <strong>accepted, no answer</strong> (a PoR without an R-APDU), <strong>accepted, no PoR</strong> (the packet was silently dropped) and <strong>refused</strong> (a non-9000 ENVELOPE, typically <code class="font-mono text-sm">6200</code> for a TAR without a registered application). The result table appears in the operation popup; the line under Execute summarises <code class="font-mono text-sm">registered N / M</code>.</p>
|
||||
<p class="mb-2">Each packet the card accepts <strong>consumes a counter</strong>, which is saved into the preset's keyset — the probe is refused when SPI1 has no counter check or the named preset does not define the key version. Observed on a live card: <code class="font-mono text-sm">000000</code>, <code class="font-mono text-sm">B00000</code> and <code class="font-mono text-sm">B00001</code> registered (compact-format applications), the expanded-format TARs, USAT, multiplexing and CASD refused with <code class="font-mono text-sm">6200</code>.</p>
|
||||
|
||||
<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 on the server (<code class="font-mono text-sm">~/.pysim-simple-server/card_presets.json</code>; <code class="font-mono text-sm">--card-presets</code> overrides it) so the Secured Packet view can auto-fill keys and parameters, the SCP81 HTTP OTA listener can look up PSK keys, and the SCP80 counters an operation consumed are persisted with the operation itself — a closed tab or a second browser window can no longer lose an increment. A preset carries <strong>one keyset per key version</strong> (the <code class="font-mono text-sm">b8..b5</code> nibble of KIc/KID, TS 102 225 §5.1.2/A.2): KIc and KID of a keyset must carry the same version (01–0F; A.2 rejects a mismatch), and each key version has its own counter on the card (Annex A.1), so counters are tracked per keyset. The list is loaded when the PWA connects and can be exported/imported as JSON (the import also takes presets exported by older builds, flat or keyset-shaped). 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; when the equipped card matches <strong>no</strong> preset — or the selected preset belongs to another card — both SCP80/RAM views show an amber warning above the preset selector. 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 RFM/ADF RFM 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. The header shows gray <strong>SCP80</strong>/<strong>SCP81</strong> markers and a key glyph on the ADM badge when the preset matching the equipped card's ICCID has those settings filled in.</p>
|
||||
<table class="w-full text-sm mb-3 border-collapse">
|
||||
|
||||
+246
-48
@@ -520,6 +520,7 @@
|
||||
<div class="flex flex-wrap justify-center gap-2 mb-3">
|
||||
<button class="scp80-subtab px-4 py-1.5 text-sm rounded-full bg-blue-600 text-white" data-scp80-sub="sp" onclick="scp80SwitchSubtab('sp')" data-l10n="Secured packet">Secured packet</button>
|
||||
<button class="scp80-subtab px-4 py-1.5 text-sm rounded-full bg-gray-200 dark:bg-slate-700 hover:bg-gray-300 dark:hover:bg-slate-600 text-gray-700 dark:text-slate-300" data-scp80-sub="ram" onclick="scp80SwitchSubtab('ram')" data-l10n="RAM">RAM</button>
|
||||
<button class="scp80-subtab px-4 py-1.5 text-sm rounded-full bg-gray-200 dark:bg-slate-700 hover:bg-gray-300 dark:hover:bg-slate-600 text-gray-700 dark:text-slate-300" data-scp80-sub="tar" onclick="scp80SwitchSubtab('tar')" data-l10n="TAR probe">TAR probe</button>
|
||||
</div>
|
||||
<div id="scp80-sub-sp">
|
||||
<div class="mb-3">
|
||||
@@ -705,7 +706,6 @@
|
||||
<option value="compat" data-l10n="CAP compatibility test">CAP compatibility test</option>
|
||||
<option value="install-app" data-l10n="INSTALL [for install] (loaded package)">INSTALL [for install] (loaded package)</option>
|
||||
<option value="make-selectable" data-l10n="INSTALL [for make selectable]">INSTALL [for make selectable]</option>
|
||||
<option value="tar-probe" data-l10n="Probe TARs (registered OTA applications)">Probe TARs (registered OTA applications)</option>
|
||||
</select>
|
||||
</div>
|
||||
<button data-needs="card" data-cap-gate="ram" onclick="ramExecute()" class="px-5 py-1.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Execute">Execute</button>
|
||||
@@ -722,13 +722,6 @@
|
||||
<div id="ram-cap-body"></div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div id="ram-tar-rows" class="hidden mb-3">
|
||||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Probe APDU (hex)">Probe APDU (hex)</label>
|
||||
<input id="ram-tar-apdu" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 font-mono" value="00A40000023F00">
|
||||
<label class="block mt-2 mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="TARs to probe (hex, one per line; empty = all standard TARs of TS 101 220 Annex D)">TARs to probe (hex, one per line; empty = all standard TARs of TS 101 220 Annex D)</label>
|
||||
<textarea id="ram-tar-list" rows="4" placeholder="000000 B20100 B00000 …" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 font-mono"></textarea>
|
||||
<p class="text-xs text-amber-600 dark:text-amber-400 mt-1" data-l10n="One secured packet per TAR: each packet the card accepts consumes a counter, which is saved to the preset.">One secured packet per TAR: each packet the card accepts consumes a counter, which is saved to the preset.</p>
|
||||
</div>
|
||||
<div id="ram-compat-rows" class="hidden mb-3">
|
||||
<fieldset class="border border-gray-200 dark:border-slate-700 rounded p-3" data-l10n-title="Runs the load only as far as the block that completes the CAP's Import component: the card checks the import list there, so its answer says whether the card can satisfy it. Nothing is installed and nothing is committed - the link gate is the real install's business. Lines are AID=version (rewrites an import) or +AID=version (appends a synthetic import, to query the card for a package the CAP does not use).">
|
||||
<legend class="px-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="Import versions to test">Import versions to test</legend>
|
||||
@@ -918,6 +911,41 @@
|
||||
<div id="ram-explorer" class="mt-2 text-xs font-mono hidden"></div>
|
||||
<div id="ram-steps" class="mt-2 text-xs font-mono whitespace-pre-wrap hidden"></div>
|
||||
</div>
|
||||
<div id="scp80-sub-tar" class="hidden">
|
||||
<p class="text-xs text-gray-500 dark:text-slate-400 mb-3" data-l10n="Sends one secured packet per selected TAR (TS 101 220 Annex D, Table D.1) carrying a harmless command, and classifies the card's answer - which OTA applications it has registered. Every packet the card accepts consumes a counter, which is saved to the preset.">Sends one secured packet per selected TAR (TS 101 220 Annex D, Table D.1) carrying a harmless command, and classifies the card's answer - which OTA applications it has registered. Every packet the card accepts consumes a counter, which is saved to the preset.</p>
|
||||
|
||||
<div class="mb-3 flex gap-2 items-end">
|
||||
<div class="flex-1">
|
||||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Card preset">Card preset</label>
|
||||
<select id="tar-card-sel" onchange="tarApplyCard(this.value)" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
|
||||
<option value="" data-l10n="— Select card —">— Select card —</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Key version (keyset)">Key version (keyset)</label>
|
||||
<select id="tar-keyset-sel" onchange="tarKeysetChanged()" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tar-preset-warning" class="hidden mb-3 px-3 py-2 rounded border border-amber-400 dark:border-amber-600 bg-amber-50 dark:bg-amber-900/20 text-sm text-amber-700 dark:text-amber-300"></div>
|
||||
|
||||
<div class="mb-1 flex items-center justify-between">
|
||||
<span class="text-xs text-gray-600 dark:text-slate-400" data-l10n="TARs to probe — TS 101 220 V18.3.0 Annex D, Table D.1">TARs to probe — TS 101 220 V18.3.0 Annex D, Table D.1</span>
|
||||
<span class="flex gap-1">
|
||||
<button type="button" onclick="tarProbeSelectAll(true)" class="px-2 py-0.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700" data-l10n="Select all">Select all</button>
|
||||
<button type="button" onclick="tarProbeSelectAll(false)" class="px-2 py-0.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700" data-l10n="Select none">Select none</button>
|
||||
</span>
|
||||
</div>
|
||||
<div id="tar-list" class="mb-2 border border-gray-200 dark:border-slate-700 rounded p-2 max-h-64 overflow-y-auto"></div>
|
||||
<div class="flex gap-2 items-center mb-3">
|
||||
<input id="tar-custom" 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 text-center" maxlength="6" placeholder="AABBCC">
|
||||
<input id="tar-custom-note" class="flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="description (optional)">
|
||||
<button type="button" onclick="tarProbeAddCustom()" 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 whitespace-nowrap" data-l10n="Add TAR">Add TAR</button>
|
||||
</div>
|
||||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Probe APDU (hex)">Probe APDU (hex)</label>
|
||||
<input id="tar-apdu" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 font-mono mb-3" value="00A40000023F00">
|
||||
<button data-needs="card" onclick="tarExecute()" class="px-5 py-1.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Execute">Execute</button>
|
||||
<div id="tar-result" class="mt-2 text-base font-semibold font-mono hidden"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1801,8 +1829,10 @@ function scp80SwitchSubtab(name) {
|
||||
});
|
||||
document.getElementById('scp80-sub-sp').classList.toggle('hidden', name !== 'sp');
|
||||
document.getElementById('scp80-sub-ram').classList.toggle('hidden', name !== 'ram');
|
||||
document.getElementById('scp80-sub-tar').classList.toggle('hidden', name !== 'tar');
|
||||
if (name === 'ram') ramRender();
|
||||
scp80HelpAnchor = { sp: 'secured-packet', ram: 'ram' }[name] || 'secured-packet';
|
||||
if (name === 'tar') tarProbeRender();
|
||||
scp80HelpAnchor = { sp: 'secured-packet', ram: 'ram', tar: 'tar-probe' }[name] || 'secured-packet';
|
||||
setHelpAnchor(scp80HelpAnchor);
|
||||
}
|
||||
|
||||
@@ -8412,7 +8442,6 @@ function ramOpChanged() {
|
||||
const isCap = op === 'install-cap';
|
||||
const isCompat = op === 'compat';
|
||||
const isApp = op === 'install-app' || op === 'make-selectable';
|
||||
const isProbe = op === 'tar-probe';
|
||||
const setHidden = (id, hidden) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.classList.toggle('hidden', hidden);
|
||||
@@ -8421,7 +8450,6 @@ function ramOpChanged() {
|
||||
setHidden('ram-cap-rows', !(isCap || isCompat));
|
||||
setHidden('ram-compat-rows', !isCompat);
|
||||
setHidden('ram-app-rows', !isApp);
|
||||
setHidden('ram-tar-rows', !isProbe);
|
||||
// The Execute button's .cap gate applies to the install-cap and compat ops.
|
||||
if (typeof pysimApplyAvailability === 'function') pysimApplyAvailability();
|
||||
}
|
||||
@@ -8793,10 +8821,11 @@ function scriptsCapFileChanged() {
|
||||
}
|
||||
|
||||
// Collect SP params from the SP form (populated by ramApplyCard -> cardsApply)
|
||||
function getRamSpParams() {
|
||||
function getRamSpParams(selId) {
|
||||
// `preset_id` names the server-side preset the operation belongs to: the
|
||||
// server persists the counter it consumes there (v3.8.0).
|
||||
const sel = document.getElementById('ram-card-sel');
|
||||
// server persists the counter it consumes there (v3.8.0). `selId` picks
|
||||
// the view's preset selector (RAM by default, the TAR probe its own).
|
||||
const sel = document.getElementById(selId || 'ram-card-sel');
|
||||
const idx = sel ? parseInt(sel.value, 10) : NaN;
|
||||
const preset = (!isNaN(idx) && cards[idx]) ? cards[idx] : null;
|
||||
return {
|
||||
@@ -10493,14 +10522,112 @@ async function ramInstallApp(sp, op) {
|
||||
// the list field is empty. Each packet the card accepts consumes a counter,
|
||||
// which the server persists into the keyset (one per accepted TAR).
|
||||
|
||||
function ramTarVerdictLabel(verdict) {
|
||||
// TAR values allocated by ETSI (TS 101 220 V18.3.0 Annex D, Table D.1) with
|
||||
// the document each allocation comes from. Kept in sync with the server's
|
||||
// TAR_PROBE_TARS by a test; custom TARs live in localStorage (best effort).
|
||||
const TAR_PROBE_TARS = [
|
||||
{ tar: '000000', label: 'Issuer Security Domain (compact)', doc: 'TS 102 226' },
|
||||
{ tar: 'B20100', label: 'Issuer Security Domain (expanded)', doc: 'TS 102 226' },
|
||||
{ tar: 'B00000', label: 'UICC shared file system RFM (compact)', doc: 'TS 102 226' },
|
||||
{ tar: 'B00001', label: 'ADF RFM (compact)', doc: 'TS 131 116' },
|
||||
{ tar: 'B00010', label: 'SIM file system RFM (compact)', doc: 'TS 131 116' },
|
||||
{ tar: 'B00120', label: 'UICC shared file system RFM (expanded)', doc: 'TS 102 226' },
|
||||
{ tar: 'B00130', label: 'SIM file system RFM (expanded)', doc: 'TS 131 116' },
|
||||
{ tar: 'B00140', label: 'ADF RFM (expanded)', doc: 'TS 131 116' },
|
||||
{ tar: 'B20000', label: 'USAT interpreter', doc: 'TS 131 114' },
|
||||
{ tar: 'B20200', label: 'Multiplexing application', doc: 'TS 102 225' },
|
||||
{ tar: 'B20201', label: 'Controlling Authority Security Domain', doc: 'GP Amendment A' },
|
||||
{ tar: 'B20202', label: 'OMA BCAST audience measurement', doc: 'OMA' },
|
||||
{ tar: 'B20203', label: 'OMA DM LWM2M application', doc: 'OMA' },
|
||||
{ tar: 'B00200', label: 'RFU (control)', doc: '' },
|
||||
];
|
||||
const TAR_PROBE_STORAGE = 'simple_tar_probe';
|
||||
|
||||
// The custom TARs added by the user (best effort: losing them is harmless).
|
||||
function tarProbeCustomLoad() {
|
||||
try {
|
||||
const d = JSON.parse(localStorage.getItem(TAR_PROBE_STORAGE) || '[]');
|
||||
if (!Array.isArray(d)) return [];
|
||||
return d.filter(e => e && /^[0-9A-F]{6}$/.test(String(e.tar || '').toUpperCase()))
|
||||
.map(e => ({ tar: String(e.tar).toUpperCase(), label: String(e.label || ''), custom: true }));
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function tarProbeCustomSave(list) {
|
||||
try {
|
||||
localStorage.setItem(TAR_PROBE_STORAGE, JSON.stringify(
|
||||
(list || []).map(e => ({ tar: e.tar, label: e.label || '' }))));
|
||||
} catch (e) { /* best effort */ }
|
||||
}
|
||||
|
||||
// The standard rows plus the custom ones.
|
||||
function tarProbeAllRows() {
|
||||
return TAR_PROBE_TARS.concat(tarProbeCustomLoad());
|
||||
}
|
||||
|
||||
// The checklist rows (pure): a checkbox per TAR, checked by default, the spec
|
||||
// document in the tooltip and a remove button on custom rows.
|
||||
function tarProbeRowsHtml(rows) {
|
||||
return (rows || []).map(r => {
|
||||
const title = r.doc ? ' title="' + esc(r.doc) + '"' : '';
|
||||
return '<label class="flex items-center gap-2 py-0.5"' + title + '>' +
|
||||
'<input type="checkbox" class="rounded border-gray-300 dark:border-slate-600" value="' + esc(r.tar) + '" checked>' +
|
||||
'<span class="font-mono text-xs">' + esc(r.tar) + '</span>' +
|
||||
'<span class="text-xs text-gray-500 dark:text-slate-400">' + esc(r.label || '') + '</span>' +
|
||||
(r.custom
|
||||
? '<button type="button" onclick="tarProbeRemoveCustom(\'' + esc(r.tar) + '\')" class="ml-auto px-1.5 py-0.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-red-100 dark:hover:bg-red-900/40" title="' + esc(t('Remove TAR')) + '">\u2715</button>'
|
||||
: '') +
|
||||
'</label>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// The selected TARs, in list order (pure read of the DOM).
|
||||
function tarProbeChecked() {
|
||||
return Array.from(document.querySelectorAll('#tar-list input[type="checkbox"]'))
|
||||
.filter(cb => cb.checked)
|
||||
.map(cb => String(cb.value).toUpperCase());
|
||||
}
|
||||
|
||||
function tarProbeRender() {
|
||||
const box = document.getElementById('tar-list');
|
||||
if (box) box.innerHTML = tarProbeRowsHtml(tarProbeAllRows());
|
||||
}
|
||||
|
||||
function tarProbeSelectAll(on) {
|
||||
document.querySelectorAll('#tar-list input[type="checkbox"]').forEach(cb => { cb.checked = !!on; });
|
||||
}
|
||||
|
||||
function tarProbeAddCustom() {
|
||||
const el = document.getElementById('tar-custom');
|
||||
const noteEl = document.getElementById('tar-custom-note');
|
||||
const tar = (el && el.value || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
|
||||
if (!/^[0-9A-F]{6}$/.test(tar)) { alert(t('A TAR is three hex bytes, e.g. B00001')); return; }
|
||||
const custom = tarProbeCustomLoad();
|
||||
if (!custom.some(e => e.tar === tar)) {
|
||||
custom.push({ tar: tar, label: (noteEl && noteEl.value || '').trim(), custom: true });
|
||||
tarProbeCustomSave(custom);
|
||||
}
|
||||
if (el) el.value = '';
|
||||
if (noteEl) noteEl.value = '';
|
||||
tarProbeRender();
|
||||
}
|
||||
|
||||
function tarProbeRemoveCustom(tar) {
|
||||
const want = String(tar || '').toUpperCase();
|
||||
tarProbeCustomSave(tarProbeCustomLoad().filter(e => e.tar !== want));
|
||||
tarProbeRender();
|
||||
}
|
||||
|
||||
function tarProbeVerdictLabel(verdict) {
|
||||
return { registered: 'registered', no_answer: 'accepted, no answer',
|
||||
no_por: 'accepted, no PoR', refused: 'refused' }[verdict] || verdict;
|
||||
}
|
||||
|
||||
// The probe result table (pure): TAR, application, ENVELOPE SW, verdict, the
|
||||
// application's status word and data.
|
||||
function ramTarProbeHtml(data) {
|
||||
function tarProbeHtml(data) {
|
||||
if (!data || !Array.isArray(data.results)) return '';
|
||||
let html = '<div class="mb-2 text-sm">' + esc(t('Registered applications:')) + ' <b>' +
|
||||
(data.registered || 0) + ' / ' + (data.total || 0) + '</b>' +
|
||||
@@ -10518,7 +10645,7 @@ function ramTarProbeHtml(data) {
|
||||
'<td class="py-1 px-2 font-mono">' + esc(r.tar) + '</td>' +
|
||||
'<td class="py-1 px-2">' + esc(r.label || '') + '</td>' +
|
||||
'<td class="py-1 px-2 font-mono">' + esc(r.sw || '-') + '</td>' +
|
||||
'<td class="py-1 px-2 ' + tone + '">' + esc(t(ramTarVerdictLabel(r.verdict))) + '</td>' +
|
||||
'<td class="py-1 px-2 ' + tone + '">' + esc(t(tarProbeVerdictLabel(r.verdict))) + '</td>' +
|
||||
'<td class="py-1 px-2 font-mono">' + esc(r.por_sw || '-') + '</td>' +
|
||||
'<td class="py-1 px-2 font-mono">' + esc(r.por_data || '-') + '</td></tr>';
|
||||
});
|
||||
@@ -10526,53 +10653,107 @@ function ramTarProbeHtml(data) {
|
||||
return html;
|
||||
}
|
||||
|
||||
async function ramTarProbe(sp) {
|
||||
const apdu = (document.getElementById('ram-tar-apdu').value || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
|
||||
// The probe result line (shared by the success and failure paths).
|
||||
function tarProbeResult(text, ok) {
|
||||
const el = document.getElementById('tar-result');
|
||||
el.textContent = text;
|
||||
el.classList.remove('hidden', 'text-green-600', 'text-red-600');
|
||||
el.classList.add(ok ? 'text-green-600' : 'text-red-600');
|
||||
return text;
|
||||
}
|
||||
|
||||
async function tarProbe(sp, tars) {
|
||||
const apdu = (document.getElementById('tar-apdu').value || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
|
||||
if (!apdu) { alert(t('Specify the probe APDU')); return; }
|
||||
const tars = (document.getElementById('ram-tar-list').value || '')
|
||||
.split(/[\s,;]+/).map(s => s.trim().toUpperCase())
|
||||
.filter(s => /^[0-9A-F]{6}$/.test(s));
|
||||
if (!tars || !tars.length) { alert(t('Select at least one TAR')); return; }
|
||||
const body = {
|
||||
preset_id: sp.preset_id,
|
||||
spi1: sp.spi1, spi2: '01',
|
||||
kic: sp.kic, kid: sp.kid, kicKey: sp.kicKey, kidKey: sp.kidKey,
|
||||
cntr: sp.cntr, apdu: apdu,
|
||||
cntr: sp.cntr, apdu: apdu, tars: tars,
|
||||
};
|
||||
if (tars.length) body.tars = tars;
|
||||
ramClearResults();
|
||||
ramOpModalBegin(t('Probe TARs (registered OTA applications)'));
|
||||
ramOpModalBegin(t('TAR probe'));
|
||||
ramOpModalStatus(t('Sending to card...'));
|
||||
let data;
|
||||
try {
|
||||
data = await pysimFetch('/api/tar-probe', body);
|
||||
} catch (e) {
|
||||
const msg = t('Error') + ': ' + e.message;
|
||||
const el = document.getElementById('ram-result');
|
||||
el.textContent = msg;
|
||||
el.classList.remove('hidden', 'text-green-600');
|
||||
el.classList.add('text-red-600');
|
||||
ramOpModalFinish(msg, false, '');
|
||||
ramOpModalFinish(tarProbeResult(t('Error') + ': ' + e.message, false), false, '');
|
||||
return;
|
||||
}
|
||||
ramHideProgress();
|
||||
if (data.final_cntr) ramSaveCntr(data.final_cntr);
|
||||
if (!data.success) {
|
||||
const msg = t('Error') + ': ' + (data.error || '?');
|
||||
const el = document.getElementById('ram-result');
|
||||
el.textContent = msg;
|
||||
el.classList.remove('hidden', 'text-green-600');
|
||||
el.classList.add('text-red-600');
|
||||
ramOpModalFinish(msg, false, '');
|
||||
ramOpModalFinish(tarProbeResult(t('Error') + ': ' + (data.error || '?'), false), false, '');
|
||||
return;
|
||||
}
|
||||
const html = ramTarProbeHtml(data);
|
||||
const resultEl = document.getElementById('ram-result');
|
||||
resultEl.textContent = t('Registered applications:') + ' ' + data.registered + ' / ' + data.total;
|
||||
resultEl.classList.remove('hidden', 'text-red-600');
|
||||
resultEl.classList.add('text-green-600');
|
||||
const stepsEl = document.getElementById('ram-steps');
|
||||
stepsEl.classList.add('hidden');
|
||||
ramOpModalFinish(resultEl.textContent, true, html, true);
|
||||
const text = tarProbeResult(t('Registered applications:') + ' ' + data.registered + ' / ' + data.total, true);
|
||||
ramOpModalFinish(text, true, tarProbeHtml(data), true);
|
||||
}
|
||||
|
||||
// The TAR-probe view's preset selector (populated from cards[], like the RAM one).
|
||||
function tarRender() {
|
||||
const sel = document.getElementById('tar-card-sel');
|
||||
if (!sel) return;
|
||||
const prev = parseInt(sel.value, 10);
|
||||
const keep = (!isNaN(prev) && cards[prev]) ? prev : -1;
|
||||
sel.innerHTML = '<option value="" data-l10n="— Select card —">— Select card —</option>';
|
||||
cards.forEach((c, i) => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = i;
|
||||
opt.textContent = c.name || ('Card ' + i);
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
if (keep >= 0) sel.value = String(keep);
|
||||
spKeysetSync();
|
||||
}
|
||||
|
||||
function tarPresetIdx() {
|
||||
const sel = document.getElementById('tar-card-sel');
|
||||
const idx = sel ? parseInt(sel.value, 10) : NaN;
|
||||
return (!isNaN(idx) && cards[idx]) ? idx : -1;
|
||||
}
|
||||
|
||||
function tarApplyCard(idx) {
|
||||
// copy the selected preset into the shared SP form fields (the probe reads
|
||||
// kic/kid/keys/counter from there, like the RAM view does)
|
||||
const i = parseInt(idx, 10);
|
||||
if (!isNaN(i) && cards[i]) _ramCardIdx = i;
|
||||
_spTarKey = 'tar';
|
||||
cardsApply(idx);
|
||||
spPresetWarningRender();
|
||||
}
|
||||
|
||||
function tarKeysetChanged() {
|
||||
const sel = document.getElementById('tar-keyset-sel');
|
||||
if (!sel) return;
|
||||
const kvn = parseInt(sel.value, 10);
|
||||
const idx = tarPresetIdx();
|
||||
const ks = (idx >= 0 && !isNaN(kvn)) ? spKeysetFor(cards[idx], kvn) : null;
|
||||
if (ks) spKeysetApply(ks);
|
||||
spKeysetSync();
|
||||
}
|
||||
|
||||
function tarKeysetGuard() {
|
||||
const idx = tarPresetIdx();
|
||||
const kic = (document.getElementById('sp-kic-hex') || {}).value;
|
||||
const kid = (document.getElementById('sp-kid-hex') || {}).value;
|
||||
return spKeysetCheck(idx >= 0 ? cards[idx] : null, kic, kid);
|
||||
}
|
||||
|
||||
async function tarExecute() {
|
||||
const tars = tarProbeChecked();
|
||||
if (!tars.length) { alert(t('Select at least one TAR')); return; }
|
||||
const cardIdx = parseInt(document.getElementById('tar-card-sel').value, 10);
|
||||
if (!isNaN(cardIdx) && cards[cardIdx]) _ramCardIdx = cardIdx;
|
||||
spRefreshFromPreset('tar-card-sel');
|
||||
const ksErr = tarKeysetGuard();
|
||||
if (ksErr) { alert(ksErr); return; }
|
||||
const sp = getRamSpParams('tar-card-sel');
|
||||
if (!sp.kicKey || !sp.kidKey) {
|
||||
alert(t('Select a card preset with keys first (RAM subtab → Card preset)'));
|
||||
return;
|
||||
}
|
||||
await tarProbe(sp, tars);
|
||||
}
|
||||
|
||||
// Measured NV footprint of an install operation: the server reads GET DATA
|
||||
@@ -10723,7 +10904,6 @@ async function ramExecute() {
|
||||
}
|
||||
try {
|
||||
if (op === 'explore') await ramExplore(sp);
|
||||
else if (op === 'tar-probe') await ramTarProbe(sp);
|
||||
else if (op === 'install-cap') await ramInstallCap(sp);
|
||||
else if (op === 'compat') await ramCompatTest(sp);
|
||||
else if (op === 'install-app' || op === 'make-selectable') await ramInstallApp(sp, op);
|
||||
@@ -11075,6 +11255,7 @@ async function cardsFetch() {
|
||||
}
|
||||
cardsRebuildSelect();
|
||||
cardsRender();
|
||||
tarRender();
|
||||
scp81PushPskMap();
|
||||
spPresetWarningRender();
|
||||
return true;
|
||||
@@ -11087,6 +11268,7 @@ function cardsSave() {
|
||||
// selects, the table and the SCP81 PSK map.
|
||||
cardsRebuildSelect();
|
||||
cardsRender();
|
||||
tarRender();
|
||||
scp81PushPskMap();
|
||||
}
|
||||
|
||||
@@ -11288,6 +11470,12 @@ function spKeysetSync() {
|
||||
const ramPreset = ramIdx >= 0 ? cards[ramIdx] : null;
|
||||
ramSel.innerHTML = spKeysetOptionsHtml(ramPreset, kic, kid);
|
||||
}
|
||||
const tarSel = document.getElementById('tar-keyset-sel');
|
||||
if (tarSel) {
|
||||
const tarIdx = tarPresetIdx();
|
||||
const tarPreset = tarIdx >= 0 ? cards[tarIdx] : null;
|
||||
tarSel.innerHTML = spKeysetOptionsHtml(tarPreset, kic, kid);
|
||||
}
|
||||
}
|
||||
|
||||
function spKeysetApply(ks) {
|
||||
@@ -11369,6 +11557,7 @@ function spPresetWarningRender() {
|
||||
};
|
||||
show('sp-preset-warning', spPresetWarning(iccid, cards, spPresetIdx()));
|
||||
show('ram-preset-warning', spPresetWarning(iccid, cards, ramPresetIdx()));
|
||||
show('tar-preset-warning', spPresetWarning(iccid, cards, tarPresetIdx()));
|
||||
}
|
||||
|
||||
// The RAM view's preset index.
|
||||
@@ -19594,6 +19783,15 @@ const LANG_RU = {
|
||||
'Select a card preset - its keysets define the key versions': 'Выберите предустановку карты — её наборы ключей определяют версии ключей',
|
||||
'SCP80 counters saved to the preset.': 'Счётчики SCP80 сохранены в предустановке.',
|
||||
'counter not tracked (SPI1 has no counter check)': 'счётчик не отслеживается (в SPI1 нет проверки счётчика)',
|
||||
'TAR probe': 'TAR-сканер',
|
||||
'Sends one secured packet per selected TAR (TS 101 220 Annex D, Table D.1) carrying a harmless command, and classifies the card\'s answer - which OTA applications it has registered. Every packet the card accepts consumes a counter, which is saved to the preset.': 'Отправляет по одному защищённому пакету на каждый выбранный TAR (TS 101 220 Annex D, Table D.1) с безобидной командой и классифицирует ответ карты — какие OTA-приложения на ней зарегистрированы. Каждый принятый картой пакет расходует счётчик, который сохраняется в предустановке.',
|
||||
'TARs to probe — TS 101 220 V18.3.0 Annex D, Table D.1': 'TAR для пробы — TS 101 220 V18.3.0 Annex D, Table D.1',
|
||||
'Select all': 'Выбрать все',
|
||||
'Select none': 'Снять все',
|
||||
'Add TAR': 'Добавить TAR',
|
||||
'Remove TAR': 'Удалить TAR',
|
||||
'A TAR is three hex bytes, e.g. B00001': 'TAR — это три hex-байта, например B00001',
|
||||
'Select at least one TAR': 'Выберите хотя бы один TAR',
|
||||
'Probe TARs (registered OTA applications)': 'Проба TAR (зарегистрированные OTA-приложения)',
|
||||
'Probe APDU (hex)': 'APDU пробы (hex)',
|
||||
'TARs to probe (hex, one per line; empty = all standard TARs of TS 101 220 Annex D)': 'TAR для пробы (hex, по одному в строке; пусто = все стандартные TAR из TS 101 220 Annex D)',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v337';
|
||||
const CACHE = 'simple-v338';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -22,70 +22,144 @@ function extractFunc(src, name) {
|
||||
}
|
||||
|
||||
let code = '';
|
||||
for (const fn of ['esc', 'escHtml', 'ramTarVerdictLabel', 'ramTarProbeHtml']) {
|
||||
for (const fn of ['esc', 'escHtml', 'tarProbeVerdictLabel', 'tarProbeHtml',
|
||||
'tarProbeRowsHtml', 'tarProbeCustomLoad', 'tarProbeCustomSave',
|
||||
'tarProbeAllRows', 'tarProbeChecked']) {
|
||||
code += extractFunc(html, fn) + '\n';
|
||||
}
|
||||
code += "var TAR_PROBE_TARS = JSON.parse('" +
|
||||
JSON.stringify(eval('(' + html.match(/const TAR_PROBE_TARS = \[[\s\S]*?\n\];/)[0]
|
||||
.replace('const TAR_PROBE_TARS = ', '').replace(/;$/, '') + ')')) + "');\n";
|
||||
code += "var TAR_PROBE_STORAGE = 'simple_tar_probe';\n";
|
||||
code += 'function t(s){return s;}\n';
|
||||
eval(code);
|
||||
|
||||
const DATA = {
|
||||
success: true,
|
||||
registered: 2,
|
||||
total: 4,
|
||||
final_cntr: '0000000187',
|
||||
results: [
|
||||
{ tar: '000000', label: 'Issuer Security Domain (compact)', verdict: 'registered',
|
||||
sw: '9000', por_status: 'por_ok', por_sw: '6D00', por_data: '' },
|
||||
{ tar: 'B00000', label: 'UICC shared file system RFM (compact)', verdict: 'registered',
|
||||
sw: '9000', por_status: 'por_ok', por_sw: '6B00', por_data: '3F00' },
|
||||
{ tar: 'B00001', label: 'ADF RFM (compact)', verdict: 'no_por',
|
||||
sw: '9000', por_status: 'no_por', por_sw: null, por_data: null },
|
||||
{ tar: 'B00200', label: 'RFU (control)', verdict: 'refused',
|
||||
sw: '6200', por_status: 'envelope_error', por_sw: null, por_data: null },
|
||||
],
|
||||
};
|
||||
function fakeStorage() {
|
||||
const map = {};
|
||||
globalThis.localStorage = {
|
||||
getItem: k => (k in map ? map[k] : null),
|
||||
setItem: (k, v) => { map[k] = String(v); },
|
||||
};
|
||||
return map;
|
||||
}
|
||||
|
||||
test('ramTarVerdictLabel names every verdict', () => {
|
||||
assert.strictEqual(ramTarVerdictLabel('registered'), 'registered');
|
||||
assert.strictEqual(ramTarVerdictLabel('no_answer'), 'accepted, no answer');
|
||||
assert.strictEqual(ramTarVerdictLabel('no_por'), 'accepted, no PoR');
|
||||
assert.strictEqual(ramTarVerdictLabel('refused'), 'refused');
|
||||
assert.strictEqual(ramTarVerdictLabel('something'), 'something');
|
||||
test('the standard list covers the Annex D allocations', () => {
|
||||
const tars = TAR_PROBE_TARS.map(e => e.tar);
|
||||
assert.ok(tars.length >= 10, tars.length);
|
||||
for (const want of ['000000', 'B20100', 'B00000', 'B00001', 'B00010',
|
||||
'B00120', 'B00130', 'B00140', 'B20000', 'B20200', 'B20201', 'B00200']) {
|
||||
assert.ok(tars.includes(want), want);
|
||||
}
|
||||
// every row carries the spec document it comes from
|
||||
TAR_PROBE_TARS.forEach(e => {
|
||||
assert.match(e.tar, /^[0-9A-F]{6}$/);
|
||||
assert.ok(e.label, e.tar);
|
||||
});
|
||||
});
|
||||
|
||||
test('ramTarProbeHtml renders the summary and one row per TAR', () => {
|
||||
const out = ramTarProbeHtml(DATA);
|
||||
test('tarProbeRowsHtml renders checked rows and marks custom ones', () => {
|
||||
const out = tarProbeRowsHtml([
|
||||
{ tar: '000000', label: 'Issuer Security Domain (compact)', doc: 'TS 102 226' },
|
||||
{ tar: '123456', label: 'my applet', custom: true },
|
||||
]);
|
||||
assert.match(out, /value="000000" checked/);
|
||||
assert.match(out, /title="TS 102 226"/);
|
||||
assert.match(out, /Issuer Security Domain \(compact\)/);
|
||||
assert.match(out, /value="123456" checked/);
|
||||
assert.match(out, /tarProbeRemoveCustom\('123456'\)/, 'custom rows get a remove button');
|
||||
assert.ok(!/tarProbeRemoveCustom\('000000'\)/.test(out), 'standard rows have none');
|
||||
// escaping
|
||||
const evil = tarProbeRowsHtml([{ tar: 'AA<BB', label: '<i>x', custom: true }]);
|
||||
assert.ok(!evil.includes('AA<BB'));
|
||||
assert.ok(!evil.includes('<i>x'));
|
||||
});
|
||||
|
||||
test('custom TARs round-trip through localStorage (best effort)', () => {
|
||||
fakeStorage();
|
||||
assert.deepStrictEqual(tarProbeCustomLoad(), []);
|
||||
tarProbeCustomSave([{ tar: '123456', label: 'mine' }, { tar: 'aabbcc' }]);
|
||||
const back = tarProbeCustomLoad();
|
||||
assert.deepStrictEqual(back, [{ tar: '123456', label: 'mine', custom: true },
|
||||
{ tar: 'AABBCC', label: '', custom: true }]);
|
||||
// garbage is dropped, never fatal
|
||||
localStorage.setItem('simple_tar_probe', '{not json');
|
||||
assert.deepStrictEqual(tarProbeCustomLoad(), []);
|
||||
localStorage.setItem('simple_tar_probe', JSON.stringify([{ tar: 'zz' }, { tar: 'B0B0B0' }]));
|
||||
assert.deepStrictEqual(tarProbeCustomLoad().map(e => e.tar), ['B0B0B0']);
|
||||
});
|
||||
|
||||
test('tarProbeAllRows appends the custom TARs to the standard ones', () => {
|
||||
fakeStorage();
|
||||
tarProbeCustomSave([{ tar: '123456', label: 'mine' }]);
|
||||
const rows = tarProbeAllRows();
|
||||
assert.strictEqual(rows.length, TAR_PROBE_TARS.length + 1);
|
||||
assert.strictEqual(rows[rows.length - 1].tar, '123456');
|
||||
assert.strictEqual(rows[rows.length - 1].custom, true);
|
||||
});
|
||||
|
||||
test('tarProbeChecked reads the checked boxes in list order', () => {
|
||||
const boxes = [
|
||||
{ value: '000000', checked: true },
|
||||
{ value: 'B00000', checked: false },
|
||||
{ value: 'b00120', checked: true },
|
||||
];
|
||||
globalThis.document = { querySelectorAll: () => boxes };
|
||||
assert.deepStrictEqual(tarProbeChecked(), ['000000', 'B00120']);
|
||||
boxes.forEach(b => { b.checked = false; });
|
||||
assert.deepStrictEqual(tarProbeChecked(), []);
|
||||
});
|
||||
|
||||
test('tarProbeVerdictLabel names every verdict', () => {
|
||||
assert.strictEqual(tarProbeVerdictLabel('registered'), 'registered');
|
||||
assert.strictEqual(tarProbeVerdictLabel('no_answer'), 'accepted, no answer');
|
||||
assert.strictEqual(tarProbeVerdictLabel('no_por'), 'accepted, no PoR');
|
||||
assert.strictEqual(tarProbeVerdictLabel('refused'), 'refused');
|
||||
assert.strictEqual(tarProbeVerdictLabel('something'), 'something');
|
||||
});
|
||||
|
||||
test('tarProbeHtml renders the summary and one row per TAR', () => {
|
||||
const out = tarProbeHtml({
|
||||
success: true, registered: 2, total: 4, final_cntr: '0000000187',
|
||||
results: [
|
||||
{ tar: '000000', label: 'ISD', verdict: 'registered', sw: '9000', por_sw: '6D00', por_data: '' },
|
||||
{ tar: 'B00000', label: 'RFM', verdict: 'registered', sw: '9000', por_sw: '6B00', por_data: '3F00' },
|
||||
{ tar: 'B00001', label: 'ADF', verdict: 'no_por', sw: '9000' },
|
||||
{ tar: 'B00200', label: 'RFU', verdict: 'refused', sw: '6200' },
|
||||
],
|
||||
});
|
||||
assert.match(out, /Registered applications: <b>2 \/ 4<\/b>/);
|
||||
assert.match(out, /counter 0000000187/);
|
||||
for (const r of DATA.results) {
|
||||
assert.ok(out.includes(r.tar), r.tar);
|
||||
}
|
||||
assert.ok(out.includes('6D00') && out.includes('6B00'), 'the application SWs are shown');
|
||||
assert.ok(out.includes('6200'), 'the refused ENVELOPE SW is shown');
|
||||
// the verdict colours: registered = green, no_por = amber, refused = red
|
||||
assert.match(out, /text-emerald-700 dark:text-emerald-400">registered/);
|
||||
assert.match(out, /text-amber-600 dark:text-amber-400">accepted, no PoR/);
|
||||
assert.match(out, /text-red-600 dark:text-red-400">refused/);
|
||||
// an empty payload renders nothing
|
||||
assert.strictEqual(ramTarProbeHtml(null), '');
|
||||
assert.strictEqual(ramTarProbeHtml({}), '');
|
||||
assert.ok(out.includes('6D00') && out.includes('6200'));
|
||||
assert.strictEqual(tarProbeHtml(null), '');
|
||||
});
|
||||
|
||||
test('the probe escapes what the card returned', () => {
|
||||
const out = ramTarProbeHtml({ results: [
|
||||
{ tar: '<b>x', label: '<i>y', verdict: 'registered', sw: '<', por_sw: '>', por_data: '&' },
|
||||
], registered: 1, total: 1 });
|
||||
assert.ok(!out.includes('<b>x'), 'the TAR must be escaped');
|
||||
assert.ok(!out.includes('<i>y'), 'the label must be escaped');
|
||||
});
|
||||
|
||||
test('the RAM form wires the TAR probe', () => {
|
||||
assert.match(html, /<option value="tar-probe" data-l10n="Probe TARs \(registered OTA applications\)">/);
|
||||
assert.match(html, /id="ram-tar-rows" class="hidden mb-3"/);
|
||||
assert.match(html, /id="ram-tar-apdu"[^>]*value="00A40000023F00"/);
|
||||
assert.match(html, /id="ram-tar-list"/);
|
||||
assert.ok(html.includes("setHidden('ram-tar-rows', !isProbe);"));
|
||||
assert.ok(html.includes("else if (op === 'tar-probe') await ramTarProbe(sp);"));
|
||||
test('the SCP80 tab wires the TAR probe pill', () => {
|
||||
assert.match(html, /data-scp80-sub="tar" onclick="scp80SwitchSubtab\('tar'\)" data-l10n="TAR probe"/);
|
||||
assert.match(html, /<div id="scp80-sub-tar" class="hidden">/);
|
||||
assert.ok(html.includes("document.getElementById('scp80-sub-tar').classList.toggle('hidden', name !== 'tar');"));
|
||||
assert.ok(html.includes("tar: 'tar-probe'"), 'the help anchor points at the new section');
|
||||
// its own preset + keyset selectors and the warning banner
|
||||
assert.match(html, /id="tar-card-sel" onchange="tarApplyCard\(this.value\)"/);
|
||||
assert.match(html, /id="tar-keyset-sel" onchange="tarKeysetChanged\(\)"/);
|
||||
assert.match(html, /id="tar-preset-warning"/);
|
||||
// the checklist, the add form, the APDU and the Execute button
|
||||
assert.match(html, /id="tar-list"/);
|
||||
assert.match(html, /onclick="tarProbeSelectAll\(true\)"/);
|
||||
assert.match(html, /onclick="tarProbeAddCustom\(\)"/);
|
||||
assert.match(html, /id="tar-apdu"[^>]*value="00A40000023F00"/);
|
||||
assert.match(html, /onclick="tarExecute\(\)"/);
|
||||
assert.match(html, /id="tar-result"/);
|
||||
// the run
|
||||
assert.ok(html.includes("pysimFetch('/api/tar-probe', body)"));
|
||||
assert.ok(html.includes("if (data.final_cntr) ramSaveCntr(data.final_cntr);"));
|
||||
assert.ok(html.includes('tars: tars'));
|
||||
assert.ok(html.includes("getRamSpParams('tar-card-sel')"));
|
||||
assert.ok(html.includes('if (data.final_cntr) ramSaveCntr(data.final_cntr);'));
|
||||
// the RAM-era leftovers are gone
|
||||
assert.ok(!html.includes('id="ram-tar-rows"'));
|
||||
assert.ok(!html.includes('id="ram-tar-list"'));
|
||||
assert.ok(!html.includes("op === 'tar-probe'"));
|
||||
assert.ok(!html.includes('function ramTarProbe'));
|
||||
});
|
||||
|
||||
@@ -429,6 +429,21 @@ class CntrLowGuardHttpTests(unittest.TestCase):
|
||||
self.assertEqual(resp['suggested_cntr'], '00000000BC')
|
||||
|
||||
|
||||
class TarProbeListTests(unittest.TestCase):
|
||||
"""The PWA's TAR checklist must match the server's TAR_PROBE_TARS - the
|
||||
panel is pre-populated client-side, the server's list is the API fallback,
|
||||
and both must describe the same allocations (TS 101 220 Annex D)."""
|
||||
|
||||
def test_the_pwa_and_the_server_share_the_standard_tar_list(self):
|
||||
from pysim_simple_server import server as srv
|
||||
html = (ROOT / 'frontend' / 'index.html').read_text(encoding='utf-8')
|
||||
block = html[html.index('const TAR_PROBE_TARS = ['):]
|
||||
block = block[:block.index('\n];')]
|
||||
pwa = re.findall(r"tar:\s*'([0-9A-F]{6})',\s*label:\s*'([^']*)'", block)
|
||||
self.assertEqual(pwa, list(srv.TAR_PROBE_TARS))
|
||||
self.assertTrue(pwa, 'the PWA list must not be empty')
|
||||
|
||||
|
||||
class PresetStoreHttpTests(unittest.TestCase):
|
||||
"""The card preset endpoints (v3.8.0): the PWA's Cards tab talks to the
|
||||
server store instead of localStorage, and the counter an operation
|
||||
|
||||
Reference in New Issue
Block a user