From bab787fac13f8037b8e60be590a62a56c43840f9 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 03:16:10 +0300 Subject: [PATCH] fix: DELETE APDU is case 3 - no phantom command (v3.6.15) The Explore delete showed "Failed: por_ok - remote SW 6700" while the delete executed: ramDeleteApdu appended the trailing Le, and this card's SCP80 layer counts a trailing byte as a phantom second command whose SW 6700 masked the real result (count=2, last SW 6700) - the same quirk as INSTALL [for load] before v3.6.8. - ramDeleteApdu and the chain builder's DELETE row emit the case-3 APDU (4F AID TLV, no Le); the SCP81 Delete-AID template follows (it reuses ramDeleteApdu). - tests updated (delete APDU expectations in ram/ram_counter_flow/scripts). 641 frontend / 496 Python green; version 3.6.15; sw simple-v288. --- frontend/index.html | 18 +++++++++++------- frontend/sw.js | 2 +- frontend/tests/ram.test.js | 13 +++++++------ frontend/tests/ram_counter_flow.test.js | 2 +- frontend/tests/scripts.test.js | 4 ++-- pyproject.toml | 2 +- pysim_simple_server/server.py | 2 +- 7 files changed, 24 insertions(+), 19 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 569a8ac..4a49416 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1682,7 +1682,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.14'; +const SIMPLE_VERSION = '3.6.15'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== Tab switching ===== @@ -4194,7 +4194,9 @@ function chainRamBuildRowHex(idx, row) { var delMode = f.delMode || '00'; var df7 = '4F' + berLenStr(aid.length / 2) + aid; var dl7 = berLenStr(df7.length / 2); - return buildApdu(0x80, 0xE4, 0x00, parseInt(delMode, 16), parseInt(dl7, 16), df7) + '00'; + // case 3 (no Le), like the server's ramDeleteApdu: a trailing Le byte + // becomes a phantom command on the card's SCP80 layer (SW 6700). + return buildApdu(0x80, 0xE4, 0x00, parseInt(delMode, 16), parseInt(dl7, 16), df7); } if (cmd === 'get-status') { var gsMode = f.gsMode || '80'; @@ -8896,15 +8898,17 @@ async function ramExplore(sp) { // GP DELETE (Card Spec v2.3.1 11.2, Tables 11-20/11-23): P1=00 (last/only // command), P2.b8 selects "object" ('00') or "object and related objects" -// ('80'), the data field carries the mandatory '4F' AID TLV and Le is '00'. -// The old inline builder called an undefined length helper (`_ber_len`): a -// ReferenceError -// after the confirm - the Delete buttons never sent anything) and omitted Le. +// ('80'), the data field carries the mandatory '4F' AID TLV. The APDU is +// case 3 (no Le): a trailing Le byte makes the card's SCP80 layer count a +// phantom second command whose SW 6700 masks the real result (live +// 2026-09-28) - same as INSTALL/LOAD since v3.6.8. The old inline builder +// called an undefined length helper (`_ber_len`): a ReferenceError after the +// confirm - the Delete buttons never sent anything. function ramDeleteApdu(aid, withCascade) { const a = (aid || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); if (!a) return ''; const data = '4F' + berLenStr(a.length / 2) + a; - return '80E400' + (withCascade ? '80' : '00') + berLenStr(data.length / 2) + data + '00'; + return '80E400' + (withCascade ? '80' : '00') + berLenStr(data.length / 2) + data; } // A successful delete drops the object from the Explore result locally (the diff --git a/frontend/sw.js b/frontend/sw.js index fb27eff..f21c44f 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v287'; +const CACHE = 'simple-v288'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/ram.test.js b/frontend/tests/ram.test.js index 2f8d055..864487a 100644 --- a/frontend/tests/ram.test.js +++ b/frontend/tests/ram.test.js @@ -416,16 +416,17 @@ test('ramParseAppStatus keeps 16-byte AIDs (no rawLen-1 truncation)', () => { assert.strictEqual(apps[3].lifecycle, '07'); }); -test('ramDeleteApdu builds the GP DELETE with the 4F AID TLV and Le (F0414C46416101)', () => { +test('ramDeleteApdu builds the GP DELETE with the 4F AID TLV (F0414C46416101)', () => { // GP Card Spec v2.3.1 Table 11-20/23: P1=00, P2.b8 = object / object+related, - // data = '4F' AID TLV, Le=00. The old handler called an undefined helper - // and no APDU was ever sent. + // data = '4F' AID TLV, case 3 (no Le - a trailing Le byte becomes a + // phantom command on the card's SCP80 layer, live 2026-09-28). The old + // handler called an undefined helper and no APDU was ever sent. assert.strictEqual(ramDeleteApdu('F0414C46416101', false), - '80E40000094F07F0414C4641610100'); + '80E40000094F07F0414C46416101'); assert.strictEqual(ramDeleteApdu('F0414C46416101', true), - '80E40080094F07F0414C4641610100'); + '80E40080094F07F0414C46416101'); assert.strictEqual(ramDeleteApdu('A1130001180002FFF7100E8904000200', true), - '80E40080124F10A1130001180002FFF7100E890400020000'); + '80E40080124F10A1130001180002FFF7100E8904000200'); assert.strictEqual(ramDeleteApdu('', false), ''); // the dead helper reference must not come back assert.ok(!html.includes('_ber_len('), 'undefined _ber_len() call is back'); diff --git a/frontend/tests/ram_counter_flow.test.js b/frontend/tests/ram_counter_flow.test.js index 2852017..a32d8c6 100644 --- a/frontend/tests/ram_counter_flow.test.js +++ b/frontend/tests/ram_counter_flow.test.js @@ -77,7 +77,7 @@ test('accepted delete persists the consumed counter and drops the record', async decoded: { last_status_word: '9000' } }); await ramDeleteFromExplorer('F0414C46416101', false); assert.strictEqual(calls.sent.cntr, '0000000005'); - assert.strictEqual(calls.sent.apdu, '80E40000094F07F0414C4641610100'); + assert.strictEqual(calls.sent.apdu, '80E40000094F07F0414C46416101'); assert.strictEqual(calls.sent.spi2, '01', 'the computed SPI2 byte must be used'); assert.strictEqual(cards[0].cntr, '0000000006', 'the preset must carry the counter the card consumed'); diff --git a/frontend/tests/scripts.test.js b/frontend/tests/scripts.test.js index 4f649f4..1eafe6b 100644 --- a/frontend/tests/scripts.test.js +++ b/frontend/tests/scripts.test.js @@ -84,9 +84,9 @@ test('scriptsParseApdus accepts comments and whitespace, rejects bad lines', () test('scp81DeleteApdus builds GP DELETE APDUs per AID', () => { assert.deepStrictEqual(scp81DeleteApdus(['A000000003000000'], '00'), - ['80E400000A4F08A00000000300000000']); + ['80E400000A4F08A000000003000000']); assert.deepStrictEqual(scp81DeleteApdus(['A000000003000000', 'A000000100'], '80'), - ['80E400800A4F08A00000000300000000', '80E40080074F05A00000010000']); + ['80E400800A4F08A000000003000000', '80E40080074F05A000000100']); assert.deepStrictEqual(scp81DeleteApdus([], '00'), []); }); diff --git a/pyproject.toml b/pyproject.toml index 47d1f7e..a7a943a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.6.14" +version = "3.6.15" 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 98447c7..e04f467 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.14' +VERSION = '3.6.15' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE