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:
+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