From 991be7353bf876c9b763eb7fd45d865c45dd145a 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: Mon, 28 Sep 2026 22:01:54 +0300 Subject: [PATCH] fix: keep the full 5-byte OTA counter on increments (v3.6.23) spNextCntr coerced the 40-bit replay counter with `>>> 0`, so any counter above FFFFFFFF lost its top byte after an accepted send: the live 10000AAAC8 came back as 00000AAAC9, and spCntrSyncPreset then stored the broken value in the card preset - the RAM/Explore flows (ramIncrementCntr, which is correct) inherited it. - spNextCntr: 40-bit arithmetic (`% 0x10000000000`, wrap at FFFFFFFFFF kept) instead of the 32-bit >>> 0 coercion plus slice(-10). - spCntrAdjust (+/-): the same 40-bit wrap (an 11-digit value could be produced at the maximum). - tests: 10000AAAC8 -> 10000AAAC9, separator handling, FFFFFFFFF7 -> FFFFFFFFF8, FFFFFFFFFFF -> 0000000000 wrap, out-of-range input clamps. The server side (_increment_counter_hex, _ram_next_cntr, final_cntr) was already exact. 653 frontend / 500 Python green; version 3.6.23; sw simple-v296. --- frontend/index.html | 10 ++++++---- frontend/sw.js | 2 +- frontend/tests/sp.test.js | 8 ++++++++ pyproject.toml | 2 +- pysim_simple_server/server.py | 2 +- 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index e387dbd..1bdd1bd 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1711,7 +1711,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.6.22'; +const SIMPLE_VERSION = '3.6.23'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== Tab switching ===== @@ -4593,7 +4593,7 @@ function packToSp(sourceId) { function spCntrAdjust(delta) { const el = document.getElementById('sp-cntr'); let val = parseInt(el.value.replace(/[^0-9a-fA-F]/g, ''), 16) || 0; - val = Math.max(0, val + delta); + val = Math.max(0, val + delta) % 0x10000000000; // wrap the 5-byte counter el.value = val.toString(16).padStart(10, '0').toUpperCase(); } function spInvalidate() { @@ -8015,8 +8015,10 @@ async function pysimApdu() { function spNextCntr(value) { // The OTA counter must strictly increase for every secured packet sent // (replay protection): increment the 5-byte counter, carries included. - const v = (parseInt((value || '').replace(/[^0-9a-fA-F]/g, ''), 16) || 0) >>> 0; - return ((v + 1) >>> 0).toString(16).toUpperCase().padStart(10, '0').slice(-10); + // 5 bytes = 40 bits - a 32-bit `>>> 0` dropped the top byte of high + // counters (`10000AAAC8` -> `00000AAAC9`, v3.6.23). + const v = (parseInt((value || '').replace(/[^0-9a-fA-F]/g, ''), 16) || 0) % 0x10000000000; + return ((v + 1) % 0x10000000000).toString(16).toUpperCase().padStart(10, '0'); } function spCntrSyncPreset() { diff --git a/frontend/sw.js b/frontend/sw.js index a6ddbaa..5450908 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v295'; +const CACHE = 'simple-v296'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/sp.test.js b/frontend/tests/sp.test.js index 16a0e2f..bf1b743 100644 --- a/frontend/tests/sp.test.js +++ b/frontend/tests/sp.test.js @@ -253,6 +253,14 @@ test('spNextCntr increments with carry', () => { assert.strictEqual(spNextCntr('0000ABCDEF'), '0000ABCDF0'); }); +test('spNextCntr keeps the full 5 bytes (no 32-bit truncation)', () => { + assert.strictEqual(spNextCntr('10000AAAC8'), '10000AAAC9'); // the reported case + assert.strictEqual(spNextCntr('1000 0AAAC8'), '10000AAAC9'); // separators stripped + assert.strictEqual(spNextCntr('FFFFFFFFF7'), 'FFFFFFFFF8'); + assert.strictEqual(spNextCntr('FFFFFFFFFF'), '0000000000'); // 40-bit wrap + assert.strictEqual(spNextCntr('10000000000'), '0000000001'); // out of range clamps +}); + test('spNextCntr tolerates lower case and separators', () => { assert.strictEqual(spNextCntr('00000000 0a'), '000000000B'); assert.strictEqual(spNextCntr(''), '0000000001'); diff --git a/pyproject.toml b/pyproject.toml index 803d337..df1e296 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.6.22" +version = "3.6.23" 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 8dff77c..1dde36e 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE from osmocom.utils import rpad -VERSION = '3.6.22' +VERSION = '3.6.23' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE