v3.10.0: preset TAR table with per-TAR MSL; derive the SPI1 from the TAR
Presets drop the card-wide SPI1/SPI2 and the fixed tar/uiccTar/usimTar fields: a preset now carries `tars` - the three mandatory role entries (isd/uiccRfm/usimRfm, spec defaults pre-settled) plus free TARs with an optional description, each with its MSL (Minimum SPI1, TS 102 226 8.2.1.3.2.4). v3.9.1 presets convert on load/import (the spi1 seeds all three MSLs); duplicate TARs and empty TAR/MSL values are refused; the store schema version is 3. Operations derive a packet's SPI1 from its TAR's MSL (_spi1_for_tar): the RAM ops, the test runner (per-step overrides still win), the counter probe and each TAR-probe row; the plain send keeps an explicit SPI1 with a `warning` when it is below the MSL, and a TAR outside the preset without an explicit SPI1 is refused with a hint. The TAR probe resolves per checked TAR before sending and reports the effective spi1 per row. The secured packet form's SPI2 default is 00 (the RAM/probe operations pin their own). PWA: the Cards form edits the TAR table (role rows with MSL inputs plus Add/Remove free rows with a description), the list shows a TARs (MSL) column, the SCP80 TAR field suggests the preset's entries (datalist) and a preset apply seeds the SPI1 from the operation TAR's MSL with an amber warning below it; the preset-wide SPI1/SPI2 fields and the header marker's SPI requirement are gone (completeness = a keyset + the three role entries with their MSLs). Help EN/RU, docs/api.md and AGENTS updated; sw cache simple-v358. Live: TAR probe 000000 at its MSL 16 (registered, por_ok, counter advanced); a send without spi1 derived 16 (por_ok, counter advanced); a send with spi1 0A returned the warning and the card refused it (6200, no counter consumed); an unknown TAR without spi1 was refused with the hint.
This commit is contained in:
+300
-81
@@ -651,7 +651,8 @@
|
||||
<div class="flex gap-2 mb-3">
|
||||
<div class="flex-1">
|
||||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">TAR (3 bytes)</label>
|
||||
<input id="sp-tar" oninput="spInvalidate()" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="B00001" maxlength="6" value="B00001">
|
||||
<input id="sp-tar" oninput="spInvalidate()" list="sp-tar-list" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="B00001" maxlength="6" value="B00001">
|
||||
<datalist id="sp-tar-list"></datalist>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">Counter (5 bytes)</label>
|
||||
@@ -669,6 +670,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sp-msl-warning" class="hidden mb-3 px-3 py-2 rounded border border-amber-400 dark:border-amber-600 bg-amber-50 dark:bg-amber-900/20 text-xs text-amber-700 dark:text-amber-300"></div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">APDU (hex)</label>
|
||||
<textarea id="sp-apdu" rows="3" oninput="spInvalidate()" 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"></textarea>
|
||||
@@ -1590,7 +1593,7 @@
|
||||
|
||||
|
||||
<div id="tab-cards" class="tab-content hidden">
|
||||
<p class="text-xs text-gray-500 dark:text-slate-400 mb-3" data-l10n="Card presets are stored on the server. JSON export/import buttons are below the card list; a preset carries several keysets, each with its own number and counter.">Card presets are stored on the server. JSON export/import buttons are below the card list; a preset carries several keysets, each with its own number and counter.</p>
|
||||
<p class="text-xs text-gray-500 dark:text-slate-400 mb-3" data-l10n="Card presets are stored on the server. JSON export/import buttons are below the card list; a preset carries several keysets, each with its own number and counter, and a TAR table — every TAR with its MSL (Minimum SPI1).">Card presets are stored on the server. JSON export/import buttons are below the card list; a preset carries several keysets, each with its own number and counter, and a TAR table — every TAR with its MSL (Minimum SPI1).</p>
|
||||
<div class="flex gap-2 mb-3">
|
||||
<input id="cards-name" class="flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="Name (e.g. Foobar SIM)">
|
||||
<input id="cards-iccid" class="flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 font-mono" placeholder="ICCID (optional)">
|
||||
@@ -1601,24 +1604,38 @@
|
||||
</div>
|
||||
<fieldset class="border border-gray-200 dark:border-slate-700 rounded p-3 mb-3">
|
||||
<legend class="px-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="SCP80 (GSM 03.48, ETSI TS 102 225)">SCP80 (GSM 03.48, ETSI TS 102 225)</legend>
|
||||
<div class="flex gap-2 mb-2 flex-wrap items-end">
|
||||
<input id="cards-spi1" class="w-12 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono text-center" placeholder="SPI1" maxlength="2">
|
||||
<input id="cards-spi2" class="w-12 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono text-center" placeholder="SPI2" maxlength="2">
|
||||
<div class="flex gap-2">
|
||||
<div>
|
||||
<label for="cards-tar" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400">ISD TAR</label>
|
||||
<input id="cards-tar" class="w-20 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono" maxlength="6" value="000000">
|
||||
</div>
|
||||
<div>
|
||||
<label for="cards-uicc-tar" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400">UICC RFM TAR</label>
|
||||
<input id="cards-uicc-tar" class="w-20 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono" maxlength="6" value="B00000">
|
||||
</div>
|
||||
<div>
|
||||
<label for="cards-usim-tar" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400">ADF RFM TAR</label>
|
||||
<input id="cards-usim-tar" class="w-20 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono" maxlength="6" value="B00001">
|
||||
</div>
|
||||
<div class="flex gap-3 mb-2 flex-wrap items-end">
|
||||
<div>
|
||||
<label for="cards-tar" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400" data-l10n="ISD TAR">ISD TAR</label>
|
||||
<input id="cards-tar" class="w-20 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono" maxlength="6" value="000000">
|
||||
</div>
|
||||
<div>
|
||||
<label for="cards-tar-msl" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400" data-l10n="MSL">MSL</label>
|
||||
<input id="cards-tar-msl" class="w-12 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono text-center" maxlength="2" value="16" data-l10n-title="MSL (Minimum SPI1, one hex byte)">
|
||||
</div>
|
||||
<div>
|
||||
<label for="cards-uicc-tar" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400" data-l10n="UICC RFM TAR">UICC RFM TAR</label>
|
||||
<input id="cards-uicc-tar" class="w-20 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono" maxlength="6" value="B00000">
|
||||
</div>
|
||||
<div>
|
||||
<label for="cards-uicc-tar-msl" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400" data-l10n="MSL">MSL</label>
|
||||
<input id="cards-uicc-tar-msl" class="w-12 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono text-center" maxlength="2" value="16" data-l10n-title="MSL (Minimum SPI1, one hex byte)">
|
||||
</div>
|
||||
<div>
|
||||
<label for="cards-usim-tar" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400" data-l10n="ADF RFM TAR">ADF RFM TAR</label>
|
||||
<input id="cards-usim-tar" class="w-20 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono" maxlength="6" value="B00001">
|
||||
</div>
|
||||
<div>
|
||||
<label for="cards-usim-tar-msl" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400" data-l10n="MSL">MSL</label>
|
||||
<input id="cards-usim-tar-msl" class="w-12 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono text-center" maxlength="2" value="16" data-l10n-title="MSL (Minimum SPI1, one hex byte)">
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-slate-400 mb-2" data-l10n="Each TAR carries its MSL (Minimum SPI1) — the card checks it before the security processing (TS 102 226 8.2.1.3.2.4); the packet SPI1 is derived from it.">Each TAR carries its MSL (Minimum SPI1) — the card checks it before the security processing (TS 102 226 8.2.1.3.2.4); the packet SPI1 is derived from it.</p>
|
||||
<div class="mb-1 flex items-center justify-between">
|
||||
<span class="text-xs text-gray-600 dark:text-slate-400" data-l10n="More TARs — each with its MSL and an optional description">More TARs — each with its MSL and an optional description</span>
|
||||
<button type="button" onclick="cardsTarAdd()" class="px-2 py-0.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700" data-l10n="Add TAR">Add TAR</button>
|
||||
</div>
|
||||
<div id="cards-tars"></div>
|
||||
<div class="mb-1 flex items-center justify-between">
|
||||
<span class="text-xs text-gray-600 dark:text-slate-400" data-l10n="OTA keysets — each with its own number and counter">OTA keysets — each with its own number and counter</span>
|
||||
<button type="button" onclick="cardsKeysetAdd()" class="px-2 py-0.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700" data-l10n="Add keyset">Add keyset</button>
|
||||
@@ -1640,9 +1657,7 @@
|
||||
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="Name">Name</th>
|
||||
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="ICCID">ICCID</th>
|
||||
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="Keysets (number: KIc/KID — counter)">Keysets (number: KIc/KID \u2014 counter)</th>
|
||||
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700">SPI1</th>
|
||||
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700">SPI2</th>
|
||||
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700">TAR (ISD/UICC RFM/ADF RFM)</th>
|
||||
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="TARs (MSL)">TARs (MSL)</th>
|
||||
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="SCP81">SCP81</th>
|
||||
<th class="border-b border-gray-200 dark:border-slate-700"></th>
|
||||
</tr></thead>
|
||||
@@ -1801,7 +1816,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.9.1';
|
||||
const SIMPLE_VERSION = '3.10.0';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== Tab switching =====
|
||||
@@ -4693,9 +4708,10 @@ async function ramQueueScp81() {
|
||||
}
|
||||
}
|
||||
|
||||
// Which TAR of the selected preset feeds the SCP80 form: RAM/GP -> ISD,
|
||||
// SIM RFM -> UICC, USIM RFM -> USIM (spec defaults when the value is empty).
|
||||
let _spTarKey = 'tar';
|
||||
// Which TAR of the selected preset feeds the SCP80 form: RAM/GP -> the ISD
|
||||
// entry, SIM RFM -> the UICC RFM entry, USIM RFM -> the ADF RFM entry. Each
|
||||
// entry carries its MSL (Minimum SPI1): the SPI1 default for that TAR.
|
||||
let _spTarKey = 'isd';
|
||||
// RAM/GP chains packed into the SCP80 form opt in to the per-operation
|
||||
// command-format detection ('auto'); other packs (HOTA, §9 push, expanded
|
||||
// script) keep their payload byte-exact.
|
||||
@@ -4703,17 +4719,31 @@ let _spRamFormat = null;
|
||||
|
||||
function spTarKeyForPack(sourceId) {
|
||||
return {
|
||||
'chain-ram-preview': 'tar',
|
||||
'chain-sim-preview': 'uiccTar',
|
||||
'chain-usim-preview': 'usimTar',
|
||||
'chain-ram-preview': 'isd',
|
||||
'chain-sim-preview': 'uiccRfm',
|
||||
'chain-usim-preview': 'usimRfm',
|
||||
}[sourceId] || null;
|
||||
}
|
||||
|
||||
function spPresetTar(key) {
|
||||
function spPresetTar(role) {
|
||||
const sel = document.getElementById('sp-card-sel');
|
||||
const idx = sel ? parseInt(sel.value, 10) : NaN;
|
||||
const c = (!isNaN(idx) && cards[idx]) ? cards[idx] : null;
|
||||
return cardsTarValue(key, c ? c[key] : '');
|
||||
return cardsRoleTar(c, role);
|
||||
}
|
||||
|
||||
// The TAR field's suggestions: the preset's TAR table, each entry labelled
|
||||
// with its description (or its role) and MSL - free text stays possible.
|
||||
function spTarListRender(preset) {
|
||||
const list = document.getElementById('sp-tar-list');
|
||||
if (!list) return;
|
||||
const roleLabel = { isd: 'ISD', uiccRfm: 'UICC RFM', usimRfm: 'ADF RFM' };
|
||||
list.innerHTML = cardsTarList(preset).map(e => {
|
||||
const label = e.desc || roleLabel[e.role] || '';
|
||||
const text = label + (e.msl ? (label ? ' ' : '') + '(MSL ' + e.msl + ')' : '');
|
||||
return '<option value="' + esc(e.tar || '') + '"' +
|
||||
(text ? ' label="' + esc(text) + '"' : '') + '></option>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function packToSp(sourceId) {
|
||||
@@ -4738,6 +4768,7 @@ function spCntrAdjust(delta) {
|
||||
function spInvalidate() {
|
||||
document.getElementById('sp-result').value = '';
|
||||
spShowSizeInfo();
|
||||
spMslWarningRender();
|
||||
}
|
||||
|
||||
function updateSp() {
|
||||
@@ -8331,6 +8362,7 @@ async function pysimSendOta() {
|
||||
if (data.bytes && data.segments) {
|
||||
msg += ' | ' + data.bytes + ' ' + t('bytes') + ' · ' + data.segments + ' SMS';
|
||||
}
|
||||
if (data.warning) msg += ' | ' + t('Warning') + ': ' + data.warning;
|
||||
const por = data.por;
|
||||
if (por && por.response_status) {
|
||||
msg += ' | PoR status: ' + por.response_status + ' (TAR ' + por.tar + ')';
|
||||
@@ -8385,6 +8417,7 @@ async function pysimSendOta() {
|
||||
let msg = 'OTA failed';
|
||||
if (data.error) msg += ': ' + data.error;
|
||||
if (data.sw) msg += (data.error ? ' | ' : ': ') + 'SW ' + data.sw;
|
||||
if (data.warning) msg += ' | ' + t('Warning') + ': ' + data.warning;
|
||||
sendResultEl.textContent = msg;
|
||||
sendResultEl.classList.remove('hidden', 'text-green-600', 'text-gray-500');
|
||||
sendResultEl.classList.add('text-red-600');
|
||||
@@ -8452,7 +8485,7 @@ function ramApplyCard(idx) {
|
||||
// getRamSpParams() picks up the right SPI/KIc/KID/TAR/CNTR/keys
|
||||
const i = parseInt(idx, 10);
|
||||
if (!isNaN(i) && cards[i]) _ramCardIdx = i;
|
||||
_spTarKey = 'tar'; // RAM/GP operations target the ISD
|
||||
_spTarKey = 'isd'; // RAM/GP operations target the ISD
|
||||
cardsApply(idx);
|
||||
spPresetWarningRender();
|
||||
}
|
||||
@@ -10845,7 +10878,7 @@ function tarApplyCard(idx) {
|
||||
// kic/kid/keys/counter from there, like the RAM view does)
|
||||
const i = parseInt(idx, 10);
|
||||
if (!isNaN(i) && cards[i]) _ramCardIdx = i;
|
||||
_spTarKey = 'tar';
|
||||
_spTarKey = 'isd'; // the probe's checklist TARs are per-row; the ISD is the form default
|
||||
cardsApply(idx);
|
||||
spPresetWarningRender();
|
||||
}
|
||||
@@ -11488,10 +11521,83 @@ function cardsPskName(identity) {
|
||||
}
|
||||
|
||||
// Spec-default TARs (GP ISD 000000, TS 31.116 UICC B00000, TS 31.115 USIM
|
||||
// B00001): an empty preset value means the default for that target.
|
||||
function cardsTarValue(key, value) {
|
||||
const defaults = { tar: '000000', uiccTar: 'B00000', usimTar: 'B00001' };
|
||||
return value || defaults[key] || '';
|
||||
// B00001) and the default MSL (Minimum SPI1, TS 102 226 8.2.1.3.2.4). The
|
||||
// three role entries of a preset are mandatory; the defaults pre-settle the
|
||||
// Cards form.
|
||||
const CARDS_TAR_DEFAULTS = { isd: '000000', uiccRfm: 'B00000', usimRfm: 'B00001' };
|
||||
const CARDS_TAR_ROLES = ['isd', 'uiccRfm', 'usimRfm'];
|
||||
const CARDS_MSL_DEFAULT = '16';
|
||||
|
||||
function cardsRoleDefault(role) {
|
||||
return CARDS_TAR_DEFAULTS[role] || '';
|
||||
}
|
||||
|
||||
// The preset's TAR table: the three role entries plus the free entries.
|
||||
function cardsTarList(preset) {
|
||||
return (preset && Array.isArray(preset.tars)) ? preset.tars : [];
|
||||
}
|
||||
|
||||
// The TAR of a role entry; an empty stored value means the role's spec default.
|
||||
function cardsRoleTar(preset, role) {
|
||||
const e = cardsTarList(preset).find(x => x.role === role);
|
||||
return (e && e.tar) || cardsRoleDefault(role);
|
||||
}
|
||||
|
||||
// The MSL of a role entry, '' when the preset carries none.
|
||||
function cardsRoleMsl(preset, role) {
|
||||
const e = cardsTarList(preset).find(x => x.role === role);
|
||||
return (e && e.msl) || '';
|
||||
}
|
||||
|
||||
// The TAR entry matching a value (case-insensitive), or null.
|
||||
function cardsTarEntry(preset, tarHex) {
|
||||
const want = String(tarHex || '').trim().toUpperCase();
|
||||
if (!want) return null;
|
||||
return cardsTarList(preset).find(e => String(e.tar || '').toUpperCase() === want) || null;
|
||||
}
|
||||
|
||||
// The MSL of a TAR value, '' when the preset has no such entry.
|
||||
function cardsTarMsl(preset, tarHex) {
|
||||
const e = cardsTarEntry(preset, tarHex);
|
||||
return e ? (e.msl || '') : '';
|
||||
}
|
||||
|
||||
// The preset list's TAR cell: `ISD 000000·16 UICC B00000·16 … AF4D02·0A (app)`
|
||||
function cardsTarText(preset) {
|
||||
const list = cardsTarList(preset);
|
||||
if (!list.length) return '\u2014';
|
||||
const roleLabel = { isd: 'ISD', uiccRfm: 'UICC', usimRfm: 'ADF' };
|
||||
return list.map(e => {
|
||||
const label = roleLabel[e.role];
|
||||
return (label ? label + ' ' : '') + (e.tar || '??') + '\u00b7' + (e.msl || '??') +
|
||||
(e.desc ? ' (' + e.desc + ')' : '');
|
||||
}).join(' ');
|
||||
}
|
||||
|
||||
// The SPI1-vs-MSL warning of the SCP80 form (pure): '' when the form's TAR has
|
||||
// no MSL in the selected preset or the SPI1 is at/above it (numeric compare).
|
||||
// The card checks the MSL before the security processing and answers 0A
|
||||
// "Insufficient security level" when the packet's SPI1 is below it
|
||||
// (TS 102 226 6.1/8.2.1.3.2.4).
|
||||
function spMslWarningText(preset, tarHex, spi1Hex) {
|
||||
const msl = cardsTarMsl(preset, tarHex);
|
||||
if (!msl) return '';
|
||||
if (!/^[0-9A-F]{1,2}$/i.test(String(spi1Hex || '').trim())) return '';
|
||||
if (parseInt(spi1Hex, 16) >= parseInt(msl, 16)) return '';
|
||||
return t('SPI1 is below the MSL of this TAR') + ': ' + String(spi1Hex).toUpperCase() +
|
||||
' < ' + msl + ' (' + t('TAR') + ' ' + String(tarHex).toUpperCase() + ')';
|
||||
}
|
||||
|
||||
function spMslWarningRender() {
|
||||
const el = document.getElementById('sp-msl-warning');
|
||||
if (!el) return;
|
||||
const idx = spPresetIdx();
|
||||
const preset = idx >= 0 ? cards[idx] : null;
|
||||
const tar = (document.getElementById('sp-tar') || {}).value || '';
|
||||
const spi1 = (document.getElementById('sp-spi1') || {}).value || '';
|
||||
const text = spMslWarningText(preset, tar, spi1);
|
||||
el.textContent = text;
|
||||
el.classList.toggle('hidden', !text);
|
||||
}
|
||||
|
||||
// ===== OTA keysets (one per keyset number) =====
|
||||
@@ -11649,6 +11755,85 @@ function cardsKeysetsFromForm() {
|
||||
return out;
|
||||
}
|
||||
|
||||
// ===== TAR table editor (Cards form) =====
|
||||
// The three role entries have fixed inputs (their labels are the immutable
|
||||
// descriptions); further TARs are free rows with an optional description.
|
||||
// Every entry carries the TAR and its MSL (Minimum SPI1, one hex byte).
|
||||
let _cardsTarCount = 0;
|
||||
|
||||
function cardsTarRowHtml(e, i) {
|
||||
const cls = 'border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono';
|
||||
return '<div class="flex gap-2 items-center mb-1">' +
|
||||
'<input id="cards-ft-' + i + '-tar" class="w-20 ' + cls + ' text-center" maxlength="6" placeholder="TAR" value="' + esc(e.tar || '') + '">' +
|
||||
'<input id="cards-ft-' + i + '-msl" class="w-12 ' + cls + ' text-center" maxlength="2" placeholder="MSL" value="' + esc(e.msl || '') + '" title="' + esc(t('MSL (Minimum SPI1, one hex byte)')) + '">' +
|
||||
'<input id="cards-ft-' + i + '-desc" class="flex-1 ' + cls + '" placeholder="' + esc(t('Description (optional)')) + '" value="' + esc(e.desc || '') + '">' +
|
||||
'<button type="button" onclick="cardsTarRemove(' + i + ')" class="px-2 py-1 text-xs rounded bg-red-600 text-white hover:bg-red-700" title="' + esc(t('Remove TAR')) + '">\u2715</button>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function cardsTarRowsRender(entries) {
|
||||
const box = document.getElementById('cards-tars');
|
||||
if (!box) return;
|
||||
const list = entries || [];
|
||||
_cardsTarCount = list.length;
|
||||
box.innerHTML = list.map((e, i) => cardsTarRowHtml(e, i)).join('');
|
||||
}
|
||||
|
||||
function cardsTarAdd() {
|
||||
cardsTarRowsRender(cardsFreeTarsFromForm().concat([{ tar: '', msl: '', desc: '' }]));
|
||||
}
|
||||
|
||||
function cardsTarRemove(i) {
|
||||
const rows = cardsFreeTarsFromForm();
|
||||
rows.splice(i, 1);
|
||||
cardsTarRowsRender(rows);
|
||||
}
|
||||
|
||||
// The free editor rows (fully empty rows are dropped).
|
||||
function cardsFreeTarsFromForm() {
|
||||
const out = [];
|
||||
for (let i = 0; i < _cardsTarCount; i++) {
|
||||
const get = id => {
|
||||
const el = document.getElementById('cards-ft-' + i + '-' + id);
|
||||
return el ? String(el.value).trim() : '';
|
||||
};
|
||||
const e = { tar: get('tar').replace(/\s/g, '').toUpperCase(),
|
||||
msl: get('msl').replace(/\s/g, '').toUpperCase(), desc: get('desc') };
|
||||
if (!e.tar && !e.msl && !e.desc) continue;
|
||||
out.push(e);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// The TAR table as preset entries: the three role entries from their fixed
|
||||
// inputs (the labels are the role descriptions) plus the free rows.
|
||||
function cardsTarsFromForm() {
|
||||
const roleInputs = { isd: 'cards-tar', uiccRfm: 'cards-uicc-tar', usimRfm: 'cards-usim-tar' };
|
||||
const out = CARDS_TAR_ROLES.map(role => {
|
||||
const tarEl = document.getElementById(roleInputs[role]);
|
||||
const mslEl = document.getElementById(roleInputs[role] + '-msl');
|
||||
const tar = (tarEl ? String(tarEl.value).trim() : '').replace(/\s/g, '').toUpperCase();
|
||||
const msl = (mslEl ? String(mslEl.value).trim() : '').replace(/\s/g, '').toUpperCase();
|
||||
// An emptied role field keeps the pre-settled default: the three role
|
||||
// entries are mandatory with a non-empty TAR and MSL.
|
||||
return { role: role, tar: tar || cardsRoleDefault(role),
|
||||
msl: msl || CARDS_MSL_DEFAULT, desc: '' };
|
||||
});
|
||||
return out.concat(cardsFreeTarsFromForm());
|
||||
}
|
||||
|
||||
// The role inputs back to their pre-settled defaults (TAR + MSL 16).
|
||||
function cardsTarsReset() {
|
||||
const roleInputs = { isd: 'cards-tar', uiccRfm: 'cards-uicc-tar', usimRfm: 'cards-usim-tar' };
|
||||
CARDS_TAR_ROLES.forEach(role => {
|
||||
const tarEl = document.getElementById(roleInputs[role]);
|
||||
if (tarEl) tarEl.value = cardsRoleDefault(role);
|
||||
const mslEl = document.getElementById(roleInputs[role] + '-msl');
|
||||
if (mslEl) mslEl.value = CARDS_MSL_DEFAULT;
|
||||
});
|
||||
cardsTarRowsRender([]);
|
||||
}
|
||||
|
||||
// ===== Keyset selectors (SCP80 / RAM) =====
|
||||
// The selector follows the KIc/KID bytes: a keyset of the selected preset that
|
||||
// matches their keyset number is selected; a number the preset does not define
|
||||
@@ -11807,11 +11992,7 @@ function cardsFormValues() {
|
||||
name: val('name'),
|
||||
iccid: val('iccid'),
|
||||
adm: val('adm').replace(/\s/g, '').toUpperCase(),
|
||||
spi1: val('spi1'),
|
||||
spi2: val('spi2'),
|
||||
tar: val('tar'),
|
||||
uiccTar: val('uicc-tar'),
|
||||
usimTar: val('usim-tar'),
|
||||
tars: cardsTarsFromForm(),
|
||||
pskIdentity: val('psk-id'),
|
||||
pskKey: val('psk-key').replace(/\s/g, '').toUpperCase(),
|
||||
keysets: cardsKeysetsFromForm(),
|
||||
@@ -11826,24 +12007,17 @@ function cardsSetFormMode() {
|
||||
}
|
||||
|
||||
function cardsClearForm() {
|
||||
['name','iccid','adm','spi1','spi2','psk-id','psk-key'].forEach(id => {
|
||||
['name','iccid','adm','psk-id','psk-key'].forEach(id => {
|
||||
const el = document.getElementById('cards-' + id);
|
||||
if (el) el.value = '';
|
||||
});
|
||||
// TARs keep their per-target spec defaults instead of being emptied
|
||||
[['tar','tar'],['uicc-tar','uiccTar'],['usim-tar','usimTar']].forEach(([id, key]) => {
|
||||
const el = document.getElementById('cards-' + id);
|
||||
if (el) el.value = cardsTarValue(key, '');
|
||||
});
|
||||
// The three role entries keep their pre-settled defaults (TAR + MSL 16)
|
||||
cardsTarsReset();
|
||||
cardsKeysetRowsRender([]);
|
||||
}
|
||||
|
||||
async function cardsAdd() {
|
||||
const v = cardsFormValues();
|
||||
// empty TAR fields fall back to the per-target spec defaults
|
||||
v.tar = cardsTarValue('tar', v.tar);
|
||||
v.uiccTar = cardsTarValue('uiccTar', v.uiccTar);
|
||||
v.usimTar = cardsTarValue('usimTar', v.usimTar);
|
||||
if (!v.name) { alert(t('Name is required')); return; }
|
||||
if (!v.keysets.length) {
|
||||
alert(t('Define at least one keyset (KIc/KID and their keys)'));
|
||||
@@ -11866,6 +12040,22 @@ async function cardsAdd() {
|
||||
alert(t('Duplicate keyset number in the keysets'));
|
||||
return;
|
||||
}
|
||||
// The TAR table: every entry needs a TAR and its MSL, and a TAR appears once
|
||||
for (const e of v.tars) {
|
||||
if (!/^[0-9A-F]{6}$/.test(e.tar)) {
|
||||
alert(t('Every TAR needs three hex bytes') + ': ' + (e.tar || t('(empty)')));
|
||||
return;
|
||||
}
|
||||
if (!/^[0-9A-F]{2}$/.test(e.msl)) {
|
||||
alert(t('Every TAR needs its MSL (one hex byte)') + ': ' + e.tar);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const tarSeen = new Set();
|
||||
for (const e of v.tars) {
|
||||
if (tarSeen.has(e.tar)) { alert(t('Duplicate TAR in the preset') + ': ' + e.tar); return; }
|
||||
tarSeen.add(e.tar);
|
||||
}
|
||||
if ((v.pskKey && !v.pskIdentity) || (!v.pskKey && v.pskIdentity)) {
|
||||
alert(t('PSK identity and PSK key must be set together'));
|
||||
return;
|
||||
@@ -11902,12 +12092,15 @@ function cardsEdit(i) {
|
||||
if (el) el.value = value == null ? '' : value;
|
||||
};
|
||||
set('name', c.name); set('iccid', c.iccid); set('adm', c.adm);
|
||||
set('spi1', c.spi1); set('spi2', c.spi2);
|
||||
set('tar', cardsTarValue('tar', c.tar));
|
||||
set('uicc-tar', cardsTarValue('uiccTar', c.uiccTar));
|
||||
set('usim-tar', cardsTarValue('usimTar', c.usimTar));
|
||||
set('tar', cardsRoleTar(c, 'isd'));
|
||||
set('tar-msl', cardsRoleMsl(c, 'isd'));
|
||||
set('uicc-tar', cardsRoleTar(c, 'uiccRfm'));
|
||||
set('uicc-tar-msl', cardsRoleMsl(c, 'uiccRfm'));
|
||||
set('usim-tar', cardsRoleTar(c, 'usimRfm'));
|
||||
set('usim-tar-msl', cardsRoleMsl(c, 'usimRfm'));
|
||||
set('psk-id', c.pskIdentity); set('psk-key', c.pskKey);
|
||||
cardsKeysetRowsRender(spKeysetList(c));
|
||||
cardsTarRowsRender(cardsTarList(c).filter(e => !e.role));
|
||||
cardsSetFormMode();
|
||||
document.getElementById('cards-name').focus();
|
||||
}
|
||||
@@ -11943,7 +12136,7 @@ function cardsRender() {
|
||||
const tbody = document.getElementById('cards-tbody');
|
||||
if (!tbody) return;
|
||||
if (!cards.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="py-2 text-gray-400">' + t('No cards defined.') + '</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="py-2 text-gray-400">' + t('No cards defined.') + '</td></tr>';
|
||||
return;
|
||||
}
|
||||
// The preset of the equipped card (normalised ICCID match): its row is
|
||||
@@ -11973,9 +12166,7 @@ function cardsRender() {
|
||||
' \u2014 ' + esc(ks.cntr || '');
|
||||
}).join(' \u00b7 ') || '\u2014';
|
||||
html += '<td class="py-1 px-2 font-mono text-xs">' + ksText + '</td>';
|
||||
html += '<td class="py-1 px-2 font-mono text-xs text-center">' + esc(c.spi1) + '</td>';
|
||||
html += '<td class="py-1 px-2 font-mono text-xs text-center">' + esc(c.spi2) + '</td>';
|
||||
html += '<td class="py-1 px-2 font-mono text-xs" title="ISD / UICC RFM / ADF RFM">' + esc(cardsTarValue('tar', c.tar)) + ' ' + esc(cardsTarValue('uiccTar', c.uiccTar)) + ' ' + esc(cardsTarValue('usimTar', c.usimTar)) + '</td>';
|
||||
html += '<td class="py-1 px-2 font-mono text-xs" title="' + esc(t('TARs (MSL)')) + '">' + esc(cardsTarText(c)) + '</td>';
|
||||
html += '<td class="py-1 px-2 text-center">' + ota + '</td>';
|
||||
html += '<td class="py-1 px-2 whitespace-nowrap"><button onclick="cardsEdit(' + i + ')" class="px-2 py-0.5 text-xs rounded bg-blue-600 text-white hover:bg-blue-700">' + t('Edit') + '</button> '
|
||||
+ '<button onclick="cardsRemove(' + i + ')" class="px-2 py-0.5 text-xs rounded bg-red-600 text-white hover:bg-red-700">' + t('Remove') + '</button></td>';
|
||||
@@ -12017,12 +12208,19 @@ function cardsFindByIccid(iccid) {
|
||||
return cards.findIndex(c => cardsNormIccid(c.iccid) === norm);
|
||||
}
|
||||
|
||||
// Completeness predicates used by the header indicators: SCP80 needs all
|
||||
// SPI/keys/counter fields, SCP81 the PSK pair; the ADM key is optional.
|
||||
// Completeness predicates used by the header indicators: SCP80 needs a
|
||||
// complete keyset plus the three role TAR entries with their MSLs, SCP81 the
|
||||
// PSK pair; the ADM key is optional.
|
||||
function cardsScp80Complete(c) {
|
||||
return !!(c && String(c.kic || '').trim() && String(c.kid || '').trim()
|
||||
&& String(c.spi1 || '').trim() && String(c.spi2 || '').trim()
|
||||
&& String(c.cntr || '').trim() && String(c.kicKey || '').trim() && String(c.kidKey || '').trim());
|
||||
if (!c) return false;
|
||||
const ks = spKeysetList(c).find(k => String(k.kic || '').trim() && String(k.kid || '').trim()
|
||||
&& String(k.kicKey || '').trim() && String(k.kidKey || '').trim()
|
||||
&& String(k.cntr || '').trim());
|
||||
if (!ks) return false;
|
||||
return CARDS_TAR_ROLES.every(role => {
|
||||
const e = cardsTarList(c).find(x => x.role === role);
|
||||
return !!(e && String(e.tar || '').trim() && String(e.msl || '').trim());
|
||||
});
|
||||
}
|
||||
|
||||
function cardsScp81Complete(c) {
|
||||
@@ -12087,23 +12285,24 @@ function cardsAutoSelectByIccid(iccid) {
|
||||
function cardsApplyFields(idx, keepTar) {
|
||||
const c = cards[parseInt(idx)];
|
||||
if (!c) return false;
|
||||
document.getElementById('sp-spi1').value = c.spi1;
|
||||
// Sync the SPI2 selects with the preset byte (low 5 bits = PoR/security,
|
||||
// bit 0x20 = PoR via SMS-SUBMIT): updateSp() recomputes sp-spi2-hex from
|
||||
// them, so without this the preset value was clobbered on every refresh.
|
||||
const spi2Val = parseInt(c.spi2, 16) || 0;
|
||||
const baseHex = (spi2Val & 0x1F).toString(16).padStart(2, '0').toUpperCase();
|
||||
const baseSel = document.getElementById('sp-spi2');
|
||||
if (baseSel) {
|
||||
if (baseSel.options && typeof Option === 'function' &&
|
||||
!Array.prototype.some.call(baseSel.options, o => o.value === baseHex)) {
|
||||
baseSel.appendChild(new Option(baseHex + ' — preset', baseHex));
|
||||
// The TAR of the current operation (ISD for RAM/GP, the RFM TAR for the RFM
|
||||
// chains) and its MSL: a preset apply seeds the SPI1 from the MSL - the
|
||||
// card's Minimum SPI1 (TS 102 226 8.2.1.3.2.4). keepTar leaves the packet
|
||||
// TAR alone (the plain send may target a manually entered TAR): its SPI1 is
|
||||
// the operator's value too.
|
||||
if (!keepTar) {
|
||||
const roleTar = cardsRoleTar(c, _spTarKey);
|
||||
document.getElementById('sp-tar').value = roleTar;
|
||||
const msl = cardsTarMsl(c, roleTar) || CARDS_MSL_DEFAULT;
|
||||
const spi1Sel = document.getElementById('sp-spi1');
|
||||
if (spi1Sel) {
|
||||
if (spi1Sel.options && typeof Option === 'function' &&
|
||||
!Array.prototype.some.call(spi1Sel.options, o => o.value === msl)) {
|
||||
spi1Sel.appendChild(new Option(msl + ' \u2014 MSL', msl));
|
||||
}
|
||||
spi1Sel.value = msl;
|
||||
}
|
||||
baseSel.value = baseHex;
|
||||
}
|
||||
const smSel = document.getElementById('sp-spi2-sm');
|
||||
if (smSel) smSel.value = String((spi2Val >> 5) & 1);
|
||||
document.getElementById('sp-spi2-hex').value = c.spi2;
|
||||
// The keyset: keep the keyset number the form carries when this preset
|
||||
// defines it, otherwise start from the preset's first keyset.
|
||||
const kicNow = (document.getElementById('sp-kic-hex') || {}).value;
|
||||
@@ -12123,11 +12322,13 @@ function cardsApplyFields(idx, keepTar) {
|
||||
document.getElementById('sp-kic-key').value = ks.kicKey || '';
|
||||
document.getElementById('sp-kid-key').value = ks.kidKey || '';
|
||||
}
|
||||
if (!keepTar) document.getElementById('sp-tar').value = cardsTarValue(_spTarKey, c[_spTarKey]);
|
||||
if (!keepTar) document.getElementById('sp-tar').value = cardsRoleTar(c, _spTarKey);
|
||||
spTarListRender(c);
|
||||
spInvalidate();
|
||||
updateSp();
|
||||
spKeysetSync();
|
||||
spPresetWarningRender();
|
||||
spMslWarningRender();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -12137,6 +12338,7 @@ function cardsApply(idx, keepTar) {
|
||||
// warning must not outlive the selection it described.
|
||||
spKeysetSync();
|
||||
spPresetWarningRender();
|
||||
spTarListRender(null);
|
||||
return;
|
||||
}
|
||||
_genSpBuild();
|
||||
@@ -12152,7 +12354,7 @@ function spRefreshFromPreset(selId, keepTar) {
|
||||
const sel = document.getElementById(selId || 'sp-card-sel');
|
||||
const idx = sel ? parseInt(sel.value, 10) : NaN;
|
||||
if (isNaN(idx) || !cards[idx]) return '';
|
||||
if (selId === 'ram-card-sel') _spTarKey = 'tar';
|
||||
if (selId === 'ram-card-sel') _spTarKey = 'isd';
|
||||
cardsApply(idx, keepTar);
|
||||
return cards[idx].cntr;
|
||||
}
|
||||
@@ -20492,6 +20694,22 @@ const LANG_RU = {
|
||||
'Remove keyset': 'Удалить keyset',
|
||||
'Keyset': 'Keyset',
|
||||
'Keyset number (b8..b5 of KIc/KID)': 'Номер keyset (b8..b5 KIc/KID)',
|
||||
'ISD TAR': 'TAR ISD',
|
||||
'UICC RFM TAR': 'TAR UICC RFM',
|
||||
'ADF RFM TAR': 'TAR ADF RFM',
|
||||
'MSL': 'MSL',
|
||||
'MSL (Minimum SPI1, one hex byte)': 'MSL (минимальный SPI1, один hex-байт)',
|
||||
'TARs (MSL)': 'TAR (MSL)',
|
||||
'Each TAR carries its MSL (Minimum SPI1) — the card checks it before the security processing (TS 102 226 8.2.1.3.2.4); the packet SPI1 is derived from it.': 'У каждого TAR есть MSL (минимальный SPI1) — карта проверяет его до обработки защиты (TS 102 226 8.2.1.3.2.4); из него выводится SPI1 пакета.',
|
||||
'More TARs — each with its MSL and an optional description': 'Дополнительные TAR — у каждого свой MSL и необязательное описание',
|
||||
'Add TAR': 'Добавить TAR',
|
||||
'Remove TAR': 'Удалить TAR',
|
||||
'Description (optional)': 'Описание (необязательно)',
|
||||
'Every TAR needs three hex bytes': 'Каждый TAR должен состоять из трёх hex-байт',
|
||||
'Every TAR needs its MSL (one hex byte)': 'Каждому TAR нужен MSL (один hex-байт)',
|
||||
'Duplicate TAR in the preset': 'Повторяющийся TAR в пресете',
|
||||
'SPI1 is below the MSL of this TAR': 'SPI1 ниже MSL этого TAR',
|
||||
'(empty)': '(пусто)',
|
||||
'Keysets (number: KIc/KID — counter)': 'Keysets (номер: KIc/KID — счётчик)',
|
||||
'must be one hex byte': 'должен быть одним hex-байтом',
|
||||
'KIc and KID must carry the same keyset number (TS 102 225 A.2)': 'KIc и KID должны содержать один и тот же номер keyset (TS 102 225 A.2)',
|
||||
@@ -20540,6 +20758,7 @@ const LANG_RU = {
|
||||
'Keysets (number: KIc/KID — counter)': 'Keysets (номер: KIc/KID — счётчик)',
|
||||
'KIc and KID must carry the same keyset number (their high nibble) in the range 01-0F — TS 102 225 A.2; each keyset has its own counter on the card (Annex A.1).': 'KIc и KID должны содержать один и тот же номер keyset (старший ниббл) в диапазоне 01-0F — TS 102 225 A.2; у каждого keyset свой счётчик на карте (Annex A.1).',
|
||||
'Card presets are stored on the server. JSON export/import buttons are below the card list; a preset carries several keysets, each with its own number and counter.': 'Пресеты карт хранятся на сервере. Кнопки экспорта/импорта JSON — под списком карт; пресет содержит несколько keysets, у каждого свой номер и счётчик.',
|
||||
'Card presets are stored on the server. JSON export/import buttons are below the card list; a preset carries several keysets, each with its own number and counter, and a TAR table — every TAR with its MSL (Minimum SPI1).': 'Пресеты карт хранятся на сервере. Кнопки экспорта/импорта JSON — под списком карт; пресет содержит несколько keysets (у каждого свой номер и счётчик) и таблицу TAR — у каждого TAR свой MSL (минимальный SPI1).',
|
||||
'Save error': 'Ошибка сохранения',
|
||||
'Delete error': 'Ошибка удаления',
|
||||
'Counter not saved:': 'Счётчик не сохранён:',
|
||||
|
||||
Reference in New Issue
Block a user