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.
This commit is contained in:
+6
-4
@@ -1711,7 +1711,7 @@
|
|||||||
// ===== Version =====
|
// ===== Version =====
|
||||||
// Single source of truth for the PWA version: shown in the header and used
|
// Single source of truth for the PWA version: shown in the header and used
|
||||||
// by the server version check in pysimConnect().
|
// 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;
|
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||||
|
|
||||||
// ===== Tab switching =====
|
// ===== Tab switching =====
|
||||||
@@ -4593,7 +4593,7 @@ function packToSp(sourceId) {
|
|||||||
function spCntrAdjust(delta) {
|
function spCntrAdjust(delta) {
|
||||||
const el = document.getElementById('sp-cntr');
|
const el = document.getElementById('sp-cntr');
|
||||||
let val = parseInt(el.value.replace(/[^0-9a-fA-F]/g, ''), 16) || 0;
|
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();
|
el.value = val.toString(16).padStart(10, '0').toUpperCase();
|
||||||
}
|
}
|
||||||
function spInvalidate() {
|
function spInvalidate() {
|
||||||
@@ -8015,8 +8015,10 @@ async function pysimApdu() {
|
|||||||
function spNextCntr(value) {
|
function spNextCntr(value) {
|
||||||
// The OTA counter must strictly increase for every secured packet sent
|
// The OTA counter must strictly increase for every secured packet sent
|
||||||
// (replay protection): increment the 5-byte counter, carries included.
|
// (replay protection): increment the 5-byte counter, carries included.
|
||||||
const v = (parseInt((value || '').replace(/[^0-9a-fA-F]/g, ''), 16) || 0) >>> 0;
|
// 5 bytes = 40 bits - a 32-bit `>>> 0` dropped the top byte of high
|
||||||
return ((v + 1) >>> 0).toString(16).toUpperCase().padStart(10, '0').slice(-10);
|
// 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() {
|
function spCntrSyncPreset() {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simple-v295';
|
const CACHE = 'simple-v296';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -253,6 +253,14 @@ test('spNextCntr increments with carry', () => {
|
|||||||
assert.strictEqual(spNextCntr('0000ABCDEF'), '0000ABCDF0');
|
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', () => {
|
test('spNextCntr tolerates lower case and separators', () => {
|
||||||
assert.strictEqual(spNextCntr('00000000 0a'), '000000000B');
|
assert.strictEqual(spNextCntr('00000000 0a'), '000000000B');
|
||||||
assert.strictEqual(spNextCntr(''), '0000000001');
|
assert.strictEqual(spNextCntr(''), '0000000001');
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pysim-simple-server"
|
name = "pysim-simple-server"
|
||||||
version = "3.6.22"
|
version = "3.6.23"
|
||||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE
|
|||||||
from osmocom.utils import rpad
|
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
|
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user