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:
2026-09-28 22:01:54 +03:00
parent 3c104f5685
commit 991be7353b
5 changed files with 17 additions and 7 deletions
+8
View File
@@ -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');