From 12da7ed59322b8c1bc6ade4140b158234d513149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Thu, 1 Oct 2026 21:41:07 +0300 Subject: [PATCH] ui: the keyset selector is disabled and cleared while no preset is selected MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A keyset only exists inside a card preset, but the SCP80/RAM/TAR keyset selectors stayed enabled with no preset selected - showing either "— no keyset —" or a stale "Keyset N — not defined in the preset" entry left over from an earlier preset. spKeysetSync() now disables the selector and reduces it to a "— select a card preset —" placeholder whenever its view has no preset selected, and re-enables/populates it as soon as one is. Clearing the preset dropdown ("— Select card —") also refreshed nothing: cardsApply() returned before the sync, so the stale keyset list stayed; it now syncs the selectors (and the preset warning) on the empty selection too. The three selects get disabled:opacity-40 disabled:cursor-not-allowed (the rules are already in the compiled style.css). Tests: keysets.test.js covers the disabled/cleared state and the re-enable plus a cardsApply source guard; cards_form.test.js and cards_counter.test.js gain tarPresetIdx in their sandboxes. Help EN/RU and both AGENTS files; sw cache simple-v343. --- frontend/help-ru.html | 4 +-- frontend/help.html | 4 +-- frontend/index.html | 50 +++++++++++++++++----------- frontend/sw.js | 2 +- frontend/tests/cards_counter.test.js | 2 +- frontend/tests/cards_form.test.js | 2 +- frontend/tests/keysets.test.js | 42 +++++++++++++++++++++++ 7 files changed, 79 insertions(+), 27 deletions(-) diff --git a/frontend/help-ru.html b/frontend/help-ru.html index 3fad878..522c65d 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -282,7 +282,7 @@

Верхнеуровневая вкладка SCP80 объединяет разделы, связанные с SCP80. Переключение — тремя переключателями: Secured Packet, RAM и TAR-сканер. Собирает защищённые пакеты SCP80 по ETSI TS 102 225. При подключении карты сервер читает её EF.ICCID (2FE2); если в пресете указан тот же номер (цифрами или сырым hex из EF), он автоматически выбирается во всех трёх представлениях SCP80. Вердикт низкого счётчика (cntr_low или предупреждающий ответ ENVELOPE, в котором всё ещё есть такой PoR) не исправляется повторной отправкой: любая операция SCP80/RAM немедленно останавливается, результат показывает счётчик карты и кнопку К пресету, открывающую соответствующий пресет на вкладке Cards, где счётчик нужно поднять выше — автоматически ничего не записывается.

3.1 Secured Packet

-

Собирает защищённые пакеты SCP80 по ETSI TS 102 225. Селектор Keyset рядом с выбором пресета указывает, из какого keyset берутся ключи и счётчик; отправка отклоняется, если в пресете нет такого номера keyset или KIc/KID несут разные номера (TS 102 225 A.2).

+

Собирает защищённые пакеты SCP80 по ETSI TS 102 225. Селектор Keyset рядом с выбором пресета указывает, из какого keyset берутся ключи и счётчик; отправка отклоняется, если в пресете нет такого номера keyset или KIc/KID несут разные номера (TS 102 225 A.2). Селектор недоступен, пока не выбран пресет карты.

Структура пакета

@@ -314,7 +314,7 @@

Кнопка Проверить в pySim сверяет собранный пакет с эталонной реализацией OtaDialectSms.encode_cmd. Кнопка Отправить на карту доставляет пакет через ENVELOPE SMS-PP-DOWNLOAD (при подключении к серверу). 2-байтовый префикс CPL передаётся, когда пакет зашифрован или несёт RC/CC/DS (входит в их входные данные), а также при конкатенации SMS; одиночное незащищённое SMS сохраняет форму pySim, начинающуюся с CHL (TS 31.115 Table 1 NOTE). Под полем пакета показывается его размер и число SMS; пакет, не помещающийся в одно SMS, отправляется конкатенированной загрузкой (TS 31.115 §4.3) — пакет делится на части SMS-пользовательских данных (первое SMS 132 октета, последующие по 134; первое дополнительно несёт информационные элементы конкатенации и CPI), и сегменты отправляются по порядку. Более 5 сегментов отклоняется: это практический предел буфера конкатенации карты. Полученный Proof of Receipt декодируется и показывается строкой статуса PoR (статус, TAR, счётчик, сырой PoR); статусное слово и данные ответа последней команды подставляются в подвкладку «Разбор R-APDU» (Remote APDU → «Разбор»), а успешный PoR увеличивает счётчик повторов (когда в SPI1 есть проверка счётчика — b5 b4 ≠ 00; без неё счётчик не увеличивается и не сохраняется) и очищает пакет.

