Response parser: add TS 51.011 SIM status word families; v1.9.6

OTA inner-APDU SWs from SIM-domain applications surfaced as 'Unknown
status word' (e.g. 9404 on a failed SELECT inside an authenticated
B00000 packet). SW_MAP.generic gains the complete TS 51.011 §9.4
families, so they decode in every resp-cmd context:

- 9200/9240 memory management (retry / memory problem)
- 9400/9402/9404/9408 referencing management (no EF selected,
  out of range, file ID/pattern not found, file inconsistent)
- 9802/9804/9808/9810/9840/9850 security management (CHV/access-
  condition/invalidation contradictions and blocked states; 9850
  also added to generic alongside its gp/uicc copy)
- 9EXX/9FXX data-download error length / response length wildcards

Exact keys safely coexist with the 92XX proactive wildcard (exact
match wins). TS 102 221 sweep found no further stragglers: 63C0-C9
PIN retry counters are covered by the existing 63CX wildcard.

Secured packet page: enforce fresh packet per send
- any change to APDU/TAR/CNTR/keys/padding/SPI now clears the secured
  packet textarea via spInvalidate(), so Send cannot reuse a stale
  packet ('No secured packet to send' guards the path)
- on por_ok the CNTR field auto-increments (10-digit hex normalized,
  wraps modulo 2^40) and the textarea clears; security-error verdicts
  leave both untouched for retry after fixing the cause

SIM/USIM SELECT: P1/P2 per spec + live RFM idioms
- USIM FID selects requested no response data (P2=0C) - that coding is
  reserved for the select-MF-by-empty-data special case; ordinary FID
  selects now request the FCP template (P2=04) with Le='00' per
  TS 102 221 Table 11.2 / pySim sel_ctrl convention; preset tables
  fixed in both genSimUsim and updateP1P2Display
- path method gains base selector: from MF (P1=08) / from current DF
  (P1=09) - reproduces the dominant live RFM idiom 09/0C
- 'silent' checkbox on fid/path/chain emits P2=0C without Le for hops
  where the FCI is not needed
- chain syntax gains GET RESPONSE hops: 'C0' emits CLA C0 00 00 00 and
  'C0:NN' sets explicit Le, enabling classic SELECT -> 9FXX ->
  GET RESPONSE pairs in a single secured packet so the PoR carries the
  actual FCI/response bytes instead of a bare length SW
- SW dictionary: 9FXX reworded to point at GET RESPONSE
- placeholders show the new chain syntax
- findings & backport decisions written to ~/WSL/RFM_notes.md

