Extended logical channels: CLA 0-19, ATR card capabilities, MANAGE CHANNEL (v2.6.0)

- CLA decode (TS 102 221 §10.1.1/§10.3): standard channels 0-3 from b2-b1
  and the extended channels 4-19 from b4-b1.  The old further-format rule
  read only b2-b1 (channel 8 read as 6, 19 as 7) and the ETSI '11x0 xxxx'
  coding (0xC0-0xEF) was not handled at all; the class ranges now follow the
  specs (0x80-0x8F UICC, 0xA0-0xAF SIM, 0xC0-0xEF ETSI extended, 0x20-0x3F
  reserved, 0xFF invalid) and SM/chain are decoded per family — the SM
  extraction and the chaining bit (b5, not b6) were off by one bit
- per-channel state: database._decode_rows/_replay_context use the shared
  cla_channel() helper — before, CLA & 0x03 with no +4, so a channel-5
  SELECT clobbered channel 1's selection and a channel-8 one channel 0's
  (the local traces already open channels: 29 MANAGE CHANNEL opens, 1-2)
- COMPRION T=0 import: the synthesized GET RESPONSE keeps the channel —
  a plain CLA for 0-3 (as real terminals do) and 01x0 for 4-19
- ATR historical bytes: the UICC's COMPACT-TLV objects (TS 102 221 §6.3.1)
  with card service data, the card capabilities (ISO 7816-4 §8.1.1.2.7
  Table 88: assignment methods and max logical channels — the workspace
  cards announce 4 and "8 or more") and the status indicator ('00' trailing
  bytes / the '8Y' TLV); a truncated ATR no longer raises
