fix: correct the GP privilege decode and remove the duplicate (v3.6.28)

The RAM explorer's ramFmtPrivileges used a private table with wrong bits and
invented names ("Cum Deletion Ctr", "GSM Card Binding", "Global PIN",
"RFM"/"CFM") and no case for 0x80: a live ISD 9E printed four wrong names
and dropped Security Domain.  Per GP Card Spec v2.3.1 11.1.2 Tables
11-7/8/9, 9E = Security Domain, Card Lock, Card Terminate, Card Reset, CVM
Management.

- ramFmtPrivileges now delegates to decodePrivileges (one spec table).
- decodePrivileges decodes byte 1's DAP pair per Table 11-7: b8+b7 = DAP
  Verification, b8+b7+b1 = Mandated DAP Verification; the flag bits alone
  are no privilege.
- The file had two decodePrivileges copies (the runtime used the later one,
  the C-APDU parser test the earlier); the duplicate and its private table
  are gone - the single implementation plus PRIVILEGE_NAMES live in the
  parser section, which the test evaluates (converted to var like PARSE_INS).
- Tests: 9E/9A live values, the DAP pair, byte 2/3 spot checks; the
  PRIVILEGE_NAMES spec pin moved to apdu_parse.

659 frontend / 504 Python green; version 3.6.28; sw simple-v301.
This commit is contained in:
2026-09-28 23:29:01 +03:00
parent 85ba2a709d
commit 90fe409dec
7 changed files with 74 additions and 72 deletions
+32 -59
View File
@@ -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
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v300';
const CACHE = 'simple-v301';
const URLS = [
'index.html',
'help.html',
+19 -1
View File
@@ -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'));
+20 -1
View File
@@ -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)');
-8
View File
@@ -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'));