fix: make the Explore Delete buttons send the GP DELETE APDU (v3.6.6)

ramDeleteFromExplorer() called an undefined `_ber_len()` helper, so clicking
Delete / Delete All raised a ReferenceError right after the confirm and no
APDU was ever sent.  The inline builder also omitted the mandatory Le byte.

- new pure `ramDeleteApdu(aid, withCascade)` = GP Card Spec v2.3.1 Table
  11-20/23 form: `80 E4 00 <p2> <Lc> 4F <len> <AID> 00` (P2 00 = object,
  80 = object and related objects), used by the Explore handler.
- `scp81DeleteApdus()` (the "Delete AID" script template) now shares the
  same builder: it used to send the raw AID without the mandatory '4F' TLV.
- tests: exact bytes for 7/16-byte AIDs and both P2 modes, the SCP81 script
  expectations updated to the TLV form, and a wiring check that the undefined
  helper call does not come back (`_ber_len(`).

611 frontend / 495 Python green; version 3.6.6; sw simple-v279.
This commit is contained in:
2026-09-28 00:42:37 +03:00
parent 40f20d539e
commit c39250f1b4
6 changed files with 45 additions and 15 deletions
+22 -9
View File
@@ -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() {
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v278';
const CACHE = 'simple-v279';
const URLS = [
'index.html',
'help.html',
+16 -1
View File
@@ -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');
});
+4 -2
View File
@@ -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'), []);
});
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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