feat: C9 + memory-quota inputs, STK rejection hint (v3.6.17)

The vendor's working install script (samples/uicc/applets/STK3) sends
`C9 22 <config> EF 08 C7 02 0000 C8 02 0000 EA 26 80 0A ... 81 18
<access>` - an applet config in C9 and memory quotas in EF.  The RAM install
form now composes that prefix:

- "Applet-specific parameters (C9, hex)" -> `C9 <len> <value>`;
- "C7 / C8 memory quotas (bytes)" -> `EF <len> C7/C8 <2-byte int>` (4-byte
  above 32767, GP 9.7);
- the prefix is prepended to the raw "Install parameters (hex)" field and
  the server appends the STK part, giving `C9 EF EA`;
- `ramParamsPrefix`/`ramQuotaValue` are pure and tested (incl. the vendor's
  `EF 08 C7 02 0000 C8 02 0000` form).

Also: `updateStkParamsHex` shows a hint naming the rejection reason (TAR not
a multiple of 3 bytes / menu ID > 7F / ADF AID not 5-16 bytes) instead of
silently emptying the field, and the UICC_SPECS.md install row notes that
`CA` sits inside `EF` and `CA`+`EA` together return 6A80.

647 frontend / 496 Python green; version 3.6.17; sw simple-v290.
This commit is contained in:
2026-09-28 07:54:35 +03:00
parent 9a3174a324
commit 6403fb9f29
5 changed files with 115 additions and 10 deletions
+67 -4
View File
@@ -832,10 +832,24 @@
</div>
</div>
</div>
<label class="block mb-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="Install parameters (hex, optional)">Install parameters (hex, optional)</label>
<div class="grid grid-cols-2 gap-x-3 gap-y-2 text-sm mt-2">
<div>
<label class="block mb-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="Applet-specific parameters (C9, hex, optional)">Applet-specific parameters (C9, hex, optional)</label>
<input id="rc-c9" oninput="updateInstallParamsHex()" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1 dark:bg-slate-800" placeholder="empty = C9 00">
</div>
<div>
<label class="block mb-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="C7 / C8 memory quotas (bytes, optional)">C7 / C8 memory quotas (bytes, optional)</label>
<div class="flex items-center gap-2">
<input id="rc-quota-c7" oninput="updateInstallParamsHex()" type="number" min="0" placeholder="C7" class="font-mono w-24 border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1 dark:bg-slate-800">
<input id="rc-quota-c8" oninput="updateInstallParamsHex()" type="number" min="0" placeholder="C8" class="font-mono w-24 border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1 dark:bg-slate-800">
</div>
</div>
</div>
<label class="block mb-1 mt-2 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="Install parameters (hex, optional)">Install parameters (hex, optional)</label>
<input id="ram-install-params-hex" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="C9 TLV">
<label class="block mb-1 text-xs font-medium text-gray-500 dark:text-slate-400 mt-2" data-l10n="STK parameters (hex, optional)">STK parameters (hex, optional)</label>
<input id="ram-stk-params" oninput="this.dataset.manual='1'" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="EA / CA TLV">
<div id="ram-stk-hint" class="hidden text-xs text-red-600 dark:text-red-400 mt-1"></div>
<label class="flex items-center gap-2 mt-2">
<input type="checkbox" id="ram-make-sel" checked> <span class="text-sm" data-l10n="Make selectable">Make selectable</span>
</label>
@@ -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 <applet-specific parameters>` + `EF <C7/C8 memory quotas>`.
// 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;
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v289';
const CACHE = 'simple-v290';
const URLS = [
'index.html',
'help.html',
+45 -3
View File
@@ -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 <config> 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;
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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