feat: expanded registry on by default, form ladder + fallback report (v3.6.54)

The 'Fetch expanded registry details' checkbox is now on by default: the
expanded pass carries the package versions, the SD/load-file associations
and the applet menu entries; an explicit stored opt-out is still honoured.

Live 2026-09-30: a card with a non-default ISD AID answered 6A88 to the
reference expanded query (P2=02 + the tag list), so the ELF rows had no
versions.  Cards differ in what they accept, so the query is now tried in
three forms (RAM_EXPANDED_FORMS):

  tags   - the reference tag list (incl. the TS 102 226 EA SCP Registry Data)
  noea   - the same list without EA (a card that errors on it)
  notags - no tag list at all (GPPro's 4F00-only data field); the card
           builds the default E3

The first form that answers is remembered for the run and tried first for
the later listings; the ISD gets a single attempt - its 6A88/6A86 is normal
('no data to report', GPPro) and must not poison the detection for the
apps/ELF queries.

Every listing's outcome is reported in the explorer (ramExpandedReport):
'Expanded registry: tag list + EA (Apps, ELFs) - ISD: no expanded data
(6A88) - compact listing used', or 'Expanded registry refused by the card -
compact listing only (package versions unavailable)'; the ELF section hints
when no version came back.  Tooltip/help (EN/RU) updated.

sw cache simple-v327; tests: 550 Python / 707 Node.
This commit is contained in:
2026-09-30 09:44:39 +03:00
parent 4906c91463
commit aa4bf8e008
7 changed files with 217 additions and 44 deletions
+1 -1
View File
@@ -320,7 +320,7 @@
<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">Обзор карты (все данные 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) и любые другие объекты реестра, которые вернула карта (показываются как есть, без потерь) &mdash; он расходует дополнительные защищённые пакеты, и не все карты поддерживают расширенный запрос, поэтому по умолчанию выключен и запоминается в браузере. Результаты отображаются в обзоре (строка успеха минимальна, счётчики — в заголовках разделов) с кнопками <strong>Удалить</strong> для каждого элемента и определённым форматом команд RAM (подсказка при наведении объясняет compact и expanded).</td></tr>
<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) и любые другие объекты реестра, которые вернула карта (показываются как есть, без потерь) &mdash; он стоит несколько дополнительных защищённых пакетов на каждый обзор и <strong>включён по умолчанию</strong> (запоминается в браузере; снимите флажок, чтобы пропустить). Карты различаются в том, что принимают, поэтому расширенный запрос пробуется в трёх формах &mdash; эталонный список тегов (с <code class="font-mono text-sm">EA</code> по TS 102 226), тот же список без <code class="font-mono text-sm">EA</code> и вовсе без списка тегов (форма GlobalPlatformPro) &mdash; а обзор сообщает, какая форма сработала и где выполнен откат к компактному списку (версии пакетов тогда недоступны). Результаты отображаются в обзоре (строка успеха минимальна, счётчики — в заголовках разделов) с кнопками <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] &rarr; LOAD &times;N &rarr; INSTALL[for install (+make selectable)]. Load-файл делится на LOAD APDU размера <strong>размер блока LOAD</strong> (1&ndash;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> &rarr; 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> &mdash; именно там JCRE проверяет список импортов &mdash; и останавливается: без флага последнего блока и без 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 проходит, только если карта экспортирует этот пакет версии &ge; указанной), а <strong>«Все 0.0»</strong> снимает все требования. Использование в два шага: нажмите «Все 0.0» &mdash; если проверка проходит, причиной был список импортов; затем возвращайте по одной строке, чтобы найти пакет и версию, которых не хватает карте. Вердикт явно указывает, что проверяется только <strong>этап LOAD/импортов</strong>: этап связывания (наличие реально вызываемых API) проверяет настоящая установка на INSTALL[for install]. Запуск и результат показываются в окне (как при установке): строки шагов, вердикт и блок деталей по каждому AID &mdash; при отказе он называет раздел CAP, который покрывает неудачный блок (диапазон байтов load-файла и компоненты, и завершает ли этот блок <strong>Import</strong>), SW команды с его значением, список импортов ровно в том виде, в каком он проверялся (версии CAP, правки пробы, добавленные синтетические записи, строки без совпадений) и что означает состояние пробы; при успехе — список импортов, который прошёл проверку.</td></tr>
</tbody>
+1 -1
View File
@@ -319,7 +319,7 @@
<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">Operation</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">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) &mdash; it costs extra secured packets and some cards reject the expanded query, so it is off by default and remembered per browser. 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 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) &mdash; 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 &mdash; 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) &mdash; 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] &rarr; LOAD &times;N &rarr; INSTALL[for install (+make selectable)]. The load file is split into LOAD APDUs of the <strong>LOAD block size</strong> (1&ndash;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) &mdash; 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 &rarr; 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>) &mdash; 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 &mdash; 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">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 &mdash; the point where the JCRE verifies the import list &mdash; 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 &ge; that version), and <strong>Set all 0.0</strong> drops every requirement. Two-step use: press Set all 0.0 &mdash; 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 &mdash; 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>
+142 -32
View File
@@ -703,7 +703,7 @@
<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>
</div>
<label class="flex items-center gap-1 mb-3 text-sm text-gray-700 dark:text-slate-300" data-l10n-title="Runs one extra expanded query per listing (P2=02, tag list 4F 9F70 C5 C4 CC CE EA, plus 84 for the ELF queries). The compact listing carries AID, life cycle and privileges only - it has no version; the expanded pass adds the Executable Load File version, the Load File AID, the Security Domain AID, the module lists and the applet STK menu entries (SCP Registry Data). It costs extra secured packets and some cards reject it - the Explore already uses it automatically when the compact listing returns nothing."><input type="checkbox" id="ram-expanded-details" class="rounded border-gray-300 dark:border-slate-600" onchange="ramExpandedDetailsChanged()"> <span data-l10n="Fetch expanded registry details (load-file / SD associations)">Fetch expanded registry details (load-file / SD associations)</span></label>
<label class="flex items-center gap-1 mb-3 text-sm text-gray-700 dark:text-slate-300" data-l10n-title="Runs one extra expanded query per listing (P2=02; on by default). The compact listing carries AID, life cycle and privileges only - it has no version; the expanded pass adds the Executable Load File version, the Load File AID, the Security Domain AID, the module lists and the applet STK menu entries (SCP Registry Data). Cards differ in what they accept, so three query forms are tried (tag list with EA, tag list without EA, no tag list - the GPPro form); the explorer reports which form worked and where the run fell back to the compact listing (package versions are then unavailable). It costs a few extra secured packets per Explore; untick to skip it."><input type="checkbox" id="ram-expanded-details" class="rounded border-gray-300 dark:border-slate-600" onchange="ramExpandedDetailsChanged()"> <span data-l10n="Fetch expanded registry details (load-file / SD associations)">Fetch expanded registry details (load-file / SD associations)</span></label>
<div id="ram-cap-rows" class="hidden mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="CAP file (max 48 kB)">CAP file (max 48 kB)</label>
@@ -1749,7 +1749,7 @@
// ===== Version =====
// Single source of truth for the PWA version: shown in the header and used
// by the server version check in pysimConnect().
const SIMPLE_VERSION = '3.6.53';
const SIMPLE_VERSION = '3.6.54';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
@@ -8274,7 +8274,10 @@ function ramExpandedDetailsInit() {
if (!el) return;
let v = null;
try { v = localStorage.getItem('simple_ram_expanded'); } catch (e) { /* private mode */ }
if (v !== null) el.checked = v === '1';
// Default ON: the expanded pass carries the package versions, the SD /
// load-file associations and the applet menu entries (it costs a few
// extra secured packets per Explore); an explicit opt-out is honoured.
el.checked = v === null ? true : v === '1';
}
function ramExpandedDetailsChanged() {
@@ -9151,8 +9154,45 @@ function ramMergeExpanded(collector, parsed) {
});
}
// Summary of the expanded-registry pass (pure). `report` holds one entry per
// listing: {label, compact, form, sw} - `form` is the query form the card
// accepted ('tags' | 'noea' | 'notags') or null when it refused every form,
// `compact` says whether that listing's compact query worked, `sw` the last
// remote status word. Shown in the explorer so a fallback to the compact
// listing (and the missing package versions) is never silent.
function ramExpandedReport(report) {
const rows = (report || []).filter(r => r && r.label);
if (!rows.length) return '';
const formName = f => f === 'tags' ? t('tag list + EA')
: (f === 'noea' ? t('tag list') : t('no tag list'));
const parts = [];
RAM_EXPANDED_FORMS.forEach(f => {
const labels = rows.filter(r => r.form === f).map(r => r.label);
if (labels.length) parts.push(formName(f) + ' (' + labels.join(', ') + ')');
});
rows.forEach(r => {
if (r.form) return;
const why = r.sw ? ' (' + r.sw + ')' : '';
if (!r.compact) {
parts.push(r.label + ': ' + (r.sw || t('no data')));
} else if (r.p1 === '80') {
// cards commonly report nothing for the ISD in the expanded form
// (GPPro treats its 6A88/6A86 as 'no data to report')
parts.push(r.label + ': ' + t('no expanded data') + why + ' \u2014 ' + t('compact listing used'));
} else {
parts.push(r.label + ': ' + t('refused') + why + ' \u2014 ' + t('compact listing used'));
}
});
const anyForm = rows.some(r => r.form);
const head = anyForm
? t('Expanded registry:')
: t('Expanded registry refused by the card') + ' \u2014 ' +
t('compact listing only (package versions unavailable)');
return head + ' ' + parts.join(' \u00b7 ');
}
// Render the full explorer result as structured HTML
function ramRenderExploreHtml(mem, isd, apps, elfs, fmt) {
function ramRenderExploreHtml(mem, isd, apps, elfs, fmt, expanded) {
let html = '';
if (mem && (mem.appCount != null || mem.freeNV != null || mem.freeV != null)) {
html += '<div class="mb-4 p-3 bg-gray-50 dark:bg-slate-800 rounded">';
@@ -9166,6 +9206,9 @@ function ramRenderExploreHtml(mem, isd, apps, elfs, fmt) {
}
html += '</div>';
}
if (expanded) {
html += '<div class="mb-4 text-xs text-gray-600 dark:text-slate-400">' + esc(expanded) + '</div>';
}
if (isd && isd.length) {
html += '<div class="mb-4">';
html += '<div class="font-semibold text-sm mb-1">' + esc(t('ISD (Issuer Security Domain)')) + '</div>';
@@ -9211,6 +9254,9 @@ function ramRenderExploreHtml(mem, isd, apps, elfs, fmt) {
if (elfs && elfs.length) {
html += '<div class="mb-4">';
html += '<div class="font-semibold text-sm mb-1">' + esc(t('Executable Load Files (ELFs) / Packages')) + ' (' + elfs.length + ')</div>';
if (!elfs.some(o => o.version)) {
html += '<div class="mb-1 text-xs text-gray-500 dark:text-slate-400">' + esc(t('Package versions come from the expanded registry query \u2014 this card did not return them.')) + '</div>';
}
elfs.forEach(o => {
html += '<div class="mb-1 pl-2 pr-2 py-1 rounded bg-gray-50 dark:bg-slate-800/60 hover:bg-gray-100 dark:hover:bg-slate-700/50 border-l-2 border-purple-400 flex items-start gap-3">';
html += '<div class="flex-1 min-w-0">';
@@ -9259,7 +9305,7 @@ function ramRenderExplorer() {
explorerEl.innerHTML = '';
return;
}
const html = ramRenderExploreHtml(_ramExplorerData.mem, _ramExplorerData.isd, _ramExplorerData.apps, _ramExplorerData.elfs, _ramExplorerData.fmt);
const html = ramRenderExploreHtml(_ramExplorerData.mem, _ramExplorerData.isd, _ramExplorerData.apps, _ramExplorerData.elfs, _ramExplorerData.fmt, _ramExplorerData.expanded);
explorerEl.innerHTML = html || esc(t('(no data)'));
explorerEl.classList.remove('hidden');
pysimApplyAvailability();
@@ -9273,6 +9319,49 @@ async function ramExplore(sp) {
const errors = [];
const mem = { appCount: null, freeNV: null, freeV: null };
const isd = [], apps = [], elfs = [];
// Expanded-registry state for this run: the query form the card accepted
// (remembered and tried first for the later listings) and the per-listing
// outcome report shown in the explorer.
let expandedForm = null;
const expandedReport = [];
// One expanded query with the form ladder (see RAM_EXPANDED_FORMS): try
// the remembered form first, then the others. The ISD gets a single
// attempt - cards commonly answer 6A88/6A86 there ("no data to report",
// GPPro), which must not poison the detection for the apps/ELF queries.
// Returns {form, parsed, sw}; form is null when every attempt was refused.
async function expandedPass(p1, label, parser, spi2) {
let forms = expandedForm
? [expandedForm].concat(RAM_EXPANDED_FORMS.filter(f => f !== expandedForm))
: RAM_EXPANDED_FORMS.slice();
if (p1 === '80') forms = [expandedForm || 'tags'];
let lastSw = '';
for (const form of forms) {
ramShowProgress(label + ' P1=' + p1 + ' P2=02 (' + form + ')...');
const res = await ramSendOta(ramExpandedQueryApdu(p1, form), Object.assign({}, sp, { cntr, spi2 }));
if (res.ram_format) sp.ram_format = res.ram_format;
if (res.final_cntr) cntr = res.final_cntr;
else if (res.success && spPorAccepted(res.por)) cntr = ramIncrementCntr(cntr);
if (!res.success || !res.por || !spPorAccepted(res.por)) {
// the packet itself was not accepted: another form will not help
lastSw = res.por ? res.por.response_status : (res.error || t('no data'));
break;
}
const data = res.por.decoded ? (res.por.decoded.last_response_data || '') : '';
const sw = ((res.por.decoded && res.por.decoded.last_status_word) || '').toUpperCase();
const parsed = data ? parser(data) : [];
if (parsed.length) {
expandedForm = form;
return { form: form, parsed: parsed, sw: sw };
}
lastSw = sw;
if (ramRemoteSwOk(sw)) {
// accepted but empty: nothing to gain from another form
return { form: form, parsed: [], sw: sw };
}
}
return { form: null, parsed: [], sw: lastSw };
}
async function paginate(p1, collector, parser, label, quiet) {
// Compact listing format (GP 11.4.2.2: P2.b2=0) with the chained GET
@@ -9335,25 +9424,22 @@ async function ramExplore(sp) {
p2 = '01';
}
// Expanded pass: the fallback when the compact listing produced
// nothing (a card may reject P2.b2=0 - it is deprecated), and the
// optional details source (privileges, ELF/SD associations) behind the
// "Fetch expanded registry details" checkbox.
// nothing (a card may reject P2.b2=0 - it is deprecated) and the
// details source (package versions, SD associations, menu entries)
// behind the "Fetch expanded registry details" checkbox (on by
// default). The form ladder and the per-listing report live in
// expandedPass()/expandedReport.
const wantDetails = !!(document.getElementById('ram-expanded-details') || {}).checked;
if (collector.length === before || wantDetails) {
ramShowProgress(label + ' P1=' + p1 + ' P2=02 (expanded)...');
const res = await ramSendOta(ramExpandedQueryApdu(p1), Object.assign({}, sp, { cntr, spi2 }));
if (res.ram_format) sp.ram_format = res.ram_format;
if (res.final_cntr) cntr = res.final_cntr;
else if (res.success && spPorAccepted(res.por)) cntr = ramIncrementCntr(cntr);
const data = (res.success && res.por && res.por.decoded)
? (res.por.decoded.last_response_data || '') : '';
const parsed = data ? parser(data) : [];
const out = await expandedPass(p1, label, parser, spi2);
expandedReport.push({ p1: p1, label: label, compact: collector.length > before,
form: out.form, sw: out.sw });
if (collector.length === before) {
// Nothing from the compact pass: the expanded result is the listing
collector.push(...parsed);
} else if (parsed.length) {
collector.push(...out.parsed);
} else if (out.parsed.length) {
// Keep the compact entries, add the expanded-only fields
ramMergeExpanded(collector, parsed);
ramMergeExpanded(collector, out.parsed);
}
}
// The compact error only surfaces when the expanded pass did not rescue it
@@ -9415,7 +9501,8 @@ async function ramExplore(sp) {
resultEl.classList.remove('hidden', 'text-red-600'); resultEl.classList.add('text-green-600');
}
_ramExplorerData = { mem: mem, isd: isd, apps: apps, elfs: elfs, fmt: sp.ram_format || '' };
_ramExplorerData = { mem: mem, isd: isd, apps: apps, elfs: elfs, fmt: sp.ram_format || '',
expanded: ramExpandedReport(expandedReport) };
ramRenderExplorer();
}
@@ -10805,18 +10892,31 @@ function ramGetStatusApdu(p1, p2) {
return '80F2' + p1 + p2 + '024F0000' + 'C0000000';
}
// P2=02 + the reference tag list: the expanded (E3) registry listing
// (GP Card Spec 11.4.2.3/11.4.3.1). Used as the fallback when a card rejects
// the deprecated compact format and for the optional details pass; the bytes
// are the reference trace form (`80F28002 0C 4F00 5C08 4F9F70C5C4CCCEEA 00`).
// The ELF queries add '84' (Executable Module AIDs, GP Table 11-37): with a
// tag list present the card returns only the listed tags (11.4.3.1), so
// without it the expanded fallback would list packages without their modules
// (no module rows / Install buttons on cards that reject the compact form).
function ramExpandedQueryApdu(p1) {
// P2=02 expanded (E3) registry listing (GP Card Spec 11.4.2.3/11.4.3.1).
// Used as the fallback when a card rejects the deprecated compact format and
// for the details pass (on by default: it carries the package versions, the
// SD/load-file associations and the applet menu entries).
//
// Cards differ in what they accept, so the query is tried in three forms:
// 'tags' - the reference trace tag list (`4F9F70C5C4CCCEEA`), the richest
// 'noea' - the same list without the TS 102 226 'EA' SCP Registry Data
// (a card that errors on the requested-but-absent EA object)
// 'notags' - no tag list (GPPro's GET STATUS data field `4F00` only): the
// card builds the default E3 (AID + life cycle + privileges or
// modules; cards commonly include the load file version anyway)
// The ELF forms add '84' (Executable Module AIDs, GP Table 11-37): with a tag
// list present the card returns only the listed tags, so without it the
// expanded fallback would list packages without their modules (no module rows
// / Install buttons on cards that reject the compact form).
const RAM_EXPANDED_FORMS = ['tags', 'noea', 'notags'];
function ramExpandedQueryApdu(p1, form) {
const f = form || 'tags';
if (f === 'notags') return '80F2' + p1 + '02' + '02' + '4F00' + '00';
const elf = p1 === '10' || p1 === '20';
const tags = '4F9F70C5C4CCCEEA' + (elf ? '84' : '');
return '80F2' + p1 + '02' + (elf ? '0D' : '0C') + '4F005C' + (elf ? '09' : '08') + tags + '00';
const tags = (f === 'noea' ? '4F9F70C5C4CCCE' : '4F9F70C5C4CCCEEA') + (elf ? '84' : '');
const hex = n => n.toString(16).toUpperCase().padStart(2, '0');
return '80F2' + p1 + '02' + hex(tags.length / 2 + 4) + '4F005C' + hex(tags.length / 2) + tags + '00';
}
// The ISD (80) and memory responses fit the ENVELOPE; the Apps (40) and ELF
@@ -18527,7 +18627,17 @@ const LANG_RU = {
'KIc key and KID key are required': 'Укажите ключ KIc и ключ KID',
'RAM command format:': 'Формат команд RAM:',
'Fetch expanded registry details (load-file / SD associations)': 'Запрашивать расширенные данные реестра (связи load-файла и SD)',
'Runs one extra expanded query per listing (P2=02, tag list 4F 9F70 C5 C4 CC CE EA, plus 84 for the ELF queries). The compact listing carries AID, life cycle and privileges only - it has no version; the expanded pass adds the Executable Load File version, the Load File AID, the Security Domain AID, the module lists and the applet STK menu entries (SCP Registry Data). It costs extra secured packets and some cards reject it - the Explore already uses it automatically when the compact listing returns nothing.': 'Запускает дополнительный расширенный запрос для каждого списка (P2=02, список тегов 4F 9F70 C5 C4 CC CE EA, плюс 84 для запросов ELF). Компактный список содержит только AID, состояние и привилегии — версии в нём нет; расширенный проход добавляет версию исполняемого load-файла, AID load-файла, AID Security Domain, списки модулей и пункты меню STK апплета (SCP Registry Data). Он расходует дополнительные защищённые пакеты, и не все карты его поддерживают — Explore и так применяет его автоматически, когда компактный список ничего не вернул.',
'Runs one extra expanded query per listing (P2=02; on by default). The compact listing carries AID, life cycle and privileges only - it has no version; the expanded pass adds the Executable Load File version, the Load File AID, the Security Domain AID, the module lists and the applet STK menu entries (SCP Registry Data). Cards differ in what they accept, so three query forms are tried (tag list with EA, tag list without EA, no tag list - the GPPro form); the explorer reports which form worked and where the run fell back to the compact listing (package versions are then unavailable). It costs a few extra secured packets per Explore; untick to skip it.': 'Запускает дополнительный расширенный запрос для каждого списка (P2=02; включено по умолчанию). Компактный список содержит только AID, состояние и привилегии — версии в нём нет; расширенный проход добавляет версию исполняемого load-файла, AID load-файла, AID Security Domain, списки модулей и пункты меню STK апплета (SCP Registry Data). Карты различаются в том, что принимают, поэтому пробуются три формы запроса (список тегов с EA, список тегов без EA, без списка тегов — форма GPPro); обзор сообщает, какая форма сработала и где выполнен откат к компактному списку (версии пакетов тогда недоступны). Это стоит несколько дополнительных защищённых пакетов на каждый обзор; снимите флажок, чтобы пропустить.',
'Expanded registry:': 'Расширенный реестр:',
'tag list + EA': 'список тегов + EA',
'tag list': 'список тегов',
'no tag list': 'без списка тегов',
'refused': 'отказ',
'compact listing used': 'использован компактный список',
'no expanded data': 'нет расширенных данных',
'Expanded registry refused by the card': 'Карта отклонила расширенный реестр',
'compact listing only (package versions unavailable)': 'только компактный список (версии пакетов недоступны)',
'Package versions come from the expanded registry query \u2014 this card did not return them.': 'Версии пакетов приходят из расширенного запроса реестра \u2014 эта карта их не вернула.',
'STK menu entry:': 'Пункт меню STK:',
'position': 'позиция',
'Other registry data:': 'Прочие данные реестра:',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v326';
const CACHE = 'simple-v327';
const URLS = [
'index.html',
'help.html',
+70 -7
View File
@@ -25,6 +25,7 @@ function extractFunc(src, name) {
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtVersion', 'ramMenuState', 'ramEntryKind', 'ramMenuHtml', 'ramRawTlvsHtml', 'ramNvLine', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramMergeExpanded', 'ramStepLine', 'ramStepComponents', 'ramInstallFailHint', 'ramProbeParse', 'ramCompatVerdict', 'ramCompatProbeNote', 'ramCompatImportRows', 'ramCompatRowsHtml', 'ramCompatFailureHtml', 'ramCompatDetailHtml', 'lookupSw', 'ramExpandedDetailsInit', 'ramExpandedDetailsChanged', 'ramGetStatusApdu', 'ramDeleteApdu',
'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer', 'ramHasInstance', 'ramExpandedQueryApdu',
'_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'ramParseModuleAids', 'parseTLV', '_parseE3Entry', '_parseMenuEntries',
'ramExpandedReport',
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges', 'ramActionBtn', 'ramCapToolkitMode', 'ramOpProgressText',
'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml'];
let code = '';
@@ -35,6 +36,8 @@ const m = html.match(/const _chains = \{\};/);
if (m) code += m[0].replace(/^const /, 'var ') + '\n';
const swm = html.match(/const SW_MAP = \{[\s\S]*?\n\};/);
if (swm) code += swm[0].replace(/^const /, 'var ') + '\n';
const ef = html.match(/const RAM_EXPANDED_FORMS = \[[^\]]*\];/);
if (ef) code += ef[0].replace(/^const /, 'var ') + '\n';
const pn = html.match(/const PRIVILEGE_NAMES = \[[\s\S]*?\n\];/);
if (pn) code += pn[0].replace(/^const /, 'var ') + '\n';
const an = html.match(/const JC_AID_NAMES = \{[\s\S]*?\n\};/);
@@ -546,19 +549,76 @@ test('ramCardIdxAfterRemove keeps the remembered index aligned', () => {
assert.strictEqual(ramCardIdxAfterRemove(null, 1), null);
});
test('ramExpandedQueryApdu matches the reference trace bytes', () => {
// TCA Loader: `AA14 2212 80F28002 0C 4F00 5C08 4F9F70C5C4CCCEEA 00`
test('ramExpandedQueryApdu builds the three expanded query forms', () => {
// the reference trace form (richest: the tag list incl. the TS 102 226 EA)
for (const p1 of ['80', '40']) {
assert.strictEqual(ramExpandedQueryApdu(p1),
'80F2' + p1 + '020C4F005C084F9F70C5C4CCCEEA00');
assert.strictEqual(ramExpandedQueryApdu(p1, 'tags'), ramExpandedQueryApdu(p1));
}
// The ELF queries add '84' (Executable Module AIDs): with a tag list the
// card returns only the listed tags, so the expanded fallback would
// otherwise list packages without their modules (v3.6.51).
// the ELF queries add '84' (Executable Module AIDs)
for (const p1 of ['20', '10']) {
assert.strictEqual(ramExpandedQueryApdu(p1),
'80F2' + p1 + '020D4F005C094F9F70C5C4CCCEEA8400');
}
// without EA: a card that errors on the requested-but-absent EA object
assert.strictEqual(ramExpandedQueryApdu('40', 'noea'), '80F240020B4F005C074F9F70C5C4CCCE00');
assert.strictEqual(ramExpandedQueryApdu('10', 'noea'), '80F210020C4F005C084F9F70C5C4CCCE8400');
// no tag list at all (GPPro's GET STATUS data field: 4F00 only)
assert.strictEqual(ramExpandedQueryApdu('80', 'notags'), '80F28002024F0000');
assert.strictEqual(ramExpandedQueryApdu('10', 'notags'), '80F21002024F0000');
assert.deepStrictEqual(RAM_EXPANDED_FORMS, ['tags', 'noea', 'notags']);
});
test('ramExpandedReport summarizes the query forms and the fallbacks', () => {
globalThis.t = s => s;
// every listing answered the reference form
assert.strictEqual(ramExpandedReport([
{ p1: '80', label: 'ISD', compact: true, form: 'tags', sw: '9000' },
{ p1: '40', label: 'Apps', compact: true, form: 'tags', sw: '9000' },
{ p1: '10', label: 'ELFs', compact: true, form: 'tags', sw: '9000' },
]), 'Expanded registry: tag list + EA (ISD, Apps, ELFs)');
// the ISD commonly has no expanded data (6A88/6A86 is normal there)
assert.strictEqual(ramExpandedReport([
{ p1: '80', label: 'ISD', compact: true, form: null, sw: '6A88' },
{ p1: '40', label: 'Apps', compact: true, form: 'tags', sw: '9000' },
{ p1: '10', label: 'ELFs', compact: true, form: 'noea', sw: '9000' },
]), 'Expanded registry: tag list + EA (Apps) \u00b7 tag list (ELFs) \u00b7 ISD: no expanded data (6A88) \u2014 compact listing used');
// a listing the card refused entirely: the compact listing carried it
assert.strictEqual(ramExpandedReport([
{ p1: '40', label: 'Apps', compact: true, form: null, sw: '6A88' },
{ p1: '10', label: 'ELFs', compact: true, form: 'notags', sw: '9000' },
]), 'Expanded registry: no tag list (ELFs) \u00b7 Apps: refused (6A88) \u2014 compact listing used');
// nothing answered: the headline says the versions are unavailable
assert.strictEqual(ramExpandedReport([
{ p1: '80', label: 'ISD', compact: true, form: null, sw: '6A88' },
{ p1: '10', label: 'ELFs', compact: true, form: null, sw: '6A86' },
]), 'Expanded registry refused by the card \u2014 compact listing only (package versions unavailable) ISD: no expanded data (6A88) \u2014 compact listing used \u00b7 ELFs: refused (6A86) \u2014 compact listing used');
// a listing with no compact data either reports its own status word
assert.strictEqual(ramExpandedReport([
{ p1: '10', label: 'ELFs', compact: false, form: null, sw: '6A88' },
]), 'Expanded registry refused by the card \u2014 compact listing only (package versions unavailable) ELFs: 6A88');
assert.strictEqual(ramExpandedReport([]), '');
delete globalThis.t;
});
test('the explorer hints when the card returned no package versions', () => {
globalThis.t = s => s;
const noVer = ramRenderExploreHtml(null, [], [], [{ aid: 'E1', lifecycle: '01', privileges: '00' }]);
const withVer = ramRenderExploreHtml(null, [], [], [{ aid: 'E1', lifecycle: '01', privileges: '00', version: '0100' }]);
delete globalThis.t;
assert.ok(noVer.includes('Package versions come from the expanded registry query'), noVer);
assert.ok(!withVer.includes('Package versions come from the expanded registry query'), withVer);
});
test('the Explore detects the expanded query form and reports it', () => {
const src = extractFunc(html, 'ramExplore');
assert.ok(src.includes('RAM_EXPANDED_FORMS'), 'the form ladder must be used');
assert.ok(src.includes('expandedReport.push'), 'each listing outcome must be recorded');
assert.ok(src.includes('ramExpandedReport(expandedReport)'), 'the report must reach the explorer');
// the ISD gets a single attempt: its 6A88 must not poison the detection
assert.ok(src.includes("if (p1 === '80') forms = [expandedForm || 'tags'];"),
'the ISD must not walk the ladder');
});
test('ramParseAppStatus parses the expanded E3 listing (vendor traces)', () => {
@@ -1199,13 +1259,16 @@ test('the expanded-details checkbox is a sticky preference', () => {
};
els['ram-expanded-details'].checked = false;
ramExpandedDetailsInit();
assert.strictEqual(els['ram-expanded-details'].checked, false); // default: off
assert.strictEqual(els['ram-expanded-details'].checked, true); // default: on (versions live there)
store['simple_ram_expanded'] = '0';
ramExpandedDetailsInit();
assert.strictEqual(els['ram-expanded-details'].checked, false); // an explicit opt-out is honoured
store['simple_ram_expanded'] = '1';
ramExpandedDetailsInit();
assert.strictEqual(els['ram-expanded-details'].checked, true); // stored choice applied
els['ram-expanded-details'].checked = false;
ramExpandedDetailsChanged();
assert.strictEqual(store['simple_ram_expanded'], '0'); // ticking stores it
assert.strictEqual(store['simple_ram_expanded'], '0'); // unticking stores it
delete globalThis.localStorage;
});
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
version = "3.6.53"
version = "3.6.54"
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.
+1 -1
View File
@@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
VERSION = '3.6.53'
VERSION = '3.6.54'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE