fix: keep the preset counter intact through the Explore delete flow (v3.6.7)

Delete All worked but reset the preset counter: the handler never re-read
the preset (unlike ramExecute), saved N+1, then re-ran the Explore with the
OLD sp (counter N).  Every page answered cntr_low, nothing was accepted, and
ramExplore's final ramSaveCntr wrote N back over the preset.

- ramDeleteFromExplorer re-reads the preset first (spRefreshFromPreset
  'ram-card-sel'), advances and saves the counter only for a packet the card
  accepted (spPorAccepted) - a rejected packet leaves it untouched - and
  continues the follow-up Explore from the consumed counter instead of
  replaying it.
- the delete result also checks the remote command's SW via a new
  `ramRemoteSwOk` (9000 / 61xx / 62xx / 63xx / CAFE, mirroring the server's
  `_ram_remote_sw_ok`): a por_ok packet whose DELETE was refused (e.g. 6A88)
  no longer shows "OK" (the counter still advances, the card consumed it).
- ramExplore only saves the counter when it advanced, so a stale caller can
  never lower a preset counter again.
- tests: flow tests for accepted / rejected / refused-remote-SW deletes
  (refresh order, saved counter, the counter passed to the re-explore) and
  the ramRemoteSwOk set.

615 frontend / 495 Python green; version 3.6.7; sw simple-v280.
This commit is contained in:
2026-09-28 00:50:07 +03:00
parent c39250f1b4
commit 50fa8744fd
5 changed files with 108 additions and 9 deletions
+33 -6
View File
@@ -1665,7 +1665,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.6';
const SIMPLE_VERSION = '3.6.7';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
@@ -8817,7 +8817,9 @@ async function ramExplore(sp) {
await paginate('10', modules, (hex) => ramParseElfStatus(hex, true), t('ELF Modules'));
ramMergeElfData(elfs, modules);
ramSaveCntr(cntr);
// Only write the counter when a packet was actually accepted: a run whose
// start value was stale must never lower the preset counter.
if (cntr !== sp.cntr) ramSaveCntr(cntr);
ramHideProgress();
if (errors.length) {
@@ -8848,6 +8850,10 @@ function ramDeleteApdu(aid, withCascade) {
}
async function ramDeleteFromExplorer(aid, withCascade) {
// The preset is the source of truth (see ramExecute): re-read it before
// the operation so a stale form copy cannot send an already-consumed
// counter.
spRefreshFromPreset('ram-card-sel');
const sp = getRamSpParams();
if (!sp.kicKey || !sp.kidKey) {
alert(t('Select a card preset with keys first (RAM subtab → Card preset)'));
@@ -8860,19 +8866,31 @@ async function ramDeleteFromExplorer(aid, withCascade) {
ramShowProgress(label + ' ' + aid + '...');
const res = await ramSendOta(apdu, sp);
ramHideProgress();
// The card consumes the counter as soon as it accepts the packet, even
// when the remote command fails; a rejected packet (cntr_low, PoR error)
// leaves it untouched.
if (res.success && spPorAccepted(res.por)) {
sp.cntr = ramIncrementCntr(sp.cntr);
ramSaveCntr(sp.cntr);
}
const resultEl = document.getElementById('ram-result');
const stepsEl = document.getElementById('ram-steps');
if (!res.success || !res.por || res.por.response_status !== 'por_ok') {
resultEl.textContent = t('Failed') + ': ' + (res.por ? res.por.response_status : res.error || res.sw);
const sw = res.por && res.por.decoded ? res.por.decoded.last_status_word : '';
if (!res.success || !res.por || res.por.response_status !== 'por_ok' ||
(sw && !ramRemoteSwOk(sw))) {
resultEl.textContent = t('Failed') + ': ' +
(res.por ? res.por.response_status : (res.error || res.sw)) +
(sw ? ' · ' + t('remote SW') + ' ' + sw : '');
resultEl.classList.remove('hidden', 'text-green-600'); resultEl.classList.add('text-red-600');
return;
}
ramSaveCntr(ramIncrementCntr(sp.cntr));
const sw = res.por.decoded ? res.por.decoded.last_status_word : '';
stepsEl.classList.remove('hidden');
stepsEl.textContent = label + ' ' + aid + ' -> ' + sw;
resultEl.textContent = t('OK');
resultEl.classList.remove('hidden', 'text-red-600'); resultEl.classList.add('text-green-600');
// Continue from the counter the delete consumed: replaying the old one
// gets cntr_low on every page and the failed run writes it back over the
// preset (the counter reset reported after Delete All).
await ramExplore(sp);
}
@@ -9475,6 +9493,15 @@ function spPorAccepted(por) {
por.response_status === 'actual_response_sms_submit';
}
// Remote-command status words that count as success (mirrors the server's
// `_ram_remote_sw_ok`): 9000 normal; 61xx more data available; 62xx/63xx
// warnings; CAFE GlobalPlatform "more data available" (GET STATUS pages).
function ramRemoteSwOk(sw) {
const s = (sw || '').toUpperCase();
return !!s && (s === '9000' || s === 'CAFE' ||
['61', '62', '63'].indexOf(s.slice(0, 2)) >= 0);
}
// GET STATUS APDU for the compact listing format: P2.b2=0 (GP Card Spec
// v2.3.1 11.4.2.2) with the mandatory '4F00' search criterion (all
// occurrences) and a chained GET RESPONSE (00C0000000) in the same secured