feat: one keyset per key version - indexed keys and counters (v3.8.0)
A card usually holds several OTA key sets, and the SCP80 counter is tracked
per key version on the card, so a preset now carries one keyset per
GlobalPlatform key version - the b8..b5 nibble of KIc/KID (TS 102 225
5.1.2/A.2), the version is implicit in the bytes. The spec is explicit:
"a dedicated counter shall be associated to each key version" (Annex A.1) and
"the key version number indicated in the KIc and KID (bits b8..b5) fields
shall be identical when different from 0" - a mismatch is rejected by the card
with "Unidentified security error" (A.2).
Server (presets.py, schema 2):
- a preset carries `keysets: [{kic, kid, kicKey, kidKey, cntr}]`; validation
refuses a KIc/KID version mismatch (A.2), a version outside 01-0F ('00'
means "no security" and is a packet-level choice, not a keyset), duplicate
versions and missing keys/counters; a v3.8.0 flat preset converts into one
keyset on load and on import;
- `set_counter(..., kvn)` writes the counter of that keyset (monotonic per
keyset), the audit line carries the kvn, and `find_keyset()` serves the
lookups;
- the operations derive the key version from the request's KIc byte and
persist into that keyset: /api/send-ota, the RAM install/install-app/compat
steps (_ram_send_gp_apdu) and the test runner's SCP80 steps (which may name
a `kvn` per step and keep one working counter per keyset);
- a non-zero key version the named preset does not define, or a KIc/KID
mismatch, is refused before anything is built (_preset_keyset_check /
_keyset_guard: 400, or a step error in the test runner). Without a
preset_id only the mismatch rule applies.
PWA:
- the Cards form's KIc/KID/counter/keys inputs become a keyset editor (one row
per version: KIc, KID, both keys, that version's counter, Add/Remove; the
version is shown derived from the bytes and a mismatch is flagged);
- the SCP80 and RAM views get a Key-version selector beside the preset
selector, driving the KIc/KID bytes and loading that keyset's keys and
counter; the packet builder refuses a version the preset does not define
(spKeysetCheck/spKeysetGuard, pure and unit-tested);
- the counter syncs are per keyset (spCntrLocalSync/ramSaveCntr), the SCP80
form's counter edit posts the keysets, and the test-run write-back syncs
every counter the server reports (scp80_counters).
Docs/help/AGENTS updated; sw cache simple-v335. Tests: 610 Python (store
keysets, endpoint refusals, the counter landing in the right keyset) and 726
Node (keysets.test.js plus the updated harnesses).
This commit is contained in:
@@ -23,10 +23,22 @@ function extractFunc(src, name) {
|
||||
|
||||
let code = '';
|
||||
for (const fn of ['cardsTarValue', 'cardsApplyFields', 'cardsApply',
|
||||
'spRefreshFromPreset', 'spPorAccepted', 'spNextCntr']) {
|
||||
'spRefreshFromPreset', 'spPorAccepted', 'spNextCntr', 'spPresetIdx',
|
||||
'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
|
||||
'spKeysetOptionsHtml', 'cardsKeysetRowHtml', 'cardsKeysetRowsRender',
|
||||
'cardsKeysetAdd', 'cardsKeysetRemove', 'cardsKeysetKvnUpdate',
|
||||
'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply',
|
||||
'spKeysetChanged', 'ramKeysetChanged', 'ramPresetIdx', 'spKeysetGuard',
|
||||
'ramKeysetGuard',
|
||||
]) {
|
||||
code += extractFunc(html, fn) + '\n';
|
||||
}
|
||||
code = 'var _cardsKeysetCount = 0;\n' + code;
|
||||
code = 'var _genSpBuildStub = function() {};\n' + code;
|
||||
eval(code);
|
||||
globalThis._genSpBuild = () => {};
|
||||
globalThis.spShowSizeInfo = () => {};
|
||||
globalThis.spKeysetSync = globalThis.spKeysetSync || (() => {});
|
||||
|
||||
// The SP form is a working copy: the preset is the source of truth, and
|
||||
// every SCP80/RAM operation re-reads it before starting (v3.6.3). The plain
|
||||
@@ -49,6 +61,9 @@ function fakeEnv(selValue, selId, presetCntr) {
|
||||
globalThis.updateSp = () => {};
|
||||
let gen = 0;
|
||||
globalThis.genSp = () => { gen++; };
|
||||
// cardsApply refreshes the preview directly (the guarded genSp() is the
|
||||
// Generate button's path), so the counter stub is _genSpBuild
|
||||
globalThis._genSpBuild = () => { gen++; };
|
||||
return { els, gen: () => gen };
|
||||
}
|
||||
|
||||
|
||||
@@ -28,21 +28,42 @@ for (const fn of ['cardsTarValue', 'cardsFormValues', 'cardsClearForm', 'cardsEd
|
||||
'spPresetIdx', 'spCntrSyncPreset', 'spCntrLocalSync', 'ramSaveCntr',
|
||||
'cardsApplyFields', 'cardsApply', 'ramApplyCard',
|
||||
'cardsScp80Complete', 'cardsScp81Complete', 'cardsAdmPresent',
|
||||
'spTarKeyForPack', 'spPresetTar', 'packToSp']) {
|
||||
'spTarKeyForPack', 'spPresetTar', 'packToSp',
|
||||
'esc', 'escHtml',
|
||||
'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
|
||||
'spKeysetOptionsHtml', 'cardsKeysetRowHtml', 'cardsKeysetRowsRender',
|
||||
'cardsKeysetAdd', 'cardsKeysetRemove', 'cardsKeysetKvnUpdate',
|
||||
'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply', 'spKeysetChanged',
|
||||
'ramKeysetChanged', 'ramPresetIdx', 'spKeysetGuard', 'ramKeysetGuard']) {
|
||||
code += extractFunc(html, fn, ASYNC_FNS.indexOf(fn) >= 0) + '\n';
|
||||
}
|
||||
code = 'var _cardsKeysetCount = 0;\n' + code;
|
||||
eval(code);
|
||||
|
||||
const CARD_IDS = ['cards-name','cards-iccid','cards-adm','cards-kic','cards-kid',
|
||||
const CARD_IDS = ['cards-name','cards-iccid','cards-adm',
|
||||
'cards-spi1','cards-spi2','cards-tar','cards-uicc-tar','cards-usim-tar',
|
||||
'cards-cntr','cards-kic-key','cards-kid-key','cards-psk-id','cards-psk-key',
|
||||
'cards-add-btn','cards-cancel-btn'];
|
||||
'cards-psk-id','cards-psk-key','cards-keysets',
|
||||
'cards-add-btn','cards-cancel-btn',
|
||||
// the keyset editor rows (two rows is enough for the tests)
|
||||
'cards-ks-0-kvn','cards-ks-0-kic','cards-ks-0-kid','cards-ks-0-kic-key',
|
||||
'cards-ks-0-kid-key','cards-ks-0-cntr',
|
||||
'cards-ks-1-kvn','cards-ks-1-kic','cards-ks-1-kid','cards-ks-1-kic-key',
|
||||
'cards-ks-1-kid-key','cards-ks-1-cntr'];
|
||||
const SP_IDS = ['sp-card-sel','sp-tar','sp-spi1','sp-spi2','sp-spi2-sm','sp-spi2-hex',
|
||||
'sp-kic-idx','sp-kic-alg','sp-kic-hex','sp-kid-idx','sp-kid-alg','sp-kid-hex',
|
||||
'sp-cntr','sp-kic-key','sp-kid-key','sp-apdu','sp-result','sp-spi1-hex'];
|
||||
'sp-cntr','sp-kic-key','sp-kid-key','sp-apdu','sp-result','sp-spi1-hex',
|
||||
'sp-keyset-sel','ram-keyset-sel'];
|
||||
const CHAIN_IDS = ['chain-sim-preview','chain-usim-preview','chain-ram-preview',
|
||||
'ber-result','hota-preview'];
|
||||
|
||||
function ksRow(els, i, ks) {
|
||||
for (const field of ['kic', 'kid', 'kic-key', 'kid-key', 'cntr']) {
|
||||
const id = 'cards-ks-' + i + '-' + field;
|
||||
if (!els[id]) els[id] = fakeEl();
|
||||
els[id].value = ks[field === 'kic-key' ? 'kicKey' : (field === 'kid-key' ? 'kidKey' : field)] || '';
|
||||
}
|
||||
}
|
||||
|
||||
function fakeEl() {
|
||||
return {
|
||||
value: '',
|
||||
@@ -63,8 +84,11 @@ function setup() {
|
||||
globalThis.cards = [];
|
||||
globalThis._cardsEditIdx = null;
|
||||
globalThis._ramCardIdx = null;
|
||||
globalThis._cardsKeysetCount = 1;
|
||||
globalThis._spTarKey = 'tar';
|
||||
globalThis.t = s => s;
|
||||
globalThis.esc = s => s;
|
||||
globalThis.escHtml = s => s;
|
||||
globalThis.alert = () => {};
|
||||
globalThis.cardsSave = () => {};
|
||||
globalThis.cardsRender = () => {};
|
||||
@@ -81,7 +105,10 @@ function setup() {
|
||||
globalThis.spInvalidate = () => {};
|
||||
globalThis.updateSp = () => {};
|
||||
globalThis.genSp = () => {};
|
||||
globalThis._genSpBuild = () => {};
|
||||
globalThis.spShowSizeInfo = () => {};
|
||||
globalThis.ramRender = () => {};
|
||||
globalThis.switchTab = () => {};
|
||||
// the server store is the source of truth (v3.8.0): the Cards tab talks to
|
||||
// /api/presets* and reloads through cardsFetch()
|
||||
apiCalls = [];
|
||||
@@ -174,10 +201,8 @@ test('the form values carry the ADM and per-target TAR fields', () => {
|
||||
test('saving a preset posts the form with spec-default TARs', async () => {
|
||||
const els = setup();
|
||||
els['cards-name'].value = 'New card';
|
||||
els['cards-kic'].value = '15';
|
||||
els['cards-kid'].value = '15';
|
||||
els['cards-kic-key'].value = 'AA';
|
||||
els['cards-kid-key'].value = 'BB';
|
||||
ksRow(els, 0, { kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB' });
|
||||
globalThis._cardsKeysetCount = 1;
|
||||
els['cards-tar'].value = '';
|
||||
els['cards-uicc-tar'].value = '';
|
||||
els['cards-usim-tar'].value = '';
|
||||
@@ -186,6 +211,8 @@ test('saving a preset posts the form with spec-default TARs', async () => {
|
||||
assert.strictEqual(apiCalls.length, 1, JSON.stringify(apiCalls));
|
||||
assert.strictEqual(apiCalls[0].path, '/api/presets');
|
||||
assert.strictEqual(apiCalls[0].body.name, 'New card');
|
||||
assert.deepStrictEqual(apiCalls[0].body.keysets, [
|
||||
{ kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '' }]);
|
||||
assert.strictEqual(apiCalls[0].body.tar, '000000');
|
||||
assert.strictEqual(apiCalls[0].body.uiccTar, 'B00000');
|
||||
assert.strictEqual(apiCalls[0].body.usimTar, 'B00001');
|
||||
@@ -194,14 +221,12 @@ test('saving a preset posts the form with spec-default TARs', async () => {
|
||||
|
||||
test('editing a preset updates it by its store id', async () => {
|
||||
const els = setup();
|
||||
globalThis.cards = [{ id: 'abc123', name: 'Old', kic: '15', kid: '15',
|
||||
kicKey: 'AA', kidKey: 'BB' }];
|
||||
globalThis.cards = [{ id: 'abc123', name: 'Old', keysets: [
|
||||
{ kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '0000000001' }] }];
|
||||
globalThis._cardsEditIdx = 0;
|
||||
els['cards-name'].value = 'Renamed';
|
||||
els['cards-kic'].value = '15';
|
||||
els['cards-kid'].value = '15';
|
||||
els['cards-kic-key'].value = 'AA';
|
||||
els['cards-kid-key'].value = 'BB';
|
||||
ksRow(els, 0, { kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB' });
|
||||
globalThis._cardsKeysetCount = 1;
|
||||
await cardsAdd();
|
||||
assert.strictEqual(apiCalls[0].path, '/api/presets/update');
|
||||
assert.strictEqual(apiCalls[0].body.id, 'abc123');
|
||||
@@ -235,19 +260,22 @@ test('import posts the entries to the server store', async () => {
|
||||
|
||||
test('the SCP80 counter edit is written to the store, a reported counter only locally', () => {
|
||||
const els = setup();
|
||||
globalThis.cards = [{ id: 'abc123', name: 'C', cntr: '0000000001' }];
|
||||
globalThis.cards = [{ id: 'abc123', name: 'C', keysets: [
|
||||
{ kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '0000000001' }] }];
|
||||
els['sp-card-sel'].value = '0';
|
||||
els['sp-kic-hex'].value = '15';
|
||||
els['sp-kid-hex'].value = '15';
|
||||
els['sp-cntr'].value = '0000000009';
|
||||
assert.strictEqual(spCntrSyncPreset(), true);
|
||||
assert.strictEqual(cards[0].cntr, '0000000009');
|
||||
assert.strictEqual(cards[0].keysets[0].cntr, '0000000009');
|
||||
assert.strictEqual(apiCalls.length, 1);
|
||||
assert.strictEqual(apiCalls[0].path, '/api/presets/update');
|
||||
assert.strictEqual(apiCalls[0].body.id, 'abc123');
|
||||
assert.deepStrictEqual(apiCalls[0].body.fields, { cntr: '0000000009' });
|
||||
assert.strictEqual(apiCalls[0].body.fields.keysets[0].cntr, '0000000009');
|
||||
// a counter the server already persisted: display only, no second write
|
||||
apiCalls = [];
|
||||
spCntrLocalSync('000000000A');
|
||||
assert.strictEqual(cards[0].cntr, '000000000A');
|
||||
assert.strictEqual(cards[0].keysets[0].cntr, '000000000A');
|
||||
assert.strictEqual(apiCalls.length, 0);
|
||||
// no preset selected: nothing to write
|
||||
els['sp-card-sel'].value = '';
|
||||
|
||||
@@ -135,8 +135,8 @@ test('the card form groups the SCP80 and SCP81 fields into labelled fieldsets',
|
||||
const scp81 = fieldsets.find(f => /data-l10n="SCP81 \(HTTP OTA\)"/.test(f));
|
||||
assert.ok(scp80, 'SCP80 card fieldset not found');
|
||||
assert.ok(scp81, 'SCP81 card fieldset not found');
|
||||
assert.match(scp80, /id="cards-kic"/);
|
||||
assert.match(scp80, /id="cards-kid-key"/);
|
||||
assert.match(scp80, /id="cards-keysets"/);
|
||||
assert.match(scp80, /onclick="cardsKeysetAdd\(\)"/);
|
||||
assert.match(scp80, /id="cards-tar"[^>]*value="000000"/);
|
||||
assert.match(scp80, /id="cards-uicc-tar"[^>]*value="B00000"/);
|
||||
assert.match(scp80, /id="cards-usim-tar"[^>]*value="B00001"/);
|
||||
@@ -144,8 +144,8 @@ test('the card form groups the SCP80 and SCP81 fields into labelled fieldsets',
|
||||
assert.match(scp80, />ISD TAR<\/label>/);
|
||||
assert.match(scp80, />UICC RFM TAR<\/label>/);
|
||||
assert.match(scp80, />ADF RFM TAR<\/label>/);
|
||||
assert.ok(scp80.indexOf('id="cards-cntr"') < scp80.indexOf('id="cards-tar"'),
|
||||
'TAR fields must follow the Counter field');
|
||||
assert.ok(scp80.indexOf('id="cards-spi1"') < scp80.indexOf('id="cards-tar"'),
|
||||
'TAR fields must follow the SPI fields');
|
||||
assert.match(scp81, /id="cards-psk-id"/);
|
||||
assert.match(scp81, /id="cards-psk-key"/);
|
||||
// the PSK explanation lives inside the SCP81 group, not outside it
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
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('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);
|
||||
}
|
||||
|
||||
let code = '';
|
||||
for (const fn of ['spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
|
||||
'spKeysetOptionsHtml', 'esc', 'escHtml']) {
|
||||
code += extractFunc(html, fn) + '\n';
|
||||
}
|
||||
code += 'function t(s){return s;}\n';
|
||||
eval(code);
|
||||
|
||||
const PRESET = {
|
||||
name: 'Card A',
|
||||
keysets: [
|
||||
{ kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '0000000001' },
|
||||
{ kic: '29', kid: '29', kicKey: 'CC', kidKey: 'DD', cntr: '0000000005' },
|
||||
],
|
||||
};
|
||||
|
||||
test('spKeysetKvnOf reads the key version from the high nibble', () => {
|
||||
assert.strictEqual(spKeysetKvnOf('15'), 1);
|
||||
assert.strictEqual(spKeysetKvnOf('29'), 2);
|
||||
assert.strictEqual(spKeysetKvnOf('3A'), 3);
|
||||
assert.strictEqual(spKeysetKvnOf('05'), 0);
|
||||
assert.strictEqual(spKeysetKvnOf('ff'), 15);
|
||||
// not a byte: no key version
|
||||
assert.strictEqual(spKeysetKvnOf(''), null);
|
||||
assert.strictEqual(spKeysetKvnOf('5'), null);
|
||||
assert.strictEqual(spKeysetKvnOf('ZZ'), null);
|
||||
assert.strictEqual(spKeysetKvnOf(null), null);
|
||||
});
|
||||
|
||||
test('spKeysetList converts a v3.8.0 flat preset into one keyset', () => {
|
||||
assert.deepStrictEqual(spKeysetList({ kic: '15', kid: '15', kicKey: 'A', kidKey: 'B',
|
||||
cntr: '0000000001' }), [{ kic: '15', kid: '15', kicKey: 'A', kidKey: 'B',
|
||||
cntr: '0000000001' }]);
|
||||
assert.deepStrictEqual(spKeysetList(PRESET).length, 2);
|
||||
assert.deepStrictEqual(spKeysetList(null), []);
|
||||
assert.deepStrictEqual(spKeysetList({}), []);
|
||||
});
|
||||
|
||||
test('spKeysetFor finds a keyset by key version', () => {
|
||||
assert.strictEqual(spKeysetFor(PRESET, 1).cntr, '0000000001');
|
||||
assert.strictEqual(spKeysetFor(PRESET, 2).cntr, '0000000005');
|
||||
assert.strictEqual(spKeysetFor(PRESET, 3), null);
|
||||
assert.strictEqual(spKeysetFor(null, 1), null);
|
||||
});
|
||||
|
||||
test('spKeysetCheck enforces the key-version rules', () => {
|
||||
// a defined key version passes
|
||||
assert.strictEqual(spKeysetCheck(PRESET, '15', '15'), '');
|
||||
assert.strictEqual(spKeysetCheck(PRESET, '29', '29'), '');
|
||||
// '00'/'00' means no security: no keyset needed (TS 102 225 A.2)
|
||||
assert.strictEqual(spKeysetCheck(PRESET, '00', '00'), '');
|
||||
// ... and one zero byte with a defined version passes too
|
||||
assert.strictEqual(spKeysetCheck(PRESET, '15', '00'), '');
|
||||
// KIc/KID versions must agree when both are non-zero (A.2)
|
||||
assert.match(spKeysetCheck(PRESET, '15', '25'), /same key version/);
|
||||
// the key version must be defined in the preset
|
||||
assert.match(spKeysetCheck(PRESET, '35', '35'), /3 .*not defined/);
|
||||
// one zero byte: the other byte's version applies (keyset 2 is defined)
|
||||
assert.strictEqual(spKeysetCheck(PRESET, '00', '25'), '');
|
||||
assert.match(spKeysetCheck(PRESET, '00', '35'), /3 .*not defined/);
|
||||
// without a preset only the byte rules apply (hand-entered keys)
|
||||
assert.strictEqual(spKeysetCheck(null, '35', '35'), '');
|
||||
assert.match(spKeysetCheck(null, '15', '25'), /15 \/ 25/);
|
||||
// malformed bytes are named
|
||||
assert.match(spKeysetCheck(PRESET, '5', '15'), /KIc/);
|
||||
assert.match(spKeysetCheck(PRESET, '15', 'ZZ'), /KID/);
|
||||
});
|
||||
|
||||
test('spKeysetOptionsHtml lists the keysets and marks an undefined version', () => {
|
||||
const html1 = spKeysetOptionsHtml(PRESET, '15', '15');
|
||||
assert.match(html1, /value="1" selected/);
|
||||
assert.match(html1, /value="2"/);
|
||||
assert.ok(html1.indexOf('0000000005') > 0, 'the counters are shown');
|
||||
// a version the preset does not define is offered but marked
|
||||
const html2 = spKeysetOptionsHtml(PRESET, '35', '35');
|
||||
assert.match(html2, /value="3" selected/);
|
||||
assert.match(html2, /not defined in the preset/);
|
||||
// a preset without keysets: the form's version shows as undefined
|
||||
const html3 = spKeysetOptionsHtml({ keysets: [] }, '15', '15');
|
||||
assert.match(html3, /not defined in the preset/);
|
||||
// no key version in the form and no keysets at all
|
||||
assert.match(spKeysetOptionsHtml({}, '', ''), /no keyset/);
|
||||
});
|
||||
|
||||
test('the Cards form wires the keyset editor', () => {
|
||||
assert.match(html, /id="cards-keysets"/);
|
||||
assert.match(html, /onclick="cardsKeysetAdd\(\)"/);
|
||||
assert.match(html, /oninput="cardsKeysetKvnUpdate\(/);
|
||||
assert.match(html, /data-l10n="Add keyset"/);
|
||||
// the keyset rows are built with the KIc/KID/key/counter inputs
|
||||
assert.match(html, /cards-ks-' \+ i \+ '-kic-key/);
|
||||
assert.match(html, /cards-ks-' \+ i \+ '-cntr/);
|
||||
// the selectors exist in both SCP80 views
|
||||
assert.match(html, /id="sp-keyset-sel" onchange="spKeysetChanged\(\)"/);
|
||||
assert.match(html, /id="ram-keyset-sel" onchange="ramKeysetChanged\(\)"/);
|
||||
// the packet actions run the guard
|
||||
assert.ok(html.includes('const ksErr = spKeysetGuard();'));
|
||||
assert.ok(html.includes('const ksErr = ramKeysetGuard();'));
|
||||
});
|
||||
@@ -27,6 +27,9 @@ const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamB
|
||||
'_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'ramParseModuleAids', 'parseTLV', '_parseE3Entry', '_parseMenuEntries',
|
||||
'ramExpandedReport', 'ramExpandedTags', 'ramExpandedGroups', 'ramExpandedElfForm', 'ramElfVersionHint', 'ramChainGetResponse', 'ramDeriveElfVersions', 'ramElfAppletCandidate',
|
||||
'spCntrLow', 'ramCntrLowHtml', 'ramCntrLowPresetIdx', 'ramShowCntrLow',
|
||||
'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
|
||||
'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply', 'spKeysetChanged',
|
||||
'ramKeysetChanged', 'ramPresetIdx', 'spKeysetGuard', 'ramKeysetGuard',
|
||||
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges', 'ramActionBtn', 'ramCapToolkitMode', 'ramOpProgressText',
|
||||
'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml'];
|
||||
let code = '';
|
||||
@@ -806,7 +809,7 @@ test('every SCP80/RAM flow checks the low counter and stops', () => {
|
||||
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);
|
||||
assert.ok(go.includes("getElementById('cards-ks-' + row + '-cntr')"), go);
|
||||
});
|
||||
|
||||
test('the Explore detects the expanded query form and reports it', () => {
|
||||
|
||||
@@ -31,9 +31,16 @@ for (const fn of ['berLenStr', 'ramDeleteApdu', 'ramIncrementCntr', 'ramSaveCntr
|
||||
'getRamSpParams', 'spPorAccepted', 'ramRemoteSwOk', 'ramShowProgress',
|
||||
'ramHideProgress', 'ramDeleteFromExplorer',
|
||||
'spCntrLow', 'ramCntrLowHtml', 'ramCntrLowPresetIdx', 'ramShowCntrLow',
|
||||
'escHtml', 'esc']) {
|
||||
'escHtml', 'esc',
|
||||
'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
|
||||
'spKeysetOptionsHtml', 'cardsKeysetRowHtml', 'cardsKeysetRowsRender',
|
||||
'cardsKeysetAdd', 'cardsKeysetRemove', 'cardsKeysetKvnUpdate',
|
||||
'spKeysetChanged', 'ramKeysetChanged', 'ramPresetIdx', 'spKeysetGuard',
|
||||
'ramKeysetGuard',
|
||||
]) {
|
||||
code += extractFunc(html, fn) + '\n';
|
||||
}
|
||||
code = 'var _cardsKeysetCount = 0;\n' + code;
|
||||
eval(code);
|
||||
|
||||
function fakeEnv(por, success) {
|
||||
@@ -49,8 +56,11 @@ function fakeEnv(por, success) {
|
||||
els['sp-kic-key'].value = 'AA';
|
||||
els['sp-kid-key'].value = 'BB';
|
||||
els['sp-spi2-hex'].value = '01';
|
||||
els['sp-kic-hex'].value = '15';
|
||||
els['sp-kid-hex'].value = '15';
|
||||
globalThis.document = { getElementById: id => els[id] || null };
|
||||
globalThis.cards = [{ id: 'preset-1', name: 'C', cntr: '0000000005', kicKey: 'AA', kidKey: 'BB' }];
|
||||
globalThis.cards = [{ id: 'preset-1', name: 'C', keysets: [
|
||||
{ kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '0000000005' }] }];
|
||||
const calls = { saved: 0, explored: null, removed: null, sent: null };
|
||||
globalThis.cardsSave = () => { calls.saved++; };
|
||||
globalThis.cardsRender = () => {};
|
||||
@@ -59,8 +69,9 @@ function fakeEnv(por, success) {
|
||||
globalThis.alert = () => {};
|
||||
globalThis.confirm = () => true;
|
||||
globalThis.spRefreshFromPreset = () => {
|
||||
els['sp-cntr'].value = cards[0].cntr;
|
||||
return cards[0].cntr;
|
||||
const cntr = cards[0].keysets[0].cntr;
|
||||
els['sp-cntr'].value = cntr;
|
||||
return cntr;
|
||||
};
|
||||
globalThis.ramSendOta = async (apdu, sp) => {
|
||||
calls.sent = { apdu: apdu, cntr: sp.cntr, spi2: sp.spi2, preset_id: sp.preset_id };
|
||||
@@ -81,7 +92,7 @@ test('accepted delete persists the consumed counter and drops the record', async
|
||||
assert.strictEqual(calls.sent.cntr, '0000000005');
|
||||
assert.strictEqual(calls.sent.apdu, '80E40000094F07F0414C46416101');
|
||||
assert.strictEqual(calls.sent.spi2, '01', 'the computed SPI2 byte must be used');
|
||||
assert.strictEqual(cards[0].cntr, '0000000006',
|
||||
assert.strictEqual(cards[0].keysets[0].cntr, '0000000006',
|
||||
'the preset must carry the counter the card consumed');
|
||||
assert.strictEqual(els['sp-cntr'].value, '0000000006');
|
||||
assert.strictEqual(calls.sent.preset_id, 'preset-1',
|
||||
@@ -94,7 +105,7 @@ test('accepted delete persists the consumed counter and drops the record', async
|
||||
test('a delete with no PoR is accepted (the envelope 9000 is the result)', async () => {
|
||||
const { els, calls } = fakeEnv(undefined);
|
||||
await ramDeleteFromExplorer('F0414C46416101', false);
|
||||
assert.strictEqual(cards[0].cntr, '0000000006');
|
||||
assert.strictEqual(cards[0].keysets[0].cntr, '0000000006');
|
||||
assert.deepStrictEqual(calls.removed, { aid: 'F0414C46416101', cascade: false });
|
||||
assert.strictEqual(els['ram-result'].textContent, 'OK');
|
||||
assert.ok(els['ram-steps'].textContent.includes('no PoR'), els['ram-steps'].textContent);
|
||||
@@ -103,7 +114,7 @@ test('a delete with no PoR is accepted (the envelope 9000 is the result)', async
|
||||
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(cards[0].keysets[0].cntr, '0000000005');
|
||||
assert.strictEqual(calls.saved, 0);
|
||||
assert.strictEqual(calls.removed, null);
|
||||
});
|
||||
@@ -112,20 +123,20 @@ test('a refused DELETE still advances the counter but drops nothing', 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(cards[0].keysets[0].cntr, '0000000006');
|
||||
assert.strictEqual(calls.removed, null);
|
||||
});
|
||||
|
||||
test('a delete accepted via actual_response_sms_submit advances and drops the record', async () => {
|
||||
const { calls } = fakeEnv({ response_status: 'actual_response_sms_submit' });
|
||||
await ramDeleteFromExplorer('F0414C46416101', false);
|
||||
assert.strictEqual(cards[0].cntr, '0000000006');
|
||||
assert.strictEqual(cards[0].keysets[0].cntr, '0000000006');
|
||||
assert.deepStrictEqual(calls.removed, { aid: 'F0414C46416101', cascade: false });
|
||||
});
|
||||
|
||||
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(cards[0].keysets[0].cntr, '0000000005');
|
||||
assert.strictEqual(calls.removed, null);
|
||||
});
|
||||
|
||||
@@ -29,7 +29,10 @@ function extractFunc(src, name) {
|
||||
const FNS = ['hexToBytes', 'bytesToHex', 'des3Keys', 'des3EncryptBlock', 'des3CbcEncrypt',
|
||||
'xorBytes', 'zeroPad', 'crc32Bytes', 'cbcMac', 'aesCbcEncrypt', 'aesShiftLeft1', 'aesCmacSubkeys',
|
||||
'aesCmac', '_genSpBuild', 'genSp', 'spNextCntr', 'scp80SegmentInfo', 'spSizeInfoText',
|
||||
'spShowSizeInfo'];
|
||||
'spShowSizeInfo',
|
||||
// the key-version guard genSp runs before building (TS 102 225 A.2)
|
||||
'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
|
||||
'spPresetIdx', 'spKeysetGuard'];
|
||||
let code = '';
|
||||
for (const f of FNS) code += extractFunc(html, f) + '\n';
|
||||
for (const c of ['SCP80_MAX_SMS', 'SCP80_SINGLE_BYTES', 'SCP80_FIRST_BYTES', 'SCP80_NEXT_BYTES']) {
|
||||
|
||||
Reference in New Issue
Block a user