- MANAGE CHANNEL success replies decode the assigned channel byte
- capture/import: a 'tpdu' packet that is not APDU-shaped (shorter than the
  header, CLA FF, ISO-reserved class) is skipped and counted, so a
  character-level or mis-sampled stream no longer pollutes the decode
  (the local DB's outlier sessions are exactly that)
- tests: CLA table vectors, per-channel state isolation on channels 8/12,
  COMPRION GR channels, filetrack extended channels, ATR compact-TLV and
  card-capability cases, the capture skip counter, the PWA rows; 527 Python
  + 47 Node tests
- README/README_RUS document the extended channels, card capabilities and
  the skip counter; PWA_VERSION/pyproject/version.py 2.6.0, SW v72
This commit is contained in:
catarrh committed 2026-10-17 22:42:11 +03:00
1 parent cbeeed3608
commit 64433f9739
18 files changed
+683 -88

No files matched your search

+25 -1
View File
@@ -1302,6 +1302,7 @@ function responseRows(resp) {
if (resp.life_cycle) rows.push(['Life cycle', resp.life_cycle]);
if (resp.file_size != null) rows.push(['File Size', resp.file_size + ' B']);
if (resp.short_ef_id) rows.push(['Short EF ID', resp.short_ef_id]);
if (resp.channel != null) rows.push(['Assigned channel', resp.channel]);
if (resp.type === 'GSM' || resp.type === '3G/EPS/5G') {
rows.push(['Auth', resp.type + (resp.status ? ' (' + resp.status + ')' : '')]);
@@ -1710,6 +1711,24 @@ function atrPpsRows(decoded) {
if (h.life_cycle) v += ' — ' + h.life_cycle;
if (h.sw) v += ' (SW ' + h.sw + ')';
rows.push(['Historical', v + ' · ' + h.raw]);
if (h.card_capabilities) {
const caps = h.card_capabilities;
const cv = [];
if (caps.max_channels != null) {
cv.push('logical channels: ' + (typeof caps.max_channels === 'number'
? 'up to ' + caps.max_channels : caps.max_channels));
}
if (caps.channel_assignment && caps.channel_assignment.length) {
cv.push('assigned ' + caps.channel_assignment.join(', '));
}
if (caps.command_chaining) cv.push('command chaining');
if (caps.extended_lc_le) cv.push('extended Lc/Le');
rows.push(['Card capabilities', cv.join(' · ') + (cv.length ? ' · ' : '') + caps.raw]);
}
if (h.objects && h.objects.length) {
const names = h.objects.map(o => o.name + (o.value ? ' (' + o.value + ')' : ''));
rows.push(['Historical objects', names.join(', ')]);
}
}
if (decoded.tck) {
rows.push(['TCK', '0x' + decoded.tck + (decoded.tck_valid ? ' (valid)' : ' (mismatch)')]);
@@ -2780,6 +2799,10 @@ function importSuccessText(data) {
if (data.time_base === 'application-layer') {
text += ' · ' + t('exact timestamps (Application Layer)');
}
if (data.skipped) {
text += ' · ' + t('{n} packets skipped (not APDU-shaped)')
.replace('{n}', data.skipped);
}
return text;
}
@@ -3168,7 +3191,7 @@ function onFilterChange() {
}
// ──────────────────── Versioning ────────────────────
const PWA_VERSION = '2.5.0';
const PWA_VERSION = '2.6.0';
const MIN_SERVER_VERSION = '2.3.0';
function parseVersion(v) {
@@ -3282,6 +3305,7 @@ const LANG_RU = {
'This looks like a COMPRION export — use \'Import COMPRION trace\'.':
'Похоже на экспорт COMPRION — используйте «Импорт трейсов COMPRION».',
'exact timestamps (Application Layer)': 'точные метки времени (ISO Application Layer)',
'{n} packets skipped (not APDU-shaped)': 'пропущено пакетов (не APDU): {n}',
'New capture': 'Новый захват',
'Tiles': 'Плитки',
'List': 'Список',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simtrace-analyser-v71';
const CACHE = 'simtrace-analyser-v72';
const PRECACHE = [
'index.html',
+7
View File
@@ -519,6 +519,13 @@ test('stopCaptureIfRunning tolerates "no active capture" (400)', async () => {
assert.equal(calls, 2);
});
test('importSuccessText mentions skipped non-APDU packets', () => {
const text = importSuccessText({name: 'x', message_count: 3, skipped: 2});
assert.match(text, /3 messages/);
assert.match(text, /2 packets skipped \(not APDU-shaped\)/);
assert.doesNotMatch(importSuccessText({name: 'x', message_count: 3}), /skipped/);
});
test('loadSession stops a capture tolerantly', () => {
const body = extractFunc(src, 'loadSession');
assert.match(body, /await stopCaptureIfRunning\(\)/);
+80
View File
@@ -0,0 +1,80 @@
// ATR/historical rows, the card-capabilities summary and the MANAGE CHANNEL
// response row (server-decoded fields rendered by the detail panel).
const { test } = require('node:test');
const assert = require('node:assert');
const { loadSrc, extractFunc } = require('./helpers.js');
const src = loadSrc();
eval(
extractFunc(src, 'ifaceValue') + '\n' +
extractFunc(src, 'atrPpsRows') + '\n' +
extractFunc(src, 'responseRows')
);
function row(rows, label) {
const found = rows.find(r => r[0] === label);
return found ? found[1] : undefined;
}
test('atrPpsRows: COMPACT-TLV historical and card capabilities', () => {
const rows = atrPpsRows({
type: 'atr',
historical: {
raw: '8031E073FE211B66D0022A761300',
category: 'COMPACT-TLV objects',
card_capabilities: {
raw: 'FE211B', tables: 3,
max_channels: 4,
channel_assignment: ['by the card', 'by the interface device'],
},
objects: [
{ tag: '3', name: 'Card service data', value: 'E0' },
{ tag: '7', name: 'Card capabilities', value: 'FE211B' },
{ tag: '6', name: 'Pre-issuing data', value: 'D0022A761300' },
],
},
});
assert.match(row(rows, 'Historical'), /COMPACT-TLV objects/);
const caps = row(rows, 'Card capabilities');
assert.match(caps, /logical channels: up to 4/);
assert.match(caps, /assigned by the card, by the interface device/);
assert.match(caps, /FE211B/);
assert.match(row(rows, 'Historical objects'), /Card service data \(E0\)/);
assert.match(row(rows, 'Historical objects'), /Pre-issuing data/);
});
test('atrPpsRows: "8 or more" logical channels', () => {
const rows = atrPpsRows({
type: 'atr',
historical: {
raw: '8031E073FE2157',
category: 'COMPACT-TLV objects',
card_capabilities: {
raw: 'FE2157', tables: 3,
max_channels: '8 or more',
channel_assignment: ['by the card'],
extended_lc_le: true,
},
},
});
const caps = row(rows, 'Card capabilities');
assert.match(caps, /logical channels: 8 or more/);
assert.match(caps, /extended Lc\/Le/);
});
test('atrPpsRows: a plain status-indicator historical stays short', () => {
const rows = atrPpsRows({
type: 'atr',
historical: { raw: '808105', category: 'COMPACT-TLV objects',
life_cycle: 'operational state (activated)' },
});
assert.match(row(rows, 'Historical'), /operational state \(activated\)/);
assert.equal(row(rows, 'Card capabilities'), undefined);
});
test('responseRows: MANAGE CHANNEL assigned channel', () => {
const html = responseRows({ channel: 5 });
assert.match(html, /Assigned channel/);
assert.match(html, /<td class="value">5<\/td>/);
});