diff --git a/frontend/index.html b/frontend/index.html index d51b445..4a998c0 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -18,7 +18,7 @@
-

OTAMan SIM OTA with a Human Face v2.1.8

+

OTAMan SIM OTA with a Human Face v2.1.9

@@ -512,7 +512,7 @@
- +
@@ -4761,6 +4761,25 @@ async function pysimApdu() { pysimRefresh(); } +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); +} + +function spCntrSyncPreset() { + // Persist the counter into the selected card preset: the OTA counter is + // part of the card's state and must survive reloads. + const sel = document.getElementById('sp-card-sel'); + const idx = sel ? parseInt(sel.value, 10) : NaN; + if (isNaN(idx) || !cards[idx]) return false; + cards[idx].cntr = document.getElementById('sp-cntr').value; + cardsSave(); + cardsRender(); + return true; +} + function spParams() { return { spi1: document.getElementById('sp-spi1').value, @@ -4835,22 +4854,16 @@ async function pysimSendOta() { porStatusEl.textContent = 'PoR: ' + por.response_status; porStatusEl.classList.remove('hidden', okPor ? 'text-red-600' : 'text-green-600'); porStatusEl.classList.add(okPor ? 'text-green-600' : 'text-red-600'); - if (okPor) { - // packet was accepted and executed - advance the counter so the - // same secured packet can never be sent twice (v1.9.6) - const cntrEl = document.getElementById('sp-cntr'); - cntrEl.value = ((parseInt(cntrEl.value, 16) || 0) + 1) - .toString(16).toUpperCase().padStart(10, '0').slice(-10); - document.getElementById('sp-result').value = ''; - // keep the selected card preset in sync with the new counter (v1.9.8) - const selIdx = parseInt(document.getElementById('sp-card-sel').value, 10); - if (!isNaN(selIdx) && cards[selIdx]) { - cards[selIdx].cntr = cntrEl.value; - cardsSave(); - cardsRender(); - } - } } + // The counter advances after every successful send, PoR or not: the + // card rejects a repeated counter (replay protection) and the same + // secured packet must never be sent twice (v1.9.6, revised 2.1.9). + const cntrEl = document.getElementById('sp-cntr'); + cntrEl.value = spNextCntr(cntrEl.value); + msg += ' | CNTR -> ' + cntrEl.value; + document.getElementById('sp-result').value = ''; + // keep the selected card preset in sync with the new counter (v1.9.8) + spCntrSyncPreset(); sendResultEl.textContent = msg; if (por && por.raw) { const rawLine = document.createElement('div'); diff --git a/frontend/sw.js b/frontend/sw.js index befdee6..bcbe342 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'otaman-v145'; +const CACHE = 'otaman-v146'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/sp.test.js b/frontend/tests/sp.test.js index a81b451..f150090 100644 --- a/frontend/tests/sp.test.js +++ b/frontend/tests/sp.test.js @@ -28,7 +28,7 @@ function extractFunc(src, name) { const FNS = ['hexToBytes', 'bytesToHex', 'des3Keys', 'des3EncryptBlock', 'des3CbcEncrypt', 'xorBytes', 'zeroPad', 'cbcMac', 'aesCbcEncrypt', 'aesShiftLeft1', 'aesCmacSubkeys', - 'aesCmac', 'genSp']; + 'aesCmac', 'genSp', 'spNextCntr']; let code = ''; for (const f of FNS) code += extractFunc(html, f) + '\n'; @@ -217,3 +217,15 @@ test('AES rejects 8-byte key', () => { }); assert.strictEqual(err, 'Error: AES KIc key must be 16, 24, or 32 bytes'); }); + +test('spNextCntr increments with carry', () => { + assert.strictEqual(spNextCntr('0000000001'), '0000000002'); + assert.strictEqual(spNextCntr('00000000FF'), '0000000100'); + assert.strictEqual(spNextCntr('000000FFFF'), '0000010000'); + assert.strictEqual(spNextCntr('0000ABCDEF'), '0000ABCDF0'); +}); + +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 af0602c..daf09b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-otaman-server" -version = "2.1.8" +version = "2.1.9" description = "HTTP REST server wrapping pysim for the OTAMan PWA" requires-python = ">=3.8" # pysim is a git-only dependency installed explicitly by setup.bat/setup.sh. diff --git a/pysim_otaman_server/server.py b/pysim_otaman_server/server.py index 9a2ede5..f5e10bc 100644 --- a/pysim_otaman_server/server.py +++ b/pysim_otaman_server/server.py @@ -21,7 +21,7 @@ from osmocom.construct import GsmOrUcs2Adapter from osmocom.tlv import BER_TLV_IE -VERSION = '2.1.8' +VERSION = '2.1.9' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE