fix: install-form grant hygiene + correct 82 access coding (v3.6.12)
Live findings (2026-09-28): a browser-restored Receipt Generation privilege bit (third byte, ISD-only per GP Table 6-2) was silently sent and produced 6985, and the '82' access entries were missing the mandatory "Length of Access Domain DAP" byte, so the card rejected the ADF.USIM entry with 6A80. - ramResetGrants() clears the privilege / toolkit-enable / file-access checkboxes and refreshes the aggregates on load; ramInstallCap re-derives the privileges from the checkboxes at send time - no stale or browser-restored grant can be sent. - the privileges aggregate emits 1 or 3 bytes, never the invalid 2-byte form (GP Table 11-43), in both updateRcPriv and the chain's computePriv. - '82' entries carry the DAP-length byte: '00 01 00 00' (shared FS) and '<len> <ADF AID> 01 00 00' (ADF); the ADF AID is editable (rc-tk-adfaid, default A0000000871002 = ADF.USIM, 5..16 bytes enforced). - tests: ram_grants.test.js (aggregate forms, the send-time derivation, the load-time reset) and the updated access-parameter shapes in stk_params.test.js. 638 frontend / 496 Python green; version 3.6.12; sw simple-v285.
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
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);
|
||||
}
|
||||
|
||||
let code = '';
|
||||
for (const fn of ['updateRcPriv']) code += extractFunc(html, fn) + '\n';
|
||||
eval(code);
|
||||
|
||||
// The install form's grants are per-session choices: the privileges are an
|
||||
// aggregate of the checkboxes, derived at send time (a browser-restored
|
||||
// Receipt Generation bit produced 6985 on the live card, 2026-09-28).
|
||||
function fakeEnv(checked) {
|
||||
const els = { 'rc-priv': { value: '' } };
|
||||
const boxes = checked.map(([cls, val]) => ({ className: cls, value: val, checked: true }));
|
||||
globalThis.document = {
|
||||
getElementById: id => els[id] || null,
|
||||
querySelectorAll: sel => boxes.filter(b => sel.indexOf('.' + b.className) >= 0),
|
||||
};
|
||||
return els;
|
||||
}
|
||||
|
||||
test('updateRcPriv aggregates the checked privilege boxes', () => {
|
||||
let els = fakeEnv([]);
|
||||
updateRcPriv();
|
||||
assert.strictEqual(els['rc-priv'].value, '00');
|
||||
els = fakeEnv([['rc-priv-b3', '80']]);
|
||||
updateRcPriv();
|
||||
assert.strictEqual(els['rc-priv'].value, '000080', 'Receipt Generation is the third byte');
|
||||
els = fakeEnv([['rc-priv-b1', '80'], ['rc-priv-b3', '80']]);
|
||||
updateRcPriv();
|
||||
assert.strictEqual(els['rc-priv'].value, '800080');
|
||||
els = fakeEnv([['rc-priv-b1', '04'], ['rc-priv-b2', '80']]);
|
||||
updateRcPriv();
|
||||
assert.strictEqual(els['rc-priv'].value, '048000');
|
||||
els = fakeEnv([['rc-priv-b1', '80'], ['rc-priv-b1', '40'], ['rc-priv-b2', '80']]);
|
||||
updateRcPriv();
|
||||
assert.strictEqual(els['rc-priv'].value, 'C08000', 'bits within a byte are OR-ed');
|
||||
});
|
||||
|
||||
test('the install derives the privileges at send time', () => {
|
||||
const fn = extractFunc(html, 'ramInstallCap');
|
||||
const iPriv = fn.indexOf('updateRcPriv();');
|
||||
const iBody = fn.indexOf('const body = {');
|
||||
assert.ok(iPriv >= 0, 'ramInstallCap must call updateRcPriv()');
|
||||
assert.ok(iBody >= 0, 'ramInstallCap body not found');
|
||||
assert.ok(iPriv < iBody, 'the privileges must be derived before the request body');
|
||||
});
|
||||
|
||||
test('the form resets the restored grants on load', () => {
|
||||
assert.ok(/function ramResetGrants\(\)/.test(html), 'ramResetGrants is missing');
|
||||
assert.ok(/querySelectorAll\('\.rc-priv-b1, \.rc-priv-b2, \.rc-priv-b3'\)/.test(html),
|
||||
'the privilege checkboxes must be cleared');
|
||||
assert.ok(/chainInit\('chain-ram'\);\s*\n\s*ramResetGrants\(\);/.test(html),
|
||||
'ramResetGrants must run on load');
|
||||
assert.ok(/getElementById\('rc-toolkit-enable'\)\.checked = false;/.test(html),
|
||||
'the toolkit enable must be cleared');
|
||||
assert.ok(/getElementById\('rc-tk-fsaccess'\)\.checked = false;/.test(html),
|
||||
'the file-access grant must be cleared');
|
||||
});
|
||||
@@ -102,7 +102,7 @@ test('every toolkit field regenerates the STK parameters hex on edit', () => {
|
||||
assert.ok(/on(?:input|change)="[^"]*updateStkParamsHex/.test(m[0]),
|
||||
m[1] + ' does not refresh the hex: ' + m[0]);
|
||||
}
|
||||
assert.strictEqual(seen.length, 16, 'expected 16 toolkit fields, got ' + seen.join(', '));
|
||||
assert.strictEqual(seen.length, 17, 'expected 17 toolkit fields, got ' + seen.join(', '));
|
||||
});
|
||||
|
||||
test('the applet TAR field has no B00001 default or placeholder', () => {
|
||||
@@ -128,7 +128,7 @@ function fakeForm(values) {
|
||||
'rc-tk-textlen', 'rc-tk-menus', 'rc-tk-firstpos', 'rc-tk-firstid',
|
||||
'rc-tk-lastpos', 'rc-tk-lastid', 'rc-tk-channels', 'rc-tk-msl',
|
||||
'rc-tk-tar', 'rc-tk-ad', 'rc-tk-services', 'rc-tk-fsaccess',
|
||||
'rc-tk-adfaccess', 'ram-stk-params'];
|
||||
'rc-tk-adfaccess', 'rc-tk-adfaid', 'ram-stk-params'];
|
||||
const checks = ['rc-toolkit-enable', 'rc-tk-fsaccess', 'rc-tk-adfaccess'];
|
||||
const els = {};
|
||||
for (const id of ids) els[id] = { value: '', checked: false, dataset: {} };
|
||||
@@ -150,31 +150,44 @@ test('the RAM form fields build the live install parameters end to end', () => {
|
||||
});
|
||||
|
||||
test('UICC file-access parameters (82) are appended in EA mode', () => {
|
||||
// TS 102 226 8.2.1.3.2.2.2: [file system AID len 00 = shared FS]
|
||||
// [Access Domain len 01][ADP 00 = full access]; the SIM path grants the
|
||||
// same rights via the CA Access Domain field. The ADF entry is an
|
||||
// extension of the file-system entry.
|
||||
// TS 102 226 8.2.1.3.2.2.2: every entry ends with the "Length of Access
|
||||
// Domain DAP" byte (00 = no DAP):
|
||||
// [FS AID len 00 = shared FS][AD len 01][ADP 00 = full][DAP len 00]
|
||||
// [ADF AID len][ADF AID][AD len 01][ADP 00][DAP len 00]
|
||||
// The SIM path grants the same rights via the CA Access Domain field; the
|
||||
// ADF entry is an extension of the file-system entry. A missing DAP
|
||||
// length byte made the card reject the ADF entry with 6A80 (live
|
||||
// 2026-09-28).
|
||||
const base = vals({ channels: '1', msl: '12', tar: 'AF4D01' });
|
||||
assert.strictEqual(stkParamsBuild(Object.assign({}, base, { fsAccess: true })),
|
||||
'EA14800D000000000102011203AF4D01008203000100');
|
||||
'EA15800D000000000102011203AF4D0100820400010000');
|
||||
assert.strictEqual(
|
||||
stkParamsBuild(Object.assign({}, base, { fsAccess: true, adfAccess: true })),
|
||||
'EA1E800D000000000102011203AF4D0100820D00010007A00000008710020100');
|
||||
'EA20800D000000000102011203AF4D0100820F0001000007A0000000871002010000');
|
||||
assert.strictEqual(
|
||||
stkParamsBuild(Object.assign({}, base, { fsAccess: true, adfAccess: true,
|
||||
adfAid: 'A0000000871002FF33FFFF89010101' })),
|
||||
'EA28800D000000000102011203AF4D01008217000100000FA0000000871002FF33FFFF89010101010000');
|
||||
assert.strictEqual(stkParamsBuild(base), 'EA0F800D000000000102011203AF4D0100');
|
||||
assert.strictEqual(stkParamsBuild(Object.assign({}, base, { adfAccess: true })),
|
||||
'EA0F800D000000000102011203AF4D0100');
|
||||
// the ADF AID must be 5..16 bytes
|
||||
assert.strictEqual(stkParamsBuild(Object.assign({}, base,
|
||||
{ fsAccess: true, adfAccess: true, adfAid: 'A00000' })), null);
|
||||
assert.strictEqual(stkParamsBuild(Object.assign({}, base,
|
||||
{ fsAccess: true, adfAccess: true, adfAid: 'A0'.repeat(17) })), null);
|
||||
});
|
||||
|
||||
test('the RAM form emits full file access when the checkbox is ticked', () => {
|
||||
fakeForm({ 'rc-toolkit-enable': true, 'rc-tk-mode': 'ea', 'rc-tk-msl': '12',
|
||||
'rc-tk-tar': 'AF4D01', 'rc-tk-channels': '1', 'rc-tk-fsaccess': true });
|
||||
assert.strictEqual(buildRcToolkitParams(),
|
||||
'EA14800D000000000102011203AF4D01008203000100');
|
||||
'EA15800D000000000102011203AF4D0100820400010000');
|
||||
fakeForm({ 'rc-toolkit-enable': true, 'rc-tk-mode': 'ea', 'rc-tk-msl': '12',
|
||||
'rc-tk-tar': 'AF4D01', 'rc-tk-channels': '1', 'rc-tk-fsaccess': true,
|
||||
'rc-tk-adfaccess': true });
|
||||
assert.strictEqual(buildRcToolkitParams(),
|
||||
'EA1E800D000000000102011203AF4D0100820D00010007A00000008710020100');
|
||||
'EA20800D000000000102011203AF4D0100820F0001000007A0000000871002010000');
|
||||
});
|
||||
|
||||
test('updateStkParamsHex refreshes the field and clears the manual flag', () => {
|
||||
|
||||
Reference in New Issue
Block a user