-
OTAMan SIM OTA with a Human Face v2.1.8
+
OTAMan SIM OTA with a Human Face v2.1.9
@@ -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