diff --git a/frontend/index.html b/frontend/index.html
index 55e72e8..c58db3a 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1713,7 +1713,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.27';
+const SIMPLE_VERSION = '3.6.28';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
@@ -5568,19 +5568,32 @@ function baseCompTag(tag) {
return (b >= 0x80 && b <= 0x9F) ? (b & 0x7F).toString(16).padStart(2, '0').toUpperCase() : tag;
}
+// GP Card Spec v2.3.1 11.1.2 privilege coding (Tables 11-7/8/9). Table 11-8
+// prints "Token Management" while the rest of the spec (and the tooling)
+// says "Token Verification" - the latter is used throughout. The table lives
+// in the C-APDU parser section because its test evaluates that slice alone.
+const PRIVILEGE_NAMES = [
+ ['Security Domain', 'DAP Verification', 'Delegated Management', 'Card Lock', 'Card Terminate', 'Card Reset', 'CVM Management', 'Mandated DAP Verification'],
+ ['Trusted Path', 'Authorized Management', 'Token Verification', 'Global Delete', 'Global Lock', 'Global Registry', 'Final Application', 'Global Service'],
+ ['Receipt Generation', 'Ciphered Load File Data Block', 'Contactless Activation', 'Contactless Self-Activation'],
+];
+
+// Byte 1's b7 flags "DAP Verification" and b1 turns it into "Mandated DAP
+// Verification" - the pair is decoded together so the flag bit cannot stand
+// alone (v3.6.28).
function decodePrivileges(hex) {
- if (hex.length < 2) return '';
- const names = [
- ['Security Domain', 'DAP Verification', 'Delegated Management', 'Card Lock', 'Card Terminate', 'Card Reset', 'CVM Management', 'Mandated DAP Verification'],
- ['Trusted Path', 'Authorized Management', 'Token Verification', 'Global Delete', 'Global Lock', 'Global Registry', 'Final Application', 'Global Service'],
- ['Receipt Generation', 'Ciphered Load File Data Block', 'Contactless Activation', 'Contactless Self-Activation'],
- ];
+ const raw = (hex || '').replace(/[^0-9a-fA-F]/g, '');
+ const bytes = [];
+ for (let i = 0; i + 1 < raw.length; i += 2) bytes.push(parseInt(raw.substr(i, 2), 16));
const parts = [];
- for (let i = 0; i < Math.min(names.length, hex.length / 2); i++) {
- const b = parseInt(hex.substr(i * 2, 2), 16);
- for (let bit = 7; bit >= 0; bit--) {
- const idx = 7 - bit;
- if (b & (1 << bit) && names[i][idx]) parts.push(names[i][idx]);
+ for (let b = 0; b < Math.min(PRIVILEGE_NAMES.length, bytes.length); b++) {
+ for (let bit = 0; bit < PRIVILEGE_NAMES[b].length; bit++) {
+ if (b === 0 && (bit === 1 || bit === 7)) continue;
+ if (bytes[b] & (0x80 >> bit) && PRIVILEGE_NAMES[b][bit]) parts.push(PRIVILEGE_NAMES[b][bit]);
+ }
+ if (b === 0 && (bytes[0] & 0xC0) === 0xC0) {
+ parts.splice(1, 0, (bytes[0] & 0x01)
+ ? 'Mandated DAP Verification' : 'DAP Verification');
}
}
return parts.join(', ') || 'None';
@@ -6401,12 +6414,6 @@ const LIFECYCLE_MAP = {
'8F': 'LOCKED (SD)',
};
-const PRIVILEGE_NAMES = [
- ['Security Domain', 'DAP Verification', 'Delegated Management', 'Card Lock', 'Card Terminate', 'Card Reset', 'CVM Management', 'Mandated DAP Verification'],
- ['Trusted Path', 'Authorized Management', 'Token Verification', 'Global Delete', 'Global Lock', 'Global Registry', 'Final Application', 'Global Service'],
- ['Receipt Generation', 'Ciphered Load File Data Block', 'Contactless Activation', 'Contactless Self-Activation'],
-];
-
function getSwCategory(cmd) {
if (!cmd) return 'generic';
return cmd.startsWith('ram-') ? 'gp' : 'uicc';
@@ -6559,21 +6566,6 @@ function decodeTlvValue(t) {
return '';
}
-function decodePrivileges(hex) {
- const bytes = [];
- for (let i = 0; i < hex.length; i += 2) bytes.push(parseInt(hex.substr(i,2),16));
- const names = [];
- for (let b = 0; b < Math.min(bytes.length, PRIVILEGE_NAMES.length); b++) {
- const byteNames = PRIVILEGE_NAMES[b] || [];
- for (let bit = 0; bit < byteNames.length; bit++) {
- if (bytes[b] & (0x80 >> bit)) {
- if (byteNames[bit]) names.push(byteNames[bit]);
- }
- }
- }
- return names.length ? names.join(', ') : 'None';
-}
-
function decodeResponse() {
const cmd = document.getElementById('resp-cmd').value;
const swHex = (document.getElementById('resp-sw').value || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase().padStart(4, '0').slice(0, 4);
@@ -8808,34 +8800,15 @@ function ramFmtLifecycle(hex) {
return RAM_LIFECYCLE[hex] || ('0x' + hex);
}
-// Decode GP Card Spec privileges TLV (tag C5) into human-readable strings
+// Decode the GP privileges value (tag C5 / compact listing) into
+// human-readable strings. Delegates to the shared GP decoder so the RAM view
+// and the C-APDU parser can never drift apart: the previous private table had
+// wrong bits and invented names like "Cum Deletion Ctr" (v3.6.28).
function ramFmtPrivileges(hex) {
- const p = hex || '';
+ const p = (hex || '').replace(/[^0-9a-fA-F]/g, '');
if (!p) return t('(none)');
- const bytes = p.match(/.{2}/g) || [];
- const privs = [];
- const b1 = parseInt(bytes[0] || '00', 16);
- if (b1 & 0x01) privs.push('Card Lock');
- if (b1 & 0x02) privs.push('Card Terminate');
- if (b1 & 0x04) privs.push('Card Reset');
- if (b1 & 0x08) privs.push('Cum Deletion Ctr');
- if (b1 & 0x10) privs.push('GSM Card Binding');
- if (b1 & 0x20) privs.push('Default Selected');
- if (b1 & 0x40) privs.push('Global PIN');
- const b2 = parseInt(bytes[1] || '00', 16);
- if (b2 & 0x01) privs.push('Mandated DAP');
- if (b2 & 0x02) privs.push('Security Domain');
- if (b2 & 0x04) privs.push('DAP Verification');
- if (b2 & 0x08) privs.push('Delegated Mgmt');
- if (b2 & 0x10) privs.push('RFM');
- if (b2 & 0x20) privs.push('CFM');
- const b3 = parseInt(bytes[2] || '00', 16);
- if (b3 & 0x01) privs.push('Receipt Gen');
- if (b3 & 0x02) privs.push('Ciphered Load');
- if (b3 & 0x04) privs.push('Delegated Perso');
- if (b3 & 0x08) privs.push('Trusted Path');
- if (b3 & 0x10) privs.push('Authorized Mgmt');
- return privs.length ? privs.join(', ') : t('(none)');
+ const decoded = decodePrivileges(p);
+ return decoded === 'None' ? t('(none)') : decoded;
}
// Render the full explorer result as structured HTML
diff --git a/frontend/sw.js b/frontend/sw.js
index bfee2fa..7bf971f 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'simple-v300';
+const CACHE = 'simple-v301';
const URLS = [
'index.html',
'help.html',
diff --git a/frontend/tests/apdu_parse.test.js b/frontend/tests/apdu_parse.test.js
index c4cc60b..b74212c 100644
--- a/frontend/tests/apdu_parse.test.js
+++ b/frontend/tests/apdu_parse.test.js
@@ -50,7 +50,9 @@ for (const c of consts) {
}
const start = html.indexOf('// ===== C-APDU Parser =====');
const end = html.indexOf('// ===== Response Parser =====');
-eval(prefix + html.slice(start, end).replace(/^const PARSE_INS = /m, 'var PARSE_INS = '));
+eval(prefix + html.slice(start, end)
+ .replace(/^const PARSE_INS = /m, 'var PARSE_INS = ')
+ .replace(/^const PRIVILEGE_NAMES = /m, 'var PRIVILEGE_NAMES = '));
test('Compact RAM INSTALL [for install] UICC', () => {
const tree = parseHexTree('80E60C00214F08A000000151000000C70100EA128010000000020101020200011603B0000100');
@@ -227,10 +229,26 @@ test('decodeTextData explicit DCS 08 with malformed payload returns ?', () => {
assert.strictEqual(decodeTextData('0841'), '?');
});
+test('PRIVILEGE_NAMES matches GPC v2.3 Tables 11-7/11-8/11-9', () => {
+ assert.strictEqual(PRIVILEGE_NAMES[0][7], 'Mandated DAP Verification');
+ assert.strictEqual(PRIVILEGE_NAMES[1][2], 'Token Verification');
+ assert.strictEqual(PRIVILEGE_NAMES[1][7], 'Global Service');
+ assert.strictEqual(PRIVILEGE_NAMES[2][0], 'Receipt Generation');
+ assert.strictEqual(PRIVILEGE_NAMES[2][3], 'Contactless Self-Activation');
+});
+
test('decodePrivileges byte 2 b6 is Token Verification per GPC v2.3 Table 11-8', () => {
assert.ok(decodePrivileges('0020').includes('Token Verification'));
});
+test('decodePrivileges byte 1: DAP Verification vs Mandated DAP (GP Table 11-7)', () => {
+ assert.strictEqual(decodePrivileges('80'), 'Security Domain');
+ assert.strictEqual(decodePrivileges('C0'), 'Security Domain, DAP Verification');
+ assert.strictEqual(decodePrivileges('C1'), 'Security Domain, Mandated DAP Verification');
+ assert.strictEqual(decodePrivileges('40'), 'None'); // b7 alone is no privilege
+ assert.strictEqual(decodePrivileges('01'), 'None'); // b1 alone is only the flag
+});
+
test('decodePrivileges', () => {
assert.ok(decodePrivileges('00').includes('None'));
assert.ok(decodePrivileges('80').includes('Security Domain'));
diff --git a/frontend/tests/ram.test.js b/frontend/tests/ram.test.js
index 85a4ad0..94e48ef 100644
--- a/frontend/tests/ram.test.js
+++ b/frontend/tests/ram.test.js
@@ -25,7 +25,7 @@ function extractFunc(src, name) {
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramStepLine', 'ramGetStatusApdu', 'ramDeleteApdu',
'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer', 'ramHasInstance', 'ramExpandedQueryApdu',
'_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'parseTLV', '_parseE3Entry',
- 'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute',
+ 'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges',
'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml'];
let code = '';
for (const f of FNS) {
@@ -35,6 +35,8 @@ const m = html.match(/const _chains = \{\};/);
if (m) code += m[0].replace(/^const /, 'var ') + '\n';
const lc = html.match(/const RAM_LIFECYCLE = \{[\s\S]*?\n\};/);
if (lc) code += lc[0].replace(/^const /, 'var ') + '\n';
+const pn = html.match(/const PRIVILEGE_NAMES = \[[\s\S]*?\n\];/);
+if (pn) code += pn[0].replace(/^const /, 'var ') + '\n';
const an = html.match(/const JC_AID_NAMES = \{[\s\S]*?\n\};/);
if (an) code += an[0].replace(/^const /, 'var ') + '\n';
const ar = html.match(/const JC_AID_RIDS = \{[\s\S]*?\n\};/);
@@ -231,6 +233,23 @@ test('ramRenderExploreHtml annotates standard package AIDs, vendor AIDs stay bar
assert.ok(out.includes('(GlobalPlatform RID)'), out);
});
+test('ramFmtPrivileges decodes the GP privilege bytes (live ISD values)', () => {
+ global.t = s => s;
+ // GP Table 11-7: 0x9E = Security Domain + Card Lock + Card Terminate +
+ // Card Reset + CVM Management (the live ISD privilege byte)
+ assert.strictEqual(ramFmtPrivileges('9E'),
+ 'Security Domain, Card Lock, Card Terminate, Card Reset, CVM Management');
+ assert.strictEqual(ramFmtPrivileges('9A'),
+ 'Security Domain, Card Lock, Card Terminate, CVM Management');
+ // DAP Verification (b8+b7) vs Mandated DAP Verification (b8+b7+b1)
+ assert.strictEqual(ramFmtPrivileges('C0'), 'Security Domain, DAP Verification');
+ assert.strictEqual(ramFmtPrivileges('C1'), 'Security Domain, Mandated DAP Verification');
+ // byte 2 / byte 3 spot checks
+ assert.strictEqual(ramFmtPrivileges('0001'), 'Global Service');
+ assert.strictEqual(ramFmtPrivileges('000080'), 'Receipt Generation');
+ delete global.t;
+});
+
test('ramFmtPrivileges uses the translated (none) placeholder', () => {
global.t = s => 'XX' + s;
assert.strictEqual(ramFmtPrivileges(''), 'XX(none)');
diff --git a/frontend/tests/response_map.test.js b/frontend/tests/response_map.test.js
index fbef7b5..cc5f16c 100644
--- a/frontend/tests/response_map.test.js
+++ b/frontend/tests/response_map.test.js
@@ -48,14 +48,6 @@ test('LIFECYCLE_MAP per GPC v2.3 life cycles', () => {
assert.strictEqual(LIFECYCLE_MAP['83'], 'LOCKED (SD)');
});
-test('PRIVILEGE_NAMES matches GPC v2.3 Tables 11-7/11-8/11-9', () => {
- assert.strictEqual(PRIVILEGE_NAMES[0][7], 'Mandated DAP Verification');
- assert.strictEqual(PRIVILEGE_NAMES[1][2], 'Token Verification');
- assert.strictEqual(PRIVILEGE_NAMES[1][7], 'Global Service');
- assert.strictEqual(PRIVILEGE_NAMES[2][0], 'Receipt Generation');
- assert.strictEqual(PRIVILEGE_NAMES[2][3], 'Contactless Self-Activation');
-});
-
test('TS 51.011 SIM families resolve (94xx/98xx/92xx/9Exx/9Fxx)', () => {
assert.ok(lookupSw('94', '04', 'uicc').includes('not found'));
assert.ok(lookupSw('94', '00', 'uicc').includes('No EF selected'));
diff --git a/pyproject.toml b/pyproject.toml
index 54cacd3..89d1210 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
-version = "3.6.27"
+version = "3.6.28"
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 b051791..4d20840 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.27'
+VERSION = '3.6.28'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE