Low-counter reporting, counter sync probe, Bad-format decode, expanded forms
Live findings that drove this: the "card counter 0000000000" the low-counter notice showed is not the card's counter at all (the PoR's CNTR is a copy of the command's counter, TS 102 225 5.2 table 3, and a rejection leaves it zeroed); and an earlier "no card accepts the expanded format" observation was a hand-made template whose declared length did not match its body - the card validates the lengths and answers with a Bad format TLV. - _cntr_low_fields(status) reports the verdict as a flag; card_cntr/ suggested_cntr and the UI's "(card counter ...)" text are gone - the value is not observable. The notice offers a bounded Sync counter probe instead. - POST /api/counter-probe: a doubling counter ladder (start+1, +2, +4, ...) with a read-only command until the card accepts one, then accepted+1 is persisted (monotonic, source counter-probe). Rejected attempts change nothing on the card; the search stops at a ceiling (default FFFFFFFF, far below the 40-bit maximum where the counter blocks) and after max_attempts (default 40, clamped 1-64); a non-cntr_low failure stops it at once. - _parse_response_scripting decodes the Bad format TLV (90, TS 101 220 table 7.20; error types per TS 102 226 table 5.12) and returns a dict; the PoR carries bad_format/bad_format_name and _ram_step_result fails the step with "bad format 02 (wrong length)" instead of falling through to the compact decoder (which read the AB tag as 171 commands). - _wrap_expanded_apdu(apdu, form) emits the definite (AA) or the indefinite (AE 80 ... 00 00) scripting template; the RAM command formats are compact/ expanded/expanded-ae and _ram_detect_format probes compact, then both expanded codings (the expanded probes use Le='00' and no chained GET RESPONSE, TS 102 226 5.2.1.1). The RAM form gains a Format selector. - Live-verified: the verdict carries no value; the probe walked 0x1000001/0x1000003 (low) then 0x1000007 (accepted, stored 0x1000008); the ISD executes both expanded codings (R-APDU 08A0000000030000000F80, SW 9000) and a length-mismatched template answers bad_format 02 - decoded now. - Tests: the ladder, the probe walk + persistence, params validation, both template forms, the Bad-format TLV (definite and plain tag styles), the live ISD response through _decode_por, the step-result failure, and the PWA guards; help EN/RU, docs/api.md, the repo AGENTS.md; sw cache simple-v352.
This commit is contained in:
@@ -279,7 +279,7 @@
|
||||
</section>
|
||||
<section class="mb-10">
|
||||
<h2 id="scp80" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">3. Вкладка SCP80</h2>
|
||||
<p class="mb-3">Верхнеуровневая вкладка <strong>SCP80</strong> объединяет разделы, связанные с SCP80. Переключение — тремя переключателями: <strong>Secured Packet</strong>, <strong>RAM</strong> и <strong>TAR-сканер</strong>. Собирает защищённые пакеты SCP80 по ETSI TS 102 225. При подключении карты сервер читает её <strong>EF.ICCID</strong> (2FE2); если в пресете указан тот же номер (цифрами или сырым hex из EF), он автоматически выбирается во всех трёх представлениях SCP80. Вердикт <strong>низкого счётчика</strong> (<code class="font-mono text-sm">cntr_low</code> или предупреждающий ответ ENVELOPE, в котором всё ещё есть такой PoR) <em>не</em> исправляется повторной отправкой: любая операция SCP80/RAM немедленно останавливается, результат показывает счётчик карты и кнопку <strong>К пресету</strong>, открывающую соответствующий пресет на вкладке Cards, где счётчик нужно поднять выше — автоматически ничего не записывается.</p>
|
||||
<p class="mb-3">Верхнеуровневая вкладка <strong>SCP80</strong> объединяет разделы, связанные с SCP80. Переключение — тремя переключателями: <strong>Secured Packet</strong>, <strong>RAM</strong> и <strong>TAR-сканер</strong>. Собирает защищённые пакеты SCP80 по ETSI TS 102 225. При подключении карты сервер читает её <strong>EF.ICCID</strong> (2FE2); если в пресете указан тот же номер (цифрами или сырым hex из EF), он автоматически выбирается во всех трёх представлениях SCP80. Вердикт <strong>низкого счётчика</strong> (<code class="font-mono text-sm">cntr_low</code> или предупреждающий ответ ENVELOPE, в котором всё ещё есть такой PoR) <em>не</em> исправляется повторной отправкой: любая операция SCP80/RAM немедленно останавливается. Карта не сообщает свой счётчик (поле CNTR в PoR — копия счётчика пакета), поэтому в уведомлении есть ограниченный пробник <strong>«Синхронизировать счётчик»</strong> — read-only команда с возрастающими значениями счётчика (шаг удваивается, потолок 32 бита, не более 40 пакетов), пока карта не примет одно; затем сохраняется принятое+1 — и кнопка <strong>К пресету</strong> для ручного повышения; больше ничего автоматически не записывается.</p>
|
||||
|
||||
<h3 id="secured-packet" class="text-lg font-medium mb-2">3.1 Secured Packet</h3>
|
||||
<p class="mb-2">Собирает защищённые пакеты SCP80 по ETSI TS 102 225. Селектор <strong>Keyset</strong> рядом с выбором пресета указывает, из какого keyset берутся ключи и счётчик; отправка отклоняется, если в пресете нет такого номера keyset или KIc/KID несут разные номера (TS 102 225 A.2). Селектор недоступен, пока не выбран пресет карты. Кнопка запуска недоступна, пока не выбран пресет карты и один из его keysets.</p>
|
||||
@@ -314,7 +314,7 @@
|
||||
<p class="text-sm mb-3">Кнопка <strong>Проверить в pySim</strong> сверяет собранный пакет с эталонной реализацией <code class="font-mono text-sm">OtaDialectSms.encode_cmd</code>. Кнопка <strong>Отправить на карту</strong> доставляет пакет через ENVELOPE SMS-PP-DOWNLOAD (при подключении к серверу). 2-байтовый префикс CPL передаётся, когда пакет зашифрован или несёт RC/CC/DS (входит в их входные данные), а также при конкатенации SMS; одиночное незащищённое SMS сохраняет форму pySim, начинающуюся с CHL (TS 31.115 Table 1 NOTE). Под полем пакета показывается его размер и число SMS; пакет, не помещающийся в одно SMS, отправляется <strong>конкатенированной</strong> загрузкой (TS 31.115 §4.3) — пакет делится на части SMS-пользовательских данных (первое SMS 132 октета, последующие по 134; первое дополнительно несёт информационные элементы конкатенации и CPI), и сегменты отправляются по порядку. Более 5 сегментов отклоняется: это практический предел буфера конкатенации карты. Полученный Proof of Receipt декодируется и показывается строкой статуса PoR (статус, TAR, счётчик, сырой PoR); статусное слово и данные ответа последней команды подставляются в подвкладку <strong>«Разбор R-APDU»</strong> (Remote APDU → «Разбор»), а успешный PoR увеличивает счётчик повторов (когда в SPI1 есть проверка счётчика — b5 b4 ≠ 00; без неё счётчик не увеличивается и не сохраняется) и очищает пакет.</p>
|
||||
|
||||
<h3 id="ram" class="text-lg font-medium mb-2">3.2 RAM</h3>
|
||||
<p class="mb-2">Выполняет операции удалённого управления приложениями (Remote Application Management) как защищённые пакеты SCP80 через SMS-PP-DOWNLOAD ENVELOPE. Карта должна поддерживать SCP03 (AES или 3DES). Пресет карты со вкладки <strong>Карты</strong> обеспечивает SPI, ключи, TAR и счётчик (RAM использует <strong>ISD TAR</strong> пресета, по умолчанию <code class="font-mono text-sm">000000</code>). Селектор <strong>Keyset</strong> рядом с выбором пресета указывает, из какого keyset берутся ключи и счётчик операции. Селектор недоступен, пока не выбран пресет карты. Кнопка запуска недоступна, пока не выбран пресет карты и один из его keysets.</p>
|
||||
<p class="mb-2">Выполняет операции удалённого управления приложениями (Remote Application Management) как защищённые пакеты SCP80 через SMS-PP-DOWNLOAD ENVELOPE. Карта должна поддерживать SCP03 (AES или 3DES). Пресет карты со вкладки <strong>Карты</strong> обеспечивает SPI, ключи, TAR и счётчик (RAM использует <strong>ISD TAR</strong> пресета, по умолчанию <code class="font-mono text-sm">000000</code>). Селектор <strong>Keyset</strong> рядом с выбором пресета указывает, из какого keyset берутся ключи и счётчик операции. Селектор недоступен, пока не выбран пресет карты. Кнопка запуска недоступна, пока не выбран пресет карты и один из его keysets. Селектор <strong>Формат</strong> фиксирует формат удалённой команды: <strong>Авто (определение)</strong> опрашивает read-only команду GET STATUS в компактном виде и в обоих expanded-кодированиях и оставляет первое, которое карта выполнила; <strong>Компактный</strong> отправляет голую C-APDU; <strong>Expanded (AA, определённая длина)</strong> / <strong>Expanded (AE 80, неопределённая длина)</strong> заворачивают её в шаблон Command Scripting из TS 102 226 §5.2.1 (на живой карте выполняются обе; шаблон, у которого заявленная длина не совпадает с телом, получает Bad format TLV — операция показывает это как ошибку шага).</p>
|
||||
|
||||
<h4 id="ram-operations" class="font-medium mb-1">Операции</h4>
|
||||
<table class="w-full text-sm mb-3 border-collapse">
|
||||
|
||||
+2
-2
@@ -310,10 +310,10 @@
|
||||
<li>AES requires a replay-protected counter: SPI1 bits b5 b4 must be <code class="font-mono text-sm">10</code> (counter higher) or <code class="font-mono text-sm">11</code> (counter +1) per TS 102 225 §5.1.2/§5.1.3.1</li>
|
||||
<li>Padding byte configurable (<code class="font-mono text-sm">00</code> default, or <code class="font-mono text-sm">FF</code>)</li>
|
||||
</ul>
|
||||
<p class="text-sm mb-3">A “Verify vs pySim” button cross-checks the assembled packet against pySim’s reference <code class="font-mono text-sm">OtaDialectSms.encode_cmd</code>. A “Send to Card” button delivers it via SMS-PP-DOWNLOAD ENVELOPE (when connected to the server). The 2-byte CPL prefix is transmitted whenever the packet is ciphered or carries RC/CC/DS (it is part of their input) and whenever the packet is concatenated; a single unprotected SMS keeps pySim’s CHL-first form (TS 31.115 Table 1 NOTE). Under the packet field the app reports its size and how many SMS it takes; a packet that does not fit one SMS is sent as a <strong>concatenated</strong> download (TS 31.115 §4.3) — the packet is split into SMS user-data parts (first SM 132 octets, following ones 134; the first one additionally carries the concatenation and CPI information elements) and the segments are sent in order. More than 5 segments is refused, that being the practical limit of the card’s concatenation buffer. The returned Proof of Receipt is decoded and shown as a PoR status line (status, TAR, counter, raw PoR); the last command’s status word and response data are filled into the <strong>R-APDU parser</strong> panel, and a successful PoR advances the replay counter (when SPI1 carries a counter check — b5 b4 ≠ 00; without one no counter is advanced or saved) and clears the packet. A <strong>low-counter</strong> verdict (<code class="font-mono text-sm">cntr_low</code>, or a warning ENVELOPE answer that still carries such a PoR) is <em>not</em> recoverable by retrying: every SCP80/RAM operation stops immediately and the result states the card's counter with a <strong>Go to preset</strong> button that opens the matching preset in the Cards tab, where the counter must be raised above it — nothing is written automatically.</p>
|
||||
<p class="text-sm mb-3">A “Verify vs pySim” button cross-checks the assembled packet against pySim’s reference <code class="font-mono text-sm">OtaDialectSms.encode_cmd</code>. A “Send to Card” button delivers it via SMS-PP-DOWNLOAD ENVELOPE (when connected to the server). The 2-byte CPL prefix is transmitted whenever the packet is ciphered or carries RC/CC/DS (it is part of their input) and whenever the packet is concatenated; a single unprotected SMS keeps pySim’s CHL-first form (TS 31.115 Table 1 NOTE). Under the packet field the app reports its size and how many SMS it takes; a packet that does not fit one SMS is sent as a <strong>concatenated</strong> download (TS 31.115 §4.3) — the packet is split into SMS user-data parts (first SM 132 octets, following ones 134; the first one additionally carries the concatenation and CPI information elements) and the segments are sent in order. More than 5 segments is refused, that being the practical limit of the card’s concatenation buffer. The returned Proof of Receipt is decoded and shown as a PoR status line (status, TAR, counter, raw PoR); the last command’s status word and response data are filled into the <strong>R-APDU parser</strong> panel, and a successful PoR advances the replay counter (when SPI1 carries a counter check — b5 b4 ≠ 00; without one no counter is advanced or saved) and clears the packet. A <strong>low-counter</strong> verdict (<code class="font-mono text-sm">cntr_low</code>, or a warning ENVELOPE answer that still carries such a PoR) is <em>not</em> recoverable by retrying: every SCP80/RAM operation stops immediately. The card never reports its counter (the PoR’s CNTR field is a copy of the packet’s), so the notice offers a bounded <strong>Sync counter</strong> probe — a read-only command with increasing counter values (doubling steps, a 32-bit ceiling, at most 40 packets) until the card accepts one, then accepted+1 is stored — and a <strong>Go to preset</strong> button for raising the counter by hand; nothing else is written automatically.</p>
|
||||
|
||||
<h3 id="ram" class="text-lg font-medium mb-2">3.2 RAM</h3>
|
||||
<p class="mb-2">Delivers Remote Application Management operations as SCP80 secured packets via SMS-PP-DOWNLOAD ENVELOPE. The card must support SCP03 (AES or 3DES). A saved card preset from the <strong>Cards</strong> tab provides the SPI, keys, TAR, and counter (RAM uses the preset's <strong>ISD TAR</strong>, <code class="font-mono text-sm">000000</code> by default). The <strong>Keyset</strong> selector next to the preset selector picks the keyset whose keys and counter the operation uses. It is disabled until a card preset is selected. The start button stays disabled until a card preset and one of its keysets are selected.</p>
|
||||
<p class="mb-2">Delivers Remote Application Management operations as SCP80 secured packets via SMS-PP-DOWNLOAD ENVELOPE. The card must support SCP03 (AES or 3DES). A saved card preset from the <strong>Cards</strong> tab provides the SPI, keys, TAR, and counter (RAM uses the preset's <strong>ISD TAR</strong>, <code class="font-mono text-sm">000000</code> by default). The <strong>Keyset</strong> selector next to the preset selector picks the keyset whose keys and counter the operation uses. It is disabled until a card preset is selected. The start button stays disabled until a card preset and one of its keysets are selected. The <strong>Format</strong> selector pins the remote command format: <strong>Auto (detect)</strong> probes the read-only GET STATUS command compact and in both expanded codings and keeps the first that the card executes, <strong>Compact</strong> sends the bare C-APDU, and <strong>Expanded (AA, definite length)</strong> / <strong>Expanded (AE 80, indefinite length)</strong> wrap it in the TS 102 226 §5.2.1 scripting template (both execute on the live ISD; a template whose declared lengths do not match its body draws a Bad format TLV, which the operation shows as the step failure).</p>
|
||||
|
||||
<h4 id="ram-operations" class="font-medium mb-1">Operations</h4>
|
||||
<table class="w-full text-sm mb-3 border-collapse">
|
||||
|
||||
+103
-51
@@ -708,6 +708,15 @@
|
||||
<option value="make-selectable" data-l10n="INSTALL [for make selectable]">INSTALL [for make selectable]</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="Format">Format</label>
|
||||
<select id="ram-format" 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="auto" data-l10n="Auto (detect)">Auto (detect)</option>
|
||||
<option value="compact" data-l10n="Compact">Compact</option>
|
||||
<option value="expanded" data-l10n="Expanded (AA, definite length)">Expanded (AA, definite length)</option>
|
||||
<option value="expanded-ae" data-l10n="Expanded (AE 80, indefinite length)">Expanded (AE 80, indefinite length)</option>
|
||||
</select>
|
||||
</div>
|
||||
<button data-needs="card" data-needs-keyset="ram" 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>
|
||||
<div id="ram-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>
|
||||
@@ -8304,13 +8313,11 @@ async function pysimSendOta() {
|
||||
try {
|
||||
const data = await pysimFetch('/api/send-ota', { sp, ...spParams() });
|
||||
// low counter: not recoverable by retrying (v3.6.58)
|
||||
const low = spCntrLow(data);
|
||||
if (low) {
|
||||
sendResultEl.textContent = t('Low counter') + ' — ' + t('the card rejected the packet') +
|
||||
(low.card_cntr ? ' (' + t('card counter') + ' ' + low.card_cntr + ')' : '');
|
||||
if (spCntrLow(data)) {
|
||||
sendResultEl.textContent = t('Low counter') + ' — ' + t('the card rejected the packet');
|
||||
sendResultEl.classList.remove('text-gray-500', 'text-green-600');
|
||||
sendResultEl.classList.add('text-red-600');
|
||||
ramShowCntrLow(low);
|
||||
ramShowCntrLow();
|
||||
return;
|
||||
}
|
||||
if (data.success) {
|
||||
@@ -8851,6 +8858,7 @@ function getRamSpParams(selId) {
|
||||
cntr: (document.getElementById('sp-cntr').value || '0000000001').replace(/[^0-9a-fA-F]/g, ''),
|
||||
kicKey: (document.getElementById('sp-kic-key').value || '').replace(/[^0-9a-fA-F]/g, ''),
|
||||
kidKey: (document.getElementById('sp-kid-key').value || '').replace(/[^0-9a-fA-F]/g, ''),
|
||||
ram_format: (document.getElementById('ram-format') || {}).value || 'auto',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9599,13 +9607,10 @@ async function ramExplore(sp) {
|
||||
// own - a retry cannot help, so the run stops immediately and the user is
|
||||
// told to raise the preset counter (v3.6.58). Every send of the run goes
|
||||
// through sendOta() so the verdict is caught wherever it appears.
|
||||
let cntrLow = null;
|
||||
let cntrLow = false;
|
||||
async function sendOta(apdu, params) {
|
||||
const res = await ramSendOta(apdu, params);
|
||||
if (!cntrLow) {
|
||||
const low = spCntrLow(res);
|
||||
if (low) cntrLow = low;
|
||||
}
|
||||
if (!cntrLow && spCntrLow(res)) cntrLow = true;
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -9900,7 +9905,7 @@ async function ramExplore(sp) {
|
||||
// the run was stopped: state the verdict, do not retry
|
||||
resultEl.textContent = t('Low counter') + ' — ' + t('the operation was stopped');
|
||||
resultEl.classList.remove('hidden', 'text-green-600'); resultEl.classList.add('text-red-600');
|
||||
ramShowCntrLow(cntrLow);
|
||||
ramShowCntrLow();
|
||||
} else if (errors.length) {
|
||||
resultEl.textContent = t('Partial') + ' — ' + errors.join('; ');
|
||||
resultEl.classList.remove('hidden', 'text-green-600'); resultEl.classList.add('text-red-600');
|
||||
@@ -10005,12 +10010,11 @@ async function ramDeleteFromExplorer(aid, withCascade) {
|
||||
const res = await ramSendOta(apdu, sp);
|
||||
ramHideProgress();
|
||||
// low counter: stop - retrying cannot help (v3.6.58)
|
||||
const low = spCntrLow(res);
|
||||
if (low) {
|
||||
if (spCntrLow(res)) {
|
||||
const el = document.getElementById('ram-result');
|
||||
el.textContent = t('Low counter') + ' — ' + t('the operation was stopped');
|
||||
el.classList.remove('hidden', 'text-green-600'); el.classList.add('text-red-600');
|
||||
ramShowCntrLow(low);
|
||||
ramShowCntrLow();
|
||||
return;
|
||||
}
|
||||
// The card consumes the counter as soon as it accepts the packet, even
|
||||
@@ -10435,10 +10439,9 @@ async function ramCompatTest(sp) {
|
||||
let modal = esc(txt);
|
||||
if (probeNote) modal += '<div class="mt-2 text-gray-500 dark:text-slate-400">' + esc(probeNote) + '</div>';
|
||||
modal += ramCompatDetailHtml(data, mem);
|
||||
const low = spCntrLow(data);
|
||||
if (low) {
|
||||
modal += ramCntrLowHtml(low, ramCntrLowPresetIdx());
|
||||
ramShowCntrLow(low);
|
||||
if (spCntrLow(data)) {
|
||||
modal += ramCntrLowHtml(ramCntrLowPresetIdx());
|
||||
ramShowCntrLow();
|
||||
}
|
||||
ramOpModalFinish(verdict, !!data.imports_ok, modal, true);
|
||||
}
|
||||
@@ -10808,27 +10811,27 @@ function ramCntrLowPresetIdx() {
|
||||
return ram >= 0 ? ram : pick('sp-card-sel');
|
||||
}
|
||||
|
||||
// Low-counter notice (pure HTML): the facts plus a button that opens the
|
||||
// failed operation's preset in the Cards tab, where the user raises the
|
||||
// counter - nothing is written automatically.
|
||||
function ramCntrLowHtml(info, presetIdx) {
|
||||
if (!info) return '';
|
||||
// Low-counter notice (pure HTML): the facts plus two actions - a bounded
|
||||
// counter sync probe and a button that opens the failed operation's preset in
|
||||
// the Cards tab, where the counter can be raised by hand.
|
||||
function ramCntrLowHtml(presetIdx) {
|
||||
const idx = Number.isInteger(presetIdx) && presetIdx >= 0 ? presetIdx : -1;
|
||||
let text = t('Low counter') + ' \u2014 ' + t('the card rejected the packet');
|
||||
if (info.card_cntr) text += ' (' + t('card counter') + ' ' + esc(info.card_cntr) + ')';
|
||||
if (info.suggested) text += ' \u2014 ' + t('the counter must be above it');
|
||||
const text = t('Low counter') + ' \u2014 ' + t('the card rejected the packet');
|
||||
const btnCls = 'px-2 py-1 text-xs rounded border border-red-300 dark:border-red-700 ' +
|
||||
'hover:bg-red-100 dark:hover:bg-red-900/40 whitespace-nowrap';
|
||||
return '<div class="flex items-center gap-3 text-sm text-red-600 dark:text-red-400">' +
|
||||
'<span>' + text + '</span>' +
|
||||
'<button type="button" onclick="ramGoToPreset(' + idx + ')" class="px-2 py-1 text-xs rounded border border-red-300 dark:border-red-700 hover:bg-red-100 dark:hover:bg-red-900/40 whitespace-nowrap">' +
|
||||
'<button type="button" onclick="ramSyncCounter(' + idx + ')" class="' + btnCls + '">' +
|
||||
esc(t('Sync counter')) + '</button>' +
|
||||
'<button type="button" onclick="ramGoToPreset(' + idx + ')" class="' + btnCls + '">' +
|
||||
esc(t('Go to preset')) + '</button></div>';
|
||||
}
|
||||
|
||||
// Show the low-counter notice in the RAM result area (and, when the operation
|
||||
// modal is still open, in its detail block).
|
||||
function ramShowCntrLow(info, presetIdx) {
|
||||
if (!info) return;
|
||||
function ramShowCntrLow(presetIdx) {
|
||||
const idx = presetIdx === undefined ? ramCntrLowPresetIdx() : presetIdx;
|
||||
const html = ramCntrLowHtml(info, idx);
|
||||
const html = ramCntrLowHtml(idx);
|
||||
const el = document.getElementById('ram-cntr-low');
|
||||
if (el && el.classList) {
|
||||
el.innerHTML = html;
|
||||
@@ -10840,6 +10843,54 @@ function ramShowCntrLow(info, presetIdx) {
|
||||
}
|
||||
}
|
||||
|
||||
// Sync the counter of the failed operation's preset with the card: the server
|
||||
// probes with a read-only command and increasing counter values (start+1, +2,
|
||||
// +4, ...) until the card accepts one, stops at a 32-bit ceiling and persists
|
||||
// accepted+1. A rejected attempt changes nothing on the card, so only the
|
||||
// accepted packet moves its counter (the card never reports its value).
|
||||
async function ramSyncCounter(presetIdx) {
|
||||
const i = Number(presetIdx);
|
||||
const preset = (Number.isInteger(i) && i >= 0 && cards[i]) ? cards[i] : null;
|
||||
const box = document.getElementById('ram-cntr-low');
|
||||
if (!preset) { alert(t('No card preset selected')); return; }
|
||||
// The keyset the failed operation used: the RAM form's selector, else the
|
||||
// SCP80 form's (both hold the keyset number as their option value).
|
||||
const kvnOf = id => {
|
||||
const sel = document.getElementById(id);
|
||||
const v = sel && sel.value !== '' ? parseInt(sel.value, 10) : NaN;
|
||||
return Number.isInteger(v) && v > 0 ? v : null;
|
||||
};
|
||||
const val = id => (document.getElementById(id) || {}).value || '';
|
||||
const body = { preset_id: preset.id };
|
||||
const kvn = kvnOf('ram-keyset-sel') || kvnOf('sp-keyset-sel');
|
||||
if (kvn) body.kvn = kvn;
|
||||
const tar = val('sp-tar').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
|
||||
if (tar) body.tar = tar;
|
||||
if (val('sp-spi1')) body.spi1 = val('sp-spi1');
|
||||
if (val('sp-spi2-hex')) body.spi2 = val('sp-spi2-hex');
|
||||
const cntr = val('sp-cntr').replace(/[^0-9a-fA-F]/g, '');
|
||||
if (cntr) body.cntr = cntr;
|
||||
if (box) box.innerHTML = '<div class="text-sm text-gray-500 dark:text-slate-400">' +
|
||||
esc(t('Syncing counter...')) + '</div>';
|
||||
try {
|
||||
const data = await pysimFetch('/api/counter-probe', body);
|
||||
if (data.success) {
|
||||
await cardsFetch();
|
||||
const cntrEl = document.getElementById('sp-cntr');
|
||||
if (cntrEl && data.stored_cntr) cntrEl.value = data.stored_cntr;
|
||||
const msg = t('Counter synced') + ': ' + esc(data.accepted_cntr) + ' \u2192 ' +
|
||||
esc(data.stored_cntr) + '. ' + t('Retry the operation.');
|
||||
if (box) box.innerHTML = '<div class="text-sm text-emerald-600 dark:text-emerald-400">' + msg + '</div>';
|
||||
} else {
|
||||
const msg = t('Counter sync failed') + ': ' + esc(data.error || '');
|
||||
if (box) box.innerHTML = '<div class="text-sm text-red-600 dark:text-red-400">' + msg + '</div>';
|
||||
}
|
||||
} catch (e) {
|
||||
if (box) box.innerHTML = '<div class="text-sm text-red-600 dark:text-red-400">' +
|
||||
esc(String(e.message || e)) + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Open a preset in the Cards tab for editing (the user fixes the counter):
|
||||
// select it, scroll to the counter field and focus it.
|
||||
function ramGoToPreset(idx) {
|
||||
@@ -10904,12 +10955,11 @@ function ramShowInstallResult(data, okText, capMem) {
|
||||
// successful run and for failures that are not LOAD/INSTALL [for load]).
|
||||
let modal = esc(txt);
|
||||
modal += ramCompatDetailHtml(data, capMem || null);
|
||||
// low counter: not recoverable by retrying - state it and point at the
|
||||
// preset, where the user raises the counter (v3.6.58)
|
||||
const low = spCntrLow(data);
|
||||
if (low) {
|
||||
modal += ramCntrLowHtml(low, ramCntrLowPresetIdx());
|
||||
ramShowCntrLow(low);
|
||||
// low counter: not recoverable by retrying - state it and offer the
|
||||
// bounded sync probe or the preset jump (v3.6.58)
|
||||
if (spCntrLow(data)) {
|
||||
modal += ramCntrLowHtml(ramCntrLowPresetIdx());
|
||||
ramShowCntrLow();
|
||||
}
|
||||
ramOpModalFinish(resultEl.textContent, !!data.success, modal, true);
|
||||
}
|
||||
@@ -12003,22 +12053,15 @@ function spPorAccepted(por) {
|
||||
// The card's low-counter verdict of a send response (v3.6.58). Every further
|
||||
// packet with a counter <= the card's is rejected again, so the operation must
|
||||
// stop and the preset counter must be raised - a retry cannot recover it.
|
||||
// Returns {card_cntr, suggested} or null.
|
||||
// Returns true for a cntr_low verdict, false otherwise. No value is reported:
|
||||
// the PoR's CNTR is a copy of the command's counter (TS 102 225 5.2, table 3)
|
||||
// and a rejection leaves it zeroed, so the card's counter is not observable -
|
||||
// the server's counter probe or a manual raise are the ways forward.
|
||||
function spCntrLow(res) {
|
||||
if (!res) return null;
|
||||
if (!res) return false;
|
||||
const por = res.por || null;
|
||||
const status = (por && por.response_status) || '';
|
||||
if (!res.cntr_low && status !== 'cntr_low') return null;
|
||||
const card = String(res.card_cntr || (por && por.cntr) || '').toUpperCase();
|
||||
let suggested = String(res.suggested_cntr || '').toUpperCase();
|
||||
if (!suggested && /^[0-9A-F]+$/.test(card)) {
|
||||
try {
|
||||
// the SCP80 counter field is fixed-width: wrap like the server does
|
||||
const mod = 16n ** BigInt(card.length);
|
||||
suggested = ((BigInt('0x' + card) + 1n) % mod).toString(16).toUpperCase().padStart(card.length, '0');
|
||||
} catch (e) { suggested = ''; }
|
||||
}
|
||||
return { card_cntr: card, suggested: suggested };
|
||||
return !!(res.cntr_low || status === 'cntr_low');
|
||||
}
|
||||
|
||||
// Remote-command status words that count as success (mirrors the server's
|
||||
@@ -20264,8 +20307,17 @@ const LANG_RU = {
|
||||
'request error': 'ошибка запроса',
|
||||
'Low counter': 'Низкий счётчик',
|
||||
'the card rejected the packet': 'карта отклонила пакет',
|
||||
'card counter': 'счётчик карты',
|
||||
'the counter must be above it': 'счётчик должен быть больше',
|
||||
'Sync counter': 'Синхронизировать счётчик',
|
||||
'Syncing counter...': 'Синхронизация счётчика...',
|
||||
'Counter synced': 'Счётчик синхронизирован',
|
||||
'Counter sync failed': 'Не удалось синхронизировать счётчик',
|
||||
'Retry the operation.': 'Повторите операцию.',
|
||||
'No card preset selected': 'Пресет карты не выбран',
|
||||
'Format': 'Формат',
|
||||
'Auto (detect)': 'Авто (определение)',
|
||||
'Compact': 'Компактный',
|
||||
'Expanded (AA, definite length)': 'Expanded (AA, определённая длина)',
|
||||
'Expanded (AE 80, indefinite length)': 'Expanded (AE 80, неопределённая длина)',
|
||||
'Go to preset': 'К пресету',
|
||||
'first page only': 'только первая страница',
|
||||
'the operation was stopped': 'операция остановлена',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v351';
|
||||
const CACHE = 'simple-v352';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
+37
-19
@@ -746,33 +746,33 @@ test('the explorer hints why the package versions are missing', () => {
|
||||
});
|
||||
|
||||
test('spCntrLow reads the low-counter verdict of a send response', () => {
|
||||
// the server's explicit fields
|
||||
const a = spCntrLow({ cntr_low: true, card_cntr: '00000000BB', suggested_cntr: '00000000BC' });
|
||||
assert.deepStrictEqual(a, { card_cntr: '00000000BB', suggested: '00000000BC' });
|
||||
// the server's explicit flag
|
||||
assert.strictEqual(spCntrLow({ cntr_low: true }), true);
|
||||
// derived from the PoR alone (a 9000/91xx ENVELOPE can carry a cntr_low PoR)
|
||||
const b = spCntrLow({ por: { response_status: 'cntr_low', cntr: '00000000BB' } });
|
||||
assert.deepStrictEqual(b, { card_cntr: '00000000BB', suggested: '00000000BC' });
|
||||
// the counter is 40-bit: the wrap keeps the width
|
||||
const c = spCntrLow({ por: { response_status: 'cntr_low', cntr: 'FFFFFFFFFF' } });
|
||||
assert.strictEqual(c.suggested, '0000000000');
|
||||
assert.strictEqual(spCntrLow({ por: { response_status: 'cntr_low', cntr: '00000000BB' } }), true);
|
||||
// every other verdict (and a missing response) is not a low counter
|
||||
assert.strictEqual(spCntrLow({ por: { response_status: 'por_ok', cntr: '01' } }), null);
|
||||
assert.strictEqual(spCntrLow({ por: { response_status: 'cntr_high' } }), null);
|
||||
assert.strictEqual(spCntrLow({}), null);
|
||||
assert.strictEqual(spCntrLow(null), null);
|
||||
assert.strictEqual(spCntrLow({ por: { response_status: 'por_ok', cntr: '01' } }), false);
|
||||
assert.strictEqual(spCntrLow({ por: { response_status: 'cntr_high' } }), false);
|
||||
assert.strictEqual(spCntrLow({}), false);
|
||||
assert.strictEqual(spCntrLow(null), false);
|
||||
});
|
||||
|
||||
test('ramCntrLowHtml states the facts and offers the preset jump', () => {
|
||||
test('ramCntrLowHtml states the facts without a fabricated counter', () => {
|
||||
globalThis.t = s => s;
|
||||
const out = ramCntrLowHtml({ card_cntr: '00000000BB', suggested: '00000000BC' }, 2);
|
||||
const noPreset = ramCntrLowHtml({ card_cntr: '00000000BB' }, -1);
|
||||
const out = ramCntrLowHtml(2);
|
||||
const noPreset = ramCntrLowHtml(-1);
|
||||
delete globalThis.t;
|
||||
assert.ok(out.includes('Low counter \u2014 the card rejected the packet (card counter 00000000BB) \u2014 the counter must be above it'), out);
|
||||
assert.ok(out.includes('Low counter \u2014 the card rejected the packet'), out);
|
||||
// the PoR's CNTR is a copy of the command's counter, never the card's: no
|
||||
// value may be shown (v3.9.x), only the verdict and the two actions
|
||||
assert.ok(!out.includes('card counter'), out);
|
||||
assert.ok(out.includes('onclick="ramSyncCounter(2)"'), out);
|
||||
assert.ok(out.includes('Sync counter'), out);
|
||||
assert.ok(out.includes('onclick="ramGoToPreset(2)"'), out);
|
||||
assert.ok(out.includes('Go to preset'), out);
|
||||
// without a known preset the button still opens the tab (-1)
|
||||
// without a known preset the buttons still work (-1)
|
||||
assert.ok(noPreset.includes('onclick="ramGoToPreset(-1)"'), noPreset);
|
||||
assert.strictEqual(ramCntrLowHtml(null, 0), '');
|
||||
assert.ok(noPreset.includes('onclick="ramSyncCounter(-1)"'), noPreset);
|
||||
});
|
||||
|
||||
test('the explorer shows versions on app and ISD rows too', () => {
|
||||
@@ -804,7 +804,7 @@ test('every SCP80/RAM flow checks the low counter and stops', () => {
|
||||
const explore = extractFunc(html, 'ramExplore');
|
||||
assert.ok(explore.includes('spCntrLow(res)'), 'the Explore must check every send');
|
||||
assert.ok(explore.includes('if (cntrLow) break;'), 'the loops must stop');
|
||||
assert.ok(explore.includes('ramShowCntrLow(cntrLow)'), 'the verdict must be shown');
|
||||
assert.ok(explore.includes('ramShowCntrLow()'), 'the verdict must be shown');
|
||||
// the delete flow and the plain send check it too
|
||||
assert.ok(extractFunc(html, 'ramDeleteFromExplorer').includes('spCntrLow(res)'),
|
||||
'the delete flow must stop');
|
||||
@@ -813,6 +813,24 @@ test('every SCP80/RAM flow checks the low counter and stops', () => {
|
||||
assert.ok(go.includes("switchTab('cards')"), go);
|
||||
assert.ok(go.includes('cardsEdit('), go);
|
||||
assert.ok(go.includes("getElementById('cards-ks-' + row + '-cntr')"), go);
|
||||
// the notice's sync probe posts to the bounded server endpoint with the
|
||||
// preset, the keyset the failed operation used and its SPI/TAR
|
||||
const sync = extractFunc(html, 'ramSyncCounter');
|
||||
assert.ok(sync.includes("'/api/counter-probe'"), sync);
|
||||
assert.ok(sync.includes("kvnOf('ram-keyset-sel')"), sync);
|
||||
assert.ok(sync.includes("kvnOf('sp-keyset-sel')"), sync);
|
||||
assert.ok(sync.includes("getElementById('sp-cntr')"), sync);
|
||||
});
|
||||
|
||||
test('the RAM form offers the command format choice', () => {
|
||||
// auto (detect) is the default; the two expanded codings can be pinned
|
||||
assert.match(html, /<select id="ram-format"[\s\S]*?<option value="auto"/,
|
||||
'the RAM format selector must exist and default to auto');
|
||||
for (const v of ['compact', 'expanded', 'expanded-ae']) {
|
||||
assert.ok(html.includes('<option value="' + v + '"'), 'missing format ' + v);
|
||||
}
|
||||
assert.ok(extractFunc(html, 'getRamSpParams').includes("getElementById('ram-format')"),
|
||||
'the RAM params must carry the selected format');
|
||||
});
|
||||
|
||||
test('the Explore detects the expanded query form and reports it', () => {
|
||||
|
||||
Reference in New Issue
Block a user