diff --git a/frontend/index.html b/frontend/index.html index 42095d6..893bb75 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1665,7 +1665,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.5'; +const SIMPLE_VERSION = '3.6.6'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== Tab switching ===== @@ -8834,6 +8834,19 @@ async function ramExplore(sp) { ramRenderExplorer(); } +// 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. +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'; +} + async function ramDeleteFromExplorer(aid, withCascade) { const sp = getRamSpParams(); if (!sp.kicKey || !sp.kidKey) { @@ -8842,9 +8855,8 @@ async function ramDeleteFromExplorer(aid, withCascade) { } const label = withCascade ? t('Delete') + ' (' + t('cascade') + ')' : t('Delete'); if (!confirm(label + ' — ' + t('AID:') + ' ' + aid + '?')) return; - const p2 = withCascade ? '80' : '00'; - const aidLen = (aid.length / 2).toString(16).padStart(2, '0'); - const apdu = '80E400' + p2 + _ber_len(2 + aid.length / 2) + '4F' + aidLen + aid; + const apdu = ramDeleteApdu(aid, withCascade); + if (!apdu) return; ramShowProgress(label + ' ' + aid + '...'); const res = await ramSendOta(apdu, sp); ramHideProgress(); @@ -12215,11 +12227,12 @@ async function scriptsGenerateInstall() { } function scp81DeleteApdus(aids, p2) { - // GP DELETE (Card Spec 2.3.1 Table 11-20/22): one APDU per AID, P2.b8 - // selects "object only" ('00') or "object and related objects" ('80'). - return (aids || []).map(aid => - '80E4' + (p2 || '00') + '00' + - (aid.length / 2).toString(16).padStart(2, '0').toUpperCase() + aid + '00'); + // GP DELETE (Card Spec v2.3.1 Table 11-20/23): one APDU per AID, P2.b8 + // selects "object only" ('00') or "object and related objects" ('80'); + // the data field carries the mandatory '4F' AID TLV and Le is '00' + // (the same builder the RAM Explore delete uses). + return (aids || []).map(aid => ramDeleteApdu(aid, (p2 || '00') === '80')) + .filter(a => a); } function scriptsGenerateDelete() { diff --git a/frontend/sw.js b/frontend/sw.js index f4558ba..e4d2c5d 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v278'; +const CACHE = 'simple-v279'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/ram.test.js b/frontend/tests/ram.test.js index 4eefc38..6feac18 100644 --- a/frontend/tests/ram.test.js +++ b/frontend/tests/ram.test.js @@ -22,7 +22,7 @@ function extractFunc(src, name) { } // Extract chain builder functions and dependencies -const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramStepLine', 'ramGetStatusApdu', +const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramStepLine', 'ramGetStatusApdu', 'ramDeleteApdu', '_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'parseTLV', '_parseE3Entry', 'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml']; @@ -414,3 +414,18 @@ 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)', () => { + // 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. + assert.strictEqual(ramDeleteApdu('F0414C46416101', false), + '80E40000094F07F0414C4641610100'); + assert.strictEqual(ramDeleteApdu('F0414C46416101', true), + '80E40080094F07F0414C4641610100'); + assert.strictEqual(ramDeleteApdu('A1130001180002FFF7100E8904000200', true), + '80E40080124F10A1130001180002FFF7100E890400020000'); + 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/scripts.test.js b/frontend/tests/scripts.test.js index 8865493..4f649f4 100644 --- a/frontend/tests/scripts.test.js +++ b/frontend/tests/scripts.test.js @@ -26,6 +26,8 @@ function extractFunc(src, name) { global.t = (s) => s; eval(extractFunc(html, 'cardsPskMap')); eval(extractFunc(html, 'scriptsParseApdus')); +eval(extractFunc(html, 'berLenStr')); +eval(extractFunc(html, 'ramDeleteApdu')); eval(extractFunc(html, 'scp81DeleteApdus')); eval(extractFunc(html, 'scp81LogLine')); eval(extractFunc(html, 'scp81LogEntryHtml')); @@ -82,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'), - ['80E4000008A00000000300000000']); + ['80E400000A4F08A00000000300000000']); assert.deepStrictEqual(scp81DeleteApdus(['A000000003000000', 'A000000100'], '80'), - ['80E4800008A00000000300000000', '80E4800005A00000010000']); + ['80E400800A4F08A00000000300000000', '80E40080074F05A00000010000']); assert.deepStrictEqual(scp81DeleteApdus([], '00'), []); }); diff --git a/pyproject.toml b/pyproject.toml index 4d42927..952489e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.6.5" +version = "3.6.6" 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 fd99002..d20f051 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.5' +VERSION = '3.6.6' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE