Fix CAT registries per TS 102 223 V18.3/TS 31.111 V18.12; decode all ENVELOPE types
Spec audit found three registries drifted from an old release, plus
missing command/envelope coverage:
- EVENT_TYPES: correct from '14' up — Access Technology Change
(multiple), IMS Registration/Incoming data, Profile Container ('19),
Secured Profile Container, Poll Interval Negotiation, Data Connection
Status Change, CAG cell selection, Slices Status Change ('1F);
'20-'22 reserved. Old table wrongly showed LTE D2D/ECall/SCEF names.
- TR_RESULTS: session/user results live at '10-'14 ('14 = USSD/SS
transaction terminated), not '0A-'0E; single '20 = ME currently
unable (per-cause details are additional-info bytes); add '22-'27,
'3A BIP error; unshift '3B-'3D; '39 permanent-problem wording.
- ENVELOPE_TYPES: add D9 USSD Download, DA/DB MMS, DC Terminal
Applications, DD Geo Location Reporting, DE Envelope Container,
DF ProSe Report, E0 5G ProSe Report, E1-E3 reserved, E4 GSMA.
- CAT_COMMAND_TYPES: add '79 LSI COMMAND; F0-FE render as Proprietary.
- PLI_QUALIFIERS: add '07 ESN, '1A Supported RATs.
Phase 2:
- _decode_envelope: MENU SELECTION (item id + help request), TIMER
EXPIRATION (id + semi-octet h/m:s value), CALL CONTROL (address +
CCP), USSD DOWNLOAD (DCS text), GEO LOCATION REPORTING, ENVELOPE
CONTAINER (recursive inner dispatch); unknown inner TLVs preserved
in raw_tlv for every type.
- Device Identities now decode to names (Keypad/Display/UICC/
Terminal/Network/Card reader n/Channel n/eCAT client) in proactive
commands and envelopes; frontend renders "src → dst".
Tags verified against ETSI TS 101 220 registry (help request '15/'95,
timer id/value '24/'A4, USSD string '0A/'8A). Tests: +16 spec-derived
fixtures, one stale expectation corrected — 306 total. Version 1.13.0,
SW cache simtrace-analyser-v16.
This commit is contained in:
1 parent
fac6bd09af
commit
174df8a8ad
8 files changed
+432
-71
No files matched your search
@@ -17,8 +17,10 @@ that both captures APDU traffic and serves that PWA.
|
|||||||
- Status words — full ISO 7816-4 set plus UICC-specific values:
|
- Status words — full ISO 7816-4 set plus UICC-specific values:
|
||||||
`91XX` (proactive command pending), `62F1/F2/F3`, `63F1/F2` (more data),
|
`91XX` (proactive command pending), `62F1/F2/F3`, `63F1/F2` (more data),
|
||||||
`9300` (SAT busy), `9850`, `9862–64`
|
`9300` (SAT busy), `9850`, `9862–64`
|
||||||
- SIM Toolkit (CAT) — proactive commands with decoded qualifiers,
|
- SIM Toolkit — SAT (GSM 11.14), CAT (TS 102 223) and USAT (TS 31.111)
|
||||||
TERMINAL RESPONSE, ENVELOPE (Menu Selection, Call Control, SMS-PP download…)
|
proactive commands with decoded qualifiers, TERMINAL RESPONSE, ENVELOPE
|
||||||
|
(Menu Selection, Call Control, SMS-PP download…); tolerates known
|
||||||
|
non-compliant legacy-card quirks and preserves unrecognized TLVs
|
||||||
- SMS TPDU — SMS-DELIVER/SUBMIT, SCTS timestamps, UDH information elements
|
- SMS TPDU — SMS-DELIVER/SUBMIT, SCTS timestamps, UDH information elements
|
||||||
- SCP80 OTA secured packets — SPI bits (ciphering, PoR requirement/mode,
|
- SCP80 OTA secured packets — SPI bits (ciphering, PoR requirement/mode,
|
||||||
RC/CC/DS, counter), KIc/KID algorithm + key set, TAR, CNTR/PCNTR;
|
RC/CC/DS, counter), KIc/KID algorithm + key set, TAR, CNTR/PCNTR;
|
||||||
|
|||||||
+18
-2
@@ -651,7 +651,11 @@ function cmdRows(cmd) {
|
|||||||
if (cmd.subaddress) rows.push(['Subaddress', cmd.subaddress]);
|
if (cmd.subaddress) rows.push(['Subaddress', cmd.subaddress]);
|
||||||
if (cmd.smsc) rows.push(['SMSC', cmd.smsc]);
|
if (cmd.smsc) rows.push(['SMSC', cmd.smsc]);
|
||||||
if (cmd.tp_da) rows.push(['TP-DA', cmd.tp_da]);
|
if (cmd.tp_da) rows.push(['TP-DA', cmd.tp_da]);
|
||||||
if (cmd.device_ids) rows.push(['Device IDs', cmd.device_ids]);
|
if (cmd.device_ids) {
|
||||||
|
const di = cmd.device_ids;
|
||||||
|
rows.push(['Device IDs', typeof di === 'string' ? di
|
||||||
|
: (di.src + ' → ' + di.dst)]);
|
||||||
|
}
|
||||||
if (cmd.cb_page) {
|
if (cmd.cb_page) {
|
||||||
const cb = cmd.cb_page;
|
const cb = cmd.cb_page;
|
||||||
if (cb.serial) rows.push(['CB serial', cb.serial]);
|
if (cb.serial) rows.push(['CB serial', cb.serial]);
|
||||||
@@ -676,6 +680,18 @@ function cmdRows(cmd) {
|
|||||||
if (cmd.duration) rows.push(['Duration', cmd.duration.value + ' ' + cmd.duration.unit]);
|
if (cmd.duration) rows.push(['Duration', cmd.duration.value + ' ' + cmd.duration.unit]);
|
||||||
if (cmd.address) rows.push(['Address', cmd.address]);
|
if (cmd.address) rows.push(['Address', cmd.address]);
|
||||||
if (cmd.response_length) rows.push(['Response length', cmd.response_length.min + '…' + cmd.response_length.max]);
|
if (cmd.response_length) rows.push(['Response length', cmd.response_length.min + '…' + cmd.response_length.max]);
|
||||||
|
if (cmd.item_id !== undefined) rows.push(['Item id', '0x' + hx(cmd.item_id, 2)]);
|
||||||
|
if (cmd.help) rows.push(['Help', 'requested']);
|
||||||
|
if (cmd.timer_id !== undefined) rows.push(['Timer id', '0x' + hx(cmd.timer_id, 2)]);
|
||||||
|
if (cmd.timer_value) rows.push(['Timer value', cmd.timer_value]);
|
||||||
|
if (cmd.ussd) rows.push(['USSD string', cmd.ussd]);
|
||||||
|
if (cmd.encapsulated && cmd.encapsulated.type) {
|
||||||
|
rows.push(['Encapsulated', cmd.encapsulated.type]);
|
||||||
|
}
|
||||||
|
if (cmd.text_note) rows.push(['Text note', cmd.text_note]);
|
||||||
|
if (cmd.raw_tlv) {
|
||||||
|
for (const t of cmd.raw_tlv) rows.push(['TLV 0x' + t.tag, t.value || '(empty)']);
|
||||||
|
}
|
||||||
|
|
||||||
// SM TPDU (nested in SMS-PP download / SEND SHORT MESSAGE)
|
// SM TPDU (nested in SMS-PP download / SEND SHORT MESSAGE)
|
||||||
const t = cmd.tpdu;
|
const t = cmd.tpdu;
|
||||||
@@ -1852,7 +1868,7 @@ function onFilterChange() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ──────────────────── Versioning ────────────────────
|
// ──────────────────── Versioning ────────────────────
|
||||||
const PWA_VERSION = '1.11.0';
|
const PWA_VERSION = '1.13.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-v14';
|
const CACHE = 'simtrace-analyser-v16';
|
||||||
|
|
||||||
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.11.0"
|
version = "1.13.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.11.0'
|
__version__ = '1.13.0'
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'SniffSession',
|
'SniffSession',
|
||||||
'DeviceDisconnected',
|
'DeviceDisconnected',
|
||||||
|
|||||||
@@ -956,6 +956,7 @@ CAT_COMMAND_TYPES = {
|
|||||||
0x71: 'CONTACTLESS STATE CHANGED',
|
0x71: 'CONTACTLESS STATE CHANGED',
|
||||||
0x72: 'COMMAND CONTAINER',
|
0x72: 'COMMAND CONTAINER',
|
||||||
0x73: 'ENCAPSULATED SESSION CONTROL',
|
0x73: 'ENCAPSULATED SESSION CONTROL',
|
||||||
|
0x79: 'LSI COMMAND',
|
||||||
0x81: 'END OF PROACTIVE SESSION',
|
0x81: 'END OF PROACTIVE SESSION',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -982,6 +983,19 @@ ENVELOPE_TYPES = {
|
|||||||
0xD5: 'MO SHORT MESSAGE CONTROL',
|
0xD5: 'MO SHORT MESSAGE CONTROL',
|
||||||
0xD6: 'EVENT DOWNLOAD',
|
0xD6: 'EVENT DOWNLOAD',
|
||||||
0xD7: 'TIMER EXPIRATION',
|
0xD7: 'TIMER EXPIRATION',
|
||||||
|
# TS 101 220 table 7.17 + TS 31.111 §9.1
|
||||||
|
0xD9: 'USSD DOWNLOAD',
|
||||||
|
0xDA: 'MMS TRANSFER STATUS',
|
||||||
|
0xDB: 'MMS NOTIFICATION',
|
||||||
|
0xDC: 'TERMINAL APPLICATIONS',
|
||||||
|
0xDD: 'GEOGRAPHICAL LOCATION REPORTING',
|
||||||
|
0xDE: 'ENVELOPE CONTAINER',
|
||||||
|
0xDF: 'PROSE REPORT',
|
||||||
|
0xE0: '5G PROSE REPORT',
|
||||||
|
0xE1: 'Reserved for 3GPP',
|
||||||
|
0xE2: 'Reserved for 3GPP',
|
||||||
|
0xE3: 'Reserved for 3GPP',
|
||||||
|
0xE4: 'GSMA',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1027,6 +1041,7 @@ PLI_QUALIFIERS = {
|
|||||||
0x04: 'Language setting',
|
0x04: 'Language setting',
|
||||||
0x05: 'Timing Advance',
|
0x05: 'Timing Advance',
|
||||||
0x06: 'Access Technology (single)',
|
0x06: 'Access Technology (single)',
|
||||||
|
0x07: 'ESN of the terminal',
|
||||||
0x08: 'IMEISV',
|
0x08: 'IMEISV',
|
||||||
0x09: 'Search Mode',
|
0x09: 'Search Mode',
|
||||||
0x0A: 'Battery charge state',
|
0x0A: 'Battery charge state',
|
||||||
@@ -1042,6 +1057,7 @@ PLI_QUALIFIERS = {
|
|||||||
0x15: 'Slices information',
|
0x15: 'Slices information',
|
||||||
0x16: 'CAG information list',
|
0x16: 'CAG information list',
|
||||||
0x17: 'Rejected slices information',
|
0x17: 'Rejected slices information',
|
||||||
|
0x1A: 'Supported Radio Access Technologies',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1095,6 +1111,8 @@ def _command_details_name(value):
|
|||||||
"""Decode a Command Details TLV value into the command type name."""
|
"""Decode a Command Details TLV value into the command type name."""
|
||||||
if len(value) < 2:
|
if len(value) < 2:
|
||||||
return None
|
return None
|
||||||
|
if 0xF0 <= value[1] <= 0xFE: # TS 102 223 §9.4 — reserved for proprietary use
|
||||||
|
return f'Proprietary ({value[1]:#04x})'
|
||||||
return CAT_COMMAND_TYPES.get(value[1])
|
return CAT_COMMAND_TYPES.get(value[1])
|
||||||
|
|
||||||
|
|
||||||
@@ -1157,31 +1175,38 @@ TR_RESULTS = {
|
|||||||
0x07: 'Command performed with modification',
|
0x07: 'Command performed with modification',
|
||||||
0x08: 'REFRESH performed but indicated NAA was not active',
|
0x08: 'REFRESH performed but indicated NAA was not active',
|
||||||
0x09: 'Command performed successfully, tone not played',
|
0x09: 'Command performed successfully, tone not played',
|
||||||
0x0A: 'Proactive UICC session terminated by the user',
|
0x10: 'Proactive UICC session terminated by the user',
|
||||||
0x0B: 'Backward move in the proactive UICC session requested by the user',
|
0x11: 'Backward move in the proactive UICC session requested by the user',
|
||||||
0x0C: 'No response from user',
|
0x12: 'No response from user',
|
||||||
0x0D: 'Help information required by the user',
|
0x13: 'Help information required by the user',
|
||||||
0x0E: 'USSD/SS transaction terminated by the user',
|
0x14: 'USSD or SS transaction terminated by the user',
|
||||||
0x10: 'Terminal currently unable to process command (screen busy)',
|
# TS 102 223 §8.12.1 / TS 31.111: '15'-'16' reserved for 3GPP;
|
||||||
0x11: 'Terminal currently unable to process command (busy on call)',
|
# per-cause "terminal unable" details are Additional information
|
||||||
0x12: 'Terminal currently unable to process command (USSD/SS ongoing)',
|
# bytes of result '20', not separate general results.
|
||||||
0x13: 'Terminal currently unable to process command (no service)',
|
0x20: 'ME currently unable to process command',
|
||||||
0x14: 'Terminal currently unable to process command (access control class bar)',
|
0x21: 'Network currently unable to process command',
|
||||||
0x15: 'Terminal currently unable to process command (radio resource unavailable)',
|
0x22: 'User did not accept the proactive command',
|
||||||
0x20: 'Network currently unable to process command',
|
0x23: 'User cleared down call before connection or network release',
|
||||||
0x30: 'Beyond terminal capability',
|
0x24: 'Action in contradiction with the current timer state',
|
||||||
0x31: 'Command type not understood by terminal',
|
0x25: 'Interaction with call control by NAA, temporary problem',
|
||||||
0x32: 'Command data not understood by terminal',
|
0x26: 'Launch browser generic error',
|
||||||
0x33: 'Command number not known by terminal',
|
0x27: 'MMS temporary problem',
|
||||||
|
# '28'-'29' reserved for 3GPP
|
||||||
|
0x30: 'Command beyond ME capabilities',
|
||||||
|
0x31: 'Command type not understood by ME',
|
||||||
|
0x32: 'Command data not understood by ME',
|
||||||
|
0x33: 'Command number not known by ME',
|
||||||
0x34: 'SS Return Error',
|
0x34: 'SS Return Error',
|
||||||
0x35: 'SMS RP-ERROR',
|
0x35: 'SMS RP-ERROR',
|
||||||
0x36: 'Error, required values are missing',
|
0x36: 'Error, required values are missing',
|
||||||
0x37: 'USSD Return Error',
|
0x37: 'USSD return error',
|
||||||
0x38: 'MultipleCard commands error',
|
0x38: 'Multiple Card command error',
|
||||||
0x39: 'Interaction with CC by NAA, wrong procedure',
|
0x39: 'Interaction with call/SM control by USIM, permanent problem',
|
||||||
0x3C: 'Access Technology unable to process command',
|
0x3A: 'Bearer Independent Protocol error',
|
||||||
0x3D: 'Frames error',
|
0x3B: 'Access Technology unable to process command',
|
||||||
0x3E: 'MMS Error',
|
0x3C: 'Frames error',
|
||||||
|
0x3D: 'MMS error',
|
||||||
|
# '3E'-'3F' reserved for 3GPP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1515,18 +1540,18 @@ EVENT_TYPES = {
|
|||||||
0x03: 'Location status', 0x04: 'User activity', 0x05: 'Idle screen available',
|
0x03: 'Location status', 0x04: 'User activity', 0x05: 'Idle screen available',
|
||||||
0x06: 'Card reader status', 0x07: 'Language selection',
|
0x06: 'Card reader status', 0x07: 'Language selection',
|
||||||
0x08: 'Browser termination', 0x09: 'Data available',
|
0x08: 'Browser termination', 0x09: 'Data available',
|
||||||
0x0A: 'Channel status', 0x0B: 'Access Technology Change',
|
0x0A: 'Channel status', 0x0B: 'Access Technology Change (single)',
|
||||||
0x0C: 'Display parameters changed', 0x0D: 'Local connection',
|
0x0C: 'Display parameters changed', 0x0D: 'Local connection',
|
||||||
0x0E: 'Network Search Mode Change', 0x0F: 'Browsing status',
|
0x0E: 'Network Search Mode Change', 0x0F: 'Browsing status',
|
||||||
0x10: 'Frames Information Change', 0x11: 'I-WLAN Access Status',
|
0x10: 'Frames Information Change', 0x11: '(I-)WLAN Access Status',
|
||||||
0x12: 'Network Rejection', 0x13: 'HCI Connectivity',
|
0x12: 'Network Rejection', 0x13: 'HCI connectivity event',
|
||||||
0x14: 'Change of UICC Access', 0x15: 'CSG Cell Change',
|
0x14: 'Access Technology Change (multiple)', 0x15: 'CSG cell selection',
|
||||||
0x16: 'Contactless state request', 0x17: 'Profile Container',
|
0x16: 'Contactless state request', 0x17: 'IMS Registration',
|
||||||
0x18: 'LTE D2D Discovery Monitoring', 0x19: 'LTE D2D Communication Monitoring',
|
0x18: 'IMS Incoming data', 0x19: 'Profile Container',
|
||||||
0x1A: 'LTE D2D Announcement Response', 0x1B: 'LTE D2D Revocation',
|
0x1A: 'Void', 0x1B: 'Secured Profile Container',
|
||||||
0x1C: 'LTE D2D Application Port', 0x1D: 'LTE D2D Security Recovery',
|
0x1C: 'Poll Interval Negotiation', 0x1D: 'Data Connection Status Change',
|
||||||
0x1E: 'Off-net Emergency Call', 0x1F: 'ECall Over IMS',
|
0x1E: 'CAG cell selection', 0x1F: 'Slices Status Change',
|
||||||
0x20: 'EARFCN Update', 0x21: 'SCEF Channel Status',
|
# TS 102 223 §8.25: '20'-'22' reserved for 3GPP (future usage)
|
||||||
}
|
}
|
||||||
|
|
||||||
LOCATION_STATUS = {
|
LOCATION_STATUS = {
|
||||||
@@ -1536,6 +1561,44 @@ LOCATION_STATUS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# TS 102 223 §8.7 device identity coding (summary per UICC_SPECS.md §6.2)
|
||||||
|
_DEVICE_ID_RANGES = [
|
||||||
|
(0x10, 0x17, lambda v: f'Card reader {v - 0x10}'),
|
||||||
|
(0x21, 0x27, lambda v: f'Channel {v - 0x21}'),
|
||||||
|
(0x31, 0x3F, lambda v: f'eCAT client {v - 0x30}'),
|
||||||
|
]
|
||||||
|
_DEVICE_IDS = {
|
||||||
|
0x01: 'Keypad', 0x02: 'Display', 0x03: 'Earpiece',
|
||||||
|
0x81: 'UICC', 0x82: 'Terminal', 0x83: 'Network',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _device_id_name(value):
|
||||||
|
"""Decode one Device Identity byte (TS 102 223 §8.7)."""
|
||||||
|
if value in _DEVICE_IDS:
|
||||||
|
return _DEVICE_IDS[value]
|
||||||
|
for lo, hi, fmt in _DEVICE_ID_RANGES:
|
||||||
|
if lo <= value <= hi:
|
||||||
|
return fmt(value)
|
||||||
|
return f'0x{value:02X}'
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_device_ids(value):
|
||||||
|
"""Decode a Device Identities TLV value into {src, dst} names."""
|
||||||
|
if len(value) < 2:
|
||||||
|
return None
|
||||||
|
return {'src': _device_id_name(value[0]), 'dst': _device_id_name(value[1])}
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_timer_value(b):
|
||||||
|
"""Decode a 3-byte Timer Value (TS 102 223 §8.38): semi-octet h/m/s."""
|
||||||
|
if len(b) != 3:
|
||||||
|
return None
|
||||||
|
def so(x):
|
||||||
|
return '%d%d' % ((x >> 4) & 0x0F, x & 0x0F)
|
||||||
|
return f'{so(b[0])}:{so(b[1])}:{so(b[2])}'
|
||||||
|
|
||||||
|
|
||||||
def _decode_bcd_address(data):
|
def _decode_bcd_address(data):
|
||||||
"""Decode a [TON/NPI + BCD digits] address into a string."""
|
"""Decode a [TON/NPI + BCD digits] address into a string."""
|
||||||
if not data:
|
if not data:
|
||||||
@@ -2184,57 +2247,109 @@ def _decode_envelope(body):
|
|||||||
return {}
|
return {}
|
||||||
tag, _length, value = tlvs[0]
|
tag, _length, value = tlvs[0]
|
||||||
result = {'type': ENVELOPE_TYPES.get(tag, f'0x{tag:02X}')}
|
result = {'type': ENVELOPE_TYPES.get(tag, f'0x{tag:02X}')}
|
||||||
inner = parse_tlv(value)
|
|
||||||
|
|
||||||
if tag == 0xD6: # EVENT DOWNLOAD
|
if tag == 0xDE: # ENVELOPE CONTAINER — wraps another complete envelope
|
||||||
|
for t, _l, v in parse_tlv(value):
|
||||||
|
if t in ENVELOPE_TYPES:
|
||||||
|
sub = _decode_envelope(bytes([t, len(v)]) + v)
|
||||||
|
sub['encapsulated'] = True
|
||||||
|
result['encapsulated'] = sub
|
||||||
|
elif (t & 0x7F) == _P_DEVICE_IDS:
|
||||||
|
d = _decode_device_ids(v)
|
||||||
|
if d:
|
||||||
|
result['device_ids'] = d
|
||||||
|
return result
|
||||||
|
|
||||||
|
inner = parse_tlv(value)
|
||||||
|
raw_tlv = []
|
||||||
for t, _l, v in inner:
|
for t, _l, v in inner:
|
||||||
if t == 0x19: # Event list
|
base = t & 0x7F
|
||||||
|
if base == _P_DEVICE_IDS: # common to every envelope type
|
||||||
|
d = _decode_device_ids(v)
|
||||||
|
if d:
|
||||||
|
result['device_ids'] = d
|
||||||
|
continue
|
||||||
|
if tag == 0xD6: # EVENT DOWNLOAD
|
||||||
|
if base == _P_EVENT_LIST and v:
|
||||||
result['events'] = [EVENT_TYPES.get(e, f'0x{e:02X}') for e in v]
|
result['events'] = [EVENT_TYPES.get(e, f'0x{e:02X}') for e in v]
|
||||||
elif t == 0x1B: # Location status
|
elif base == _P_LOCATION_STATUS:
|
||||||
if v:
|
if v:
|
||||||
result['location_status'] = LOCATION_STATUS.get(v[0], f'0x{v[0]:02X}')
|
result['location_status'] = LOCATION_STATUS.get(v[0], f'0x{v[0]:02X}')
|
||||||
elif t == 0x13: # Location information
|
elif base == _P_LOCAL_INFO:
|
||||||
li = _decode_local_info(PLI_LOCATION_INFO, v)
|
li = _decode_local_info(PLI_LOCATION_INFO, v)
|
||||||
result['location_info'] = li['value'] if li else v.hex().upper()
|
result['location_info'] = li['value'] if li else v.hex().upper()
|
||||||
elif t in (0x06, 0x86): # Address (caller's number, MT call)
|
elif base == _P_ADDRESS and v: # caller's number (MT call)
|
||||||
result['caller'] = _decode_bcd_address(v)
|
result['caller'] = _decode_bcd_address(v)
|
||||||
elif t == 0x1C: # Transaction identifier (MT call)
|
elif base == _P_TRANSACTION_ID:
|
||||||
if v:
|
if v:
|
||||||
result['transaction_id'] = v[0]
|
result['transaction_id'] = v[0]
|
||||||
elif t in (0x08, 0x88): # Subaddress
|
elif base == _P_SUBADDRESS:
|
||||||
result['subaddress'] = v.hex().upper()
|
result['subaddress'] = v.hex().upper()
|
||||||
elif t == 0x02: # Device identities
|
else:
|
||||||
result['device_ids'] = v.hex().upper()
|
raw_tlv.append({'tag': f'{base:02X}', 'value': v.hex().upper()})
|
||||||
elif tag == 0xD5: # MO SHORT MESSAGE CONTROL
|
elif tag == 0xD5: # MO SHORT MESSAGE CONTROL
|
||||||
for t, _l, v in inner:
|
if base == _P_ADDRESS and v:
|
||||||
if t == 0x06: # Address objects
|
|
||||||
if 'smsc' not in result:
|
if 'smsc' not in result:
|
||||||
result['smsc'] = _decode_bcd_address(v)
|
result['smsc'] = _decode_bcd_address(v)
|
||||||
else:
|
else:
|
||||||
result['tp_da'] = _decode_bcd_address(v)
|
result['tp_da'] = _decode_bcd_address(v)
|
||||||
elif t == 0x13:
|
elif base == _P_LOCAL_INFO:
|
||||||
result['location_info'] = v.hex().upper()
|
result['location_info'] = v.hex().upper()
|
||||||
elif t == 0x02:
|
else:
|
||||||
result['device_ids'] = v.hex().upper()
|
raw_tlv.append({'tag': f'{base:02X}', 'value': v.hex().upper()})
|
||||||
elif tag == 0xD1: # SMS-PP DOWNLOAD
|
elif tag == 0xD1: # SMS-PP DOWNLOAD
|
||||||
for t, _l, v in inner:
|
if base == _P_ADDRESS and v:
|
||||||
if t == 0x06:
|
|
||||||
result['smsc'] = _decode_bcd_address(v)
|
result['smsc'] = _decode_bcd_address(v)
|
||||||
elif t in (0x0B, 0x8B): # SMS TPDU (SMS-DELIVER)
|
elif base == _P_SMS_TPDU and v:
|
||||||
result['tpdu'] = _decode_sm_tpdu(v)
|
result['tpdu'] = _decode_sm_tpdu(v)
|
||||||
elif t == 0x02:
|
else:
|
||||||
result['device_ids'] = v.hex().upper()
|
raw_tlv.append({'tag': f'{base:02X}', 'value': v.hex().upper()})
|
||||||
elif tag == 0xD2: # CELL BROADCAST DOWNLOAD
|
elif tag == 0xD2: # CELL BROADCAST DOWNLOAD
|
||||||
for t, _l, v in inner:
|
if base == _P_CB_PAGE and v:
|
||||||
if t == 0x02:
|
|
||||||
result['device_ids'] = v.hex().upper()
|
|
||||||
elif t in (0x0C, 0x8C): # CB page
|
|
||||||
result['cb_page'] = _decode_cb_page(v)
|
result['cb_page'] = _decode_cb_page(v)
|
||||||
else: # D3 / D4 / D7 — device identities only
|
else:
|
||||||
for t, _l, v in inner:
|
raw_tlv.append({'tag': f'{base:02X}', 'value': v.hex().upper()})
|
||||||
if t == 0x02:
|
elif tag == 0xD3: # MENU SELECTION
|
||||||
result['device_ids'] = v.hex().upper()
|
if base == _P_ITEM_ID and v:
|
||||||
|
result['item_id'] = v[0]
|
||||||
|
elif base == _P_HELP_REQUEST: # TS 101 220: '15'/'95', length 00
|
||||||
|
result['help'] = True
|
||||||
|
else:
|
||||||
|
raw_tlv.append({'tag': f'{base:02X}', 'value': v.hex().upper()})
|
||||||
|
elif tag == 0xD4: # CALL CONTROL
|
||||||
|
if base == _P_ADDRESS and v:
|
||||||
|
result['address'] = _decode_bcd_address(v)
|
||||||
|
elif base == _P_CCP and v:
|
||||||
|
result['ccp'] = v.hex().upper()
|
||||||
|
elif base == _P_SUBADDRESS and v:
|
||||||
|
result['subaddress'] = v.hex().upper()
|
||||||
|
else:
|
||||||
|
raw_tlv.append({'tag': f'{base:02X}', 'value': v.hex().upper()})
|
||||||
|
elif tag == 0xD7: # TIMER EXPIRATION
|
||||||
|
if base == _P_TIMER_ID and v:
|
||||||
|
result['timer_id'] = v[0] if len(v) == 1 else v.hex().upper()
|
||||||
|
elif base == _P_TIMER_VALUE:
|
||||||
|
tv = _decode_timer_value(v)
|
||||||
|
if tv:
|
||||||
|
result['timer_value'] = tv
|
||||||
|
else:
|
||||||
|
raw_tlv.append({'tag': f'{base:02X}', 'value': v.hex().upper()})
|
||||||
|
elif tag == 0xD9: # USSD DOWNLOAD
|
||||||
|
if base == _P_USSD_STRING and v:
|
||||||
|
result['ussd'] = _decode_dcs_text(v)
|
||||||
|
else:
|
||||||
|
raw_tlv.append({'tag': f'{base:02X}', 'value': v.hex().upper()})
|
||||||
|
elif tag == 0xDD: # GEOGRAPHICAL LOCATION REPORTING
|
||||||
|
if base == _P_LOCAL_INFO and v:
|
||||||
|
li = _decode_local_info(PLI_LOCATION_INFO, v)
|
||||||
|
result['location_info'] = li['value'] if li else v.hex().upper()
|
||||||
|
else:
|
||||||
|
raw_tlv.append({'tag': f'{base:02X}', 'value': v.hex().upper()})
|
||||||
|
else: # DA/DB/DC/DF/E0/… — keep objects visible as hex
|
||||||
|
raw_tlv.append({'tag': f'{base:02X}', 'value': v.hex().upper()})
|
||||||
|
|
||||||
|
if raw_tlv:
|
||||||
|
result['raw_tlv'] = raw_tlv
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -2254,6 +2369,24 @@ _P_RESPONSE_LEN = 0x11
|
|||||||
_P_EVENT_LIST = 0x19
|
_P_EVENT_LIST = 0x19
|
||||||
_P_ICON_ID = 0x1E
|
_P_ICON_ID = 0x1E
|
||||||
_P_AID = 0x2F
|
_P_AID = 0x2F
|
||||||
|
# TS 101 220 table (COMPREHENSION-TLV tags, base values; CR form = +0x80)
|
||||||
|
_P_CCP = 0x07
|
||||||
|
_P_SUBADDRESS = 0x08
|
||||||
|
_P_USSD_STRING = 0x0A
|
||||||
|
_P_CB_PAGE = 0x0C
|
||||||
|
_P_LOCAL_INFO = 0x13
|
||||||
|
_P_HELP_REQUEST = 0x15
|
||||||
|
_P_ITEM_ID = 0x10
|
||||||
|
_P_LOCATION_STATUS = 0x1B
|
||||||
|
_P_TRANSACTION_ID = 0x1C
|
||||||
|
_P_TIMER_ID = 0x24
|
||||||
|
_P_TIMER_VALUE = 0x25
|
||||||
|
|
||||||
|
# Commands whose payload is defined as a Text String (GSM 11.14 / TS 102 223):
|
||||||
|
# DISPLAY TEXT, GET INKEY, GET INPUT, SET UP IDLE MODE TEXT. Used for a
|
||||||
|
# guarded fallback against non-compliant cards that send the Text String
|
||||||
|
# with the next object's tag ('10', Item identifier).
|
||||||
|
_TEXT_STRING_COMMANDS = {0x21, 0x22, 0x23, 0x28}
|
||||||
|
|
||||||
|
|
||||||
def _decode_proactive(body):
|
def _decode_proactive(body):
|
||||||
@@ -2278,6 +2411,8 @@ def _decode_proactive(body):
|
|||||||
items = []
|
items = []
|
||||||
file_list = []
|
file_list = []
|
||||||
aid = None
|
aid = None
|
||||||
|
raw_tlv = []
|
||||||
|
text_seen = False
|
||||||
for tag, _length, value in inner:
|
for tag, _length, value in inner:
|
||||||
base = tag & 0x7F
|
base = tag & 0x7F
|
||||||
if base == _P_CMD_DETAILS and len(value) >= 2:
|
if base == _P_CMD_DETAILS and len(value) >= 2:
|
||||||
@@ -2288,6 +2423,7 @@ def _decode_proactive(body):
|
|||||||
result['title'] = _decode_annex_a(value)
|
result['title'] = _decode_annex_a(value)
|
||||||
elif base == _P_TEXT_STRING:
|
elif base == _P_TEXT_STRING:
|
||||||
result['text'] = _decode_dcs_text(value)
|
result['text'] = _decode_dcs_text(value)
|
||||||
|
text_seen = True
|
||||||
elif base == _P_ITEM and len(value) >= 2:
|
elif base == _P_ITEM and len(value) >= 2:
|
||||||
items.append({'id': value[0], 'text': _decode_annex_a(value[1:])})
|
items.append({'id': value[0], 'text': _decode_annex_a(value[1:])})
|
||||||
elif base == _P_SMS_TPDU and value:
|
elif base == _P_SMS_TPDU and value:
|
||||||
@@ -2302,13 +2438,31 @@ def _decode_proactive(body):
|
|||||||
result['address'] = _decode_bcd_address(value)
|
result['address'] = _decode_bcd_address(value)
|
||||||
elif base == _P_ICON_ID and value:
|
elif base == _P_ICON_ID and value:
|
||||||
result['icon_id'] = value[0]
|
result['icon_id'] = value[0]
|
||||||
|
elif base == _P_ITEM_ID and len(value) == 1:
|
||||||
|
result['item_id'] = value[0]
|
||||||
|
elif base == _P_ITEM_ID and len(value) >= 2 and cmd_type in _TEXT_STRING_COMMANDS \
|
||||||
|
and not text_seen:
|
||||||
|
# Non-compliant card quirk (observed on a real SAT SIM): the
|
||||||
|
# Text String carries the Item identifier tag '10' instead of
|
||||||
|
# '8D'. A genuine Item identifier is one byte, so length plus
|
||||||
|
# command context disambiguate the two.
|
||||||
|
result['text'] = _decode_dcs_text(value)
|
||||||
|
result['text_note'] = "text string with non-standard tag '10'"
|
||||||
|
text_seen = True
|
||||||
elif base == _P_FILE_LIST and value:
|
elif base == _P_FILE_LIST and value:
|
||||||
file_list = [value[i:i + 2].hex().upper() for i in range(0, len(value), 2)]
|
file_list = [value[i:i + 2].hex().upper() for i in range(0, len(value), 2)]
|
||||||
elif base == _P_AID and value:
|
elif base == _P_AID and value:
|
||||||
aid = value.hex().upper()
|
aid = value.hex().upper()
|
||||||
|
elif base == _P_DEVICE_IDS and len(value) >= 2:
|
||||||
|
result['device_ids'] = _decode_device_ids(value)
|
||||||
|
else:
|
||||||
|
raw_tlv.append({'tag': f'{base:02X}', 'value': value.hex().upper()})
|
||||||
|
|
||||||
if cmd_type is None:
|
if cmd_type is None:
|
||||||
return None
|
return None
|
||||||
|
if 0xF0 <= cmd_type <= 0xFE: # TS 102 223 §9.4 — reserved for proprietary use
|
||||||
|
result['type'] = f'Proprietary ({cmd_type:#04x})'
|
||||||
|
else:
|
||||||
result['type'] = CAT_COMMAND_TYPES.get(cmd_type, f'0x{cmd_type:02X}')
|
result['type'] = CAT_COMMAND_TYPES.get(cmd_type, f'0x{cmd_type:02X}')
|
||||||
qualifier_desc = _command_qualifier(cmd_type, qualifier)
|
qualifier_desc = _command_qualifier(cmd_type, qualifier)
|
||||||
if qualifier_desc:
|
if qualifier_desc:
|
||||||
@@ -2319,6 +2473,8 @@ def _decode_proactive(body):
|
|||||||
result['aid'] = aid
|
result['aid'] = aid
|
||||||
if items:
|
if items:
|
||||||
result['items'] = items
|
result['items'] = items
|
||||||
|
if raw_tlv:
|
||||||
|
result['raw_tlv'] = raw_tlv
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+190
-3
@@ -101,7 +101,7 @@ class TestCatDecoding(unittest.TestCase):
|
|||||||
self.assertEqual(r['cat_command'], 'SMS-PP DOWNLOAD')
|
self.assertEqual(r['cat_command'], 'SMS-PP DOWNLOAD')
|
||||||
cmd = r['cmd']
|
cmd = r['cmd']
|
||||||
self.assertEqual(cmd['smsc'], '+79043490004')
|
self.assertEqual(cmd['smsc'], '+79043490004')
|
||||||
self.assertEqual(cmd['device_ids'], '8381')
|
self.assertEqual(cmd['device_ids'], {'src': 'Network', 'dst': 'UICC'})
|
||||||
t = cmd['tpdu']
|
t = cmd['tpdu']
|
||||||
self.assertEqual(t['mti'], 'SMS-DELIVER')
|
self.assertEqual(t['mti'], 'SMS-DELIVER')
|
||||||
self.assertEqual(t['oa'], '455')
|
self.assertEqual(t['oa'], '455')
|
||||||
@@ -355,8 +355,8 @@ class TestTrResult(unittest.TestCase):
|
|||||||
|
|
||||||
def test_session_terminated(self):
|
def test_session_terminated(self):
|
||||||
from simtrace2_pysniff.server.decode import _decode_tr_result
|
from simtrace2_pysniff.server.decode import _decode_tr_result
|
||||||
r = _decode_tr_result(bytes.fromhex('81030105000202828103010A'))
|
r = _decode_tr_result(bytes.fromhex('810301050002028281030110'))
|
||||||
self.assertEqual(r['code'], '0x0A')
|
self.assertEqual(r['code'], '0x10')
|
||||||
self.assertEqual(r['name'], 'Proactive UICC session terminated by the user')
|
self.assertEqual(r['name'], 'Proactive UICC session terminated by the user')
|
||||||
|
|
||||||
|
|
||||||
@@ -1592,6 +1592,193 @@ class TestIdleModeText(unittest.TestCase):
|
|||||||
self.assertNotIn('icon_id', r['cmd'])
|
self.assertNotIn('icon_id', r['cmd'])
|
||||||
|
|
||||||
|
|
||||||
|
class TestSatLegacyQuirks(unittest.TestCase):
|
||||||
|
"""Real-card SAT quirks and unknown-TLV transparency.
|
||||||
|
|
||||||
|
Tag assignments verified against GSM 11.14 v5.1.0/v5.9.0 §12.3 and
|
||||||
|
TS 102 223 V18.3.0: Text String = '0D'/'8D' in all generations;
|
||||||
|
'10'/'90' = Item identifier.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _fetch(self, inner_hex):
|
||||||
|
inner = bytes.fromhex(inner_hex)
|
||||||
|
body = b'\xd0' + bytes([len(inner)]) + inner
|
||||||
|
return decode_message(
|
||||||
|
bytes.fromhex('80120000%02x' % len(body)) + body + b'\x90\x00')
|
||||||
|
|
||||||
|
def test_idle_mode_text_nonstandard_tag(self):
|
||||||
|
# Real-card capture: SET UP IDLE MODE TEXT whose Text String ("037",
|
||||||
|
# 8-bit data) is tagged '10' instead of '8D'.
|
||||||
|
r = decode_message(bytes.fromhex(
|
||||||
|
'8012000011d00f8103012800820281821004043033379000'))
|
||||||
|
self.assertEqual(r['cat_command'], 'SET UP IDLE MODE TEXT')
|
||||||
|
self.assertEqual(r['cmd']['text'], '037')
|
||||||
|
self.assertIn('non-standard', r['cmd']['text_note'])
|
||||||
|
self.assertNotIn('raw_tlv', r['cmd'])
|
||||||
|
|
||||||
|
def test_item_identifier_single_byte(self):
|
||||||
|
# Genuine 1-byte Item identifier (SELECT ITEM flow) must not be
|
||||||
|
# mistaken for the quirk text fallback.
|
||||||
|
r = self._fetch('810301240082028182900102')
|
||||||
|
self.assertEqual(r['cat_command'], 'SELECT ITEM')
|
||||||
|
self.assertEqual(r['cmd']['item_id'], 2)
|
||||||
|
self.assertNotIn('text', r['cmd'])
|
||||||
|
|
||||||
|
def test_unknown_tlv_preserved(self):
|
||||||
|
r = self._fetch('8103012800820281827102aabb')
|
||||||
|
self.assertEqual(r['cmd']['raw_tlv'],
|
||||||
|
[{'tag': '71', 'value': 'AABB'}])
|
||||||
|
|
||||||
|
def test_device_ids_not_in_raw_tlv(self):
|
||||||
|
# Structural Device Identities TLV must not surface as unknown.
|
||||||
|
r = self._fetch('810301280082028182')
|
||||||
|
self.assertNotIn('raw_tlv', r['cmd'])
|
||||||
|
|
||||||
|
def test_compliant_text_still_wins_over_quirk(self):
|
||||||
|
# If both a compliant 8D text and a '10'-tagged object exist,
|
||||||
|
# the compliant one is used and the other lands in raw_tlv.
|
||||||
|
r = self._fetch('8103012100820281828d0204411003010102')
|
||||||
|
self.assertEqual(r['cmd']['text'], 'A')
|
||||||
|
self.assertIn({'tag': '10', 'value': '010102'}, r['cmd']['raw_tlv'])
|
||||||
|
|
||||||
|
|
||||||
|
class TestSpecRegistry(unittest.TestCase):
|
||||||
|
"""Registries verified against TS 102 223 V18.3.0 / TS 31.111 V18.12.0."""
|
||||||
|
|
||||||
|
def _fetch(self, inner_hex):
|
||||||
|
inner = bytes.fromhex(inner_hex)
|
||||||
|
body = b'\xd0' + bytes([len(inner)]) + inner
|
||||||
|
return decode_message(
|
||||||
|
bytes.fromhex('80120000%02x' % len(body)) + body + b'\x90\x00')
|
||||||
|
|
||||||
|
def test_event_names_multi_rat_and_profile_container(self):
|
||||||
|
# SET UP EVENT LIST with events '14', '19', '1E'
|
||||||
|
from simtrace2_pysniff.server.decode import EVENT_TYPES
|
||||||
|
self.assertEqual(EVENT_TYPES[0x14],
|
||||||
|
'Access Technology Change (multiple)')
|
||||||
|
self.assertEqual(EVENT_TYPES[0x19], 'Profile Container')
|
||||||
|
self.assertEqual(EVENT_TYPES[0x1E], 'CAG cell selection')
|
||||||
|
self.assertEqual(EVENT_TYPES[0x1F], 'Slices Status Change')
|
||||||
|
|
||||||
|
def test_event_list_fetch_decodes_new_events(self):
|
||||||
|
# SET UP EVENT LIST with events '14', '19', '1E'
|
||||||
|
r = self._fetch('810301050002028281190314191e')
|
||||||
|
self.assertEqual(r['cat_command'], 'SET UP EVENT LIST')
|
||||||
|
self.assertEqual(r['cmd']['events'],
|
||||||
|
['Access Technology Change (multiple)',
|
||||||
|
'Profile Container', 'CAG cell selection'])
|
||||||
|
|
||||||
|
def test_tr_result_me_unable_vs_network(self):
|
||||||
|
from simtrace2_pysniff.server.decode import TR_RESULTS
|
||||||
|
self.assertIn('ME currently unable', TR_RESULTS[0x20])
|
||||||
|
self.assertIn('Network currently unable', TR_RESULTS[0x21])
|
||||||
|
|
||||||
|
def test_tr_result_user_session_codes(self):
|
||||||
|
from simtrace2_pysniff.server.decode import TR_RESULTS
|
||||||
|
self.assertEqual(TR_RESULTS[0x14],
|
||||||
|
'USSD or SS transaction terminated by the user')
|
||||||
|
self.assertNotIn(0x0A, TR_RESULTS)
|
||||||
|
self.assertNotIn(0x0E, TR_RESULTS)
|
||||||
|
|
||||||
|
def test_tr_result_permanent_problems(self):
|
||||||
|
from simtrace2_pysniff.server.decode import TR_RESULTS
|
||||||
|
self.assertEqual(TR_RESULTS[0x3A],
|
||||||
|
'Bearer Independent Protocol error')
|
||||||
|
self.assertEqual(TR_RESULTS[0x3B],
|
||||||
|
'Access Technology unable to process command')
|
||||||
|
self.assertEqual(TR_RESULTS[0x3C], 'Frames error')
|
||||||
|
self.assertEqual(TR_RESULTS[0x3D], 'MMS error')
|
||||||
|
|
||||||
|
def test_envelope_types_extended(self):
|
||||||
|
from simtrace2_pysniff.server.decode import ENVELOPE_TYPES
|
||||||
|
self.assertEqual(ENVELOPE_TYPES[0xD9], 'USSD DOWNLOAD')
|
||||||
|
self.assertEqual(ENVELOPE_TYPES[0xDD],
|
||||||
|
'GEOGRAPHICAL LOCATION REPORTING')
|
||||||
|
self.assertEqual(ENVELOPE_TYPES[0xDE], 'ENVELOPE CONTAINER')
|
||||||
|
self.assertEqual(ENVELOPE_TYPES[0xE0], '5G PROSE REPORT')
|
||||||
|
|
||||||
|
def test_lsi_command_type(self):
|
||||||
|
from simtrace2_pysniff.server.decode import CAT_COMMAND_TYPES
|
||||||
|
self.assertEqual(CAT_COMMAND_TYPES[0x79], 'LSI COMMAND')
|
||||||
|
|
||||||
|
def test_proprietary_type_fallback(self):
|
||||||
|
r = self._fetch('810301f20082028182')
|
||||||
|
self.assertEqual(r['cat_command'], 'Proprietary (0xf2)')
|
||||||
|
self.assertEqual(r['cmd']['type'], 'Proprietary (0xf2)')
|
||||||
|
|
||||||
|
def test_pli_qualifiers_esn_and_supported_rat(self):
|
||||||
|
from simtrace2_pysniff.server.decode import PLI_QUALIFIERS
|
||||||
|
self.assertEqual(PLI_QUALIFIERS[0x07], 'ESN of the terminal')
|
||||||
|
self.assertEqual(PLI_QUALIFIERS[0x1A],
|
||||||
|
'Supported Radio Access Technologies')
|
||||||
|
|
||||||
|
|
||||||
|
class TestEnvelopeDecoders(unittest.TestCase):
|
||||||
|
"""Phase 2: inner TLV decoding for the remaining ENVELOPE types."""
|
||||||
|
|
||||||
|
def _envelope(self, hexstr):
|
||||||
|
apdu = '80C20000%02x' % (len(hexstr) // 2) + hexstr + '9000'
|
||||||
|
return decode_message(bytes.fromhex(apdu))
|
||||||
|
|
||||||
|
def test_menu_selection_item_and_help(self):
|
||||||
|
# D3 | device ids (UICC→Terminal) | item identifier 02 | help request
|
||||||
|
r = self._envelope('D309820281821001029500')
|
||||||
|
cmd = r['cmd']
|
||||||
|
self.assertEqual(cmd['type'], 'MENU SELECTION')
|
||||||
|
self.assertEqual(cmd['item_id'], 2)
|
||||||
|
self.assertTrue(cmd['help'])
|
||||||
|
self.assertEqual(cmd['device_ids'], {'src': 'UICC', 'dst': 'Terminal'})
|
||||||
|
|
||||||
|
def test_timer_expiration(self):
|
||||||
|
# D7 | device ids | timer id 01 | timer value semi-octet h/m/s
|
||||||
|
r = self._envelope('D70C820282812401012503010203')
|
||||||
|
cmd = r['cmd']
|
||||||
|
self.assertEqual(cmd['type'], 'TIMER EXPIRATION')
|
||||||
|
self.assertEqual(cmd['timer_id'], 1)
|
||||||
|
self.assertEqual(cmd['timer_value'], '01:02:03')
|
||||||
|
|
||||||
|
def test_ussd_download(self):
|
||||||
|
# D9 | device ids | USSD string, DCS 0x04 (8-bit) "*100#"
|
||||||
|
r = self._envelope('D90C820283810A06042A31303023')
|
||||||
|
cmd = r['cmd']
|
||||||
|
self.assertEqual(cmd['type'], 'USSD DOWNLOAD')
|
||||||
|
self.assertEqual(cmd['ussd'], '*100#')
|
||||||
|
self.assertEqual(cmd['device_ids'], {'src': 'Network', 'dst': 'UICC'})
|
||||||
|
|
||||||
|
def test_call_control_address_and_ccp(self):
|
||||||
|
# D4 | device ids | address (intl '+79') | CCP 0xAA
|
||||||
|
r = self._envelope('D40C8202838106039179F10701AA')
|
||||||
|
cmd = r['cmd']
|
||||||
|
self.assertEqual(cmd['type'], 'CALL CONTROL')
|
||||||
|
self.assertIn('+', cmd['address'])
|
||||||
|
self.assertEqual(cmd['ccp'], 'AA')
|
||||||
|
|
||||||
|
def test_envelope_container_recurses(self):
|
||||||
|
# DE wrapping a complete MENU SELECTION envelope
|
||||||
|
inner = bytes.fromhex('D309820281821001029500')
|
||||||
|
body = b'\xDE' + bytes([len(inner)]) + inner
|
||||||
|
lc = len(body)
|
||||||
|
r = decode_message(
|
||||||
|
bytes.fromhex('80C20000%02x' % lc) + body + b'\x90\x00')
|
||||||
|
cmd = r['cmd']
|
||||||
|
self.assertEqual(cmd['type'], 'ENVELOPE CONTAINER')
|
||||||
|
self.assertEqual(cmd['encapsulated']['type'], 'MENU SELECTION')
|
||||||
|
self.assertEqual(cmd['encapsulated']['item_id'], 2)
|
||||||
|
|
||||||
|
def test_prose_report_keeps_raw_objects(self):
|
||||||
|
# E0 | device ids | unknown data object → preserved in raw_tlv
|
||||||
|
r = self._envelope('E008820282819902AABB')
|
||||||
|
cmd = r['cmd']
|
||||||
|
self.assertEqual(cmd['type'], '5G PROSE REPORT')
|
||||||
|
self.assertEqual(cmd['raw_tlv'], [{'tag': '19', 'value': 'AABB'}])
|
||||||
|
|
||||||
|
def test_proactive_device_ids_decoded(self):
|
||||||
|
r = decode_message(bytes.fromhex(
|
||||||
|
'801200000dd00b8103012800820283818d00'))
|
||||||
|
self.assertEqual(r['cmd']['device_ids'],
|
||||||
|
{'src': 'Network', 'dst': 'UICC'})
|
||||||
|
|
||||||
|
|
||||||
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.11.0')
|
self.assertEqual(__version__, '1.13.0')
|
||||||
|
|
||||||
def test_status_includes_version(self):
|
def test_status_includes_version(self):
|
||||||
import os
|
import os
|
||||||
|
|||||||
Reference in new issue
Block a user