Decode command qualifiers for all CAT/SAT/USAT commands; bump to 1.14.0

Phase 3 of the spec audit: TS 102 223 §8.6 qualifier coverage for every
command that defines a coding (verified against V18.3.0 text):

- Value tables: SET UP CALL ('00-'05), LAUNCH BROWSER, GET READER
  STATUS, SET FRAMES, ENCAPSULATED SESSION CONTROL ('00-'04), LSI
  COMMAND ('00/'01).
- Bitmask decoders (deviations from default shown; all-default hidden;
  pure-RFU falls back to raw hex): SEND SHORT MESSAGE packing bit,
  PLAY TONE vibrate, DISPLAY TEXT / DISPLAY MULTIMEDIA MESSAGE
  priority + clear-delay, GET INKEY (alphabet/UCS2/Yes-No/immediate-
  digit/help), GET INPUT (alphabet/UCS2/hidden/packed/help),
  SELECT ITEM (presentation type/soft key/help), SET UP MENU,
  TIMER MANAGEMENT (start/deactivate/get value), LANGUAGE NOTIFICATION,
  OPEN CHANNEL (link establishment/reconnection/background/DNS with
  bearer-dependent note), CLOSE CHANNEL (NAA reuse note), SEND DATA,
  DECLARE SERVICE.
- RFU-byte commands (MORE TIME, POLL INTERVAL, SEND SS/USSD, ACTIVATE…)
  keep raw-hex fallback.

Tests: +11 TestQualifiers cases — 317 total. Version 1.14.0, SW cache
simtrace-analyser-v17.
This commit is contained in:
catarrh committed 2026-08-22 23:39:41 +03:00
1 parent 174df8a8ad
commit d0f8fb8f36
7 files changed
+229 -5

No files matched your search

+1 -1
View File
@@ -1868,7 +1868,7 @@ function onFilterChange() {
} }
// ──────────────────── Versioning ──────────────────── // ──────────────────── Versioning ────────────────────
const PWA_VERSION = '1.13.0'; const PWA_VERSION = '1.14.0';
const MIN_SERVER_VERSION = '1.0.0'; const MIN_SERVER_VERSION = '1.0.0';
function parseVersion(v) { function parseVersion(v) {
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simtrace-analyser-v16'; const CACHE = 'simtrace-analyser-v17';
const PRECACHE = [ const PRECACHE = [
'index.html', 'index.html',
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
[project] [project]
name = "simtrace2-pysniff" name = "simtrace2-pysniff"
version = "1.13.0" version = "1.14.0"
description = "Python-based sniffing tool for SIMtrace2 hardware (SIM card communication sniffer)" description = "Python-based sniffing tool for SIMtrace2 hardware (SIM card communication sniffer)"
readme = {file = "README.md", content-type = "text/markdown"} readme = {file = "README.md", content-type = "text/markdown"}
license = {text = "GPL-2.0-or-later"} license = {text = "GPL-2.0-or-later"}
+1 -1
View File
@@ -6,7 +6,7 @@ from .gsmtap import GsmtapSender, GsmtapReceiver
from .pcap import PcapWriter from .pcap import PcapWriter
from .dump import FileDumper, format_message from .dump import FileDumper, format_message
__version__ = '1.13.0' __version__ = '1.14.0'
__all__ = [ __all__ = [
'SniffSession', 'SniffSession',
'DeviceDisconnected', 'DeviceDisconnected',
+163
View File
@@ -1085,6 +1085,169 @@ def _idle_text_qualifier(qualifier):
COMMAND_QUALIFIERS[0x28] = _idle_text_qualifier COMMAND_QUALIFIERS[0x28] = _idle_text_qualifier
# ──────────────────── §8.6 bitmask qualifier decoders ────────────────────
# Each decoder lists the *deviations from default*; an all-default (0x00)
# qualifier stays hidden, a non-zero qualifier with only RFU bits set
# falls back to raw hex.
def _bitmask(fn):
def decoder(q):
val = fn(q)
parts = [val] if isinstance(val, str) else [p for p in val if p]
if parts:
return ', '.join(parts)
return None if q == 0 else f'0x{q:02X}'
return decoder
def _alphabet_bit(q):
return 'UCS2 alphabet requested' if q & 0x02 else None
def _help_bit(q):
return 'help information available' if q & 0x80 else None
@_bitmask
def _send_sm_qualifier(q):
return ['SMS packing by terminal required'] if q & 0x01 else []
@_bitmask
def _play_tone_qualifier(q):
return ['vibrate alert if available'] if q & 0x01 else []
@_bitmask
def _display_text_qualifier(q):
return [('high priority' if q & 0x01 else None),
('wait for user to clear message' if q & 0x80 else None)]
@_bitmask
def _get_inkey_qualifier(q):
return [
'alphabet set requested' if q & 0x01 else None,
_alphabet_bit(q),
'Yes/No response requested' if q & 0x04 else None,
'immediate digit response requested' if q & 0x08 else None,
_help_bit(q),
]
@_bitmask
def _get_input_qualifier(q):
return [
'alphabet set requested' if q & 0x01 else None,
_alphabet_bit(q),
'input shall not be revealed' if q & 0x04 else None,
'SMS packed format requested' if q & 0x08 else None,
_help_bit(q),
]
@_bitmask
def _select_item_qualifier(q):
pres = []
if q & 0x01:
pres.append('navigation options presentation'
if q & 0x02 else 'data values presentation')
return pres + ['selection using soft key preferred' if q & 0x04 else None,
_help_bit(q)]
@_bitmask
def _set_up_menu_qualifier(q):
return ['selection using soft key preferred' if q & 0x01 else None,
_help_bit(q)]
@_bitmask
def _timer_mgmt_qualifier(q):
op = (q >> 1) & 0x03
return {1: 'deactivate timer', 2: 'get current timer value'}.get(op, [])
@_bitmask
def _language_notification_qualifier(q):
return ['specific language notification'] if q & 0x01 else []
@_bitmask
def _open_channel_qualifier(q):
# Coding depends on the bearer; bits decoded per the packet-data /
# local bearer definition (TS 102 223 §8.6).
parts = [p for p in [
'immediate link establishment' if q & 0x01 else None,
'automatic reconnection' if q & 0x02 else None,
'background mode link establishment' if q & 0x04 else None,
'DNS server address(es) requested' if q & 0x08 else None] if p]
return [p + ' (bearer-dependent)' for p in parts]
@_bitmask
def _close_channel_qualifier(q):
# Packet data service: b1 = reuse Network Access Name indication;
# other bearers code b1 differently — hence the note.
if q & 0x01:
return ['next OPEN CHANNEL reuses same NAA/bearer (bearer-dependent)']
return []
@_bitmask
def _send_data_qualifier(q):
return ['send data immediately'] if q & 0x01 else []
@_bitmask
def _declare_service_qualifier(q):
return ['delete service from terminal database'] if q & 0x01 else []
@_bitmask
def _display_mms_qualifier(q):
return [('high priority' if q & 0x01 else None),
('wait for user to clear message' if q & 0x80 else None)]
COMMAND_QUALIFIERS.update({
0x10: {0x00: 'set up call, but only if not currently busy',
0x01: 'set up call, not busy, with redial',
0x02: 'set up call, holding all other calls',
0x03: 'set up call, holding others, with redial',
0x04: 'set up call, disconnecting all other calls (if any)',
0x05: 'set up call, disconnecting others, with redial'},
0x13: _send_sm_qualifier, # SEND SHORT MESSAGE
0x15: {0x00: 'launch browser if not already launched',
0x02: 'use existing browser (no secured session)',
0x03: 'close browser session and launch new'}, # LAUNCH BROWSER
0x20: _play_tone_qualifier, # PLAY TONE
0x21: _display_text_qualifier, # DISPLAY TEXT
0x22: _get_inkey_qualifier, # GET INKEY
0x23: _get_input_qualifier, # GET INPUT
0x24: _select_item_qualifier, # SELECT ITEM
0x25: _set_up_menu_qualifier, # SET UP MENU
0x27: _timer_mgmt_qualifier, # TIMER MANAGEMENT
0x33: {0x00: 'Card reader status',
0x01: 'Card reader identifier'}, # GET READER STATUS
0x35: _language_notification_qualifier, # LANGUAGE NOTIFICATION
0x40: _open_channel_qualifier, # OPEN CHANNEL
0x41: _close_channel_qualifier, # CLOSE CHANNEL
0x43: _send_data_qualifier, # SEND DATA
0x47: _declare_service_qualifier, # DECLARE SERVICE
0x50: {0x00: 'draw separator between adjoining frames',
0x01: 'no separator between frames'}, # SET FRAMES
0x62: _display_mms_qualifier, # DISPLAY MULTIMEDIA MESSAGE
0x73: {0x00: 'end encapsulated command session',
0x01: 'request Master SA setup',
0x02: 'request Connection SA setup',
0x03: 'request Secure Channel Start',
0x04: 'close Master/Connection SA, keep session'}, # eCAT
0x79: {0x00: 'Proactive Session Request',
0x01: 'UICC Platform Reset'}, # LSI COMMAND
})
def _command_qualifier(cmd_type, qualifier): def _command_qualifier(cmd_type, qualifier):
"""Decode a Command Qualifier (Command Details byte 3) into a string. """Decode a Command Qualifier (Command Details byte 3) into a string.
+61
View File
@@ -1779,6 +1779,67 @@ class TestEnvelopeDecoders(unittest.TestCase):
{'src': 'Network', 'dst': 'UICC'}) {'src': 'Network', 'dst': 'UICC'})
class TestQualifiers(unittest.TestCase):
"""Phase 3: §8.6 qualifier decoding for the remaining commands."""
def _fetch_qual(self, cmd_type, qual):
inner_hex = '810301%02x%02x820281828d00' % (cmd_type, qual)
inner = bytes.fromhex(inner_hex)
body = b'\xd0' + bytes([len(inner)]) + inner
r = decode_message(
bytes.fromhex('80120000%02x' % len(body)) + body + b'\x90\x00')
return r['cmd'].get('qualifier')
def test_set_up_call_value_table(self):
self.assertEqual(self._fetch_qual(0x10, 0x04),
'0x04 set up call, disconnecting all other calls (if any)')
def test_display_text_bits(self):
self.assertEqual(self._fetch_qual(0x21, 0x81),
'high priority, wait for user to clear message')
self.assertIsNone(self._fetch_qual(0x21, 0x00))
def test_get_inkey_bits(self):
self.assertEqual(self._fetch_qual(0x22, 0x0B),
'alphabet set requested, UCS2 alphabet requested, '
'immediate digit response requested')
def test_get_input_bits(self):
self.assertEqual(self._fetch_qual(0x23, 0x04),
'input shall not be revealed')
def test_select_item_presentation_and_help(self):
self.assertEqual(self._fetch_qual(0x24, 0x05),
'data values presentation, '
'selection using soft key preferred')
def test_timer_management_ops(self):
self.assertEqual(self._fetch_qual(0x27, 0x02), 'deactivate timer')
self.assertEqual(self._fetch_qual(0x27, 0x04),
'get current timer value')
self.assertIsNone(self._fetch_qual(0x27, 0x00)) # start = default
def test_send_sm_packing_bit(self):
self.assertEqual(self._fetch_qual(0x13, 0x01),
'SMS packing by terminal required')
def test_open_channel_bearer_dependent(self):
self.assertIn('immediate link establishment',
self._fetch_qual(0x40, 0x01))
self.assertIn('bearer-dependent', self._fetch_qual(0x40, 0x01))
def test_launch_browser_value_table(self):
self.assertEqual(self._fetch_qual(0x15, 0x03),
"0x03 close browser session and launch new")
def test_lsi_command_values(self):
self.assertEqual(self._fetch_qual(0x79, 0x01), '0x01 UICC Platform Reset')
def test_rfu_byte_commands_stay_raw(self):
self.assertEqual(self._fetch_qual(0x02, 0xFF), '0xFF') # MORE TIME
self.assertIsNone(self._fetch_qual(0x02, 0x00))
class TestSwUiccSpecific(unittest.TestCase): class TestSwUiccSpecific(unittest.TestCase):
"""TS 102 221 UICC-specific SWs (tables 10.7-10.15).""" """TS 102 221 UICC-specific SWs (tables 10.7-10.15)."""
+1 -1
View File
@@ -197,7 +197,7 @@ class TestImportEndpoint(unittest.TestCase):
class TestVersion(unittest.TestCase): class TestVersion(unittest.TestCase):
def test_package_version(self): def test_package_version(self):
from simtrace2_pysniff import __version__ from simtrace2_pysniff import __version__
self.assertEqual(__version__, '1.13.0') self.assertEqual(__version__, '1.14.0')
def test_status_includes_version(self): def test_status_includes_version(self):
import os import os