feat: Explore expanded (E3) listing fallback + details pass + format display (v3.6.25)

The E3 parsers existed since the vendor-trace work; what was missing was
issuing the query.  Per GP 11.4.2.2 the compact listing format (P2.b2=0) is
deprecated and a card may reject it, while the expanded form (P2=02 + the
tag list 4F 9F70 C5 C4 CC CE EA) returns the richer registry data.

- ramExpandedQueryApdu(p1): the reference trace bytes
  (80F2 <p1> 02 0C 4F005C084F9F70C5C4CCCEEA 00), pinned in a test.
- Explore: when the compact pass for a P1 yields nothing, the query is
  retried in the expanded format and the result becomes the listing (the
  compact error only surfaces if the retry also fails); the new "Query
  expanded registry details (E3)" checkbox runs the expanded pass on top of
  a working compact listing and merges the expanded-only fields
  (privileges, ELF AID C4, SD AID CC, module lists) into the entries.
- The detected RAM command format (v3.6.24 ram_format) is now shown in the
  Explore result + explorer view, the delete verdict and the plain-send
  message.
- Tests parse the real trace payloads (NC ISD, ET application, NP ELF E3
  templates); help EN/RU updated.

656 frontend / 504 Python green; version 3.6.25; sw simple-v298.
This commit is contained in:
2026-09-28 22:51:20 +03:00
parent cf931da2f9
commit 7c81f02308
7 changed files with 101 additions and 16 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 для информации о памяти. Результаты отображаются в обзоре с кнопками <strong>Удалить</strong> для каждого элемента.</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 для информации о памяти. Если карта отвергает компактный формат листинга (<code class="font-mono text-sm">P2.b2=0</code>, устаревший по GP), запросы повторяются в расширенном формате <code class="font-mono text-sm">E3</code>; флажок <strong>Запрашивать расширенные данные реестра (E3)</strong> дополнительно запрашивает их и объединяет расширенные поля (привилегии, связи ELF/SD, списки модулей). Результаты отображаются в обзоре с кнопками <strong>Удалить</strong> для каждого элемента и определённым форматом команд RAM.</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>Execute</strong> остаётся недоступной до успешного анализа.</td></tr>
</tbody>
</table>
+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 for memory info. Results appear in an explorer view with per-item <strong>Delete</strong> buttons.</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 for memory info. If a card rejects the compact listing (<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; the <strong>Query expanded registry details (E3)</strong> checkbox also requests it as an extra pass and merges the expanded-only fields (privileges, ELF/SD associations, module lists). Results appear in an explorer view with per-item <strong>Delete</strong> buttons and the detected RAM command format.</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. A corrupt or wrong-format file fails the analysis, and <strong>Execute</strong> stays disabled until a successful analysis.</td></tr>
</tbody>
</table>
+60 -10
View File
@@ -703,6 +703,8 @@
<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"><input type="checkbox" id="ram-expanded-details" class="rounded border-gray-300 dark:border-slate-600"> <span data-l10n="Query expanded registry details (E3)">Query expanded registry details (E3)</span></label>
<div id="ram-install-params" class="hidden mb-3">
<div id="ram-cap-rows">
<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>
@@ -1711,7 +1713,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.24';
const SIMPLE_VERSION = '3.6.25';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
@@ -8141,6 +8143,7 @@ async function pysimSendOta() {
} else {
msg += ' | CNTR unchanged (' + ((por && por.response_status) || 'PoR error') + ')';
}
if (data.ram_format) msg += ' | ' + t('RAM command format:') + ' ' + data.ram_format;
sendResultEl.textContent = msg;
if (por && por.raw) {
const rawLine = document.createElement('div');
@@ -8852,13 +8855,14 @@ function ramMergeElfData(elfs, modules) {
}
// Render the full explorer result as structured HTML
function ramRenderExploreHtml(mem, isd, apps, elfs) {
function ramRenderExploreHtml(mem, isd, apps, elfs, fmt) {
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">';
html += '<div class="font-semibold text-sm mb-1">' + esc(t('Memory (GET DATA FF21)')) + '</div>';
html += '<div>' + esc(t('Applications:')) + ' ' + (mem.appCount != null ? mem.appCount : '?') + '</div>';
html += '<div>' + esc(t('Free NV:')) + ' ' + (mem.freeNV != null ? mem.freeNV + ' B' : '?') + '</div>';
if (fmt) html += '<div>' + esc(t('RAM command format:')) + ' ' + esc(fmt) + '</div>';
html += '<div>' + esc(t('Free Volatile:')) + ' ' + (mem.freeV != null ? mem.freeV + ' B' : '?') + '</div>';
html += '</div>';
}
@@ -8940,7 +8944,7 @@ function ramRenderExplorer() {
explorerEl.innerHTML = '';
return;
}
const html = ramRenderExploreHtml(_ramExplorerData.mem, _ramExplorerData.isd, _ramExplorerData.apps, _ramExplorerData.elfs);
const html = ramRenderExploreHtml(_ramExplorerData.mem, _ramExplorerData.isd, _ramExplorerData.apps, _ramExplorerData.elfs, _ramExplorerData.fmt);
explorerEl.innerHTML = html || esc(t('(no data)'));
explorerEl.classList.remove('hidden');
pysimApplyAvailability();
@@ -8961,6 +8965,8 @@ async function ramExplore(sp) {
// commands, so the PoR carries the listing; ramListingSpi2() picks the
// PoR transport per query.
const spi2 = ramListingSpi2(p1);
const before = collector.length;
let compactError = null;
let p2 = '00';
let guard = 0;
while (guard++ < 32) {
@@ -8976,8 +8982,8 @@ async function ramExplore(sp) {
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)) {
const errorMsg = res.por ? res.por.response_status : (res.error || t('no data'));
errors.push(label + ': ' + errorMsg);
compactError = label + ': ' + (res.por ? res.por.response_status
: (res.error || t('no data')));
break;
}
const data = res.por.decoded ? res.por.decoded.last_response_data : '';
@@ -8985,7 +8991,7 @@ async function ramExplore(sp) {
if (!data && res.por.response_status === 'actual_response_sms_submit') {
// Accepted, but the actual-response SMS was not captured: there
// is nothing to parse for this page (not a card error).
errors.push(label + ': ' + t('response via SMS not captured'));
compactError = label + ': ' + t('response via SMS not captured');
break;
}
// Defensive: a remote 61XX means the data is still pending (the
@@ -9001,7 +9007,7 @@ async function ramExplore(sp) {
}
if (sw === '6F00') break;
if (!data) {
if (sw !== '9000') errors.push(label + ': ' + t('(no data)') + ' — SW ' + sw);
if (sw !== '9000') compactError = label + ': ' + t('(no data)') + ' — SW ' + sw;
break;
}
const parsed = parser(data);
@@ -9013,6 +9019,39 @@ async function ramExplore(sp) {
if (p1 === '80') break;
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
// "Query expanded registry details (E3)" checkbox.
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) : [];
if (collector.length === before) {
// Nothing from the compact pass: the expanded result is the listing
collector.push(...parsed);
} else if (parsed.length) {
// Keep the compact entries, add the expanded-only fields
const byAid = {};
collector.forEach(e => { if (e.aid) byAid[e.aid.toUpperCase()] = e; });
parsed.forEach(p => {
const cur = byAid[(p.aid || '').toUpperCase()];
if (!cur) return;
['privileges', 'lifecycle', 'version', 'elfAid', 'sdAid', 'implicitSel'].forEach(k => {
if (p[k] && !cur[k]) cur[k] = p[k];
});
if (p.moduleAids && p.moduleAids.length) cur.moduleAids = p.moduleAids;
});
}
}
// The compact error only surfaces when the expanded pass did not rescue it
if (collector.length === before && compactError) errors.push(compactError);
}
ramShowProgress(t('Memory (GET DATA FF21)') + '...');
@@ -9056,11 +9095,11 @@ async function ramExplore(sp) {
stepsEl.classList.remove('hidden');
stepsEl.textContent = errors.join('\n');
} else {
resultEl.textContent = t('OK') + ' — ' + isd.length + ' ' + t('ISD') + ', ' + apps.length + ' ' + t('Apps') + ', ' + elfs.length + ' ' + t('ELFs') + (mem.freeNV ? ', ' + mem.freeNV + ' ' + t('free NV') : '');
resultEl.textContent = t('OK') + ' — ' + isd.length + ' ' + t('ISD') + ', ' + apps.length + ' ' + t('Apps') + ', ' + elfs.length + ' ' + t('ELFs') + (mem.freeNV ? ', ' + mem.freeNV + ' ' + t('free NV') : '') + (sp.ram_format ? ' — ' + t('RAM command format:') + ' ' + sp.ram_format : '');
resultEl.classList.remove('hidden', 'text-red-600'); resultEl.classList.add('text-green-600');
}
_ramExplorerData = { mem: mem, isd: isd, apps: apps, elfs: elfs };
_ramExplorerData = { mem: mem, isd: isd, apps: apps, elfs: elfs, fmt: sp.ram_format || '' };
ramRenderExplorer();
}
@@ -9170,7 +9209,8 @@ async function ramDeleteFromExplorer(aid, withCascade) {
const removed = ramRemoveFromExplorer(aid, withCascade);
stepsEl.classList.remove('hidden');
stepsEl.textContent = label + ' ' + aid + ' -> ' + (sw || t('no PoR'));
resultEl.textContent = t('OK') + (removed ? '' : ' (' + t('not in the list') + ')');
resultEl.textContent = t('OK') + (removed ? '' : ' (' + t('not in the list') + ')')
+ (res.ram_format ? ' — ' + t('RAM command format:') + ' ' + res.ram_format : '');
resultEl.classList.remove('hidden', 'text-red-600'); resultEl.classList.add('text-green-600');
// The record is dropped locally (no re-explore); the counter the card
// consumed was saved above.
@@ -9872,6 +9912,14 @@ 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`).
function ramExpandedQueryApdu(p1) {
return '80F2' + p1 + '02' + '0C' + '4F005C084F9F70C5C4CCCEEA' + '00';
}
// The ISD (80) and memory responses fit the ENVELOPE; the Apps (40) and ELF
// (20/10) listings can exceed it, and the card then delivers the actual
// response as an SMS-SUBMIT - those queries must request the PoR in submit
@@ -17511,6 +17559,8 @@ const LANG_RU = {
'Card reader server is not connected': 'Сервер картридера не подключён',
'KIc and KID are required': 'Укажите KIc и KID',
'KIc key and KID key are required': 'Укажите ключ KIc и ключ KID',
'RAM command format:': 'Формат команд RAM:',
'Query expanded registry details (E3)': 'Запрашивать расширенные данные реестра (E3)',
'Memory (GET DATA FF21)': 'Память (GET DATA FF21)',
'No secured packet to send. Generate a secure packet first.': 'Нет защищённого пакета для отправки. Сначала соберите пакет.',
'No secured packet to verify. Generate a secure packet first.': 'Нет защищённого пакета для проверки. Сначала соберите пакет.',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v297';
const CACHE = 'simple-v298';
const URLS = [
'index.html',
'help.html',
+36 -1
View File
@@ -23,7 +23,7 @@ function extractFunc(src, name) {
// Extract chain builder functions and dependencies
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramStepLine', 'ramGetStatusApdu', 'ramDeleteApdu',
'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer', 'ramHasInstance',
'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer', 'ramHasInstance', 'ramExpandedQueryApdu',
'_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'parseTLV', '_parseE3Entry',
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute',
'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml'];
@@ -382,6 +382,41 @@ 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`
for (const p1 of ['80', '40', '20', '10']) {
assert.strictEqual(ramExpandedQueryApdu(p1),
'80F2' + p1 + '020C4F005C084F9F70C5C4CCCEEA00');
}
});
test('ramParseAppStatus parses the expanded E3 listing (vendor traces)', () => {
// Explore_NC.log, ISD: E3 { 4F AID, 9F70 LC, C5 privileges }
const isd = ramParseAppStatus('E3134F08A0000000030000009F70010FC5039AFE80');
assert.strictEqual(isd.length, 1);
assert.strictEqual(isd[0].aid, 'A000000003000000');
assert.strictEqual(isd[0].lifecycle, '0F');
assert.strictEqual(isd[0].privileges, '9AFE80');
assert.strictEqual(isd[0].type, 'app');
// Explore_ET.log, application entry: adds the ELF AID (C4) and SD AID (CC)
const apps = ramParseAppStatus('E3374F10A1130001180001FFFFFFFF89A10039089F700107C503000000C410A1130001180001FFFFFFFF89A1003900CC08A000000151000000');
assert.strictEqual(apps.length, 1);
assert.strictEqual(apps[0].aid, 'A1130001180001FFFFFFFF89A1003908');
assert.strictEqual(apps[0].elfAid, 'A1130001180001FFFFFFFF89A1003900');
assert.strictEqual(apps[0].sdAid, 'A000000151000000');
assert.strictEqual(apps[0].privileges, '000000');
});
test('ramParseElfStatus parses the expanded E3 listing (NP trace)', () => {
const elfs = ramParseElfStatus('E31B4F07A00000015153509F700101CE020100CC08A000000151000000');
assert.strictEqual(elfs.length, 1);
assert.strictEqual(elfs[0].aid, 'A0000001515350');
assert.strictEqual(elfs[0].lifecycle, '01');
assert.strictEqual(elfs[0].version, '0100');
assert.strictEqual(elfs[0].sdAid, 'A000000151000000');
assert.strictEqual(elfs[0].type, 'elf');
});
test('ramParseElfStatus lists the compact ELF and module listings (F0414C46416101)', () => {
// Exact bytes from a live RAM Explore: the P1=20 ELF page and the P1=10
// (ELF+modules) page. The old 0x10-scan walk dropped everything after the