feat: low-counter guard for every SCP80/RAM operation (v3.6.58)
A cntr_low PoR verdict is not recoverable by retrying - every packet whose
counter is not above the card's is rejected again - so it now stops the
operation and is stated as plain data instead of a generic PoR error.
Server:
- _cntr_low_fields(status, cntr) -> {cntr_low, card_cntr, suggested_cntr}
(fixed-width wrap like the counter field); carried by /api/send-ota (plus
an 'OTA CNTR-LOW: card counter ...' log line) and by the RAM install /
install-app / cap-compat failure responses (from the failed step);
- _send_envelope's late-PoR poll now also runs after a warning ENVELOPE
answer (62xx/63xx - the live card answers 6200 for a low counter) and
_send_secured_packet passes the captured PoR on, so the verdict is named
instead of a bare 'ENVELOPE failed' (that is why it only surfaced on the
later SPI2=0x21 query in the live log).
PWA:
- spCntrLow(res) guards every flow; the Explore routes the whole run through
a checked sendOta(), so the paginate/expanded loops stop, no further
packet is sent and no counter is written back (delete and Send to Card
check it too);
- the result states the card's counter and offers 'Go to preset'
(ramGoToPreset: opens the failed operation's preset in the Cards tab for
editing and focuses the counter field) - nothing is written
automatically; the operation modal carries the same notice;
- help (EN/RU) documents the guard.
sw cache simple-v331; tests: 557 Python / 714 Node.
This commit is contained in:
@@ -26,6 +26,7 @@ const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamB
|
||||
'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer', 'ramHasInstance', 'ramExpandedQueryApdu',
|
||||
'_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'ramParseModuleAids', 'parseTLV', '_parseE3Entry', '_parseMenuEntries',
|
||||
'ramExpandedReport', 'ramExpandedTags', 'ramExpandedGroups', 'ramExpandedElfForm', 'ramElfVersionHint', 'ramChainGetResponse', 'ramDeriveElfVersions',
|
||||
'spCntrLow', 'ramCntrLowHtml', 'ramCntrLowPresetIdx', 'ramShowCntrLow',
|
||||
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges', 'ramActionBtn', 'ramCapToolkitMode', 'ramOpProgressText',
|
||||
'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml'];
|
||||
let code = '';
|
||||
@@ -703,6 +704,52 @@ test('the explorer hints why the package versions are missing', () => {
|
||||
assert.ok(!withVer.includes('Package versions'), withVer);
|
||||
});
|
||||
|
||||
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' });
|
||||
// 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');
|
||||
// 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);
|
||||
});
|
||||
|
||||
test('ramCntrLowHtml states the facts and offers the preset jump', () => {
|
||||
globalThis.t = s => s;
|
||||
const out = ramCntrLowHtml({ card_cntr: '00000000BB', suggested: '00000000BC' }, 2);
|
||||
const noPreset = ramCntrLowHtml({ card_cntr: '00000000BB' }, -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('onclick="ramGoToPreset(2)"'), out);
|
||||
assert.ok(out.includes('Go to preset'), out);
|
||||
// without a known preset the button still opens the tab (-1)
|
||||
assert.ok(noPreset.includes('onclick="ramGoToPreset(-1)"'), noPreset);
|
||||
assert.strictEqual(ramCntrLowHtml(null, 0), '');
|
||||
});
|
||||
|
||||
test('every SCP80/RAM flow checks the low counter and stops', () => {
|
||||
// the Explore: the guarded sender, the loop guards and the final branch
|
||||
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');
|
||||
// the delete flow and the plain send check it too
|
||||
assert.ok(extractFunc(html, 'ramDeleteFromExplorer').includes('spCntrLow(res)'),
|
||||
'the delete flow must stop');
|
||||
// the preset jump opens the Cards tab, selects the preset and focuses the counter
|
||||
const go = extractFunc(html, 'ramGoToPreset');
|
||||
assert.ok(go.includes("switchTab('cards')"), go);
|
||||
assert.ok(go.includes('cardsEdit('), go);
|
||||
assert.ok(go.includes("getElementById('cards-cntr')"), go);
|
||||
});
|
||||
|
||||
test('the Explore detects the expanded query form and reports it', () => {
|
||||
const src = extractFunc(html, 'ramExplore');
|
||||
assert.ok(src.includes('RAM_EXPANDED_FORMS'), 'the form ladder must be used');
|
||||
|
||||
@@ -29,7 +29,9 @@ function extractFunc(src, name) {
|
||||
let code = '';
|
||||
for (const fn of ['berLenStr', 'ramDeleteApdu', 'ramIncrementCntr', 'ramSaveCntr',
|
||||
'getRamSpParams', 'spPorAccepted', 'ramRemoteSwOk', 'ramShowProgress',
|
||||
'ramHideProgress', 'ramDeleteFromExplorer']) {
|
||||
'ramHideProgress', 'ramDeleteFromExplorer',
|
||||
'spCntrLow', 'ramCntrLowHtml', 'ramCntrLowPresetIdx', 'ramShowCntrLow',
|
||||
'escHtml', 'esc']) {
|
||||
code += extractFunc(html, fn) + '\n';
|
||||
}
|
||||
eval(code);
|
||||
|
||||
Reference in New Issue
Block a user