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:
1 parent
174df8a8ad
commit
d0f8fb8f36
7 files changed
+229
-5
No files matched your search
+1
-1
@@ -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
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simtrace-analyser-v16';
|
const CACHE = 'simtrace-analyser-v17';
|
||||||
|
|
||||||
const PRECACHE = [
|
const PRECACHE = [
|
||||||
'index.html',
|
'index.html',
|
||||||
|
|||||||
+1
-1
@@ -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"}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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)."""
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user