fix: send the STK install parameters as entered (v3.6.9)

The RAM install form sent the `STK parameters (hex)` field, which was only
regenerated when the toolkit checkbox or the mode select changed - editing
the TAR (or any other toolkit field) afterwards was silently dropped.  The
live install therefore carried the form defaults (TAR B00001, textLen 0,
menus 0, MSL 16, channels 0) instead of the entered AF4D01 / MSL 12 /
channels 1, and the card rejected the install parameters with 6A80
(TS 102 226 8.2.1.3.2.7: a TAR already assigned on the card).

- every `rc-tk-*` field regenerates the hex on input/change; the install
  recomputes it at send time unless the user hand-edited the hex
  (`dataset.manual`), so the edit order can no longer drop values;
- one pure `stkParamsBuild` serves the RAM form (`buildRcToolkitParams`) and
  the RAM/GP chain rows (`buildTkParams`) - no drift between them (the chain
  row also gains the menu-ID <= 7F check);
- applet TAR field: empty by default, no placeholder - B0 00 01 is the
  allocated ADF RFM TAR (TS 101 220 Annex D), not an applet TAR; an empty
  field is coded as TAR length 00 (the card may take a TAR from the AID only
  when the AID carries one, PIX hex digits 15-20);
- the chain builder's INSTALL/LOAD rows drop the trailing Le (the v3.6.8
  server form; a trailing Le made the card execute a phantom command);
- tests: stk_params.test.js (the decrypted live parameters, empty-TAR
  coding, CA wrapper, long-form lengths, rejection cases, form wiring, and
  the form -> hex path with the real builders) and ram_counter_flow.test.js
  (the Explore delete persists the consumed counter through the real
  ramSaveCntr); STK fixtures no longer use B00001.