3.2 RAM

-

Выполняет операции удалённого управления приложениями (Remote Application Management) как защищённые пакеты SCP80 через SMS-PP-DOWNLOAD ENVELOPE. Карта должна поддерживать SCP03 (AES или 3DES). Пресет карты со вкладки Карты обеспечивает SPI, ключи, TAR и счётчик (RAM использует ISD TAR пресета, по умолчанию 000000). Селектор Keyset рядом с выбором пресета указывает, из какого keyset берутся ключи и счётчик операции.

+

Выполняет операции удалённого управления приложениями (Remote Application Management) как защищённые пакеты SCP80 через SMS-PP-DOWNLOAD ENVELOPE. Карта должна поддерживать SCP03 (AES или 3DES). Пресет карты со вкладки Карты обеспечивает SPI, ключи, TAR и счётчик (RAM использует ISD TAR пресета, по умолчанию 000000). Селектор Keyset рядом с выбором пресета указывает, из какого keyset берутся ключи и счётчик операции. Селектор недоступен, пока не выбран пресет карты.

Операции

ПолеРазмерОписание
diff --git a/frontend/help.html b/frontend/help.html index 271c3c4..a3fb412 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -281,7 +281,7 @@

The SCP80 top-level tab groups the SCP80-related views. It is switched by three pills: Secured Packet, RAM and TAR probe. Assembles SCP80 secured packets per ETSI TS 102 225. When a card is equipped the server reads its EF.ICCID (2FE2); if a card preset carries the same number (digits, or the raw EF hex), it is selected automatically in all three SCP80 views.

3.1 Secured Packet

-

Builds SCP80 secured packets per ETSI TS 102 225. The Keyset selector next to the card preset selector picks which keyset of the preset supplies the keys and the counter; the send is refused when the preset does not define that keyset number or when its KIc/KID carry different numbers (TS 102 225 A.2).

+

Builds SCP80 secured packets per ETSI TS 102 225. The Keyset selector next to the card preset selector picks which keyset of the preset supplies the keys and the counter; the send is refused when the preset does not define that keyset number or when its KIc/KID carry different numbers (TS 102 225 A.2). It is disabled until a card preset is selected.

Packet structure

@@ -313,7 +313,7 @@

A “Verify vs pySim” button cross-checks the assembled packet against pySim’s reference OtaDialectSms.encode_cmd. A “Send to Card” button delivers it via SMS-PP-DOWNLOAD ENVELOPE (when connected to the server). The 2-byte CPL prefix is transmitted whenever the packet is ciphered or carries RC/CC/DS (it is part of their input) and whenever the packet is concatenated; a single unprotected SMS keeps pySim’s CHL-first form (TS 31.115 Table 1 NOTE). Under the packet field the app reports its size and how many SMS it takes; a packet that does not fit one SMS is sent as a concatenated download (TS 31.115 §4.3) — the packet is split into SMS user-data parts (first SM 132 octets, following ones 134; the first one additionally carries the concatenation and CPI information elements) and the segments are sent in order. More than 5 segments is refused, that being the practical limit of the card’s concatenation buffer. The returned Proof of Receipt is decoded and shown as a PoR status line (status, TAR, counter, raw PoR); the last command’s status word and response data are filled into the R-APDU parser panel, and a successful PoR advances the replay counter (when SPI1 carries a counter check — b5 b4 ≠ 00; without one no counter is advanced or saved) and clears the packet. A low-counter verdict (cntr_low, or a warning ENVELOPE answer that still carries such a PoR) is not recoverable by retrying: every SCP80/RAM operation stops immediately and the result states the card's counter with a Go to preset button that opens the matching preset in the Cards tab, where the counter must be raised above it — nothing is written automatically.

3.2 RAM

-