response_map.test.js: +8 assertions across the new families.
sim.test.js: USIM FID expectation updated; silent/base/chain cases.
apdu_parse.test.js: live capture lines 1-3 as regression fixtures.
Version 1.9.5 -> 1.9.6 everywhere; SW cache otaman-v16 -> otaman-v17
This commit is contained in:
2026-08-24 21:10:37 +03:00
parent 33255dc35e
commit 7ece8a531c
7 changed files with 177 additions and 41 deletions
+22
View File
@@ -360,3 +360,25 @@ test('SELECT P2 response field per ETSI b5-b3', () => {
const none = parseHexTree('00A4000C026F3B');
assert.ok(findNode(none.children[0], 'P2').desc.includes('no response data'));
});
test('live RFM captures decode (silent hop / implied CLA / SIM chain)', () => {
// capture line 1 head: silent relative hop 09/0C
const t1 = parseHexTree('00A4090C026F46');
const a1 = t1.children[0];
assert.ok(findNode(a1, 'P1').desc.includes('path from current DF'));
assert.ok(findNode(a1, 'P2').desc.includes('no response data'));
assert.strictEqual(findNode(a1, 'Data').desc, '6F46');
// capture line 2: silent hop + READ RECORD as implied-CLA continuation
const t2 = parseHexTree('00A4090C026FC5B2010414');
assert.strictEqual(t2.children.length, 2);
assert.strictEqual(t2.children[1].label, 'APDU (implied CLA)');
assert.ok(findNode(t2.children[1], 'INS').desc.includes('READ RECORD'));
// capture line 3: SIM chained FID selects + UPDATE RECORD (34 x FF)
const t3 = parseHexTree('A0A40000023F00A0A40000027F20A0A40000026FC5A0DC010422' + 'FF'.repeat(34));
assert.strictEqual(t3.children.length, 4);
assert.strictEqual(t3.children[3].label, 'APDU');
assert.ok(findNode(t3.children[3], 'INS').desc.includes('UPDATE RECORD'));
assert.ok(findNode(t3.children[3], 'Lc').desc.includes('34'));
});
+11
View File
@@ -55,3 +55,14 @@ test('PRIVILEGE_NAMES matches GPC v2.3 Tables 11-7/11-8/11-9', () => {
assert.strictEqual(PRIVILEGE_NAMES[2][0], 'Receipt Generation');
assert.strictEqual(PRIVILEGE_NAMES[2][3], 'Contactless Self-Activation');
});
test('TS 51.011 SIM families resolve (94xx/98xx/92xx/9Exx/9Fxx)', () => {
assert.ok(lookupSw('94', '04', 'uicc').includes('not found'));
assert.ok(lookupSw('94', '00', 'uicc').includes('No EF selected'));
assert.ok(lookupSw('94', '08', 'uicc').includes('inconsistent with the command'));
assert.ok(lookupSw('98', '40', 'uicc').includes('blocked'));
assert.ok(lookupSw('92', '40', 'uicc').includes('Memory problem'));
assert.ok(lookupSw('98', '50', 'gp').includes('max value'));
assert.ok(lookupSw('9E', '15', 'uicc').includes('download'));
assert.ok(lookupSw('9F', '20', 'uicc').includes('GET RESPONSE'));
});
+49 -3
View File
@@ -31,16 +31,17 @@ class StubEl {
}
const els = {};
let usimBase = 'mf';
const doc = {
getElementById: (id) => {
if (!els[id]) els[id] = new StubEl();
return els[id];
},
querySelector: () => new StubEl(),
querySelector: (sel) => (sel.includes('usim-sel-base') ? { value: usimBase } : null),
};
global.document = doc;
const FNS = ['buildApdu', 'buildOpNoCla', 'buildSelect', 'getSelValue', 'buildSelectApdu',
const FNS = ['buildApdu', 'buildSelect', 'getSelValue', 'buildSelectApdu',
'updateSimUsimFields', 'updateP1P2Display', 'genSimUsim', 'OPS'];
let code = '';
for (const f of FNS) {
@@ -54,11 +55,13 @@ for (const f of FNS) {
code += '\nvar SIM_PRESETS = {};';
eval(code);
function set(id, v) { els[id] = Object.assign(new StubEl(), { value: v }); }
function setChecked(id, v) { els[id] = Object.assign(new StubEl(), { checked: v }); }
function setup(mode, opts) {
for (const k of Object.keys(els)) delete els[k];
usimBase = opts.base || 'mf';
set(mode + '-cmd', opts.cmd);
setChecked(mode + '-select', !!opts.doSelect);
set(mode + '-sel-method', opts.selMethod || 'fid');
@@ -78,6 +81,8 @@ function setup(mode, opts) {
set(mode + '-pin-new', opts.pinNew || '');
set(mode + '-act-target', opts.actTarget || 'current');
set(mode + '-act-file', opts.actFile || '');
els['usim-sel-silent'] = Object.assign(new StubEl(), { checked: !!opts.silent });
els[mode + '-select'] = Object.assign(new StubEl(), { checked: !!opts.doSelect });
set(mode + '-override', '');
els[mode + '-result'] = new StubEl();
els[mode + '-pack-btn'] = new StubEl();
@@ -140,8 +145,49 @@ test('READ RECORD next mode forces P1=00', () => {
assert.strictEqual(apdu, '00B2000220');
});
test('SELECT by FID USIM: P2=0C, no Le', () => {
test('SELECT by FID USIM: P2=04 requests FCP, Le=00', () => {
const apdu = setup('usim', { cmd: 'select', selMethod: 'fid', fid: '6FC5' });
assert.strictEqual(apdu, '00A40004026FC500');
});
test('USIM chain: every hop requests FCP (04 + Le)', () => {
const apdu = setup('usim', { cmd: 'select', selMethod: 'chain', chain: '3F00,2FE2' });
assert.strictEqual(apdu, '00A40004023F000000A40004022FE200');
});
test('SIM chain with GET RESPONSE hop', () => {
const apdu = setup('sim', { cmd: 'select', selMethod: 'chain', chain: '3F00,2FE2,C0' });
assert.strictEqual(apdu, 'A0A40000023F00A0A40000022FE2A0C0000000');
});
test('GET RESPONSE hop with explicit Le (C0:NN)', () => {
const apdu = setup('usim', { cmd: 'select', selMethod: 'chain', chain: 'C0:0F' });
assert.strictEqual(apdu, '00C000000F');
});
test('silent path select + READ RECORD keeps full CLA (live PNN read)', () => {
const apdu = setup('usim', {
cmd: 'read-record', selMethod: 'path', base: 'df', silent: true, doSelect: true,
path: '6FC5', record: '1', recMode: '04', le: '14',
});
assert.strictEqual(apdu, '00A4090C026FC500B2010414');
});
test('select + READ BINARY emits explicit CLA on second command', () => {
const apdu = setup('usim', {
cmd: 'read-binary', selMethod: 'path', base: 'df', silent: true, doSelect: true,
path: '6FC5', offset: '0000', le: '0A',
});
assert.strictEqual(apdu, '00A4090C026FC500B000000A');
});
test('USIM silent path-from-current-DF hop (live RFM idiom 09/0C)', () => {
const apdu = setup('usim', { cmd: 'select', selMethod: 'path', path: '6F46', base: 'df', silent: true });
assert.strictEqual(apdu, '00A4090C026F46');
});
test('USIM silent FID select (P2=0C, no Le)', () => {
const apdu = setup('usim', { cmd: 'select', selMethod: 'fid', fid: '6FC5', silent: true });
assert.strictEqual(apdu, '00A4000C026FC5');
});