631 frontend / 496 Python green; version 3.6.9; sw simple-v282.
This commit is contained in:
2026-09-28 01:48:51 +03:00
parent 29864617eb
commit 9fb2ad5d2b
8 changed files with 378 additions and 124 deletions
+86 -96
View File
@@ -744,7 +744,7 @@
<div id="rc-toolkit-body" class="hidden"> <div id="rc-toolkit-body" class="hidden">
<div class="mb-3"> <div class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Toolkit mode">Toolkit mode</label> <label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Toolkit mode">Toolkit mode</label>
<select id="rc-tk-mode" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800"> <select id="rc-tk-mode" onchange="updateRcTkMode();updateStkParamsHex()" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="ca">SIM Toolkit (Tag CA)</option> <option value="ca">SIM Toolkit (Tag CA)</option>
<option value="ea">UICC Toolkit (Tag EA)</option> <option value="ea">UICC Toolkit (Tag EA)</option>
</select> </select>
@@ -752,43 +752,43 @@
<div class="grid grid-cols-2 gap-x-3 gap-y-2 text-sm"> <div class="grid grid-cols-2 gap-x-3 gap-y-2 text-sm">
<div> <div>
<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="Priority">Priority</label> <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" 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"> <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">
</div> </div>
<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> <label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="Timers (max)">Timers (max)</label>
<input id="rc-tk-timers" 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"> <input id="rc-tk-timers" 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">
</div> </div>
<div> <div>
<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="Menu text length">Menu text length</label> <label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="Menu text length">Menu text length</label>
<input id="rc-tk-textlen" 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"> <input id="rc-tk-textlen" 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">
</div> </div>
<div> <div>
<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="Menu items">Menu items</label> <label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="Menu items">Menu items</label>
<input id="rc-tk-menus" 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"> <input id="rc-tk-menus" 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">
</div> </div>
<div> <div>
<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="First menu position">First menu position</label> <label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="First menu position">First menu position</label>
<input id="rc-tk-firstpos" 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"> <input id="rc-tk-firstpos" 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">
</div> </div>
<div> <div>
<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="First menu ID (hex)">First menu ID (hex)</label> <label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="First menu ID (hex)">First menu ID (hex)</label>
<input id="rc-tk-firstid" 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" placeholder="00" maxlength="2" value="00"> <input id="rc-tk-firstid" oninput="updateStkParamsHex()" 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" placeholder="00" maxlength="2" value="00">
</div> </div>
<div> <div>
<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="Last menu position">Last menu position</label> <label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="Last menu position">Last menu position</label>
<input id="rc-tk-lastpos" 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"> <input id="rc-tk-lastpos" 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">
</div> </div>
<div> <div>
<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="Last menu ID (hex)">Last menu ID (hex)</label> <label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="Last menu ID (hex)">Last menu ID (hex)</label>
<input id="rc-tk-lastid" 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" placeholder="00" maxlength="2" value="00"> <input id="rc-tk-lastid" oninput="updateStkParamsHex()" 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" placeholder="00" maxlength="2" value="00">
</div> </div>
<div> <div>
<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="Channels (max)">Channels (max)</label> <label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="Channels (max)">Channels (max)</label>
<input id="rc-tk-channels" 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"> <input id="rc-tk-channels" 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">
</div> </div>
<div> <div>
<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300">MSL (SPI1, hex)</label> <label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300">MSL (SPI1, hex)</label>
<select id="rc-tk-msl" class="w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1.5 dark:bg-slate-800"> <select id="rc-tk-msl" onchange="updateStkParamsHex()" class="w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1.5 dark:bg-slate-800">
<option value="00">00 — no verification</option> <option value="00">00 — no verification</option>
<option value="11">11 — RC/CC/DS</option> <option value="11">11 — RC/CC/DS</option>
<option value="12">12 — RC/DS/CC</option> <option value="12">12 — RC/DS/CC</option>
@@ -799,15 +799,15 @@
</div> </div>
<div> <div>
<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300">TAR (3 bytes)</label> <label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300">TAR (3 bytes)</label>
<input id="rc-tk-tar" 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" placeholder="B00001" maxlength="6" value="B00001"> <input id="rc-tk-tar" oninput="updateStkParamsHex()" 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" maxlength="6">
</div> </div>
<div id="rc-tk-ad-row"> <div id="rc-tk-ad-row">
<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="Access domain (hex)">Access domain (hex)</label> <label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="Access domain (hex)">Access domain (hex)</label>
<input id="rc-tk-ad" 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" placeholder="00" maxlength="2" value="00"> <input id="rc-tk-ad" oninput="updateStkParamsHex()" 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" placeholder="00" maxlength="2" value="00">
</div> </div>
<div id="rc-tk-services-row"> <div id="rc-tk-services-row">
<label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="Max services">Max services</label> <label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300" data-l10n="Max services">Max services</label>
<input id="rc-tk-services" 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"> <input id="rc-tk-services" 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">
</div> </div>
</div> </div>
</div> </div>
@@ -815,7 +815,7 @@
<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> <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>
<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"> <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> <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" 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="CA TLV"> <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">
<label class="flex items-center gap-2 mt-2"> <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> <input type="checkbox" id="ram-make-sel" checked> <span class="text-sm" data-l10n="Make selectable">Make selectable</span>
</label> </label>
@@ -1665,7 +1665,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.8'; const SIMPLE_VERSION = '3.6.9';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching ===== // ===== Tab switching =====
@@ -2503,40 +2503,39 @@ function updateRcTkMode() {
document.getElementById('rc-tk-ad-row').style.display = isSim ? '' : 'none'; document.getElementById('rc-tk-ad-row').style.display = isSim ? '' : 'none';
document.getElementById('rc-tk-services-row').style.display = isSim ? 'none' : ''; document.getElementById('rc-tk-services-row').style.display = isSim ? 'none' : '';
} }
document.getElementById('rc-tk-mode').addEventListener('change', () => { updateRcTkMode(); updateStkParamsHex(); });
function buildRcToolkitParams() { // Pure SIM/UICC toolkit install-parameter builder shared by the RAM install
if (!document.getElementById('rc-toolkit-enable').checked) return ''; // form (buildRcToolkitParams) and the RAM/GP chain rows (buildTkParams).
const tkMode = document.getElementById('rc-tk-mode').value; // `v` carries the field values; returns the CA/EA TLV hex, or null when a
const priority = parseInt(document.getElementById('rc-tk-priority').value) || 0; // value cannot be coded: each TAR is 3 bytes (6 hex digits), and menu item
const timers = parseInt(document.getElementById('rc-tk-timers').value) || 0; // identifiers 128..255 are reserved for the toolkit framework, so only
const textLen = parseInt(document.getElementById('rc-tk-textlen').value) || 0; // 01..7F may be requested (TS 102 226 8.2.1.3.2.3).
const menus = parseInt(document.getElementById('rc-tk-menus').value) || 0; function stkParamsBuild(v) {
const firstPos = parseInt(document.getElementById('rc-tk-firstpos').value) || 0; const priority = parseInt(v.priority, 10) || 0;
const firstId = (document.getElementById('rc-tk-firstid').value || '00').replace(/[^0-9a-fA-F]/g, '').padStart(2, '0').slice(0, 2) || '00'; const timers = parseInt(v.timers, 10) || 0;
const lastPos = parseInt(document.getElementById('rc-tk-lastpos').value) || 0; const textLen = parseInt(v.textLen, 10) || 0;
const lastId = (document.getElementById('rc-tk-lastid').value || '00').replace(/[^0-9a-fA-F]/g, '').padStart(2, '0').slice(0, 2) || '00'; const menus = parseInt(v.menus, 10) || 0;
const channels = parseInt(document.getElementById('rc-tk-channels').value) || 0; const firstPos = parseInt(v.firstPos, 10) || 0;
const msl = document.getElementById('rc-tk-msl').value; const firstId = (v.firstId || '00').replace(/[^0-9a-fA-F]/g, '').padStart(2, '0').slice(0, 2) || '00';
const tar = (document.getElementById('rc-tk-tar').value || '').replace(/[^0-9a-fA-F]/g, ''); const lastPos = parseInt(v.lastPos, 10) || 0;
const adRaw = (document.getElementById('rc-tk-ad').value || '').replace(/[^0-9a-fA-F]/g, ''); const lastId = (v.lastId || '00').replace(/[^0-9a-fA-F]/g, '').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 adRaw = (v.ad || '').replace(/[^0-9a-fA-F]/g, '');
const ad = adRaw.padStart(2, '0').slice(0, 2); const ad = adRaw.padStart(2, '0').slice(0, 2);
const services = parseInt(document.getElementById('rc-tk-services').value) || 0; const services = parseInt(v.services, 10) || 0;
if (tar && tar.length % 6 !== 0) return null; if (tar && tar.length % 6 !== 0) return null;
if (parseInt(firstId, 16) > 0x7F || parseInt(lastId, 16) > 0x7F) return null; if (parseInt(firstId, 16) > 0x7F || parseInt(lastId, 16) > 0x7F) return null;
let menuPairs = ''; let menuPairs = '';
for (let i = 1; i <= menus; i++) { for (let i = 1; i <= menus; i++) {
if (i === 1) menuPairs += firstPos.toString(16).padStart(2, '0') + firstId; if (i === 1) menuPairs += firstPos.toString(16).padStart(2, '0') + firstId;
else if (i === menus) menuPairs += lastPos.toString(16).padStart(2, '0') + lastId; else if (i === menus) menuPairs += lastPos.toString(16).padStart(2, '0') + lastId;
else menuPairs += '0000'; else menuPairs += '0000';
} }
const mslField = msl === '00' ? '00' : '0201' + msl; const mslField = msl === '00' ? '00' : '0201' + msl;
let tkPayload = ''; let tkPayload = '';
if (tkMode === 'ca') { if (v.mode === 'ca') {
tkPayload = (adRaw ? '01' + ad : '00') + tkPayload = (adRaw ? '01' + ad : '00') +
priority.toString(16).padStart(2, '0') + priority.toString(16).padStart(2, '0') +
timers.toString(16).padStart(2, '0') + timers.toString(16).padStart(2, '0') +
@@ -2548,7 +2547,7 @@ function buildRcToolkitParams() {
(tar ? (tar.length / 2).toString(16).padStart(2, '0') + tar : '00'); (tar ? (tar.length / 2).toString(16).padStart(2, '0') + tar : '00');
const caTlv = 'CA' + berLenStr(tkPayload.length / 2) + tkPayload; const caTlv = 'CA' + berLenStr(tkPayload.length / 2) + tkPayload;
return 'EF' + berLenStr(caTlv.length / 2) + caTlv; return 'EF' + berLenStr(caTlv.length / 2) + caTlv;
} else { }
tkPayload = priority.toString(16).padStart(2, '0') + tkPayload = priority.toString(16).padStart(2, '0') +
timers.toString(16).padStart(2, '0') + timers.toString(16).padStart(2, '0') +
textLen.toString(16).padStart(2, '0') + textLen.toString(16).padStart(2, '0') +
@@ -2560,12 +2559,30 @@ function buildRcToolkitParams() {
services.toString(16).padStart(2, '0'); services.toString(16).padStart(2, '0');
const innerTlv = '80' + berLenStr(tkPayload.length / 2) + tkPayload; const innerTlv = '80' + berLenStr(tkPayload.length / 2) + tkPayload;
return 'EA' + berLenStr(innerTlv.length / 2) + innerTlv; return 'EA' + berLenStr(innerTlv.length / 2) + innerTlv;
} }
// The RAM install form's toolkit fields -> the install parameters hex.
function buildRcToolkitParams() {
if (!document.getElementById('rc-toolkit-enable').checked) return '';
const g = id => document.getElementById(id).value;
return stkParamsBuild({
mode: g('rc-tk-mode'),
priority: g('rc-tk-priority'), timers: g('rc-tk-timers'),
textLen: g('rc-tk-textlen'), menus: g('rc-tk-menus'),
firstPos: g('rc-tk-firstpos'), firstId: g('rc-tk-firstid'),
lastPos: g('rc-tk-lastpos'), lastId: g('rc-tk-lastid'),
channels: g('rc-tk-channels'), msl: g('rc-tk-msl'),
tar: g('rc-tk-tar'), ad: g('rc-tk-ad'), services: g('rc-tk-services'),
});
} }
function updateStkParamsHex() { function updateStkParamsHex() {
const tk = buildRcToolkitParams(); const tk = buildRcToolkitParams();
document.getElementById('ram-stk-params').value = tk || ''; const el = document.getElementById('ram-stk-params');
el.value = tk || '';
// 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;
} }
function updateInstallParamsHex() { function updateInstallParamsHex() {
@@ -3377,7 +3394,7 @@ function chainRamToolkitHtml(chainId, idx, f, uf) {
var lastId = f.tkLastid || '00'; var lastId = f.tkLastid || '00';
var channels = f.tkChannels || '0'; var channels = f.tkChannels || '0';
var msl = f.tkMsl || '16'; var msl = f.tkMsl || '16';
var tar = f.tkTar || 'B00001'; var tar = f.tkTar || '';
var ad = f.tkAd || ''; var ad = f.tkAd || '';
var services = f.tkServices || '0'; var services = f.tkServices || '0';
html += '<div class="p-2 bg-gray-50 dark:bg-slate-900 rounded border border-gray-200 dark:border-slate-700">' + html += '<div class="p-2 bg-gray-50 dark:bg-slate-900 rounded border border-gray-200 dark:border-slate-700">' +
@@ -4028,57 +4045,22 @@ function chainRamBuildRowHex(idx, row) {
if (b3) r += b3.toString(16).padStart(2, '0').toUpperCase(); if (b3) r += b3.toString(16).padStart(2, '0').toUpperCase();
return r; return r;
} }
// The chain row's toolkit fields -> the install parameters hex.
function buildTkParams() { function buildTkParams() {
if (!f.tkEnabled) return ''; if (!f.tkEnabled) return '';
var tkMode = f.tkMode || 'ea'; return stkParamsBuild({
var priority = parseInt(f.tkPriority) || 0; mode: f.tkMode || 'ea',
var timers = parseInt(f.tkTimers) || 0; priority: f.tkPriority, timers: f.tkTimers, textLen: f.tkTextlen,
var textLen = parseInt(f.tkTextlen) || 0; menus: f.tkMenus, firstPos: f.tkFirstpos, firstId: f.tkFirstid,
var menus = parseInt(f.tkMenus) || 0; lastPos: f.tkLastpos, lastId: f.tkLastid, channels: f.tkChannels,
var firstPos = parseInt(f.tkFirstpos) || 0; msl: f.tkMsl || '16', tar: f.tkTar, ad: f.tkAd,
var firstId = (f.tkFirstid || '00').replace(/[^0-9a-fA-F]/g, '').padStart(2, '0').slice(0, 2) || '00'; services: f.tkServices,
var lastPos = parseInt(f.tkLastpos) || 0; });
var lastId = (f.tkLastid || '00').replace(/[^0-9a-fA-F]/g, '').padStart(2, '0').slice(0, 2) || '00';
var channels = parseInt(f.tkChannels) || 0;
var msl = f.tkMsl || '00';
var tar = (f.tkTar || '').replace(/[^0-9a-fA-F]/g, '');
var ad = (f.tkAd || '').replace(/[^0-9a-fA-F]/g, '').padStart(2, '0').slice(0, 2);
var services = parseInt(f.tkServices) || 0;
if (tar && tar.length % 6 !== 0) return null;
var menuPairs = '';
for (var 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;
else menuPairs += '0000';
}
var mslField = msl === '00' ? '00' : '0201' + msl;
var tkPayload = '';
if (tkMode === 'ca') {
tkPayload = (f.tkAd ? '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') +
menuPairs +
channels.toString(16).padStart(2, '0') +
mslField +
(tar ? (tar.length / 2).toString(16).padStart(2, '0') + tar : '00');
var caTlv = 'CA' + berLenStr(tkPayload.length / 2) + tkPayload;
return 'EF' + berLenStr(caTlv.length / 2) + caTlv;
} else {
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') +
menuPairs +
channels.toString(16).padStart(2, '0') +
mslField +
(tar ? (tar.length / 2).toString(16).padStart(2, '0') + tar : '00') +
services.toString(16).padStart(2, '0');
var innerTlv = '80' + berLenStr(tkPayload.length / 2) + tkPayload;
return 'EA' + berLenStr(innerTlv.length / 2) + innerTlv;
}
} }
// INSTALL/LOAD are case-3 commands (no trailing Le), matching the
// reference terminal form and the server's _cap_apdu_sequence: a trailing
// Le made the card execute a phantom second command whose status word
// masked the real result (v3.6.8). DELETE/GET STATUS/GET DATA keep Le.
var INSTALL_P1 = { var INSTALL_P1 = {
'install-load': 0x02, 'install-install': 0x0C, 'install-load': 0x02, 'install-install': 0x0C,
'install-make-sel': 0x08, 'install-reg-update': 0x40, 'install-make-sel': 0x08, 'install-reg-update': 0x40,
@@ -4092,7 +4074,7 @@ function chainRamBuildRowHex(idx, row) {
if (!loadFileAid) return ''; if (!loadFileAid) return '';
var dataField = lv(loadFileAid) + lv(sdAid) + '00' + (loadData ? lv(loadData) : '00') + '00'; var dataField = lv(loadFileAid) + lv(sdAid) + '00' + (loadData ? lv(loadData) : '00') + '00';
var dataLen = berLenStr(dataField.length / 2); var dataLen = berLenStr(dataField.length / 2);
return buildApdu(0x80, 0xE6, INSTALL_P1[cmd], 0x00, parseInt(dataLen, 16), dataField) + '00'; return buildApdu(0x80, 0xE6, INSTALL_P1[cmd], 0x00, parseInt(dataLen, 16), dataField);
} }
if (cmd === 'install-install') { if (cmd === 'install-install') {
var elfAid = (f.elfAid || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); var elfAid = (f.elfAid || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
@@ -4105,35 +4087,35 @@ function chainRamBuildRowHex(idx, row) {
if (tkParams) installParams += tkParams; if (tkParams) installParams += tkParams;
var df2 = lv(elfAid) + lv(modAid) + lv(aid) + lv(privBytes) + lv(installParams) + '00'; var df2 = lv(elfAid) + lv(modAid) + lv(aid) + lv(privBytes) + lv(installParams) + '00';
var dl2 = berLenStr(df2.length / 2); var dl2 = berLenStr(df2.length / 2);
return buildApdu(0x80, 0xE6, INSTALL_P1[cmd], 0x00, parseInt(dl2, 16), df2) + '00'; return buildApdu(0x80, 0xE6, INSTALL_P1[cmd], 0x00, parseInt(dl2, 16), df2);
} }
if (cmd === 'install-make-sel') { if (cmd === 'install-make-sel') {
if (!aid) return ''; if (!aid) return '';
var privBytes2 = computePriv(); var privBytes2 = computePriv();
var df3 = '00' + '00' + lv(aid) + lv(privBytes2) + '00' + '00'; var df3 = '00' + '00' + lv(aid) + lv(privBytes2) + '00' + '00';
var dl3 = berLenStr(df3.length / 2); var dl3 = berLenStr(df3.length / 2);
return buildApdu(0x80, 0xE6, INSTALL_P1[cmd], 0x00, parseInt(dl3, 16), df3) + '00'; return buildApdu(0x80, 0xE6, INSTALL_P1[cmd], 0x00, parseInt(dl3, 16), df3);
} }
if (cmd === 'install-reg-update') { if (cmd === 'install-reg-update') {
if (!aid) return ''; if (!aid) return '';
var privBytes3 = computePriv(); var privBytes3 = computePriv();
var df4 = lv(sdAid) + '00' + lv(aid) + lv(privBytes3) + '00' + '00'; var df4 = lv(sdAid) + '00' + lv(aid) + lv(privBytes3) + '00' + '00';
var dl4 = berLenStr(df4.length / 2); var dl4 = berLenStr(df4.length / 2);
return buildApdu(0x80, 0xE6, INSTALL_P1[cmd], 0x00, parseInt(dl4, 16), df4) + '00'; return buildApdu(0x80, 0xE6, INSTALL_P1[cmd], 0x00, parseInt(dl4, 16), df4);
} }
if (cmd === 'install-extradition') { if (cmd === 'install-extradition') {
if (!sdAid || sdAid.length < 10 || sdAid.length > 20) return ''; if (!sdAid || sdAid.length < 10 || sdAid.length > 20) return '';
if (!aid) return ''; if (!aid) return '';
var df5 = lv(sdAid) + '00' + lv(aid) + '00' + '00' + '00'; var df5 = lv(sdAid) + '00' + lv(aid) + '00' + '00' + '00';
var dl5 = berLenStr(df5.length / 2); var dl5 = berLenStr(df5.length / 2);
return buildApdu(0x80, 0xE6, INSTALL_P1[cmd], 0x00, parseInt(dl5, 16), df5) + '00'; return buildApdu(0x80, 0xE6, INSTALL_P1[cmd], 0x00, parseInt(dl5, 16), df5);
} }
if (cmd === 'load') { if (cmd === 'load') {
var ramData = (f.data || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); var ramData = (f.data || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
var blockNum = parseInt(f.block) || 0; var blockNum = parseInt(f.block) || 0;
if (!ramData) return ''; if (!ramData) return '';
var dl6 = berLenStr(ramData.length / 2); var dl6 = berLenStr(ramData.length / 2);
return buildApdu(0x80, 0xE8, 0x80, blockNum, parseInt(dl6, 16), ramData) + '00'; return buildApdu(0x80, 0xE8, 0x80, blockNum, parseInt(dl6, 16), ramData);
} }
if (cmd === 'delete') { if (cmd === 'delete') {
if (!aid) return ''; if (!aid) return '';
@@ -8931,6 +8913,14 @@ async function ramInstallCap(sp) {
const capHex = await ramReadFileHex(file); const capHex = await ramReadFileHex(file);
ramShowProgress(t('Sending to server for install...')); ramShowProgress(t('Sending to server for install...'));
// The STK hex is regenerated on every toolkit field edit; recompute it
// once more at send time unless the user pasted a custom hex directly
// (dataset.manual), so the sent parameters always match the fields.
const stkEl = document.getElementById('ram-stk-params');
if (document.getElementById('rc-toolkit-enable').checked && !(stkEl.dataset && stkEl.dataset.manual)) {
updateStkParamsHex();
}
const body = { const body = {
cap_hex: capHex, cap_hex: capHex,
sd_aid: (document.getElementById('ram-sd-aid').value || '').replace(/[^0-9a-fA-F]/g, ''), sd_aid: (document.getElementById('ram-sd-aid').value || '').replace(/[^0-9a-fA-F]/g, ''),
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v281'; const CACHE = 'simple-v282';
const URLS = [ const URLS = [
'index.html', 'index.html',
'help.html', 'help.html',
+14 -14
View File
@@ -23,7 +23,7 @@ function extractFunc(src, name) {
// Extract chain builder functions and dependencies // Extract chain builder functions and dependencies
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramStepLine', 'ramGetStatusApdu', 'ramDeleteApdu', const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramStepLine', 'ramGetStatusApdu', 'ramDeleteApdu',
'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer',
'_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'parseTLV', '_parseE3Entry', '_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'parseTLV', '_parseE3Entry',
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute',
'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml']; 'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml'];
@@ -79,7 +79,7 @@ function genRamApdu() {
tkLastid: '02', tkLastid: '02',
tkChannels: '0', tkChannels: '0',
tkMsl: '16', tkMsl: '16',
tkTar: 'B00001', tkTar: 'AF4D01',
tkAd: '', tkAd: '',
tkServices: '0', tkServices: '0',
}); });
@@ -87,7 +87,7 @@ function genRamApdu() {
test('UICC toolkit nested inside EA (m=2, services 0)', () => { test('UICC toolkit nested inside EA (m=2, services 0)', () => {
const apdu = genRamApdu(); const apdu = genRamApdu();
assert.ok(apdu.includes('EA13801100000002010102020002011603B0000100'), apdu); assert.ok(apdu.includes('EA13801100000002010102020002011603AF4D0100'), apdu);
}); });
test('UICC m=1 emits single pair', () => { test('UICC m=1 emits single pair', () => {
@@ -95,9 +95,9 @@ test('UICC m=1 emits single pair', () => {
cmd: 'install-install', aid: 'A000000151000000', priv: '00', cmd: 'install-install', aid: 'A000000151000000', priv: '00',
tkEnabled: true, tkMode: 'ea', tkPriority: '0', tkTimers: '0', tkTextlen: '0', tkEnabled: true, tkMode: 'ea', tkPriority: '0', tkTimers: '0', tkTextlen: '0',
tkMenus: '1', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '0', tkLastid: '00', tkMenus: '1', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '0', tkLastid: '00',
tkChannels: '0', tkMsl: '16', tkTar: 'B00001', tkAd: '', tkServices: '0', tkChannels: '0', tkMsl: '16', tkTar: 'AF4D01', tkAd: '', tkServices: '0',
}); });
assert.ok(apdu.includes('EA11800F0000000101010002011603B0000100'), apdu); assert.ok(apdu.includes('EA11800F0000000101010002011603AF4D0100'), apdu);
}); });
test('UICC m=3 fills middle pair with 0000', () => { test('UICC m=3 fills middle pair with 0000', () => {
@@ -105,9 +105,9 @@ test('UICC m=3 fills middle pair with 0000', () => {
cmd: 'install-install', aid: 'A000000151000000', priv: '00', cmd: 'install-install', aid: 'A000000151000000', priv: '00',
tkEnabled: true, tkMode: 'ea', tkPriority: '0', tkTimers: '0', tkTextlen: '0', tkEnabled: true, tkMode: 'ea', tkPriority: '0', tkTimers: '0', tkTextlen: '0',
tkMenus: '3', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '3', tkLastid: '03', tkMenus: '3', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '3', tkLastid: '03',
tkChannels: '0', tkMsl: '16', tkTar: 'B00001', tkAd: '', tkServices: '0', tkChannels: '0', tkMsl: '16', tkTar: 'AF4D01', tkAd: '', tkServices: '0',
}); });
assert.ok(apdu.includes('EA158013000000030101000003030002011603B0000100'), apdu); assert.ok(apdu.includes('EA158013000000030101000003030002011603AF4D0100'), apdu);
}); });
test('UICC services 7 appended as final byte', () => { test('UICC services 7 appended as final byte', () => {
@@ -115,9 +115,9 @@ test('UICC services 7 appended as final byte', () => {
cmd: 'install-install', aid: 'A000000151000000', priv: '00', cmd: 'install-install', aid: 'A000000151000000', priv: '00',
tkEnabled: true, tkMode: 'ea', tkPriority: '0', tkTimers: '0', tkTextlen: '0', tkEnabled: true, tkMode: 'ea', tkPriority: '0', tkTimers: '0', tkTextlen: '0',
tkMenus: '2', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '2', tkLastid: '02', tkMenus: '2', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '2', tkLastid: '02',
tkChannels: '0', tkMsl: '16', tkTar: 'B00001', tkAd: '', tkServices: '7', tkChannels: '0', tkMsl: '16', tkTar: 'AF4D01', tkAd: '', tkServices: '7',
}); });
assert.ok(apdu.includes('EA13801100000002010102020002011603B0000107'), apdu); assert.ok(apdu.includes('EA13801100000002010102020002011603AF4D0107'), apdu);
}); });
test('SIM (CA) access domain FIRST, no services byte', () => { test('SIM (CA) access domain FIRST, no services byte', () => {
@@ -125,9 +125,9 @@ test('SIM (CA) access domain FIRST, no services byte', () => {
cmd: 'install-install', aid: 'A000000151000000', priv: '00', cmd: 'install-install', aid: 'A000000151000000', priv: '00',
tkEnabled: true, tkMode: 'ca', tkPriority: '0', tkTimers: '0', tkTextlen: '0', tkEnabled: true, tkMode: 'ca', tkPriority: '0', tkTimers: '0', tkTextlen: '0',
tkMenus: '2', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '2', tkLastid: '02', tkMenus: '2', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '2', tkLastid: '02',
tkChannels: '0', tkMsl: '16', tkTar: 'B00001', tkAd: '5A', tkServices: '0', tkChannels: '0', tkMsl: '16', tkTar: 'AF4D01', tkAd: '5A', tkServices: '0',
}); });
assert.ok(apdu.includes('EF14CA12015A00000002010102020002011603B00001'), apdu); assert.ok(apdu.includes('EF14CA12015A00000002010102020002011603AF4D01'), apdu);
}); });
test('SIM (CA) blank access domain emits length byte 00', () => { test('SIM (CA) blank access domain emits length byte 00', () => {
@@ -135,9 +135,9 @@ test('SIM (CA) blank access domain emits length byte 00', () => {
cmd: 'install-install', aid: 'A000000151000000', priv: '00', cmd: 'install-install', aid: 'A000000151000000', priv: '00',
tkEnabled: true, tkMode: 'ca', tkPriority: '0', tkTimers: '0', tkTextlen: '0', tkEnabled: true, tkMode: 'ca', tkPriority: '0', tkTimers: '0', tkTextlen: '0',
tkMenus: '2', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '2', tkLastid: '02', tkMenus: '2', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '2', tkLastid: '02',
tkChannels: '0', tkMsl: '16', tkTar: 'B00001', tkAd: '', tkServices: '0', tkChannels: '0', tkMsl: '16', tkTar: 'AF4D01', tkAd: '', tkServices: '0',
}); });
assert.ok(apdu.includes('EF13CA110000000002010102020002011603B00001'), apdu); assert.ok(apdu.includes('EF13CA110000000002010102020002011603AF4D01'), apdu);
}); });
test('SIM (CA) m=3, no TAR, blank access domain', () => { test('SIM (CA) m=3, no TAR, blank access domain', () => {
@@ -155,7 +155,7 @@ test('UICC m=60 uses long-form BER lengths (EA 81 87 / inner 81 84)', () => {
cmd: 'install-install', aid: 'A000000151000000', priv: '00', cmd: 'install-install', aid: 'A000000151000000', priv: '00',
tkEnabled: true, tkMode: 'ea', tkPriority: '0', tkTimers: '0', tkTextlen: '0', tkEnabled: true, tkMode: 'ea', tkPriority: '0', tkTimers: '0', tkTextlen: '0',
tkMenus: '60', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '60', tkLastid: '3C', tkMenus: '60', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '60', tkLastid: '3C',
tkChannels: '0', tkMsl: '16', tkTar: 'B00001', tkAd: '', tkServices: '0', tkChannels: '0', tkMsl: '16', tkTar: 'AF4D01', tkAd: '', tkServices: '0',
}); });
assert.ok(apdu.includes('EA8188808185'), apdu); assert.ok(apdu.includes('EA8188808185'), apdu);
}); });
+106
View File
@@ -0,0 +1,106 @@
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);
}
// The real functions behind the Explore Delete flow: the counter the card
// consumed must be persisted into the preset, or the next operation starts
// one behind and is rejected with cntr_low.
let code = '';
for (const fn of ['berLenStr', 'ramDeleteApdu', 'ramIncrementCntr', 'ramSaveCntr',
'getRamSpParams', 'spPorAccepted', 'ramRemoteSwOk', 'ramShowProgress',
'ramHideProgress', 'ramDeleteFromExplorer']) {
code += extractFunc(html, fn) + '\n';
}
eval(code);
function fakeEnv(por, success) {
const els = {};
const mk = () => ({ value: '', textContent: '',
classList: { add() {}, remove() {}, toggle() {} } });
for (const id of ['sp-spi1', 'sp-spi2', 'sp-kic-hex', 'sp-kid-hex', 'sp-tar',
'sp-cntr', 'sp-kic-key', 'sp-kid-key', 'ram-result', 'ram-steps',
'ram-progress', 'ram-progress-text']) els[id] = mk();
els['ram-card-sel'] = { value: '0' };
// the preset keys the real spRefreshFromPreset/cardsApply would copy into
// the form (the delete flow aborts without them)
els['sp-kic-key'].value = 'AA';
els['sp-kid-key'].value = 'BB';
globalThis.document = { getElementById: id => els[id] || null };
globalThis.cards = [{ name: 'C', cntr: '0000000005', kicKey: 'AA', kidKey: 'BB' }];
const calls = { saved: 0, explored: null, sent: null };
globalThis.cardsSave = () => { calls.saved++; };
globalThis.cardsRender = () => {};
globalThis.ramRender = () => {};
globalThis.t = s => s;
globalThis.alert = () => {};
globalThis.confirm = () => true;
// the preset re-read (cardsApply -> the sp-* form fields); the real
// spRefreshFromPreset is covered by cards_counter.test.js
globalThis.spRefreshFromPreset = () => {
els['sp-cntr'].value = cards[0].cntr;
return cards[0].cntr;
};
globalThis.ramSendOta = async (apdu, sp) => {
calls.sent = { apdu: apdu, cntr: sp.cntr };
return { success: success !== false, por: por };
};
globalThis.ramExplore = async sp => { calls.explored = sp.cntr; };
return { els, calls };
}
test('accepted delete persists the consumed counter and re-explores from it', async () => {
const { els, calls } = fakeEnv({ response_status: 'por_ok',
decoded: { last_status_word: '9000' } });
await ramDeleteFromExplorer('F0414C46416101', false);
assert.strictEqual(calls.sent.cntr, '0000000005');
assert.strictEqual(calls.sent.apdu, '80E40000094F07F0414C4641610100');
assert.strictEqual(cards[0].cntr, '0000000006',
'the preset must carry the counter the card consumed');
assert.strictEqual(els['sp-cntr'].value, '0000000006');
assert.ok(calls.saved > 0, 'cardsSave() must persist it');
assert.strictEqual(calls.explored, '0000000006',
'the re-explore must start at N+1, never replay N');
assert.strictEqual(els['ram-result'].textContent, 'OK');
});
test('cntr_low leaves the preset untouched (the card did not consume the packet)', async () => {
const { calls } = fakeEnv({ response_status: 'cntr_low' });
await ramDeleteFromExplorer('F0414C46416101', false);
assert.strictEqual(cards[0].cntr, '0000000005');
assert.strictEqual(calls.saved, 0);
assert.strictEqual(calls.explored, null);
});
test('a refused DELETE still advances the counter but does not re-explore', async () => {
const { calls } = fakeEnv({ response_status: 'por_ok',
decoded: { last_status_word: '6A88' } });
await ramDeleteFromExplorer('F0414C46416101', true);
assert.strictEqual(cards[0].cntr, '0000000006');
assert.strictEqual(calls.explored, null);
});
test('a send failure leaves the preset untouched', async () => {
const { calls } = fakeEnv({ response_status: 'por_ok' }, false);
await ramDeleteFromExplorer('F0414C46416101', false);
assert.strictEqual(cards[0].cntr, '0000000005');
assert.strictEqual(calls.explored, null);
});
+158
View File
@@ -0,0 +1,158 @@
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 ['berLenStr', 'stkParamsBuild', 'buildRcToolkitParams',
'updateStkParamsHex']) code += extractFunc(html, fn) + '\n';
eval(code);
const base = { mode: 'ea', priority: '0', timers: '0', textLen: '0', menus: '0',
firstPos: '0', firstId: '00', lastPos: '0', lastId: '00',
channels: '0', msl: '00', tar: '', ad: '', services: '0' };
function vals(over) { return Object.assign({}, base, over); }
// ===== install-parameter coding (TS 102 226 8.2.1.3.2.2.1) =====
test('UICC (EA) parameters match the decrypted live packet', () => {
// The install parameters of the live CAP install (decrypted):
// EA 0F | 80 0D <prio 0 timers 0 textLen 0 menus 0> <channels 1>
// <MSL len 2 / 01 12> <TAR len 3 / AF 4D 01> <services 0>
assert.strictEqual(
stkParamsBuild(vals({ channels: '1', msl: '12', tar: 'AF4D01' })),
'EA0F800D000000000102011203AF4D0100');
});
test('empty TAR emits a zero-length TAR field, never a B00001 default', () => {
// B0 00 01 is the allocated ADF RFM TAR (TS 101 220 Annex D), not an
// applet TAR: the field stays empty and the TAR Value(s) length is 00
// (TS 102 226 8.2.1.3.2.7 - the card may then use a TAR from the AID,
// but only when the AID carries one).
const out = stkParamsBuild(vals({}));
assert.strictEqual(out, 'EA0A80080000000000000000');
assert.ok(!out.includes('B00001'), out);
});
test('SIM (CA) parameters carry the access domain first and no services byte', () => {
assert.strictEqual(stkParamsBuild(vals({ mode: 'ca' })),
'EF0ACA080000000000000000');
assert.strictEqual(stkParamsBuild(vals({ mode: 'ca', ad: '00' })),
'EF0BCA09010000000000000000');
});
test('menu item pairs run first .. last with 0000 fillers between', () => {
assert.strictEqual(stkParamsBuild(vals({ menus: '1', firstId: '7F' })),
'EA0C800A00000001007F00000000');
const out = stkParamsBuild(vals({ menus: '3', firstPos: '1', firstId: '01',
lastPos: '3', lastId: '03' }));
assert.ok(out.includes('010100000303'), out);
});
test('long menu lists use BER long-form lengths', () => {
const out = stkParamsBuild(vals({ menus: '60', firstPos: '1', firstId: '01',
lastPos: '60', lastId: '3C', msl: '16', tar: 'AF4D01' }));
assert.ok(out.startsWith('EA8188808185'), out);
});
test('rejects a TAR that is not a whole number of 3-byte values', () => {
assert.strictEqual(stkParamsBuild(vals({ tar: 'AF4D' })), null);
assert.strictEqual(stkParamsBuild(vals({ tar: 'AF4D0' })), null);
assert.strictEqual(stkParamsBuild(vals({ tar: 'AF4D0102' })), null);
// several TAR values are allowed (3 bytes each)
assert.ok(stkParamsBuild(vals({ tar: 'AF4D01AFFA01' })).includes('06AF4D01AFFA01'));
});
test('rejects menu item identifiers above 7F (reserved for the toolkit framework)', () => {
assert.strictEqual(stkParamsBuild(vals({ menus: '1', firstId: '80' })), null);
assert.strictEqual(stkParamsBuild(vals({ menus: '1', lastId: 'FF' })), null);
});
// ===== form wiring: the STK settings must reach the sent hex =====
test('every toolkit field regenerates the STK parameters hex on edit', () => {
// v3.6.9: the install used to send the hex computed once when the
// checkbox was ticked - edits to the fields afterwards were dropped
// (the live install carried the defaults, not the entered TAR).
const block = html.slice(html.indexOf('id="rc-toolkit-body"'),
html.indexOf('id="ram-install-params-hex"'));
const re = /<(?:input|select)\b[^>]*id="(rc-tk-[^"]+)"[^>]*>/g;
const seen = [];
let m;
while ((m = re.exec(block))) {
seen.push(m[1]);
assert.ok(/on(?:input|change)="[^"]*updateStkParamsHex/.test(m[0]),
m[1] + ' does not refresh the hex: ' + m[0]);
}
assert.strictEqual(seen.length, 14, 'expected 14 toolkit fields, got ' + seen.join(', '));
});
test('the applet TAR field has no B00001 default or placeholder', () => {
const m = /<input id="rc-tk-tar"[^>]*>/.exec(html);
assert.ok(m, 'rc-tk-tar not found');
assert.ok(!/value="B00001"/.test(m[0]), m[0]);
assert.ok(!/placeholder="B00001"/.test(m[0]), m[0]);
assert.ok(/oninput="updateStkParamsHex\(\)"/.test(m[0]), m[0]);
assert.ok(!html.includes("f.tkTar || 'B00001'"), 'the chain toolkit default must stay empty');
});
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');
assert.ok(/rc-toolkit-enable'\)\.checked && !\(stkEl\.dataset && stkEl\.dataset\.manual\)/.test(html),
'the send-time recompute guard is missing');
});
// ===== form -> hex (the live regression) =====
function fakeForm(values) {
const ids = ['rc-toolkit-enable', 'rc-tk-mode', 'rc-tk-priority', 'rc-tk-timers',
'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', 'ram-stk-params'];
const els = {};
for (const id of ids) els[id] = { value: '', checked: false, dataset: {} };
for (const [id, v] of Object.entries(values || {})) {
if (id === 'rc-toolkit-enable') els[id].checked = v;
else els[id].value = v;
}
globalThis.document = { getElementById: id => els[id] || null };
return els;
}
test('the RAM form fields build the live install parameters end to end', () => {
// The reported regression: the values were entered after the toolkit
// checkbox was ticked and never reached the sent hex - the install
// 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');
});
test('updateStkParamsHex refreshes the field and clears the manual flag', () => {
const els = fakeForm({ 'rc-toolkit-enable': true, 'rc-tk-mode': 'ea',
'rc-tk-tar': 'AF4D01' });
els['ram-stk-params'].dataset.manual = '1';
updateStkParamsHex();
assert.ok(els['ram-stk-params'].value.startsWith('EA'), els['ram-stk-params'].value);
assert.ok(!els['ram-stk-params'].dataset.manual,
'a regeneration supersedes a manual edit');
});
+1 -1
View File
@@ -30,7 +30,7 @@ function extractFunc(src, name) {
// GlobalPlatform Card Spec 11.8 (PUT KEY) // GlobalPlatform Card Spec 11.8 (PUT KEY)
const FNS = ['berLenStr', 'buildApdu', 'buildSelect', 'escHtml', 'esc', 'chainInit', const FNS = ['berLenStr', 'buildApdu', 'buildSelect', 'escHtml', 'esc', 'chainInit',
'chainKind', 'chainIsEmbedded', 'chainCommands', 'chainBuildRowHex', 'chainKind', 'chainIsEmbedded', 'chainCommands', 'chainBuildRowHex',
'chainSimBuildRowHex', 'chainRamBuildRowHex', 'chainPushData', '_hotaAsciiHex', 'chainSimBuildRowHex', 'chainRamBuildRowHex', 'stkParamsBuild', 'chainPushData', '_hotaAsciiHex',
'chainApduList', 'chainBuildFcp', 'pushSectionApdus', 'pushOpenChannelTlvs']; 'chainApduList', 'chainBuildFcp', 'pushSectionApdus', 'pushOpenChannelTlvs'];
let code = ''; let code = '';
for (const f of FNS) code += extractFunc(html, f) + '\n'; for (const f of FNS) code += extractFunc(html, f) + '\n';
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "pysim-simple-server" name = "pysim-simple-server"
version = "3.6.8" version = "3.6.9"
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.
+1 -1
View File
@@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad from osmocom.utils import rpad
VERSION = '3.6.8' VERSION = '3.6.9'
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