Delivers Remote Application Management operations as SCP80 secured packets via SMS-PP-DOWNLOAD ENVELOPE. The card must support SCP03 (AES or 3DES). A saved card preset from the Cards tab provides the SPI, keys, TAR, and counter (RAM uses the preset's ISD TAR, 000000 by default). The Keyset selector next to the preset selector picks the keyset whose keys and counter the operation uses.

+

Delivers Remote Application Management operations as SCP80 secured packets via SMS-PP-DOWNLOAD ENVELOPE. The card must support SCP03 (AES or 3DES). A saved card preset from the Cards tab provides the SPI, keys, TAR, and counter (RAM uses the preset's ISD TAR, 000000 by default). The Keyset selector next to the preset selector picks the keyset whose keys and counter the operation uses. It is disabled until a card preset is selected.

Operations

FieldSizeDescription
diff --git a/frontend/index.html b/frontend/index.html index b86a63d..031058c 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -529,7 +529,7 @@ - + @@ -696,7 +696,7 @@
- +
@@ -923,7 +923,7 @@
- +
@@ -11464,24 +11464,27 @@ function cardsKeysetsFromForm() { // matches their keyset number is selected; a number the preset does not define // shows as "not defined" (and the packet actions refuse it). function spKeysetSync() { - const spIdx = spPresetIdx(); - const spPreset = spIdx >= 0 ? cards[spIdx] : null; const kic = (document.getElementById('sp-kic-hex') || {}).value; const kid = (document.getElementById('sp-kid-hex') || {}).value; - const spSel = document.getElementById('sp-keyset-sel'); - if (spSel) spSel.innerHTML = spKeysetOptionsHtml(spPreset, kic, kid); - const ramSel = document.getElementById('ram-keyset-sel'); - if (ramSel) { - const ramIdx = ramPresetIdx(); - const ramPreset = ramIdx >= 0 ? cards[ramIdx] : null; - ramSel.innerHTML = spKeysetOptionsHtml(ramPreset, kic, kid); - } - const tarSel = document.getElementById('tar-keyset-sel'); - if (tarSel) { - const tarIdx = tarPresetIdx(); - const tarPreset = tarIdx >= 0 ? cards[tarIdx] : null; - tarSel.innerHTML = spKeysetOptionsHtml(tarPreset, kic, kid); - } + // A keyset only exists inside a card preset: with no preset selected the + // selector is disabled and cleared, so a stale "not defined in the preset" + // entry can never linger. + const sync = (selId, idxFn) => { + const sel = document.getElementById(selId); + if (!sel) return; + const idx = idxFn(); + const preset = idx >= 0 ? cards[idx] : null; + if (!preset) { + sel.disabled = true; + sel.innerHTML = ''; + return; + } + sel.disabled = false; + sel.innerHTML = spKeysetOptionsHtml(preset, kic, kid); + }; + sync('sp-keyset-sel', spPresetIdx); + sync('ram-keyset-sel', ramPresetIdx); + sync('tar-keyset-sel', tarPresetIdx); } function spKeysetApply(ks) { @@ -11915,7 +11918,13 @@ function cardsApplyFields(idx, keepTar) { } function cardsApply(idx, keepTar) { - if (!cardsApplyFields(idx, keepTar)) return; + if (!cardsApplyFields(idx, keepTar)) { + // "— Select card —": there is no keyset to choose, and the preset + // warning must not outlive the selection it described. + spKeysetSync(); + spPresetWarningRender(); + return; + } _genSpBuild(); spShowSizeInfo(); } @@ -19834,6 +19843,7 @@ const LANG_RU = { 'is not defined in the preset': 'не определён в пресете', 'not defined in the preset': 'не определён в пресете', '— no keyset —': '— нет keyset —', + '— select a card preset —': '— выберите пресет карты —', 'counter': 'счётчик', 'Define at least one keyset (KIc/KID and their keys)': 'Определите хотя бы один keyset (KIc/KID и его ключи)', 'Each keyset needs KIc, KID and both keys': 'Каждому набору нужны KIc, KID и оба ключа', diff --git a/frontend/sw.js b/frontend/sw.js index 6db5692..1f75208 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v342'; +const CACHE = 'simple-v343'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/cards_counter.test.js b/frontend/tests/cards_counter.test.js index e259b7e..74b0621 100644 --- a/frontend/tests/cards_counter.test.js +++ b/frontend/tests/cards_counter.test.js @@ -28,7 +28,7 @@ for (const fn of ['cardsTarValue', 'cardsApplyFields', 'cardsApply', 'spKeysetOptionsHtml', 'cardsKeysetRowHtml', 'cardsKeysetRowsRender', 'cardsKeysetAdd', 'cardsKeysetRemove', 'cardsKeysetKvnUpdate', 'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply', - 'spKeysetChanged', 'ramKeysetChanged', 'ramPresetIdx', 'spKeysetGuard', + 'spKeysetChanged', 'ramKeysetChanged', 'ramPresetIdx', 'tarPresetIdx', 'spKeysetGuard', 'ramKeysetGuard', ]) { code += extractFunc(html, fn) + '\n'; diff --git a/frontend/tests/cards_form.test.js b/frontend/tests/cards_form.test.js index f9bdf68..55b46ac 100644 --- a/frontend/tests/cards_form.test.js +++ b/frontend/tests/cards_form.test.js @@ -34,7 +34,7 @@ for (const fn of ['cardsTarValue', 'cardsFormValues', 'cardsClearForm', 'cardsEd 'spKeysetOptionsHtml', 'cardsKeysetRowHtml', 'cardsKeysetRowsRender', 'cardsKeysetAdd', 'cardsKeysetRemove', 'cardsKeysetKvnUpdate', 'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply', 'spKeysetChanged', - 'ramKeysetChanged', 'ramPresetIdx', 'spKeysetGuard', 'ramKeysetGuard']) { + 'ramKeysetChanged', 'ramPresetIdx', 'tarPresetIdx', 'spKeysetGuard', 'ramKeysetGuard']) { code += extractFunc(html, fn, ASYNC_FNS.indexOf(fn) >= 0) + '\n'; } code = 'var _cardsKeysetCount = 0;\nglobalThis.spPresetWarningRender = function() {};\n' + code; diff --git a/frontend/tests/keysets.test.js b/frontend/tests/keysets.test.js index b29b88f..703051d 100644 --- a/frontend/tests/keysets.test.js +++ b/frontend/tests/keysets.test.js @@ -175,3 +175,45 @@ test('the RAM and TAR views rebuild their keyset lists when their preset is know // opening the TAR pill populates both its selectors assert.ok(html.includes("if (name === 'tar') { tarRender(); tarProbeRender(); }")); }); + +test('the keyset selector is disabled and cleared while no preset is selected', () => { + const els = { + 'sp-keyset-sel': { value: '', innerHTML: '' }, + 'ram-keyset-sel': { value: '', innerHTML: '' }, + 'tar-keyset-sel': { value: '', innerHTML: '' }, + 'sp-card-sel': { value: '' }, + 'ram-card-sel': { value: '' }, + 'tar-card-sel': { value: '' }, + 'sp-kic-hex': { value: '' }, + 'sp-kid-hex': { value: '' }, + }; + globalThis.document = { getElementById: id => els[id] || null }; + globalThis.cards = []; + spKeysetSync(); + for (const id of ['sp-keyset-sel', 'ram-keyset-sel', 'tar-keyset-sel']) { + assert.strictEqual(els[id].disabled, true, id + ' must be disabled'); + assert.match(els[id].innerHTML, /select a card preset/); + assert.ok(!els[id].innerHTML.includes('not defined'), id); + } + // a selected preset enables the selector and lists its keysets + globalThis.cards = [{ name: 'EP', keysets: [ + { kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '0000000001' }] }]; + els['sp-card-sel'].value = '0'; + els['ram-card-sel'].value = '0'; + els['tar-card-sel'].value = '0'; + els['sp-kic-hex'].value = '15'; + els['sp-kid-hex'].value = '15'; + spKeysetSync(); + for (const id of ['sp-keyset-sel', 'ram-keyset-sel', 'tar-keyset-sel']) { + assert.strictEqual(els[id].disabled, false, id + ' must be enabled'); + assert.match(els[id].innerHTML, /value="1" selected/); + } +}); + +test('clearing the preset selection clears the keyset selectors', () => { + const at = html.indexOf('function cardsApply('); + assert.ok(at > 0); + const body = html.slice(at, at + 600); + assert.ok(body.includes('spKeysetSync();'), + 'cardsApply must sync the keyset selectors when no preset is selected'); +});