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:
+53
-14
@@ -820,6 +820,8 @@
|
|||||||
<input id="rc-tk-adfaccess" type="checkbox" onchange="updateStkParamsHex()" class="rounded border-gray-300 dark:border-slate-600 dark:bg-slate-800">
|
<input id="rc-tk-adfaccess" type="checkbox" onchange="updateStkParamsHex()" class="rounded border-gray-300 dark:border-slate-600 dark:bg-slate-800">
|
||||||
<span data-l10n="ADF.USIM access (full)">ADF.USIM access (full)</span>
|
<span data-l10n="ADF.USIM access (full)">ADF.USIM access (full)</span>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="block mb-1 mt-1 text-xs text-gray-500 dark:text-slate-400" data-l10n="ADF AID (hex)">ADF AID (hex)</label>
|
||||||
|
<input id="rc-tk-adfaid" oninput="updateStkParamsHex()" value="A0000000871002" maxlength="32" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1 dark:bg-slate-800">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1677,7 +1679,7 @@
|
|||||||
// ===== Version =====
|
// ===== Version =====
|
||||||
// Single source of truth for the PWA version: shown in the header and used
|
// Single source of truth for the PWA version: shown in the header and used
|
||||||
// by the server version check in pysimConnect().
|
// by the server version check in pysimConnect().
|
||||||
const SIMPLE_VERSION = '3.6.11';
|
const SIMPLE_VERSION = '3.6.12';
|
||||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||||
|
|
||||||
// ===== Tab switching =====
|
// ===== Tab switching =====
|
||||||
@@ -2498,9 +2500,14 @@ function updateRcPriv() {
|
|||||||
document.querySelectorAll('.rc-priv-b1:checked').forEach(cb => b1 |= parseInt(cb.value, 16));
|
document.querySelectorAll('.rc-priv-b1:checked').forEach(cb => b1 |= parseInt(cb.value, 16));
|
||||||
document.querySelectorAll('.rc-priv-b2:checked').forEach(cb => b2 |= parseInt(cb.value, 16));
|
document.querySelectorAll('.rc-priv-b2:checked').forEach(cb => b2 |= parseInt(cb.value, 16));
|
||||||
document.querySelectorAll('.rc-priv-b3:checked').forEach(cb => b3 |= parseInt(cb.value, 16));
|
document.querySelectorAll('.rc-priv-b3:checked').forEach(cb => b3 |= parseInt(cb.value, 16));
|
||||||
|
// GP Card Spec Table 11-43: the privileges length is 1 or 3 bytes - a
|
||||||
|
// 2-byte form is invalid, so the third byte is padded when either of the
|
||||||
|
// upper bytes is set.
|
||||||
let hex = b1.toString(16).padStart(2, '0').toUpperCase();
|
let hex = b1.toString(16).padStart(2, '0').toUpperCase();
|
||||||
hex += b2.toString(16).padStart(2, '0').toUpperCase();
|
if (b2 || b3) {
|
||||||
if (b3) hex += b3.toString(16).padStart(2, '0').toUpperCase();
|
hex += b2.toString(16).padStart(2, '0').toUpperCase();
|
||||||
|
hex += b3.toString(16).padStart(2, '0').toUpperCase();
|
||||||
|
}
|
||||||
document.getElementById('rc-priv').value = hex;
|
document.getElementById('rc-priv').value = hex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2518,6 +2525,20 @@ function updateRcTkMode() {
|
|||||||
document.getElementById('rc-tk-adfaccess-row').style.display = isSim ? 'none' : '';
|
document.getElementById('rc-tk-adfaccess-row').style.display = isSim ? 'none' : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The install form's checkbox grants (privileges, toolkit enable, file
|
||||||
|
// access) are per-session choices: clear anything the browser restored on
|
||||||
|
// reload and refresh the aggregates, so a stale grant can never silently
|
||||||
|
// apply (live 2026-09-28: an inherited Receipt Generation bit produced 6985).
|
||||||
|
function ramResetGrants() {
|
||||||
|
document.querySelectorAll('.rc-priv-b1, .rc-priv-b2, .rc-priv-b3')
|
||||||
|
.forEach(cb => { cb.checked = false; });
|
||||||
|
document.getElementById('rc-toolkit-enable').checked = false;
|
||||||
|
document.getElementById('rc-tk-fsaccess').checked = false;
|
||||||
|
document.getElementById('rc-tk-adfaccess').checked = false;
|
||||||
|
updateRcToolkit(); // hides the toolkit block and refreshes the STK hex
|
||||||
|
updateRcPriv(); // privileges aggregate back to 00
|
||||||
|
}
|
||||||
|
|
||||||
// Pure SIM/UICC toolkit install-parameter builder shared by the RAM install
|
// Pure SIM/UICC toolkit install-parameter builder shared by the RAM install
|
||||||
// form (buildRcToolkitParams) and the RAM/GP chain rows (buildTkParams).
|
// form (buildRcToolkitParams) and the RAM/GP chain rows (buildTkParams).
|
||||||
// `v` carries the field values; returns the CA/EA TLV hex, or null when a
|
// `v` carries the field values; returns the CA/EA TLV hex, or null when a
|
||||||
@@ -2525,8 +2546,8 @@ function updateRcTkMode() {
|
|||||||
// identifiers 128..255 are reserved for the toolkit framework, so only
|
// identifiers 128..255 are reserved for the toolkit framework, so only
|
||||||
// 01..7F may be requested (TS 102 226 8.2.1.3.2.3). In EA mode `v.fsAccess`
|
// 01..7F may be requested (TS 102 226 8.2.1.3.2.3). In EA mode `v.fsAccess`
|
||||||
// adds the UICC file-access parameters (tag '82') and `v.adfAccess` extends
|
// adds the UICC file-access parameters (tag '82') and `v.adfAccess` extends
|
||||||
// them with an ADF.USIM entry; the SIM path grants access via the CA Access
|
// them with an ADF entry (AID from `v.adfAid`, default ADF.USIM); the SIM path
|
||||||
// Domain field instead.
|
// grants access via the CA Access Domain field instead.
|
||||||
function stkParamsBuild(v) {
|
function stkParamsBuild(v) {
|
||||||
const priority = parseInt(v.priority, 10) || 0;
|
const priority = parseInt(v.priority, 10) || 0;
|
||||||
const timers = parseInt(v.timers, 10) || 0;
|
const timers = parseInt(v.timers, 10) || 0;
|
||||||
@@ -2577,11 +2598,19 @@ function stkParamsBuild(v) {
|
|||||||
let eaValue = '80' + berLenStr(tkPayload.length / 2) + tkPayload;
|
let eaValue = '80' + berLenStr(tkPayload.length / 2) + tkPayload;
|
||||||
if (v.fsAccess) {
|
if (v.fsAccess) {
|
||||||
// UICC Access Application specific parameters (TS 102 226
|
// UICC Access Application specific parameters (TS 102 226
|
||||||
// 8.2.1.3.2.2.2): [file system AID length 00 = shared file system]
|
// 8.2.1.3.2.2.2): every entry ends with the "Length of Access Domain
|
||||||
// [Access Domain length 01][ADP 00 = full access], optionally with an
|
// DAP" byte (00 = no DAP):
|
||||||
// ADF entry [AID length 07][ADF.USIM][AD length 01][ADP 00].
|
// [file system AID length 00 = shared FS][AD length 01]
|
||||||
let acc = '000100';
|
// [ADP 00 = full access][DAP length 00]
|
||||||
if (v.adfAccess) acc += '07A00000008710020100';
|
// [ADF AID length][ADF AID][AD length 01][ADP 00][DAP length 00]
|
||||||
|
// (without the DAP length byte the card rejected the ADF entry with
|
||||||
|
// 6A80; the ADF AID must be 5..16 bytes.)
|
||||||
|
let acc = '000100' + '00';
|
||||||
|
if (v.adfAccess) {
|
||||||
|
const adf = (v.adfAid || 'A0000000871002').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
|
||||||
|
if (adf.length < 10 || adf.length > 32) return null;
|
||||||
|
acc += (adf.length / 2).toString(16).padStart(2, '0').toUpperCase() + adf + '0100' + '00';
|
||||||
|
}
|
||||||
eaValue += '82' + berLenStr(acc.length / 2) + acc;
|
eaValue += '82' + berLenStr(acc.length / 2) + acc;
|
||||||
}
|
}
|
||||||
return 'EA' + berLenStr(eaValue.length / 2) + eaValue;
|
return 'EA' + berLenStr(eaValue.length / 2) + eaValue;
|
||||||
@@ -2601,6 +2630,7 @@ function buildRcToolkitParams() {
|
|||||||
tar: g('rc-tk-tar'), ad: g('rc-tk-ad'), services: g('rc-tk-services'),
|
tar: g('rc-tk-tar'), ad: g('rc-tk-ad'), services: g('rc-tk-services'),
|
||||||
fsAccess: document.getElementById('rc-tk-fsaccess').checked,
|
fsAccess: document.getElementById('rc-tk-fsaccess').checked,
|
||||||
adfAccess: document.getElementById('rc-tk-adfaccess').checked,
|
adfAccess: document.getElementById('rc-tk-adfaccess').checked,
|
||||||
|
adfAid: g('rc-tk-adfaid'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3471,6 +3501,8 @@ function chainRamToolkitHtml(chainId, idx, f, uf) {
|
|||||||
html += '<div class="col-span-4"><label class="flex items-center gap-1 text-gray-600 dark:text-slate-400">' +
|
html += '<div class="col-span-4"><label class="flex items-center gap-1 text-gray-600 dark:text-slate-400">' +
|
||||||
'<input type="checkbox"' + (f.tkAdfAccess ? ' checked' : '') + ' onchange="chainUpdateField(\'' + chainId + '\',' + idx + ',\'tkAdfAccess\',this.checked);chainRender(\'' + chainId + '\')" class="rounded">' +
|
'<input type="checkbox"' + (f.tkAdfAccess ? ' checked' : '') + ' onchange="chainUpdateField(\'' + chainId + '\',' + idx + ',\'tkAdfAccess\',this.checked);chainRender(\'' + chainId + '\')" class="rounded">' +
|
||||||
'ADF.USIM access (full)</label></div>';
|
'ADF.USIM access (full)</label></div>';
|
||||||
|
html += '<div class="col-span-4"><label class="block text-gray-600 dark:text-slate-400 mb-0.5">ADF AID (hex)</label>' +
|
||||||
|
'<input value="' + escHtml(f.tkAdfAid || 'A0000000871002') + '" oninput="' + uf('tkAdfAid') + '" class="w-full font-mono border border-gray-300 dark:border-slate-600 rounded px-1.5 py-0.5 dark:bg-slate-800" maxlength="32"></div>';
|
||||||
}
|
}
|
||||||
html += '</div></div>';
|
html += '</div></div>';
|
||||||
return html;
|
return html;
|
||||||
@@ -4074,10 +4106,11 @@ function chainRamBuildRowHex(idx, row) {
|
|||||||
var b1 = parseInt(ph.substring(0, 2), 16) || 0;
|
var b1 = parseInt(ph.substring(0, 2), 16) || 0;
|
||||||
var b2 = ph.length >= 4 ? parseInt(ph.substring(2, 4), 16) : 0;
|
var b2 = ph.length >= 4 ? parseInt(ph.substring(2, 4), 16) : 0;
|
||||||
var b3 = ph.length >= 6 ? parseInt(ph.substring(4, 6), 16) : 0;
|
var b3 = ph.length >= 6 ? parseInt(ph.substring(4, 6), 16) : 0;
|
||||||
|
// Table 11-43: privileges are 1 or 3 bytes (never 2).
|
||||||
if (b2 === 0 && b3 === 0) return b1.toString(16).padStart(2, '0').toUpperCase();
|
if (b2 === 0 && b3 === 0) return b1.toString(16).padStart(2, '0').toUpperCase();
|
||||||
var r = b1.toString(16).padStart(2, '0').toUpperCase() + b2.toString(16).padStart(2, '0').toUpperCase();
|
return b1.toString(16).padStart(2, '0').toUpperCase() +
|
||||||
if (b3) r += b3.toString(16).padStart(2, '0').toUpperCase();
|
b2.toString(16).padStart(2, '0').toUpperCase() +
|
||||||
return r;
|
b3.toString(16).padStart(2, '0').toUpperCase();
|
||||||
}
|
}
|
||||||
// The chain row's toolkit fields -> the install parameters hex.
|
// The chain row's toolkit fields -> the install parameters hex.
|
||||||
function buildTkParams() {
|
function buildTkParams() {
|
||||||
@@ -4089,7 +4122,7 @@ function chainRamBuildRowHex(idx, row) {
|
|||||||
lastPos: f.tkLastpos, lastId: f.tkLastid, channels: f.tkChannels,
|
lastPos: f.tkLastpos, lastId: f.tkLastid, channels: f.tkChannels,
|
||||||
msl: f.tkMsl || '16', tar: f.tkTar, ad: f.tkAd,
|
msl: f.tkMsl || '16', tar: f.tkTar, ad: f.tkAd,
|
||||||
services: f.tkServices,
|
services: f.tkServices,
|
||||||
fsAccess: f.tkFsAccess, adfAccess: f.tkAdfAccess,
|
fsAccess: f.tkFsAccess, adfAccess: f.tkAdfAccess, adfAid: f.tkAdfAid,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// INSTALL/LOAD are case-3 commands (no trailing Le), matching the
|
// INSTALL/LOAD are case-3 commands (no trailing Le), matching the
|
||||||
@@ -8959,6 +8992,10 @@ async function ramInstallCap(sp) {
|
|||||||
if (document.getElementById('rc-toolkit-enable').checked && !(stkEl.dataset && stkEl.dataset.manual)) {
|
if (document.getElementById('rc-toolkit-enable').checked && !(stkEl.dataset && stkEl.dataset.manual)) {
|
||||||
updateStkParamsHex();
|
updateStkParamsHex();
|
||||||
}
|
}
|
||||||
|
// The privileges are an aggregate of the checkboxes: derive them now, so a
|
||||||
|
// value restored by the browser or left from an earlier experiment can
|
||||||
|
// never be sent silently.
|
||||||
|
updateRcPriv();
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
cap_hex: capHex,
|
cap_hex: capHex,
|
||||||
@@ -16428,6 +16465,7 @@ const LANG_RU = {
|
|||||||
'Access domain (hex)': 'Домен доступа (hex)',
|
'Access domain (hex)': 'Домен доступа (hex)',
|
||||||
'File system access (full)': 'Доступ к файловой системе (полный)',
|
'File system access (full)': 'Доступ к файловой системе (полный)',
|
||||||
'ADF.USIM access (full)': 'Доступ к ADF.USIM (полный)',
|
'ADF.USIM access (full)': 'Доступ к ADF.USIM (полный)',
|
||||||
|
'ADF AID (hex)': 'AID ADF (hex)',
|
||||||
'Load data (hex)': 'Данные загрузки (hex)',
|
'Load data (hex)': 'Данные загрузки (hex)',
|
||||||
'Block number': 'Номер блока',
|
'Block number': 'Номер блока',
|
||||||
'Encryption': 'Шифрование',
|
'Encryption': 'Шифрование',
|
||||||
@@ -17397,6 +17435,7 @@ updateSpKid();
|
|||||||
chainInit('chain-sim');
|
chainInit('chain-sim');
|
||||||
chainInit('chain-usim');
|
chainInit('chain-usim');
|
||||||
chainInit('chain-ram');
|
chainInit('chain-ram');
|
||||||
|
ramResetGrants();
|
||||||
translatePage();
|
translatePage();
|
||||||
// PWA
|
// PWA
|
||||||
let deferredPrompt;
|
let deferredPrompt;
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simple-v284';
|
const CACHE = 'simple-v285';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -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]),
|
assert.ok(/on(?:input|change)="[^"]*updateStkParamsHex/.test(m[0]),
|
||||||
m[1] + ' does not refresh the hex: ' + 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', () => {
|
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-textlen', 'rc-tk-menus', 'rc-tk-firstpos', 'rc-tk-firstid',
|
||||||
'rc-tk-lastpos', 'rc-tk-lastid', 'rc-tk-channels', 'rc-tk-msl',
|
'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-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 checks = ['rc-toolkit-enable', 'rc-tk-fsaccess', 'rc-tk-adfaccess'];
|
||||||
const els = {};
|
const els = {};
|
||||||
for (const id of ids) els[id] = { value: '', checked: false, dataset: {} };
|
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', () => {
|
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]
|
// TS 102 226 8.2.1.3.2.2.2: every entry ends with the "Length of Access
|
||||||
// [Access Domain len 01][ADP 00 = full access]; the SIM path grants the
|
// Domain DAP" byte (00 = no DAP):
|
||||||
// same rights via the CA Access Domain field. The ADF entry is an
|
// [FS AID len 00 = shared FS][AD len 01][ADP 00 = full][DAP len 00]
|
||||||
// extension of the file-system entry.
|
// [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' });
|
const base = vals({ channels: '1', msl: '12', tar: 'AF4D01' });
|
||||||
assert.strictEqual(stkParamsBuild(Object.assign({}, base, { fsAccess: true })),
|
assert.strictEqual(stkParamsBuild(Object.assign({}, base, { fsAccess: true })),
|
||||||
'EA14800D000000000102011203AF4D01008203000100');
|
'EA15800D000000000102011203AF4D0100820400010000');
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
stkParamsBuild(Object.assign({}, base, { fsAccess: true, adfAccess: true })),
|
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(base), 'EA0F800D000000000102011203AF4D0100');
|
||||||
assert.strictEqual(stkParamsBuild(Object.assign({}, base, { adfAccess: true })),
|
assert.strictEqual(stkParamsBuild(Object.assign({}, base, { adfAccess: true })),
|
||||||
'EA0F800D000000000102011203AF4D0100');
|
'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', () => {
|
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',
|
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-tar': 'AF4D01', 'rc-tk-channels': '1', 'rc-tk-fsaccess': true });
|
||||||
assert.strictEqual(buildRcToolkitParams(),
|
assert.strictEqual(buildRcToolkitParams(),
|
||||||
'EA14800D000000000102011203AF4D01008203000100');
|
'EA15800D000000000102011203AF4D0100820400010000');
|
||||||
fakeForm({ 'rc-toolkit-enable': true, 'rc-tk-mode': 'ea', 'rc-tk-msl': '12',
|
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-tar': 'AF4D01', 'rc-tk-channels': '1', 'rc-tk-fsaccess': true,
|
||||||
'rc-tk-adfaccess': true });
|
'rc-tk-adfaccess': true });
|
||||||
assert.strictEqual(buildRcToolkitParams(),
|
assert.strictEqual(buildRcToolkitParams(),
|
||||||
'EA1E800D000000000102011203AF4D0100820D00010007A00000008710020100');
|
'EA20800D000000000102011203AF4D0100820F0001000007A0000000871002010000');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('updateStkParamsHex refreshes the field and clears the manual flag', () => {
|
test('updateStkParamsHex refreshes the field and clears the manual flag', () => {
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pysim-simple-server"
|
name = "pysim-simple-server"
|
||||||
version = "3.6.11"
|
version = "3.6.12"
|
||||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE
|
|||||||
from osmocom.utils import rpad
|
from osmocom.utils import rpad
|
||||||
|
|
||||||
|
|
||||||
VERSION = '3.6.11'
|
VERSION = '3.6.12'
|
||||||
|
|
||||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user