diff --git a/frontend/index.html b/frontend/index.html index 437e208..f423365 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -832,10 +832,24 @@ - +
+
+ + +
+
+ +
+ + +
+
+
+ + @@ -1685,7 +1699,7 @@ // ===== Version ===== // Single source of truth for the PWA version: shown in the header and used // by the server version check in pysimConnect(). -const SIMPLE_VERSION = '3.6.16'; +const SIMPLE_VERSION = '3.6.17'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== Tab switching ===== @@ -2651,6 +2665,16 @@ function updateStkParamsHex() { // A hand-edited hex is an explicit override: keep it until a toolkit // field is edited again (this clears the flag). if (el.dataset) delete el.dataset.manual; + // A rejected value must not vanish silently: say why the field is empty. + const hint = document.getElementById('ram-stk-hint'); + if (!hint) return; + if (tk === null) { + hint.textContent = t('STK parameters not generated: check the TAR (multiple of 3 bytes), menu IDs (<= 7F) and the ADF AID (5-16 bytes)'); + hint.classList.remove('hidden'); + } else { + hint.textContent = ''; + hint.classList.add('hidden'); + } } // The ADF.USIM AID is card-specific: the FCI's DF name (tag 84) is the full @@ -2701,8 +2725,42 @@ async function ramAdfAidFromCard() { } } +// GP memory-quota values (TS 102 226 8.2.1.3.2.2.1 / GP 9.7): a 2-byte +// integer up to 32767, a 4-byte integer above. +function ramQuotaValue(n) { + const v = Math.max(0, Math.min(0x7FFFFFFF, parseInt(n, 10) || 0)); + return v <= 0x7FFF + ? '02' + v.toString(16).padStart(4, '0').toUpperCase() + : '04' + v.toString(16).padStart(8, '0').toUpperCase(); +} + +// The applet-specific prefix of the install parameters, in the vendor's +// order: `C9 ` + `EF `. +// The server appends the STK (`EA`/`EF+CA`) part after it. +function ramParamsPrefix(c9Hex, c7, c8) { + const c9 = (c9Hex || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); + let out = ''; + if (c9) out += 'C9' + berLenStr(c9.length / 2) + c9; + let ef = ''; + if (String(c7 ?? '') !== '' && !isNaN(parseInt(c7, 10))) ef += 'C7' + ramQuotaValue(c7); + if (String(c8 ?? '') !== '' && !isNaN(parseInt(c8, 10))) ef += 'C8' + ramQuotaValue(c8); + if (ef) out += 'EF' + berLenStr(ef.length / 2) + ef; + return out; +} + +function ramInstallParamsPrefix() { + return ramParamsPrefix( + document.getElementById('rc-c9').value, + document.getElementById('rc-quota-c7').value, + document.getElementById('rc-quota-c8').value); +} + function updateInstallParamsHex() { - document.getElementById('ram-install-params-hex').value = 'C900'; + const prefix = ramInstallParamsPrefix(); + const el = document.getElementById('ram-install-params-hex'); + // The field carries extra raw TLVs; the generated prefix is shown in the + // placeholder so the composed parameters stay visible. + el.placeholder = prefix ? prefix + ' …' : 'C9 TLV'; } // ===== GSM 7-bit alphabet ===== @@ -9090,7 +9148,8 @@ async function ramInstallCap(sp) { cap_hex: capHex, sd_aid: (document.getElementById('ram-sd-aid').value || '').replace(/[^0-9a-fA-F]/g, ''), privileges: (document.getElementById('rc-priv').value || '00').replace(/[^0-9a-fA-F]/g, ''), - install_params: (document.getElementById('ram-install-params-hex').value || '').replace(/[^0-9a-fA-F]/g, ''), + install_params: (ramInstallParamsPrefix() + + (document.getElementById('ram-install-params-hex').value || '').replace(/[^0-9a-fA-F]/g, '')).toUpperCase(), stk_params: (document.getElementById('ram-stk-params').value || '').replace(/[^0-9a-fA-F]/g, ''), make_selectable: document.getElementById('ram-make-sel').checked, spi1: sp.spi1, spi2: '01', kic: sp.kic, kid: sp.kid, @@ -17292,6 +17351,9 @@ const LANG_RU = { 'Name is required': 'Укажите имя', 'From card': 'С карты', 'Priority (0 = RFU)': 'Приоритет (0 = RFU)', + 'Applet-specific parameters (C9, hex, optional)': 'Параметры апплета (C9, hex, опционально)', + 'C7 / C8 memory quotas (bytes, optional)': 'Квоты памяти C7 / C8 (байты, опционально)', + 'STK parameters not generated: check the TAR (multiple of 3 bytes), menu IDs (<= 7F) and the ADF AID (5-16 bytes)': 'STK-параметры не сформированы: проверьте TAR (кратность 3 байтам), ID меню (<= 7F) и ADF AID (5-16 байт)', 'SCP80 (GSM 03.48, ETSI TS 102 225)': 'SCP80 (GSM 03.48, ETSI TS 102 225)', 'SCP81 (HTTP OTA)': 'SCP81 (HTTP OTA)', 'Start': 'Запустить', @@ -17543,6 +17605,7 @@ chainInit('chain-sim'); chainInit('chain-usim'); chainInit('chain-ram'); ramResetGrants(); +updateInstallParamsHex(); translatePage(); // PWA let deferredPrompt; diff --git a/frontend/sw.js b/frontend/sw.js index 767b7f4..c4252da 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v289'; +const CACHE = 'simple-v290'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/stk_params.test.js b/frontend/tests/stk_params.test.js index 746aece..b980665 100644 --- a/frontend/tests/stk_params.test.js +++ b/frontend/tests/stk_params.test.js @@ -23,7 +23,8 @@ function extractFunc(src, name) { let code = ''; for (const fn of ['berLenStr', 'stkParamsBuild', 'buildRcToolkitParams', - 'updateStkParamsHex', 'adfAidFromFci']) code += extractFunc(html, fn) + '\n'; + 'updateStkParamsHex', 'adfAidFromFci', 'ramParamsPrefix', 'ramQuotaValue', + 'ramInstallParamsPrefix']) code += extractFunc(html, fn) + '\n'; eval(code); const base = { mode: 'ea', priority: '0', timers: '0', textLen: '0', menus: '0', @@ -127,6 +128,45 @@ test('adfAidFromFci extracts the DF name (tag 84) from an FCI', () => { assert.strictEqual(adfAidFromFci('ZZZZ'), ''); }); +test('ramParamsPrefix composes C9 + EF (C7/C8) like the vendor scripts', () => { + // The vendor's working install (samples/uicc/applets/STK3): + // C9 22 EF 08 C7 02 0000 C8 02 0000 EA ... + assert.strictEqual(ramParamsPrefix('', '', ''), ''); + assert.strictEqual(ramParamsPrefix('082905112000012066', '', ''), + 'C909082905112000012066'); + assert.strictEqual(ramParamsPrefix('', '0', '0'), 'EF08C7020000C8020000'); + assert.strictEqual(ramParamsPrefix('', '32768', ''), 'EF06C70400008000'); + assert.strictEqual(ramParamsPrefix('AA', '', '1234'), 'C901AAEF04C80204D2'); +}); + +test('the RAM form composes the C9/quota prefix into the install parameters', () => { + const els = fakeForm({ 'rc-c9': 'AA', 'rc-quota-c7': '0' }); + assert.strictEqual(ramInstallParamsPrefix(), 'C901AAEF04C7020000'); + els['rc-quota-c8'].value = '5'; + assert.strictEqual(ramInstallParamsPrefix(), 'C901AAEF08C7020000C8020005'); +}); + +test('updateStkParamsHex names the rejection reason', () => { + globalThis.t = s => s; + const els = fakeForm({ 'rc-toolkit-enable': true, 'rc-tk-mode': 'ea', + 'rc-tk-tar': 'AF4D' }); // not a multiple of 3 bytes + updateStkParamsHex(); + assert.strictEqual(els['ram-stk-params'].value, ''); + assert.ok(els['ram-stk-hint'].textContent.includes('STK parameters not generated'), + els['ram-stk-hint'].textContent); + // a valid form clears the hint + els['rc-tk-tar'].value = 'AF4D01'; + updateStkParamsHex(); + assert.ok(els['ram-stk-params'].value.startsWith('EA'), els['ram-stk-params'].value); + assert.strictEqual(els['ram-stk-hint'].textContent, ''); + delete globalThis.t; +}); + +test('the install body composes the C9/quota prefix', () => { + const fn = extractFunc(html, 'ramInstallCap'); + assert.ok(/install_params: \(ramInstallParamsPrefix\(\)/.test(fn), fn); +}); + test('the ADF AID From-card button is wired', () => { assert.ok(/id="rc-tk-adfaid-from-card"[^>]*onclick="ramAdfAidFromCard\(\)"/.test(html), 'the ADF AID From-card button is missing'); @@ -149,10 +189,12 @@ 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', 'rc-tk-adfaid', 'ram-stk-params']; + 'rc-tk-adfaccess', 'rc-tk-adfaid', 'rc-c9', 'rc-quota-c7', + 'rc-quota-c8', 'ram-stk-params', 'ram-stk-hint']; const checks = ['rc-toolkit-enable', 'rc-tk-fsaccess', 'rc-tk-adfaccess']; const els = {}; - for (const id of ids) els[id] = { value: '', checked: false, dataset: {} }; + for (const id of ids) els[id] = { value: '', checked: false, dataset: {}, + classList: { add() {}, remove() {} } }; for (const [id, v] of Object.entries(values || {})) { if (checks.indexOf(id) >= 0) els[id].checked = !!v; else els[id].value = v; diff --git a/pyproject.toml b/pyproject.toml index 7df7785..187d276 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.6.16" +version = "3.6.17" description = "HTTP REST server wrapping pysim for the SIMple PWA" requires-python = ">=3.8" # pysim is a git-only dependency installed explicitly by setup.bat/setup.sh. diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index 405239a..4aa219f 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE from osmocom.utils import rpad -VERSION = '3.6.16' +VERSION = '3.6.17' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE