fix: send the STK install parameters as entered (v3.6.9)
The RAM install form sent the `STK parameters (hex)` field, which was only regenerated when the toolkit checkbox or the mode select changed - editing the TAR (or any other toolkit field) afterwards was silently dropped. The live install therefore carried the form defaults (TAR B00001, textLen 0, menus 0, MSL 16, channels 0) instead of the entered AF4D01 / MSL 12 / channels 1, and the card rejected the install parameters with 6A80 (TS 102 226 8.2.1.3.2.7: a TAR already assigned on the card). - every `rc-tk-*` field regenerates the hex on input/change; the install recomputes it at send time unless the user hand-edited the hex (`dataset.manual`), so the edit order can no longer drop values; - one pure `stkParamsBuild` serves the RAM form (`buildRcToolkitParams`) and the RAM/GP chain rows (`buildTkParams`) - no drift between them (the chain row also gains the menu-ID <= 7F check); - applet TAR field: empty by default, no placeholder - B0 00 01 is the allocated ADF RFM TAR (TS 101 220 Annex D), not an applet TAR; an empty field is coded as TAR length 00 (the card may take a TAR from the AID only when the AID carries one, PIX hex digits 15-20); - the chain builder's INSTALL/LOAD rows drop the trailing Le (the v3.6.8 server form; a trailing Le made the card execute a phantom command); - tests: stk_params.test.js (the decrypted live parameters, empty-TAR coding, CA wrapper, long-form lengths, rejection cases, form wiring, and the form -> hex path with the real builders) and ram_counter_flow.test.js (the Explore delete persists the consumed counter through the real ramSaveCntr); STK fixtures no longer use B00001. 631 frontend / 496 Python green; version 3.6.9; sw simple-v282.
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
|
||||
|
||||
function extractFunc(src, name) {
|
||||
const re = new RegExp('(?:async\\s+)?function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
|
||||
const m = re.exec(src);
|
||||
if (!m) throw new Error('function ' + name + ' not found');
|
||||
let i = m.index + m[0].length - 1;
|
||||
let depth = 0;
|
||||
for (; i < src.length; i++) {
|
||||
if (src[i] === '{') depth++;
|
||||
else if (src[i] === '}') {
|
||||
depth--;
|
||||
if (depth === 0) break;
|
||||
}
|
||||
}
|
||||
return src.slice(m.index, i + 1);
|
||||
}
|
||||
|
||||
// The real functions behind the Explore Delete flow: the counter the card
|
||||
// consumed must be persisted into the preset, or the next operation starts
|
||||
// one behind and is rejected with cntr_low.
|
||||
let code = '';
|
||||
for (const fn of ['berLenStr', 'ramDeleteApdu', 'ramIncrementCntr', 'ramSaveCntr',
|
||||
'getRamSpParams', 'spPorAccepted', 'ramRemoteSwOk', 'ramShowProgress',
|
||||
'ramHideProgress', 'ramDeleteFromExplorer']) {
|
||||
code += extractFunc(html, fn) + '\n';
|
||||
}
|
||||
eval(code);
|
||||
|
||||
function fakeEnv(por, success) {
|
||||
const els = {};
|
||||
const mk = () => ({ value: '', textContent: '',
|
||||
classList: { add() {}, remove() {}, toggle() {} } });
|
||||
for (const id of ['sp-spi1', 'sp-spi2', 'sp-kic-hex', 'sp-kid-hex', 'sp-tar',
|
||||
'sp-cntr', 'sp-kic-key', 'sp-kid-key', 'ram-result', 'ram-steps',
|
||||
'ram-progress', 'ram-progress-text']) els[id] = mk();
|
||||
els['ram-card-sel'] = { value: '0' };
|
||||
// the preset keys the real spRefreshFromPreset/cardsApply would copy into
|
||||
// the form (the delete flow aborts without them)
|
||||
els['sp-kic-key'].value = 'AA';
|
||||
els['sp-kid-key'].value = 'BB';
|
||||
globalThis.document = { getElementById: id => els[id] || null };
|
||||
globalThis.cards = [{ name: 'C', cntr: '0000000005', kicKey: 'AA', kidKey: 'BB' }];
|
||||
const calls = { saved: 0, explored: null, sent: null };
|
||||
globalThis.cardsSave = () => { calls.saved++; };
|
||||
globalThis.cardsRender = () => {};
|
||||
globalThis.ramRender = () => {};
|
||||
globalThis.t = s => s;
|
||||
globalThis.alert = () => {};
|
||||
globalThis.confirm = () => true;
|
||||
// the preset re-read (cardsApply -> the sp-* form fields); the real
|
||||
// spRefreshFromPreset is covered by cards_counter.test.js
|
||||
globalThis.spRefreshFromPreset = () => {
|
||||
els['sp-cntr'].value = cards[0].cntr;
|
||||
return cards[0].cntr;
|
||||
};
|
||||
globalThis.ramSendOta = async (apdu, sp) => {
|
||||
calls.sent = { apdu: apdu, cntr: sp.cntr };
|
||||
return { success: success !== false, por: por };
|
||||
};
|
||||
globalThis.ramExplore = async sp => { calls.explored = sp.cntr; };
|
||||
return { els, calls };
|
||||
}
|
||||
|
||||
test('accepted delete persists the consumed counter and re-explores from it', async () => {
|
||||
const { els, calls } = fakeEnv({ response_status: 'por_ok',
|
||||
decoded: { last_status_word: '9000' } });
|
||||
await ramDeleteFromExplorer('F0414C46416101', false);
|
||||
assert.strictEqual(calls.sent.cntr, '0000000005');
|
||||
assert.strictEqual(calls.sent.apdu, '80E40000094F07F0414C4641610100');
|
||||
assert.strictEqual(cards[0].cntr, '0000000006',
|
||||
'the preset must carry the counter the card consumed');
|
||||
assert.strictEqual(els['sp-cntr'].value, '0000000006');
|
||||
assert.ok(calls.saved > 0, 'cardsSave() must persist it');
|
||||
assert.strictEqual(calls.explored, '0000000006',
|
||||
'the re-explore must start at N+1, never replay N');
|
||||
assert.strictEqual(els['ram-result'].textContent, 'OK');
|
||||
});
|
||||
|
||||
test('cntr_low leaves the preset untouched (the card did not consume the packet)', async () => {
|
||||
const { calls } = fakeEnv({ response_status: 'cntr_low' });
|
||||
await ramDeleteFromExplorer('F0414C46416101', false);
|
||||
assert.strictEqual(cards[0].cntr, '0000000005');
|
||||
assert.strictEqual(calls.saved, 0);
|
||||
assert.strictEqual(calls.explored, null);
|
||||
});
|
||||
|
||||
test('a refused DELETE still advances the counter but does not re-explore', async () => {
|
||||
const { calls } = fakeEnv({ response_status: 'por_ok',
|
||||
decoded: { last_status_word: '6A88' } });
|
||||
await ramDeleteFromExplorer('F0414C46416101', true);
|
||||
assert.strictEqual(cards[0].cntr, '0000000006');
|
||||
assert.strictEqual(calls.explored, null);
|
||||
});
|
||||
|
||||
test('a send failure leaves the preset untouched', async () => {
|
||||
const { calls } = fakeEnv({ response_status: 'por_ok' }, false);
|
||||
await ramDeleteFromExplorer('F0414C46416101', false);
|
||||
assert.strictEqual(cards[0].cntr, '0000000005');
|
||||
assert.strictEqual(calls.explored, null);
|
||||
});
|
||||
Reference in New Issue
Block a user