v3.26.0: per-TAR default keysets (the preset/SCP80/RAM rework)

Every preset TAR entry carries `keyset` (int 1-15) - the keyset every
operation addressed to that TAR uses: mandatory for any non-zero MSL
(the counters are keyset-linked), optional for MSL 00 (allowed: some
cards reject the no-security 00/00 KIc/KID form).  The store fills it
automatically when the preset has a single keyset, the validation
refuses an entry naming an undefined keyset, and the schema is 6 (a v5
file loads leniently; editing it forces the fix).

The keyset selectors are gone: the SCP80 TAR field became a select of
the preset's entries (applying the TAR's keyset to the form and seeding
the SPI1 from its MSL); the RAM operations use the preset's ISD role
entry (the PWA sends only the preset id, the ISD TAR, SPI and the
counter override - the server resolves the keyset and the keys:
presets.tar_keyset, _preset_tar_keyset, _resolve_operation_keyset,
_op_keyset, used by send-ota's APDU path, ram-install, ram-install-app
and cap-compat); the counter probe and the test runner's scp80 steps
default to their TAR's keyset (an explicit kvn stays an override).  A
security TAR without a keyset refuses the operation ("set it in the
Cards tab") - never a silent keyless send - and the start buttons wait
for a usable operation-TAR keyset.

The TAR probe checks the preset's TAR list only (the standard
TAR_PROBE_TARS list and the custom-TAR localStorage are gone; a `tars`
list selects a subset, a TAR outside the preset is refused), each row
using its own MSL and default keyset.  The Cards editor's TAR table
gained a Keyset column (the only keyset preselected, an amber marker
when a security MSL lacks one).

Docs: help EN/RU, api.md's tar-probe contract, both AGENTS.  Tests: the
store (the field, the auto-fill, the schema, the string forms), the
server resolution (send-ota + the refusal, the probe's per-row keysets
and refusals), the PWA (the TAR select, the RAM ISD resolution, the
keyset-free requests) - several fixtures updated for the new schema.
891 frontend + 885 Python; sw cache simple-v434.
This commit is contained in:
2026-10-14 12:21:16 +03:00
parent 6e4494195c
commit cb40662e4e
18 changed files with 955 additions and 611 deletions
+23 -8
View File
@@ -34,12 +34,12 @@ for (const fn of ['cardsTarList', 'cardsRoleDefault', 'cardsRoleTar',
'spPresetCounterValue', 'spCntrPlaceholder', 'spCntrEffective', 'spCntrAhead',
'spCntrAdjust',
'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
'spKeysetOptionsHtml', 'cardsKeysetRowHtml', 'cardsKeysetRowsRender',
'spTarOptionsHtml', 'spTarRender', 'spSpi1Seed', 'spTarKeysetGuard', 'cardsTarKeysetCell', 'cardsTarKeysetFromForm', 'cardsTarKeysetRefresh', 'cardsTarRowsRender', 'cardsTarRowHtml', 'cardsTarsFromTable', 'cardsFreeTarsFromForm', 'cardsKeysetRowHtml', 'cardsKeysetRowsRender',
'cardsKeysetAdd', 'cardsKeysetRemove', 'cardsKeysetKvnUpdate',
'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply',
'spKeysetChanged', 'ramKeysetChanged', 'ramPresetIdx', 'tarPresetIdx', 'spKeysetGuard',
'spTarListRender',
'ramPresetIdx', 'tarPresetIdx', 'spKeysetGuard',
'ramKeysetGuard',
'esc',
]) {
code += extractFunc(html, fn) + '\n';
}
@@ -61,9 +61,22 @@ globalThis.t = s => s;
function fakeEnv(selValue, selId, presetCntr) {
const els = {};
for (const id of ['sp-spi1', 'sp-spi2-hex', 'sp-kic-idx', 'sp-kic-alg',
'sp-kid-idx', 'sp-kid-alg', 'sp-tar', 'sp-cntr', 'sp-kic-key', 'sp-kid-key']) {
'sp-kid-idx', 'sp-kid-alg', 'sp-cntr', 'sp-kic-key', 'sp-kid-key']) {
els[id] = { value: '' };
}
// the TAR is a select (v3.26.0): the stub models the surface the renderer
// touches - setting innerHTML parses the option values and selectedIndex
// moves the value, like a real <select>
els['sp-tar'] = {
value: '', disabled: false, options: [], _sel: 0, _html: '',
set innerHTML(v) {
this._html = v;
this.options = Array.from(v.matchAll(/value="([^"]*)"/g), m => ({ value: m[1] }));
},
get innerHTML() { return this._html; },
set selectedIndex(i) { this._sel = i; if (this.options[i]) this.value = this.options[i].value; },
get selectedIndex() { return this._sel; },
};
// 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' };
@@ -114,14 +127,16 @@ test('spRefreshFromPreset for the RAM selector targets the ISD TAR', () => {
assert.strictEqual(env.els['sp-tar'].value, '000000');
});
test('the pre-send refresh keeps a hand-edited packet TAR (v3.6.21)', () => {
test('the pre-send refresh keeps the selected packet TAR', () => {
// v3.26.0: the TAR is a preset selector - the pre-send refresh must keep
// the operator's selection (a TAR outside the preset is not offered)
const env = fakeEnv('0', 'sp-card-sel', '0000000020');
env.els['sp-tar'].value = 'AF4D01'; // e.g. a push/link trigger target
env.els['sp-tar'].value = 'B00000'; // the UICC RFM entry of the preset
assert.strictEqual(spRefreshFromPreset('sp-card-sel', true), '0000000020');
assert.strictEqual(env.els['sp-tar'].value, 'AF4D01', 'the typed TAR must survive the send');
assert.strictEqual(env.els['sp-tar'].value, 'B00000', 'the selected TAR must survive the send');
assert.strictEqual(env.els['sp-cntr'].value, '', 'the counter field stays an override');
assert.match(env.els['sp-cntr'].placeholder, /0000000020/);
// an explicit preset apply (or a pack) still sets the TAR
// an explicit preset apply (or a pack) selects the operation's TAR
cardsApplyFields(0);
assert.strictEqual(env.els['sp-tar'].value, 'B00000');
});