From 7c2841ca0674c112e175e763a0c7153eca444fad 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 14:11:03 +0300 Subject: [PATCH] v3.13.4: Cards editor counter field: explicit heading, placeholder, validation The keyset table's column is headed "Counter (hex)" with the placeholder 0000000001, and cardsAdd refuses a save whose counter is not 1-10 hex digits (empty included; whitespace stripped) - the same rule the store enforces. The store keeps its import-compatible empty -> 1 default for API/import callers. Docs: repo AGENTS (v3.13.4 note). sw cache simple-v374. Tests: 667 Python + 797 frontend green. --- frontend/index.html | 14 +++++++++--- frontend/sw.js | 2 +- frontend/tests/cards_form.test.js | 37 ++++++++++++++++++++++++++----- frontend/tests/html.test.js | 4 ++-- pyproject.toml | 2 +- pysim_simple_server/server.py | 2 +- 6 files changed, 48 insertions(+), 13 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 8c58d51..3df769e 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1710,7 +1710,7 @@ KID KIc key KID key - Counter + Counter (hex) @@ -1877,7 +1877,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.13.3'; +const SIMPLE_VERSION = '3.13.4'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== Tab switching ===== @@ -11999,7 +11999,7 @@ function cardsKeysetRowHtml(ks, i) { '' + '' + '' + - '' + + '' + '' + ''; } @@ -12408,6 +12408,12 @@ async function cardsAdd() { alert(t('Keyset 00 means "no security" and is not a preset keyset')); return; } + // The counter is required: 1-10 hex digits - the same rule the server + // enforces (whitespace is stripped on both sides) + if (!/^[0-9A-F]{1,10}$/.test(ks.cntr.replace(/\s+/g, ''))) { + alert(t('Counter must be 1-10 hex digits') + ': ' + (ks.cntr || t('(empty)'))); + return; + } } const kvns = v.keysets.map(ks => spKeysetKvnOf(ks.kic)); if (new Set(kvns).size !== kvns.length) { @@ -20848,6 +20854,7 @@ const LANG_RU = { 'Test script running': 'Выполняется тест-скрипт', 'Preset': 'Пресет', 'Counter': 'Счётчик', + 'Counter (hex)': 'Счётчик (hex)', 'No run yet.': 'Запусков пока нет.', 'running': 'выполняется', 'Received': 'Получено', @@ -21124,6 +21131,7 @@ const LANG_RU = { 'Remove TAR': 'Удалить TAR', 'Description (optional)': 'Описание (необязательно)', 'Every TAR needs three hex bytes': 'Каждый TAR должен состоять из трёх hex-байт', + 'Counter must be 1-10 hex digits': 'Счётчик должен быть 1-10 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', diff --git a/frontend/sw.js b/frontend/sw.js index 828e032..0e0e25d 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v373'; +const CACHE = 'simple-v374'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/cards_form.test.js b/frontend/tests/cards_form.test.js index fd407fa..6f49ef0 100644 --- a/frontend/tests/cards_form.test.js +++ b/frontend/tests/cards_form.test.js @@ -337,7 +337,7 @@ test('the release field is collected, cleared and restored', () => { test('saving a preset posts the form with the pre-settled TAR table', async () => { const els = setup(); els['cards-name'].value = 'New card'; - ksRow(els, 0, { kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB' }); + ksRow(els, 0, { kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '0000000001' }); globalThis._cardsKeysetCount = 1; // the role fields are pre-settled by the markup; the test's fake elements // start empty - the defaults fill in @@ -347,7 +347,7 @@ test('saving a preset posts the form with the pre-settled TAR table', async () = assert.strictEqual(apiCalls[0].path, '/api/presets'); assert.strictEqual(apiCalls[0].body.name, 'New card'); assert.deepStrictEqual(apiCalls[0].body.keysets, [ - { kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '' }]); + { kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '0000000001' }]); assert.deepStrictEqual(apiCalls[0].body.tars, [ { role: 'isd', tar: '000000', msl: '16', desc: '' }, { role: 'uiccRfm', tar: 'B00000', msl: '16', desc: '' }, @@ -359,7 +359,7 @@ test('saving a preset posts the form with the pre-settled TAR table', async () = test('saving refuses a free TAR row without its MSL or with a duplicate TAR', async () => { const els = setup(); els['cards-name'].value = 'New card'; - ksRow(els, 0, { kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB' }); + ksRow(els, 0, { kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '0000000001' }); globalThis._cardsKeysetCount = 1; const seen = []; globalThis.alert = msg => seen.push(msg); @@ -381,7 +381,7 @@ test('editing a preset updates it by its store id', async () => { globalThis.cards = [presetFixture({ id: 'abc123', name: 'Old' })]; globalThis._cardsEditIdx = 0; els['cards-name'].value = 'Renamed'; - ksRow(els, 0, { kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB' }); + ksRow(els, 0, { kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '0000000001' }); globalThis._cardsKeysetCount = 1; await cardsAdd(); assert.strictEqual(apiCalls[0].path, '/api/presets/update'); @@ -629,7 +629,7 @@ test('editing opens the editor with the preset title', () => { test('the editor refuses PIN/PUK codes that are not 4-8 digits', async () => { const els = setup(); els['cards-name'].value = 'X'; - ksRow(els, 0, { kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB' }); + ksRow(els, 0, { kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '0000000001' }); globalThis._cardsKeysetCount = 1; const seen = []; globalThis.alert = msg => seen.push(msg); @@ -644,3 +644,30 @@ test('the editor refuses PIN/PUK codes that are not 4-8 digits', async () => { assert.strictEqual(apiCalls.length, 0); assert.match(seen[0], /PUK2/); }); + +test('the editor refuses a counter that is not 1-10 hex digits', async () => { + const els = setup(); + els['cards-name'].value = 'X'; + ksRow(els, 0, { kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: 'XYZ' }); + globalThis._cardsKeysetCount = 1; + const seen = []; + globalThis.alert = msg => seen.push(msg); + await cardsAdd(); + assert.strictEqual(apiCalls.length, 0); + assert.match(seen[0], /Counter must be 1-10 hex digits/); + // a valid value - spaces included, the server strips them - is posted + seen.length = 0; + ksRow(els, 0, { kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '00 00 00 00 01' }); + await cardsAdd(); + assert.strictEqual(apiCalls.length, 1, JSON.stringify(seen)); + assert.strictEqual(apiCalls[0].body.keysets[0].cntr, '00 00 00 00 01'); + // an empty counter is refused too (the value is required) + seen.length = 0; + apiCalls.length = 0; + els['cards-name'].value = 'X'; + globalThis._cardsKeysetCount = 1; + ksRow(els, 0, { kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '' }); + await cardsAdd(); + assert.strictEqual(apiCalls.length, 0); + assert.match(seen[0], /Counter must be 1-10 hex digits/); +}); diff --git a/frontend/tests/html.test.js b/frontend/tests/html.test.js index a9c1959..f893431 100644 --- a/frontend/tests/html.test.js +++ b/frontend/tests/html.test.js @@ -155,8 +155,8 @@ test('the card editor groups its fields into labelled fieldsets', () => { // ... and mirror the TAR table: a fixed-layout table with headers assert.match(scp80, //); assert.match(scp80, //); - for (const h of ['KIc', 'KID', 'KIc key', 'KID key', 'Counter']) { - assert.match(scp80, new RegExp('>' + h + '')); + for (const h of ['KIc', 'KID', 'KIc key', 'KID key', 'Counter (hex)']) { + assert.ok(scp80.includes('>' + h + ''), h); } assert.match(scp81, /id="cards-psk-id"/); assert.match(scp81, /id="cards-psk-key"/); diff --git a/pyproject.toml b/pyproject.toml index 4c8b984..f84dc67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.13.3" +version = "3.13.4" description = "HTTP REST server wrapping pysim for the SIMple PWA" requires-python = ">=3.8" # pysim is a git-only dependency installed explicitly by setup.bat/setup.sh. diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index 68cff38..ac08996 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -32,7 +32,7 @@ from osmocom.tlv import BER_TLV_IE from osmocom.utils import rpad -VERSION = '3.13.3' +VERSION = '3.13.4' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE