From 8de81a6a33064dbdb9e90b29218e8f7968d14e55 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, 31 Aug 2026 00:37:20 +0300 Subject: [PATCH] extract more details about installed ELFs --- frontend/index.html | 210 ++++++++++++++++++++++++---------- frontend/sw.js | 2 +- pysim_otaman_server/server.py | 10 +- 3 files changed, 156 insertions(+), 66 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 89df1d7..d5b5200 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -18,7 +18,7 @@
-

OTAMan SIM OTA with a Human Face v1.9.18

+

OTAMan SIM OTA with a Human Face v1.9.19

github @@ -3667,9 +3667,9 @@ function parseTLV(hex) { let i = 0; while (i < hex.length) { let tag = hex.substr(i, 2); i += 2; - if (tag >= '1F' && tag <= '7F') { tag += hex.substr(i, 2); i += 2; } - else if (tag >= '9F' && tag <= 'BF') { tag += hex.substr(i, 2); i += 2; } - else if (tag >= 'DF' && tag <= 'FF') { tag += hex.substr(i, 2); i += 2; } + const firstByte = parseInt(tag, 16); + // Multi-byte tags: bits 4-0 of first byte are 0x1F (ISO 7816) + if ((firstByte & 0x1F) === 0x1F) { tag += hex.substr(i, 2); i += 2; } let lenByte = parseInt(hex.substr(i, 2), 16); i += 2; let len; if (lenByte < 0x80) { len = lenByte; } @@ -3677,7 +3677,8 @@ function parseTLV(hex) { else if (lenByte === 0x82) { len = parseInt(hex.substr(i, 4), 16); i += 4; } else { len = lenByte; } const value = hex.substr(i, len * 2); i += len * 2; - const isConstructed = parseInt(tag, 16) >= 0xA0 && parseInt(tag, 16) < 0xC0; + // Constructed if bit 5 of first tag byte is set (ISO 7816) + const isConstructed = (firstByte & 0x20) !== 0; const children = isConstructed ? parseTLV(value) : null; tags.push({ tag, len, value, children, isConstructed }); } @@ -4339,39 +4340,109 @@ async function ramSendOta(apduHex, sp) { return await res.json(); } -// ===== Raw parsers for GET STATUS responses (P2=00 format) ===== -// Raw format: consecutive entries (no tag wrappers). -// Per GP Card Spec Table 11-33: AID is length-prefixed, lifecycle is 1 byte, -// privileges is 1 byte (bitmask). +// ===== TLV parsers for GET STATUS responses (P2=02 format) ===== +// P2=02 wraps each entry in an 'E3' GP Registry Data template containing TLV tags. +// Fallback: if the card only supports P2=00 (raw format), raw parsers are used. +function _parseE3Entry(hex) { + const children = parseTLV(hex || ''); + const r = {}; + for (const c of children) { + if (c.tag === '4F') r.aid = c.value.toUpperCase(); + else if (c.tag === '9F70') r.lifecycle = c.value.toUpperCase(); + else if (c.tag === 'C5') r.privileges = c.value.toUpperCase(); + else if (c.tag === 'CE') r.version = c.value.toUpperCase(); + else if (c.tag === 'CC') r.sdAid = c.value.toUpperCase(); + else if (c.tag === 'C4') r.elfAid = c.value.toUpperCase(); + else if (c.tag === 'CF') r.implicitSel = c.value.toUpperCase(); + else if (c.tag === '84') r.moduleAids = ramParseModuleAids(c.value); + } + return r; +} + +// Raw format (P2=00): ISD uses pure AID length, Apps use combined length (AID+lifecycle). +// Heuristic: length > 8 means combined (AID = length-1 bytes, lifecycle inside length). +function _parseRawAppEntry(hex, i) { + const rawLen = parseInt(hex.substr(i, 2), 16); + if (rawLen < 1 || i + 2 + rawLen * 2 + 2 > hex.length) return null; + const aidLen = rawLen > 8 ? rawLen - 1 : rawLen; + const aid = hex.substr(i + 2, aidLen * 2).toUpperCase(); + let j = i + 2 + rawLen * 2; + const lifecycle = hex.substr(j, 2).toUpperCase(); j += 2; + const privileges = hex.substr(j, 2).toUpperCase(); j += 2; + return { aid, lifecycle, privileges, next: j }; +} + +// Raw format (P2=00): ELF header then trailing bytes with module entries. +// P1=20: <00> +// P1=10: +// In both, a module entry starts with 0x10 (len=16) followed by 15-byte AID + lifecycle. +// Scan for 0x10 markers, advance past each full entry to avoid AID-internal false positives. +function _parseRawElfEntry(hex, i) { + const aidLen = parseInt(hex.substr(i, 2), 16); + if (aidLen < 1 || i + 2 + aidLen * 2 + 2 > hex.length) return null; + const aid = hex.substr(i + 2, aidLen * 2).toUpperCase(); + let j = i + 2 + aidLen * 2; + const lifecycle = hex.substr(j, 2).toUpperCase(); j += 2; + // Scan: when 0x10 found, validate 15-byte AID follows, then advance past full entry + const moduleAids = []; + const seen = new Set(); + while (j + 32 <= hex.length) { + const tag = parseInt(hex.substr(j, 2), 16); + if (tag === 0x10) { + const modAid = hex.substr(j + 2, 30).toUpperCase(); + if (!seen.has(modAid)) { seen.add(modAid); moduleAids.push(modAid); } + j += 32; // skip marker(1) + AID(15), continue past lifecycle+appCount + } else { + j += 2; + } + } + return { aid, lifecycle, moduleAids: moduleAids.length ? moduleAids : undefined, next: j }; +} + function ramParseAppStatus(hex) { + const s = (hex || '').toUpperCase(); + if (!s) return []; + // TLV format (P2=02): E3 templates with structured tags + if (s.length >= 4 && s.substr(0, 2) === 'E3') { + const tlvs = parseTLV(s); + return tlvs.filter(t => t.tag === 'E3').map(t => { + const r = _parseE3Entry(t.value); + r.type = 'app'; + return r; + }).filter(r => r.aid); + } + // Raw format (P2=00 fallback): consecutive const out = []; let i = 0; - const s = (hex || '').toUpperCase(); while (i + 6 <= s.length) { - const aidLen = parseInt(s.substr(i, 2), 16); - if (aidLen < 1 || i + 2 + aidLen * 2 + 4 > s.length) break; - const aid = s.substr(i + 2, aidLen * 2); - i += 2 + aidLen * 2; - const lifecycle = s.substr(i, 2); i += 2; - const privileges = s.substr(i, 2); i += 2; - out.push({ type: 'app', aid: aid.toUpperCase(), lifecycle, privileges }); + const r = _parseRawAppEntry(s, i); + if (!r) break; + out.push({ type: 'app', ...r }); + i = r.next; } return out; } -// ELF raw format (P2=00): per entry. -// Some cards may append version/module data but it's not guaranteed in raw mode. function ramParseElfStatus(hex) { + const s = (hex || '').toUpperCase(); + if (!s) return []; + // TLV format (P2=02): E3 templates with structured tags + if (s.length >= 4 && s.substr(0, 2) === 'E3') { + const tlvs = parseTLV(s); + return tlvs.filter(t => t.tag === 'E3').map(t => { + const r = _parseE3Entry(t.value); + r.type = 'elf'; + return r; + }).filter(r => r.aid); + } + // Raw format (P2=00 fallback): consecutive const out = []; let i = 0; - const s = (hex || '').toUpperCase(); while (i + 6 <= s.length) { - const aidLen = parseInt(s.substr(i, 2), 16); - if (aidLen < 1 || i + 2 + aidLen * 2 + 2 > s.length) break; - const aid = s.substr(i + 2, aidLen * 2); - i += 2 + aidLen * 2; - const lifecycle = s.substr(i, 2); i += 2; - out.push({ type: 'elf', aid: aid.toUpperCase(), lifecycle }); + const r = _parseRawElfEntry(s, i); + if (!r) break; + out.push({ type: 'elf', ...r }); + i = r.next; } return out; } @@ -4380,13 +4451,13 @@ function ramParseElfStatus(hex) { function ramParseModuleAids(hex) { const out = []; let j = 0; - const s = hex || ''; + const s = (hex || '').toUpperCase(); while (j + 4 <= s.length) { const t = s.substr(j, 2); const l = parseInt(s.substr(j + 2, 2), 16); const v = s.substr(j + 4, l * 2); j += 4 + l * 2; - if (t === '4F') out.push(v.toUpperCase()); + if (t === '4F') out.push(v); } return out; } @@ -4449,7 +4520,9 @@ function ramFmtPrivileges(hex) { return privs.length ? privs.join(', ') : '(none)'; } -// Merge ELF-module entries (P1=10, carry module AIDs) into ELF entries (P1=40) +// Merge ELF-module entries (P1=10, carry module AIDs) into ELF entries (P1=20). +// With TLV format (P2=02), module AIDs come from tag '84' inside E3 templates. +// With raw format (P2=00), module AIDs are not available in the response. function ramMergeElfData(elfs, modules) { const byAid = {}; elfs.forEach(e => { if (e.aid) byAid[e.aid] = e; }); @@ -4541,40 +4614,57 @@ async function ramExplore(sp) { // ELF data won't fit in the ENVELOPE response. const isElf = (p1 === '20' || p1 === '10'); const spi2 = isElf ? '21' : '01'; - let p2 = '00'; - let guard = 0; - while (guard++ < 32) { - const apdu = '80F2' + p1 + p2 + '024F0000' + 'C0000000'; - ramShowProgress(label + ' P1=' + p1 + ' P2=' + p2 + '...'); - const res = await ramSendOta(apdu, Object.assign({}, sp, { cntr, spi2 })); - cntr = ramIncrementCntr(cntr); - if (!res.success || !res.por || res.por.response_status !== 'por_ok') { - const errorMsg = res.por ? res.por.response_status : (res.error || 'no data'); - errors.push(label + ': ' + errorMsg); - return; - } - const data = res.por.decoded ? res.por.decoded.last_response_data : ''; - const sw = res.por.decoded ? res.por.decoded.last_status_word : ''; - // Defensive: 61XX means more data available (shouldn't happen with - // chained GET RESPONSE, but handle it if the card responds this way). - if (sw && sw.startsWith('61')) { - if (data) { - const parsed61 = parser(data); - if (parsed61.length) collector.push(...parsed61); + // Try TLV format (P2=02) first for structured data; fall back to raw + // (P2=00) if the card doesn't support TLV GET STATUS. + for (const p2Init of ['02', '00']) { + let p2 = p2Init; + let guard = 0; + let tlvFailed = false; + while (guard++ < 32) { + // P2=02: no data field (card returns all tags). P2=00: Lc=02 TagList=4F00 (ignored by card). + const dataField = p2 === '02' ? '' : '024F0000'; + const apdu = '80F2' + p1 + p2 + dataField + 'C0000000'; + ramShowProgress(label + ' P1=' + p1 + ' P2=' + p2 + '...'); + const res = await ramSendOta(apdu, Object.assign({}, sp, { cntr, spi2 })); + cntr = ramIncrementCntr(cntr); + if (!res.success || !res.por || res.por.response_status !== 'por_ok') { + const errorMsg = res.por ? res.por.response_status : (res.error || 'no data'); + errors.push(label + ': ' + errorMsg); + tlvFailed = true; + break; } + const data = res.por.decoded ? res.por.decoded.last_response_data : ''; + const sw = (res.por.decoded ? res.por.decoded.last_status_word : '').toUpperCase(); + // If first attempt with P2=02 gets an unsupported error, retry with P2=00. + if (guard === 1 && p2Init === '02' && (sw === '6A86' || sw === '6A88')) { + tlvFailed = true; + break; + } + // Defensive: 61XX means more data available (shouldn't happen with + // chained GET RESPONSE, but handle it if the card responds this way). + if (sw && sw.startsWith('61')) { + if (data) { + const parsed61 = parser(data); + if (parsed61.length) collector.push(...parsed61); + } + p2 = '01'; + continue; + } + if (sw === '6F00') { tlvFailed = true; break; } + if (!data) { + if (sw !== '9000') { + errors.push(label + ': (no data) — SW ' + sw); + tlvFailed = true; + } + break; + } + const parsed = parser(data); + if (!parsed.length) break; + collector.push(...parsed); + if (sw === '9000') break; p2 = '01'; - continue; } - if (sw === '6F00') break; - if (!data) { - if (sw !== '9000') errors.push(label + ': (no data) — SW ' + sw); - break; - } - const parsed = parser(data); - if (!parsed.length) break; - collector.push(...parsed); - if (sw === '9000') break; - p2 = '01'; + if (!tlvFailed) break; } } diff --git a/frontend/sw.js b/frontend/sw.js index c61eb62..c67fecc 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'otaman-v30'; +const CACHE = 'otaman-v31'; const URLS = [ 'index.html', 'help.html', diff --git a/pysim_otaman_server/server.py b/pysim_otaman_server/server.py index 88178f1..d0ac42d 100644 --- a/pysim_otaman_server/server.py +++ b/pysim_otaman_server/server.py @@ -18,7 +18,7 @@ from osmocom.construct import GsmOrUcs2Adapter from osmocom.tlv import BER_TLV_IE -VERSION = '1.9.18' +VERSION = '1.9.19' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE @@ -352,7 +352,7 @@ def _decode_por(spi1, spi2, kic, kid, cntr_hex, kic_key_hex, kid_key_hex, respon 'pcntr': res['pcntr'], 'rpl': res['rpl'], 'rhl': res['rhl'], - 'cc_rc': res['cc_rc'].hex(), + 'cc_rc': res['cc_rc'].hex().upper(), 'raw': response_hex, } @@ -1352,11 +1352,11 @@ class PysimHandler(BaseHTTPRequestHandler): self._send_json(resp) self._log_resp(resp) elif self.path == '/api/pli-qualifiers': - qualifiers = [{'code': '%02x' % q, 'name': PLI_QUALIFIER_NAMES[q]} for q in PLI_QUALIFIER_NAMES] + qualifiers = [{'code': '%02X' % q, 'name': PLI_QUALIFIER_NAMES[q]} for q in PLI_QUALIFIER_NAMES] self._send_json(qualifiers) self._log_resp(qualifiers) elif self.path == '/api/pli-dict': - resp = {('%02x' % q): v for q, v in _PLI_DATA.items()} + resp = {('%02X' % q): v for q, v in _PLI_DATA.items()} self._send_json(resp) self._log_resp(resp) elif self.path == '/api/poll-status': @@ -1890,7 +1890,7 @@ class PysimHandler(BaseHTTPRequestHandler): _PLI_DATA[code] = v except (ValueError, KeyError): pass - resp = {('%02x' % q): v for q, v in _PLI_DATA.items()} + resp = {('%02X' % q): v for q, v in _PLI_DATA.items()} self._send_json(resp) self._log_resp(resp) elif self.path == '/api/poll-toggle':