feat: card-specific ADF AID helper + priority default (v3.6.16)
- the ADF AID field gains a "From card" button: it reads the ADF.USIM FCI via /api/select and extracts the DF name (tag 84) with the new adfAidFromFci() - the AID is card-specific (the live card's is A0000000871002FF49FF0589, 12 bytes; the base A0000000871002 is only a prefix, and a shorter AID makes the card reject the access entry with 6A80). - the priority field defaults to 255 (0xFF) and is labelled "(0 = RFU)": TS 102 226 8.2.1.3.2.6 makes '00' RFU, which can break the toolkit registration; the builder defaults an unset priority to 0xFF and the chain rows follow. - stkParamsBuild's formatted payload fields are uppercase now (the rest of the tool's hex is; toString(16) emits lowercase). - tests: adfAidFromFci pinned to the live FCI, the From-card wiring, the updated form expectations. 643 frontend / 496 Python green; version 3.6.16; sw simple-v289.
This commit is contained in:
+80
-25
@@ -754,8 +754,8 @@
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-x-3 gap-y-2 text-sm">
|
||||
<div>
|
||||
<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="Priority">Priority</label>
|
||||
<input id="rc-tk-priority" oninput="updateStkParamsHex()" type="number" min="0" max="255" value="0" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1.5 dark:bg-slate-800">
|
||||
<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="Priority (0 = RFU)">Priority (0 = RFU)</label>
|
||||
<input id="rc-tk-priority" oninput="updateStkParamsHex()" type="number" min="0" max="255" value="255" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1.5 dark:bg-slate-800">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="Timers (max)">Timers (max)</label>
|
||||
@@ -824,7 +824,10 @@
|
||||
<span data-l10n="ADF.USIM access (full)">ADF.USIM access (full)</span>
|
||||
</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 class="flex items-center gap-2">
|
||||
<input id="rc-tk-adfaid" oninput="updateStkParamsHex()" value="A0000000871002" maxlength="32" class="font-mono flex-1 border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1 dark:bg-slate-800">
|
||||
<button type="button" id="rc-tk-adfaid-from-card" data-needs="card" onclick="ramAdfAidFromCard()" class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300 whitespace-nowrap disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="From card">From card</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1682,7 +1685,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.15';
|
||||
const SIMPLE_VERSION = '3.6.16';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== Tab switching =====
|
||||
@@ -2552,17 +2555,20 @@ function ramResetGrants() {
|
||||
// them with an ADF entry (AID from `v.adfAid`, default ADF.USIM); the SIM path
|
||||
// grants access via the CA Access Domain field instead.
|
||||
function stkParamsBuild(v) {
|
||||
const priority = parseInt(v.priority, 10) || 0;
|
||||
// Priority '00' is RFU (TS 102 226 8.2.1.3.2.6): an unset field defaults
|
||||
// to 255 (0xFF, lowest priority) instead of the invalid zero.
|
||||
const priority = (v.priority === '' || v.priority === undefined)
|
||||
? 0xFF : (parseInt(v.priority, 10) || 0);
|
||||
const timers = parseInt(v.timers, 10) || 0;
|
||||
const textLen = parseInt(v.textLen, 10) || 0;
|
||||
const menus = parseInt(v.menus, 10) || 0;
|
||||
const firstPos = parseInt(v.firstPos, 10) || 0;
|
||||
const firstId = (v.firstId || '00').replace(/[^0-9a-fA-F]/g, '').padStart(2, '0').slice(0, 2) || '00';
|
||||
const firstId = (v.firstId || '00').replace(/[^0-9a-fA-F]/g, '').toUpperCase().padStart(2, '0').slice(0, 2) || '00';
|
||||
const lastPos = parseInt(v.lastPos, 10) || 0;
|
||||
const lastId = (v.lastId || '00').replace(/[^0-9a-fA-F]/g, '').padStart(2, '0').slice(0, 2) || '00';
|
||||
const lastId = (v.lastId || '00').replace(/[^0-9a-fA-F]/g, '').toUpperCase().padStart(2, '0').slice(0, 2) || '00';
|
||||
const channels = parseInt(v.channels, 10) || 0;
|
||||
const msl = v.msl || '00';
|
||||
const tar = (v.tar || '').replace(/[^0-9a-fA-F]/g, '');
|
||||
const msl = (v.msl || '00').toUpperCase();
|
||||
const tar = (v.tar || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
|
||||
const adRaw = (v.ad || '').replace(/[^0-9a-fA-F]/g, '');
|
||||
const ad = adRaw.padStart(2, '0').slice(0, 2);
|
||||
const services = parseInt(v.services, 10) || 0;
|
||||
@@ -2570,33 +2576,33 @@ function stkParamsBuild(v) {
|
||||
if (parseInt(firstId, 16) > 0x7F || parseInt(lastId, 16) > 0x7F) return null;
|
||||
let menuPairs = '';
|
||||
for (let i = 1; i <= menus; i++) {
|
||||
if (i === 1) menuPairs += firstPos.toString(16).padStart(2, '0') + firstId;
|
||||
else if (i === menus) menuPairs += lastPos.toString(16).padStart(2, '0') + lastId;
|
||||
if (i === 1) menuPairs += firstPos.toString(16).padStart(2, '0').toUpperCase() + firstId;
|
||||
else if (i === menus) menuPairs += lastPos.toString(16).padStart(2, '0').toUpperCase() + lastId;
|
||||
else menuPairs += '0000';
|
||||
}
|
||||
const mslField = msl === '00' ? '00' : '0201' + msl;
|
||||
let tkPayload = '';
|
||||
if (v.mode === 'ca') {
|
||||
tkPayload = (adRaw ? '01' + ad : '00') +
|
||||
priority.toString(16).padStart(2, '0') +
|
||||
timers.toString(16).padStart(2, '0') +
|
||||
textLen.toString(16).padStart(2, '0') +
|
||||
menus.toString(16).padStart(2, '0') +
|
||||
priority.toString(16).padStart(2, '0').toUpperCase() +
|
||||
timers.toString(16).padStart(2, '0').toUpperCase() +
|
||||
textLen.toString(16).padStart(2, '0').toUpperCase() +
|
||||
menus.toString(16).padStart(2, '0').toUpperCase() +
|
||||
menuPairs +
|
||||
channels.toString(16).padStart(2, '0') +
|
||||
channels.toString(16).padStart(2, '0').toUpperCase() +
|
||||
mslField +
|
||||
(tar ? (tar.length / 2).toString(16).padStart(2, '0') + tar : '00');
|
||||
(tar ? (tar.length / 2).toString(16).padStart(2, '0').toUpperCase() + tar : '00');
|
||||
const caTlv = 'CA' + berLenStr(tkPayload.length / 2) + tkPayload;
|
||||
return 'EF' + berLenStr(caTlv.length / 2) + caTlv;
|
||||
}
|
||||
tkPayload = priority.toString(16).padStart(2, '0') +
|
||||
timers.toString(16).padStart(2, '0') +
|
||||
textLen.toString(16).padStart(2, '0') +
|
||||
menus.toString(16).padStart(2, '0') +
|
||||
tkPayload = priority.toString(16).padStart(2, '0').toUpperCase() +
|
||||
timers.toString(16).padStart(2, '0').toUpperCase() +
|
||||
textLen.toString(16).padStart(2, '0').toUpperCase() +
|
||||
menus.toString(16).padStart(2, '0').toUpperCase() +
|
||||
menuPairs +
|
||||
channels.toString(16).padStart(2, '0') +
|
||||
channels.toString(16).padStart(2, '0').toUpperCase() +
|
||||
mslField +
|
||||
(tar ? (tar.length / 2).toString(16).padStart(2, '0') + tar : '00') +
|
||||
(tar ? (tar.length / 2).toString(16).padStart(2, '0').toUpperCase() + tar : '00') +
|
||||
services.toString(16).padStart(2, '0');
|
||||
let eaValue = '80' + berLenStr(tkPayload.length / 2) + tkPayload;
|
||||
if (v.fsAccess) {
|
||||
@@ -2647,6 +2653,54 @@ function updateStkParamsHex() {
|
||||
if (el.dataset) delete el.dataset.manual;
|
||||
}
|
||||
|
||||
// The ADF.USIM AID is card-specific: the FCI's DF name (tag 84) is the full
|
||||
// 5..16-byte AID the access parameters need - the base A0000000871002 is only
|
||||
// a prefix, and a shorter AID makes the card reject the entry with 6A80
|
||||
// (live 2026-09-28: the card's DF name is A0000000871002FF49FF0589).
|
||||
function adfAidFromFci(fciHex) {
|
||||
const s = (fciHex || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
|
||||
let i = 0;
|
||||
if (s.length >= 4 && (s.startsWith('62') || s.startsWith('6F'))) {
|
||||
const n = parseInt(s.substr(2, 2), 16);
|
||||
i = 4 + (n > 0x80 ? (n & 0x7F) * 2 : 0); // skip the FCP/FCI template head
|
||||
}
|
||||
while (i + 4 <= s.length) {
|
||||
const tag = s.substr(i, 2);
|
||||
let n = parseInt(s.substr(i + 2, 2), 16);
|
||||
let j = i + 4;
|
||||
if (n > 0x80) {
|
||||
const nb = n & 0x7F;
|
||||
n = parseInt(s.substr(i + 4, nb * 2), 16);
|
||||
j = i + 4 + nb * 2;
|
||||
}
|
||||
if (tag === '84') return s.substr(j, n * 2).toUpperCase();
|
||||
i = j + n * 2;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// Fill the ADF AID from the card's ADF.USIM FCI (one click; the AID cannot be
|
||||
// known in advance - it carries a card-specific PIX).
|
||||
async function ramAdfAidFromCard() {
|
||||
const el = document.getElementById('rc-tk-adfaid');
|
||||
const btn = document.getElementById('rc-tk-adfaid-from-card');
|
||||
if (!el || !btn) return;
|
||||
const label = btn.textContent;
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const data = await pysimFetch('/api/select', { name: 'ADF.USIM' });
|
||||
const aid = adfAidFromFci(data && data.fci_hex);
|
||||
if (!aid) throw new Error('no DF name');
|
||||
el.value = aid;
|
||||
updateStkParamsHex();
|
||||
btn.textContent = t('OK');
|
||||
} catch (e) {
|
||||
btn.textContent = t('Failed');
|
||||
} finally {
|
||||
setTimeout(() => { btn.textContent = label; btn.disabled = false; }, 1500);
|
||||
}
|
||||
}
|
||||
|
||||
function updateInstallParamsHex() {
|
||||
document.getElementById('ram-install-params-hex').value = 'C900';
|
||||
}
|
||||
@@ -3446,7 +3500,7 @@ function chainRamToolkitHtml(chainId, idx, f, uf) {
|
||||
'SIM/UICC Toolkit parameters</label></div>';
|
||||
if (!enabled) return html;
|
||||
var tkMode = f.tkMode || 'ea';
|
||||
var priority = f.tkPriority || '0';
|
||||
var priority = f.tkPriority || '255';
|
||||
var timers = f.tkTimers || '0';
|
||||
var textLen = f.tkTextlen || '0';
|
||||
var menus = f.tkMenus || '0';
|
||||
@@ -3465,7 +3519,7 @@ function chainRamToolkitHtml(chainId, idx, f, uf) {
|
||||
'<option value="ca"' + (tkMode === 'ca' ? ' selected' : '') + '>SIM Toolkit (Tag CA)</option>' +
|
||||
'<option value="ea"' + (tkMode === 'ea' ? ' selected' : '') + '>UICC Toolkit (Tag EA)</option></select></div>' +
|
||||
'<div class="grid grid-cols-4 gap-x-2 gap-y-1.5 text-xs">' +
|
||||
'<div><label class="block text-gray-600 dark:text-slate-400 mb-0.5">Priority</label>' +
|
||||
'<div><label class="block text-gray-600 dark:text-slate-400 mb-0.5">Priority (0 = RFU)</label>' +
|
||||
'<input type="number" min="0" max="255" value="' + escHtml(priority) + '" oninput="' + uf('tkPriority') + '" class="w-full font-mono border border-gray-300 dark:border-slate-600 rounded px-1.5 py-0.5 dark:bg-slate-800"></div>' +
|
||||
'<div><label class="block text-gray-600 dark:text-slate-400 mb-0.5">Timers</label>' +
|
||||
'<input type="number" min="0" max="255" value="' + escHtml(timers) + '" oninput="' + uf('tkTimers') + '" class="w-full font-mono border border-gray-300 dark:border-slate-600 rounded px-1.5 py-0.5 dark:bg-slate-800"></div>' +
|
||||
@@ -17237,6 +17291,7 @@ const LANG_RU = {
|
||||
'pending': 'ожидает',
|
||||
'Name is required': 'Укажите имя',
|
||||
'From card': 'С карты',
|
||||
'Priority (0 = RFU)': 'Приоритет (0 = RFU)',
|
||||
'SCP80 (GSM 03.48, ETSI TS 102 225)': 'SCP80 (GSM 03.48, ETSI TS 102 225)',
|
||||
'SCP81 (HTTP OTA)': 'SCP81 (HTTP OTA)',
|
||||
'Start': 'Запустить',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v288';
|
||||
const CACHE = 'simple-v289';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -23,7 +23,7 @@ function extractFunc(src, name) {
|
||||
|
||||
let code = '';
|
||||
for (const fn of ['berLenStr', 'stkParamsBuild', 'buildRcToolkitParams',
|
||||
'updateStkParamsHex']) code += extractFunc(html, fn) + '\n';
|
||||
'updateStkParamsHex', 'adfAidFromFci']) code += extractFunc(html, fn) + '\n';
|
||||
eval(code);
|
||||
|
||||
const base = { mode: 'ea', priority: '0', timers: '0', textLen: '0', menus: '0',
|
||||
@@ -114,6 +114,27 @@ test('the applet TAR field has no B00001 default or placeholder', () => {
|
||||
assert.ok(!html.includes("f.tkTar || 'B00001'"), 'the chain toolkit default must stay empty');
|
||||
});
|
||||
|
||||
test('adfAidFromFci extracts the DF name (tag 84) from an FCI', () => {
|
||||
// the live ADF.USIM FCI (read from the card, 2026-09-28): the DF name is
|
||||
// 12 bytes - the base A0000000871002 is only a prefix, and a shorter AID
|
||||
// makes the card reject the access entry with 6A80.
|
||||
assert.strictEqual(
|
||||
adfAidFromFci('622882027821840CA0000000871002FF49FF05898A01058B032F0617C60C90016083010183010A830181'),
|
||||
'A0000000871002FF49FF0589');
|
||||
// an FCI without a DF name, and garbage
|
||||
assert.strictEqual(adfAidFromFci('6207820278218A0105'), '');
|
||||
assert.strictEqual(adfAidFromFci(''), '');
|
||||
assert.strictEqual(adfAidFromFci('ZZZZ'), '');
|
||||
});
|
||||
|
||||
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');
|
||||
assert.ok(/async function ramAdfAidFromCard\(\)/.test(html), 'ramAdfAidFromCard is missing');
|
||||
assert.ok(/pysimFetch\('\/api\/select', \{ name: 'ADF.USIM' \}\)/.test(html),
|
||||
'the button must read the ADF.USIM FCI');
|
||||
});
|
||||
|
||||
test('the RAM install recomputes the STK hex at send time unless hand-edited', () => {
|
||||
assert.ok(/oninput="this\.dataset\.manual='1'"/.test(html),
|
||||
'the hex field must flag manual edits');
|
||||
@@ -146,7 +167,7 @@ test('the RAM form fields build the live install parameters end to end', () => {
|
||||
// carried TAR B00001 / MSL 16 / channels 0 and the card answered 6A80.
|
||||
fakeForm({ 'rc-toolkit-enable': true, 'rc-tk-mode': 'ea', 'rc-tk-msl': '12',
|
||||
'rc-tk-tar': 'AF4D01', 'rc-tk-channels': '1' });
|
||||
assert.strictEqual(buildRcToolkitParams(), 'EA0F800D000000000102011203AF4D0100');
|
||||
assert.strictEqual(buildRcToolkitParams(), 'EA0F800DFF0000000102011203AF4D0100');
|
||||
});
|
||||
|
||||
test('UICC file-access parameters (82) are appended in EA mode', () => {
|
||||
@@ -182,12 +203,12 @@ 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(),
|
||||
'EA15800D000000000102011203AF4D0100810400010000');
|
||||
'EA15800DFF0000000102011203AF4D0100810400010000');
|
||||
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(),
|
||||
'EA20800D000000000102011203AF4D0100810F0001000007A0000000871002010000');
|
||||
'EA20800DFF0000000102011203AF4D0100810F0001000007A0000000871002010000');
|
||||
});
|
||||
|
||||
test('updateStkParamsHex refreshes the field and clears the manual flag', () => {
|
||||
|
||||
Reference in New Issue
Block a user