v3.20.0 (review fixes): the PoR decode uses the effective counter; the PWA fallback matches the server's seed

Two boundary flaws found reviewing v3.20.0:

- /api/send-ota decoded the PoR with the request's `cntr`, not the counter
  the packet was actually built with.  With the new seeding an omitted
  counter is the normal case, so a warning ENVELOPE's verdict - notably the
  live card's cntr_low PoR - could fail its RC/CC check and be lost (the
  success and the failure path are both fixed).  Tests pin the decode counter
  on both paths; the failure path also echoes the effective `cntr`.
- the PWA's effective counter (preview / verify / the older-server fallback)
  fell back to the preset's first keyset and then 1, while the server's empty
  seed builds counter 0 (no preset / kvn 0 / an unknown keyset).  A manual
  form or a keyless packet then produced a spurious "Verify vs pySim"
  MISMATCH and a preview that differed from the delivered packet.
  spPresetCounterValue()/spCntrEffective() now mirror _preset_counter_seed()
  exactly: the packet's matched keyset, else 0.
- the `OTA SEND: counter not tracked` line reflects the effective counter (a
  seeded value is logged too); docs/api.md records that the seed uses the
  packet's keyset whatever SPI1.b5b4 says.

Tests: 823 frontend + 737 Python; sw cache simple-v394.
This commit is contained in:
2026-10-05 22:37:28 +03:00
parent 85cc4b9535
commit bf6730f028
6 changed files with 120 additions and 14 deletions
+9 -6
View File
@@ -8583,10 +8583,12 @@ function spPresetCounterValue() {
if (idx < 0) return '';
const kvn = spKeysetKvnOf((document.getElementById('sp-kic-hex') || {}).value) ||
spKeysetKvnOf((document.getElementById('sp-kid-hex') || {}).value) || 0;
// the same fallback cardsApplyFields uses: the matching keyset, else the
// preset's first one (an undefined duplicate would otherwise leave the
// placeholder empty)
const ks = (kvn && spKeysetFor(cards[idx], kvn)) || spKeysetList(cards[idx])[0] || null;
// Only a keyset the packet's KIc/KID actually resolve - exactly the value
// the server's _preset_counter_seed() uses (the body carries the same two
// bytes). No preset / kvn 0 / an unknown keyset = no seed: the packet
// then carries counter 0, and neither the preview nor the pySim reference
// may show a value the send would not use.
const ks = kvn ? spKeysetFor(cards[idx], kvn) : null;
return (ks && ks.cntr) || '';
}
@@ -8598,11 +8600,12 @@ function spCntrPlaceholder() {
}
// The counter the packet is built with (preview / verify): the typed
// override, else the preset's next value, else the historic 1 default.
// override, else the preset's next value, else 0 - the same value the
// server's empty seed puts into the packet (v3.20.0).
function spCntrEffective() {
const el = document.getElementById('sp-cntr');
const typed = ((el && el.value) || '').replace(/[^0-9a-fA-F]/g, '');
const v = typed || spPresetCounterValue() || '1';
const v = typed || spPresetCounterValue() || '0';
return v.toUpperCase().padStart(10, '0').slice(0, 10);
}
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v393';
const CACHE = 'simple-v394';
const URLS = [
'index.html',
'help.html',
+20
View File
@@ -64,6 +64,10 @@ function fakeEnv(selValue, selId, presetCntr) {
'sp-kid-idx', 'sp-kid-alg', 'sp-tar', 'sp-cntr', 'sp-kic-key', 'sp-kid-key']) {
els[id] = { value: '' };
}
// the combined key bytes the server reads (updateSpKic maintains them in
// the real form): the fake preset's keyset resolves to kvn 1
els['sp-kic-hex'] = { value: '15' };
els['sp-kid-hex'] = { value: '15' };
els[selId || 'sp-card-sel'] = { value: selValue };
globalThis.document = { getElementById: id => els[id] || null };
globalThis.cards = [{ name: 'C', cntr: presetCntr,
@@ -143,6 +147,22 @@ test('the counter +/- buttons start from the preset value and create an override
assert.strictEqual(env.els['sp-cntr'].value, '0000000020');
});
test('spCntrEffective mirrors the server seed: matched keyset, else 0', () => {
const env = fakeEnv('0', 'sp-card-sel', '0000000020');
assert.strictEqual(spCntrEffective(), '0000000020');
env.els['sp-cntr'].value = '0000000042'; // a typed override wins
assert.strictEqual(spCntrEffective(), '0000000042');
env.els['sp-cntr'].value = '';
env.els['sp-kic-hex'].value = '00'; // keyless: no keyset
env.els['sp-kid-hex'].value = '00';
assert.strictEqual(spCntrEffective(), '0000000000',
'the server seeds nothing for kvn 0 - the packet carries 0');
env.els['sp-kic-hex'].value = '15';
env.els['sp-kid-hex'].value = '15';
globalThis.cards = []; // no preset: no seed
assert.strictEqual(spCntrEffective(), '0000000000');
});
test('spPorAccepted treats a missing PoR and por_ok as accepted', () => {
assert.strictEqual(spPorAccepted(undefined), true);
assert.strictEqual(spPorAccepted({ response_status: 'por_ok' }), true);