From 5a3c4c352911509963c6531462093ff92c844371 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: Fri, 2 Oct 2026 10:27:42 +0300 Subject: [PATCH] v3.11.0 review fixes: ADM length, TAR re-render hygiene - the ADM input capped the key at 8 chars, but the server's _verify_adm takes 4-16 hex digits (padded to 8 CHV bytes) and the old input had no limit; maxlength=16 now, with a static guard in html.test.js. - cardsTarAdd/cardsTarRemove re-rendered the table from the save-path reader, so an emptied role field was silently refilled with its default. The renderer no longer invents defaults; a raw reader (cardsTarsFromTable) feeds Add/Remove and the language-switch refresh, while the save path (cardsTarsFromForm) keeps the default-filling for the mandatory roles. - the editor's dynamic TAR/keyset rows now rebuild on a language switch (refreshDynamicI18n) from what is typed - their placeholders and titles carry t() strings. (The title/Save label were already refreshed there.) - dropped the now-unused cardsRoleMsl helper and its test references. - docs: the ADM is documented as 4-16 hex digits (help EN/RU, api.md, AGENTS). sw cache simple-v362. Tests: 665 Python + 784 frontend green; the whole inline PWA script syntax-checked. --- docs/api.md | 2 +- frontend/help-ru.html | 2 +- frontend/help.html | 2 +- frontend/index.html | 57 ++++++++++++++++++---------- frontend/sw.js | 2 +- frontend/tests/cards_counter.test.js | 2 +- frontend/tests/cards_form.test.js | 15 ++++++-- frontend/tests/cards_iccid.test.js | 2 +- frontend/tests/html.test.js | 10 +++++ 9 files changed, 63 insertions(+), 31 deletions(-) diff --git a/docs/api.md b/docs/api.md index 849935d..c59bc8a 100644 --- a/docs/api.md +++ b/docs/api.md @@ -243,7 +243,7 @@ pass `spi1` itself, otherwise it is refused with `400` and a hint. **Card codes.** The optional `pin1`/`puk1`/`pin2`/`puk2`/`adm` fields are stored for later use (nothing consumes them yet). PIN/PUK must be **4–8 decimal digits** when set (empty is fine; anything else is refused with `400` -naming the field); the ADM key stays free-form (hex or ASCII digits). +naming the field); the ADM key is 4–16 hex digits (a decimal PIN is fine). **Counters.** The store is the source of truth for the SCP80 counters, one per keyset ("a dedicated counter shall be associated to each key version", diff --git a/frontend/help-ru.html b/frontend/help-ru.html index 6aaa1e9..b93fb3b 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -352,7 +352,7 @@ NameПонятная метка (обязательна) ICCIDОпциональный идентификатор карты. Кнопка С карты подставляет EF.ICCID подключённой карты (активна, только когда ICCID читается); при сохранении ICCID, уже присутствующий в списке, отклоняется (сравнение без пробелов и с учётом сырой hex-формы, конфликтующий пресет называется) - PIN1 / PUK1 / PIN2 / PUK2 / ADMНеобязательные коды карты, сохраняются для будущего использования (пока ничем не используются). PIN/PUK — строго 4–8 десятичных цифр (иначе сохранение отклоняется); ADM — администраторский ключ (hex или до 8 ASCII-цифр), используется файловым менеджером. Представлениями SCP80/SCP81 не используются + PIN1 / PUK1 / PIN2 / PUK2 / ADMНеобязательные коды карты, сохраняются для будущего использования (пока ничем не используются). PIN/PUK — строго 4–8 десятичных цифр (иначе сохранение отклоняется); ADM — администраторский ключ (4–16 hex-цифр, например десятичный PIN), используется файловым менеджером. Представлениями SCP80/SCP81 не используются KIc / KIDИндикаторы ключа и алгоритма (например, 15 = индекс 1, 3DES-CBC2; x2 = AES); задаются вместе с ключами TAR / MSL / DescriptionТаблица TAR: одна строка на TAR с его MSL (минимальный SPI1, TS 102 226 §8.2.1.3.2.4). Первые три строки — фиксированные роли (предзаполнены, не удаляются) с фиксированными описаниями; дополнительные TAR добавляются кнопкой Добавить TAR, несут необязательное описание и могут быть удалены. SPI1 пакета берётся из MSL его TAR ISD compact formatTAR Issuer Security Domain (TS 101 220, приложение D); используется для операций RAM/GP (представление SCP80 RAM, цепочки RAM/GP); по умолчанию по спецификации 000000, подставляется при пустом значении diff --git a/frontend/help.html b/frontend/help.html index 6436fae..f3492d8 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -351,7 +351,7 @@ NameHuman-readable label (required) ICCIDOptional card identifier. From card fills it with the equipped card's EF.ICCID (enabled only while a readable ICCID is known); saving refuses an ICCID that already exists in the list (compared ignoring spaces and the raw-hex form, and the conflicting preset is named) - PIN1 / PUK1 / PIN2 / PUK2 / ADMOptional card codes, all stored for later use (nothing consumes them yet). PIN/PUK must be 4–8 decimal digits (saving refuses anything else); ADM is the administrator key (hex, or up to 8 ASCII digits), used by the file manager. None is used by the SCP80/SCP81 views + PIN1 / PUK1 / PIN2 / PUK2 / ADMOptional card codes, all stored for later use (nothing consumes them yet). PIN/PUK must be 4–8 decimal digits (saving refuses anything else); ADM is the administrator key (4–16 hex digits, e.g. a decimal PIN), used by the file manager. None is used by the SCP80/SCP81 views KIc / KIDKey and algorithm indicators (e.g. 15 = index 1, 3DES-CBC2; x2 = AES), required together with the keys TAR / MSL / DescriptionThe TAR table: one uniform row per TAR with its MSL (Minimum SPI1, TS 102 226 §8.2.1.3.2.4). The first three rows are the fixed role entries (pre-settled, not removable) with their fixed descriptions; further TARs are added with Add TAR, carry an optional description and can be removed. A packet's SPI1 is derived from its TAR's MSL ISD compact formatIssuer Security Domain TAR (TS 101 220 Annex D); used for RAM/GP operations (SCP80 RAM view, RAM/GP chains); spec default 000000, prefilled when empty diff --git a/frontend/index.html b/frontend/index.html index b615def..d52229f 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1658,7 +1658,7 @@
- +

All codes are optional: PIN/PUK are 4-8 decimal digits (stored for later use by the card operations — not consumed yet); ADM is the administrator key.

@@ -11675,12 +11675,6 @@ function cardsRoleTar(preset, 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(); @@ -11918,15 +11912,16 @@ function cardsTarRowHtml(e, i) { } // Render the whole table from a preset's TAR list (roles first, then free -// rows); the role rows are always shown, with the pre-settled defaults when -// the list carries none. +// rows); the role rows are always shown - with the values given (a fresh form +// passes the pre-settled defaults). A re-render must keep what is displayed, +// so it never invents defaults here. function cardsTarRowsRender(entries) { const box = document.getElementById('cards-tars'); if (!box) return; const list = entries || []; const roles = CARDS_TAR_ROLES.map(role => { const e = list.find(x => x.role === role) || {}; - return { tar: e.tar || cardsRoleDefault(role), msl: e.msl || CARDS_MSL_DEFAULT }; + return { tar: e.tar || '', msl: e.msl || '' }; }); const free = list.filter(e => !e.role); _cardsTarCount = free.length; @@ -11935,13 +11930,13 @@ function cardsTarRowsRender(entries) { } function cardsTarAdd() { - cardsTarRowsRender(cardsTarsFromForm().concat([{ tar: '', msl: '', desc: '' }])); + cardsTarRowsRender(cardsTarsFromTable().concat([{ tar: '', msl: '', desc: '' }])); } // `i` is the table row index; the role rows are fixed and never removed. function cardsTarRemove(i) { if (i < CARDS_TAR_ROLES.length) return; - const rows = cardsTarsFromForm(); + const rows = cardsTarsFromTable(); rows.splice(i, 1); cardsTarRowsRender(rows); } @@ -11963,26 +11958,37 @@ function cardsFreeTarsFromForm() { return out; } -// The TAR table as preset entries: the three role entries from the first three -// rows (an emptied field keeps the pre-settled default) plus the free rows. -function cardsTarsFromForm() { +// The TAR table exactly as displayed: the role rows as typed (an emptied field +// stays empty) plus the free rows - the re-render source for Add/Remove, so an +// emptied field is not silently refilled with its default. +function cardsTarsFromTable() { const out = CARDS_TAR_ROLES.map((role, i) => { const get = id => { const el = document.getElementById('cards-tr-' + i + '-' + id); return el ? String(el.value).trim() : ''; }; - const tar = get('tar').replace(/\s/g, '').toUpperCase(); - const msl = get('msl').replace(/\s/g, '').toUpperCase(); - return { role: role, tar: tar || cardsRoleDefault(role), - msl: msl || CARDS_MSL_DEFAULT, desc: '' }; + return { role: role, + tar: get('tar').replace(/\s/g, '').toUpperCase(), + msl: get('msl').replace(/\s/g, '').toUpperCase(), desc: '' }; }); return out.concat(cardsFreeTarsFromForm()); } +// The TAR table as preset entries (the save path): an emptied role field keeps +// the pre-settled default - the three role entries are mandatory with a +// non-empty TAR and MSL. +function cardsTarsFromForm() { + return cardsTarsFromTable().map(e => e.role + ? { role: e.role, tar: e.tar || cardsRoleDefault(e.role), + msl: e.msl || CARDS_MSL_DEFAULT, desc: '' } + : e); +} + // The role rows back to their pre-settled defaults (TAR + MSL 16) and no free // rows. function cardsTarsReset() { - cardsTarRowsRender([]); + cardsTarRowsRender(CARDS_TAR_ROLES.map(role => ({ + role: role, tar: cardsRoleDefault(role), msl: CARDS_MSL_DEFAULT, desc: '' }))); } // ===== Keyset selectors (SCP80 / RAM) ===== @@ -21141,7 +21147,16 @@ function refreshDynamicI18n() { if (isViewVisible('profiler-scan-modal')) profilerScanRefreshOptions(); pysimUpdateCardActionLabels(); pysimUpdateStateIndicator(); - if (isViewVisible('tab-cards')) { cardsRender(); cardsSetFormMode(); } + if (isViewVisible('tab-cards')) { + cardsRender(); + cardsSetFormMode(); + // the editor's dynamic rows carry t() strings (placeholders, titles): + // rebuild them from what is typed, not from the defaults + if (isViewVisible('cards-editor-view')) { + cardsKeysetRowsRender(cardsKeysetsFromForm()); + cardsTarRowsRender(cardsTarsFromTable()); + } + } if (isViewVisible('scp81-sub-scripts')) { scriptsRender(); scriptsRebuildSelect(); } if (_ramExplorerData && isViewVisible('ram-explorer')) ramRenderExplorer(); // the CAP analysis bodies are built with t(); the legends are static diff --git a/frontend/sw.js b/frontend/sw.js index 0e5c049..f610dc6 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v361'; +const CACHE = 'simple-v362'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/cards_counter.test.js b/frontend/tests/cards_counter.test.js index 11cb17f..3eeaae0 100644 --- a/frontend/tests/cards_counter.test.js +++ b/frontend/tests/cards_counter.test.js @@ -28,7 +28,7 @@ function extractFunc(src, name) { } let code = ''; -for (const fn of ['cardsTarList', 'cardsRoleDefault', 'cardsRoleTar', 'cardsRoleMsl', +for (const fn of ['cardsTarList', 'cardsRoleDefault', 'cardsRoleTar', 'cardsTarEntry', 'cardsTarMsl', 'cardsApplyFields', 'cardsApply', 'spRefreshFromPreset', 'spPorAccepted', 'spNextCntr', 'spPresetIdx', 'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck', diff --git a/frontend/tests/cards_form.test.js b/frontend/tests/cards_form.test.js index 1a53c59..40c45e4 100644 --- a/frontend/tests/cards_form.test.js +++ b/frontend/tests/cards_form.test.js @@ -41,10 +41,10 @@ for (const fn of ['cardsFormValues', 'cardsClearForm', 'cardsEdit', 'cardsKeysetAdd', 'cardsKeysetRemove', 'cardsKeysetKvnUpdate', 'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply', 'spKeysetChanged', 'ramKeysetChanged', 'ramPresetIdx', 'tarPresetIdx', 'spKeysetGuard', 'ramKeysetGuard', - 'cardsTarList', 'cardsRoleDefault', 'cardsRoleTar', 'cardsRoleMsl', + 'cardsTarList', 'cardsRoleDefault', 'cardsRoleTar', 'cardsTarEntry', 'cardsTarMsl', 'cardsTarText', 'spMslWarningText', 'cardsTarRowHtml', 'cardsTarRowsRender', 'cardsTarAdd', 'cardsTarRemove', - 'cardsFreeTarsFromForm', 'cardsTarsFromForm', 'cardsTarsReset', + 'cardsFreeTarsFromForm', 'cardsTarsFromTable', 'cardsTarsFromForm', 'cardsTarsReset', 'cardsShowList', 'cardsShowEditor', 'cardsSetFormMode', 'cardsNew', 'cardsCancelEdit']) { code += extractFunc(html, fn, ASYNC_FNS.indexOf(fn) >= 0) + '\n'; @@ -181,7 +181,6 @@ test('the preset TAR table helpers read the role entries and the MSLs', () => { { role: 'usimRfm', tar: 'B00001', msl: '16' }, { tar: 'AF4D02', msl: '0A', desc: 'applet' }] }); assert.strictEqual(cardsRoleTar(p, 'isd'), 'AF4D01'); - assert.strictEqual(cardsRoleMsl(p, 'isd'), '1A'); assert.strictEqual(cardsTarMsl(p, 'af4d02'), '0A'); assert.strictEqual(cardsTarMsl(p, '123456'), ''); assert.strictEqual(cardsTarEntry(p, 'AF4D02').desc, 'applet'); @@ -538,7 +537,7 @@ test('the TAR table renders the role rows and adds/removes free rows', () => { cardsTarRowsRender([{ tar: 'AF4D01', msl: '0A', desc: 'one' }, { tar: 'AF4D02', msl: '0A', desc: 'two' }]); assert.strictEqual(_cardsTarCount, 2); - assert.match(els['cards-tars'].innerHTML, /cards-tr-0-tar"[^>]*value="000000"/); + assert.match(els['cards-tars'].innerHTML, /cards-tr-0-tar"[^>]*value=""/); assert.match(els['cards-tars'].innerHTML, /ISD compact format/); assert.match(els['cards-tars'].innerHTML, /cards-tr-3-tar"[^>]*value="AF4D01"/); assert.match(els['cards-tars'].innerHTML, /cards-tr-4-tar"[^>]*value="AF4D02"/); @@ -560,6 +559,14 @@ test('the TAR table renders the role rows and adds/removes free rows', () => { // the role rows are never removable cardsTarRemove(0); assert.strictEqual(_cardsTarCount, 2); + // an emptied role field survives a re-render (no silent default refill), + // while the save path still fills the default + tarRow(els, 0, { tar: '', msl: '' }); + cardsTarRemove(3); + assert.strictEqual(_cardsTarCount, 0); + assert.match(els['cards-tars'].innerHTML, /cards-tr-0-tar"[^>]*value=""/); + assert.strictEqual(cardsTarsFromForm()[0].tar, '000000'); + assert.strictEqual(cardsTarsFromForm()[0].msl, '16'); }); test('the list view opens the editor for a new preset and Cancel returns', () => { diff --git a/frontend/tests/cards_iccid.test.js b/frontend/tests/cards_iccid.test.js index 52c1876..979f846 100644 --- a/frontend/tests/cards_iccid.test.js +++ b/frontend/tests/cards_iccid.test.js @@ -32,7 +32,7 @@ for (const fn of ['swapNibbles', 'encIccid', 'decIccid', 'cardsNormIccid', 'cardsFindByIccid', 'cardsFindDuplicateIccid', 'cardsIccidFromCard', 'cardsAutoSelectByIccid', 'cardsRender', 'esc', 'spKeysetList', 'spKeysetKvnOf', - 'cardsTarList', 'cardsRoleDefault', 'cardsRoleTar', 'cardsRoleMsl', + 'cardsTarList', 'cardsRoleDefault', 'cardsRoleTar', 'cardsTarEntry', 'cardsTarMsl', 'cardsTarText', 'cardsScp81Complete']) { code += extractFunc(html, fn) + '\n'; } diff --git a/frontend/tests/html.test.js b/frontend/tests/html.test.js index c7bb4a4..9119f94 100644 --- a/frontend/tests/html.test.js +++ b/frontend/tests/html.test.js @@ -164,6 +164,16 @@ test('the card editor groups its fields into labelled fieldsets', () => { assert.ok(!fieldsets.some(f => f.includes('id="cards-name"'))); }); +test('the ADM input allows the full 4-16 hex digit key', () => { + // the server's _verify_adm takes 4-16 hex digits (padded to 8 CHV bytes); + // the input must not cap it at the PIN length + const m = /]*>/.exec(html); + assert.ok(m, 'the ADM input is missing'); + const limit = /maxlength="(\d+)"/.exec(m[0]); + assert.ok(!limit || parseInt(limit[1], 10) >= 16, + 'the ADM input must allow 4-16 hex digits (server _verify_adm)'); +}); + test('the Cards tab splits the preset list and the editor into views', () => { assert.match(html, /id="cards-list-view"/); assert.match(html, /id="cards-editor-view" class="hidden"/);