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:
2026-10-02 08:28:46 +03:00
parent f059d05040
commit 6360dc5bae
19 changed files with 1187 additions and 269 deletions
+300 -81
View File
@@ -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:': 'Счётчик не сохранён:',