Files
simtrace-analyser/tests/test_decode.py
T

1550 lines
73 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests for server APDU/CAT decoding."""
import unittest
from simtrace2_pysniff.server.decode import (
decode_message,
decode_sniff_msg,
CAT_COMMAND_TYPES,
ENVELOPE_TYPES,
parse_tlv,
)
class TestParseTlv(unittest.TestCase):
def test_simple(self):
data = bytes.fromhex('8103010500')
self.assertEqual(parse_tlv(data), [(0x81, 3, bytes.fromhex('010500'))])
def test_multiple(self):
data = bytes.fromhex('810301050082028182')
self.assertEqual(parse_tlv(data), [
(0x81, 3, bytes.fromhex('010500')),
(0x82, 2, bytes.fromhex('8182')),
])
def test_empty(self):
self.assertEqual(parse_tlv(b''), [])
def test_long_form_length(self):
# D0 81 BC … — a proactive command whose length is 0xBC (188),
# encoded in BER-TLV long form.
inner = bytes.fromhex('8103012500') + b'\x82\x02\x81\x82'
data = b'\xd0' + b'\x81' + bytes([len(inner)]) + inner
self.assertEqual(parse_tlv(data), [(0xD0, len(inner), inner)])
def test_long_form_two_byte(self):
# 0x82 followed by two length bytes.
value = b'\x00' * 300
data = b'\x70' + b'\x82\x01\x2c' + value
self.assertEqual(parse_tlv(data), [(0x70, 300, value)])
class TestAuthenticateNaming(unittest.TestCase):
def test_ins_88(self):
r = decode_message(bytes.fromhex('0088008122' + '00' * 34 + '6135'))
self.assertEqual(r['ins_name'], 'AUTHENTICATE')
def test_ins_89(self):
r = decode_message(bytes.fromhex('0089008122' + '00' * 34 + '6135'))
self.assertEqual(r['ins_name'], 'AUTHENTICATE')
class TestCatDecoding(unittest.TestCase):
def test_fetch_setup_menu(self):
r = decode_message(bytes.fromhex(
'8012000030D02E810301250082028182050B416C6661204D6F62696C65'
'8F16808112089DB0C1C2C0BEB9BAB82F53657474696E67739000'))
self.assertEqual(r['ins_name'], 'FETCH')
self.assertEqual(r['cat_command'], 'SET UP MENU')
def test_fetch_setup_event_list(self):
r = decode_message(bytes.fromhex(
'801200000FD00D810301050082028182990203129000'))
self.assertEqual(r['cat_command'], 'SET UP EVENT LIST')
def test_fetch_poll_interval(self):
r = decode_message(bytes.fromhex(
'801200000FD00D8103010300820281820402011E9000'))
self.assertEqual(r['cat_command'], 'POLL INTERVAL')
def test_fetch_provide_local_info(self):
r = decode_message(bytes.fromhex(
'801200000BD0098103012600820281829000'))
self.assertEqual(r['cat_command'], 'PROVIDE LOCAL INFORMATION')
self.assertEqual(r['cmd']['qualifier'],
'0x00 Location Info (MCC, MNC, LAC/TAC, Cell ID)')
def test_envelope_event_download(self):
r = decode_message(bytes.fromhex(
'80C200000CD60A190103020282811B01029000'))
self.assertEqual(r['ins_name'], 'ENVELOPE')
self.assertEqual(r['cat_command'], 'EVENT DOWNLOAD')
def test_tr_has_response_to(self):
r = decode_message(bytes.fromhex(
'801400000C8103010500020282810301009130'))
self.assertEqual(r['ins_name'], 'TERMINAL RESPONSE')
self.assertNotIn('cat_command', r)
self.assertEqual(r['response_to'], 'SET UP EVENT LIST')
def test_select_no_cat(self):
r = decode_message(bytes.fromhex('a0a40000023f009000'))
self.assertEqual(r['ins_name'], 'SELECT')
self.assertNotIn('cat_command', r)
def test_envelope_sms_pp_download(self):
r = decode_message(bytes.fromhex(
'80C2000022D120020283810607919740430900F40B1104038154F50004'
'628021410300210248699000'))
self.assertEqual(r['ins_name'], 'ENVELOPE')
self.assertEqual(r['cat_command'], 'SMS-PP DOWNLOAD')
cmd = r['cmd']
self.assertEqual(cmd['smsc'], '+79043490004')
self.assertEqual(cmd['device_ids'], '8381')
t = cmd['tpdu']
self.assertEqual(t['mti'], 'SMS-DELIVER')
self.assertEqual(t['oa'], '455')
self.assertEqual(t['text'], 'Hi')
self.assertEqual(t['scts'], '2026-08-12 14:30:00 (UTC+03:00)')
class TestCommandTypeTables(unittest.TestCase):
def test_known_values(self):
self.assertEqual(CAT_COMMAND_TYPES[0x03], 'POLL INTERVAL')
self.assertEqual(CAT_COMMAND_TYPES[0x05], 'SET UP EVENT LIST')
self.assertEqual(CAT_COMMAND_TYPES[0x25], 'SET UP MENU')
self.assertEqual(CAT_COMMAND_TYPES[0x21], 'DISPLAY TEXT')
self.assertEqual(CAT_COMMAND_TYPES[0x24], 'SELECT ITEM')
self.assertEqual(CAT_COMMAND_TYPES[0x26], 'PROVIDE LOCAL INFORMATION')
def test_envelope_known_values(self):
self.assertEqual(ENVELOPE_TYPES[0xD3], 'MENU SELECTION')
self.assertEqual(ENVELOPE_TYPES[0xD6], 'EVENT DOWNLOAD')
class TestChangeAndFidiDecoding(unittest.TestCase):
def test_change_reset_assert(self):
r = decode_sniff_msg(b'', 'change', flags=1 << 2)
self.assertEqual(r['type'], 'change')
self.assertEqual(r['flags'], ['Reset asserted'])
def test_change_card_insert(self):
r = decode_sniff_msg(b'', 'change', flags=1 << 0)
self.assertEqual(r['flags'], ['Card inserted'])
def test_change_multiple_flags(self):
r = decode_sniff_msg(b'', 'change', flags=(1 << 2) | (1 << 3))
self.assertEqual(r['flags'], ['Reset asserted', 'Reset de-asserted'])
def test_change_no_flags(self):
r = decode_sniff_msg(b'', 'change', flags=0)
self.assertEqual(r['flags'], ['no changes'])
def test_fidi(self):
r = decode_sniff_msg(b'\x97', 'fidi')
self.assertEqual(r['type'], 'fidi')
self.assertEqual(r['fi'], 9)
self.assertEqual(r['di'], 7)
self.assertEqual(r['fi_val'], 512)
self.assertEqual(r['di_val'], 64)
def test_tpdu_error_flags_surfaced(self):
r = decode_sniff_msg(bytes.fromhex('00a40000023f009000'), 'tpdu',
flags=(1 << 7) | (1 << 6))
self.assertEqual(r['errors'], ['malformed', 'checksum error'])
def test_tpdu_no_error_flags(self):
r = decode_sniff_msg(bytes.fromhex('00a40000023f009000'), 'tpdu', flags=0)
self.assertNotIn('errors', r)
def test_atr_error_flags_surfaced(self):
r = decode_sniff_msg(bytes.fromhex('3b00'), 'atr', flags=(1 << 5))
self.assertEqual(r['errors'], ['incomplete'])
class TestTerminalResponse(unittest.TestCase):
def test_tr_setup_event_list(self):
r = decode_message(bytes.fromhex(
'801400000C8103010500020282810301009130'))
self.assertEqual(r['ins_name'], 'TERMINAL RESPONSE')
self.assertEqual(r['response_to'], 'SET UP EVENT LIST')
def test_tr_setup_menu(self):
r = decode_message(bytes.fromhex(
'801400000C810301250002028281830100910F'))
self.assertEqual(r['response_to'], 'SET UP MENU')
def test_tr_poll_interval(self):
r = decode_message(bytes.fromhex(
'80140000108103010300020282810301000402011E9000'))
self.assertEqual(r['response_to'], 'POLL INTERVAL')
class TestPliQualifier(unittest.TestCase):
def test_tr_pli_imei(self):
r = decode_message(bytes.fromhex(
'8014000016810301260102028281030106130952F0991EC57A68009F'))
self.assertEqual(r['response_to'], 'PROVIDE LOCAL INFORMATION')
self.assertEqual(r['response']['qualifier'], '0x01 IMEI')
def test_fetch_pli_location(self):
r = decode_message(bytes.fromhex(
'801200000BD0098103012600820281829000'))
self.assertEqual(r['cat_command'], 'PROVIDE LOCAL INFORMATION')
self.assertEqual(r['cmd']['qualifier'],
'0x00 Location Info (MCC, MNC, LAC/TAC, Cell ID)')
def test_fetch_pli_date_time(self):
r = decode_message(bytes.fromhex(
'801200000BD0098103012603820281829000'))
self.assertEqual(r['cat_command'], 'PROVIDE LOCAL INFORMATION')
self.assertEqual(r['cmd']['qualifier'], '0x03 Date, time and time zone')
def test_non_pli_no_qualifier(self):
r = decode_message(bytes.fromhex(
'801400000C810301250002028281830100910F'))
self.assertEqual(r['response_to'], 'SET UP MENU')
class TestGetResponseContext(unittest.TestCase):
GET_RESPONSE = bytes.fromhex('00C000002962278202782183023F00A50A800171')
def test_with_prev_select(self):
r = decode_message(self.GET_RESPONSE, prev={'ins_name': 'SELECT', 'sw1': '61'})
self.assertEqual(r['response_for'], 'SELECT')
def test_with_prev_non_61(self):
r = decode_message(self.GET_RESPONSE, prev={'ins_name': 'SELECT', 'sw1': '90'})
self.assertIsNone(r['response_for'])
def test_orphaned(self):
r = decode_message(self.GET_RESPONSE, prev=None)
self.assertIsNone(r['response_for'])
def test_get_response_por(self):
# GET RESPONSE after ENVELOPE SMS-PP DOWNLOAD → Response Packet (PoR).
r = decode_message(
bytes.fromhex('00C0000024') +
bytes.fromhex('027100001F0AB0000100000000C100000290000080041C043004400438044FD83CDF37FF') +
bytes.fromhex('9000'),
prev={'ins': 0xC2, 'ins_name': 'ENVELOPE', 'sw1': '61'})
self.assertEqual(r['response_for'], 'ENVELOPE')
self.assertEqual(r['response']['rpl'], 31)
self.assertEqual(r['response']['rhl'], 10)
self.assertEqual(r['response']['tar'], 'B00001')
self.assertEqual(r['response']['cntr'], '00000000C1')
self.assertEqual(r['response']['status'], {'code': '00', 'name': 'PoR OK'})
self.assertEqual(r['response']['data'], '0290000080041C043004400438044FD83CDF37FF')
self.assertIn('PoR OK · TAR B00001', r['summary'])
def test_get_response_non_por_not_parsed(self):
# A GET RESPONSE after ENVELOPE that is not a PoR must stay undecoded.
r = decode_message(bytes.fromhex('00C0000004') + bytes.fromhex('AABBCCDD') + bytes.fromhex('9000'),
prev={'ins': 0xC2, 'ins_name': 'ENVELOPE', 'sw1': '61'})
self.assertEqual(r['response_for'], 'ENVELOPE')
self.assertNotIn('response', r)
class TestFcpResponse(unittest.TestCase):
GET_RESPONSE = bytes.fromhex(
'00C000002962278202782183023F00A50A8001718302F9628701018A01058B032F0601C60990014083010183010A9000')
def test_select_response_fcp(self):
r = decode_message(self.GET_RESPONSE, prev={'ins': 0xA4, 'ins_name': 'SELECT', 'sw1': '61'})
self.assertEqual(r['response_for'], 'SELECT')
resp = r['response']
self.assertEqual(resp['template'], 'FCP')
self.assertEqual(resp['file_id'], '3F00')
self.assertEqual(resp['file_id_name'], 'MF')
self.assertEqual(resp['file_descriptor']['file_type'], 'DF or ADF')
self.assertEqual(resp['file_descriptor']['shareable'], 'shareable')
self.assertEqual(resp['life_cycle'], 'operational state (activated)')
def test_ef_descriptor_transparent(self):
from simtrace2_pysniff.server.decode import _decode_file_descriptor
fd = _decode_file_descriptor(bytes.fromhex('0121'))
self.assertEqual(fd['file_type'], 'Working EF')
self.assertEqual(fd['structure'], 'transparent')
def test_ef_descriptor_linear_fixed(self):
from simtrace2_pysniff.server.decode import _decode_file_descriptor
fd = _decode_file_descriptor(bytes.fromhex('0221'))
self.assertEqual(fd['file_type'], 'Working EF')
self.assertEqual(fd['structure'], 'linear fixed')
def test_fcp_file_size(self):
from simtrace2_pysniff.server.decode import _decode_fcp
# FCP: file descriptor (transparent EF) + FID 6F07 + file size 9 + total 9
fcp = _decode_fcp(bytes.fromhex('8202012183026f07800109810109'))
self.assertEqual(fcp['file_descriptor']['file_type'], 'Working EF')
self.assertEqual(fcp['file_descriptor']['structure'], 'transparent')
self.assertEqual(fcp['file_id_name'], 'EF_IMSI')
self.assertEqual(fcp['file_size'], 9)
self.assertEqual(fcp['total_file_size'], 9)
def test_fcp_sfi(self):
# FCP SFI tag 0x88 carries the SFI in bits b8..b4 (value >> 3).
from simtrace2_pysniff.server.decode import _decode_fcp
fcp = _decode_fcp(bytes.fromhex('8202412183026f7e880158'))
self.assertEqual(fcp['sfi'], 11)
def test_fcp_sfi_empty(self):
# FCP SFI tag 0x88 with length 0 → file does not support SFI.
from simtrace2_pysniff.server.decode import _decode_fcp
fcp = _decode_fcp(bytes.fromhex('82054221005a0d83026f068800'))
self.assertIsNone(fcp['sfi'])
def test_fcp_sfi_absent_uses_fid_lsbs(self):
# No SFI tag → SFI = 5 LSBs of the FID (TS 102 221 §11.1.2).
from simtrace2_pysniff.server.decode import _decode_fcp
fcp = _decode_fcp(bytes.fromhex('8202412183022f06'))
self.assertEqual(fcp['sfi'], 0x06)
def test_fcp_summary_transparent(self):
# GET RESPONSE (FCP) for a transparent EF of 9 bytes
gr = bytes.fromhex('00c000000c') + bytes.fromhex('620c8202012183026f07800109810109') + bytes.fromhex('9000')
r = decode_message(gr, prev={'ins': 0xA4, 'ins_name': 'SELECT', 'sw1': '61'})
self.assertEqual(r['summary'], 'response for SELECT, Working EF, transparent, 9 B')
def test_fcp_summary_linear_fixed(self):
# linear fixed EF, 3 records x 64 bytes
gr = bytes.fromhex('00c000000e') + bytes.fromhex('620e8205022100400383026f07800109') + bytes.fromhex('9000')
r = decode_message(gr, prev={'ins': 0xA4, 'ins_name': 'SELECT', 'sw1': '61'})
self.assertEqual(r['summary'], 'response for SELECT, Working EF, linear fixed, 3 rec × 64 B')
def test_fcp_summary_df(self):
r = decode_message(self.GET_RESPONSE, prev={'ins': 0xA4, 'ins_name': 'SELECT', 'sw1': '61'})
self.assertEqual(r['summary'], 'response for SELECT, DF or ADF (MF)')
class TestAuthResponse(unittest.TestCase):
def test_3g_success(self):
from simtrace2_pysniff.server.decode import _decode_auth
data = bytes.fromhex('DB084CFA9017FD0DD85A101A2622F60E8ABD2C2497B9A8EFAF55E510CAA393329FF97868B9537369D5266A4F084D417B05ABFAFAEE')
r = _decode_auth(data)
self.assertEqual(r['type'], '3G/EPS/5G')
self.assertEqual(r['status'], 'success')
self.assertEqual(r['res'], '4CFA9017FD0DD85A')
self.assertEqual(r['ck'], '1A2622F60E8ABD2C2497B9A8EFAF55E5')
self.assertEqual(r['ik'], 'CAA393329FF97868B9537369D5266A4F')
def test_3g_sync_fail(self):
from simtrace2_pysniff.server.decode import _decode_auth
data = bytes.fromhex('DC0E' + '00' * 14)
r = _decode_auth(data)
self.assertEqual(r['type'], '3G/EPS/5G')
self.assertEqual(r['status'], 'sync fail')
self.assertEqual(r['auts'], '00' * 14)
def test_gsm(self):
from simtrace2_pysniff.server.decode import _decode_auth
sres = bytes.fromhex('AABBCCDD')
kc = bytes.fromhex('0011223344556677')
r = _decode_auth(sres + kc)
self.assertEqual(r['type'], 'GSM')
self.assertEqual(r['sres'], 'AABBCCDD')
self.assertEqual(r['kc'], '0011223344556677')
class TestTrResult(unittest.TestCase):
def test_success(self):
r = decode_message(bytes.fromhex('801400000C8103010500020282810301009130'))
self.assertEqual(r['response']['code'], '0x00')
self.assertEqual(r['response']['name'], 'Command performed successfully')
def test_session_terminated(self):
from simtrace2_pysniff.server.decode import _decode_tr_result
r = _decode_tr_result(bytes.fromhex('81030105000202828103010A'))
self.assertEqual(r['code'], '0x0A')
self.assertEqual(r['name'], 'Proactive UICC session terminated by the user')
class TestTrAdditionalInfo(unittest.TestCase):
def test_poll_interval_duration(self):
r = decode_message(bytes.fromhex(
'80140000108103010300020282810301000402011E9000'))
self.assertEqual(r['response_to'], 'POLL INTERVAL')
self.assertEqual(r['response']['duration'], {'value': 30, 'unit': 'seconds'})
def test_pli_location_info(self):
r = decode_message(bytes.fromhex(
'8014000017810301260002028281030106130952F0991EC57A43C09F9000'))
self.assertEqual(
r['response']['local_info']['Location'],
'MCC 250 MNC 99 · LAC 0x1EC5 · Cell 0x7A43C09F')
def test_pli_imei(self):
r = decode_message(bytes.fromhex(
'801400001681030126010202828103010014088A763201652428089000'))
self.assertEqual(r['response']['local_info']['IMEI'],
'867231056428280')
def test_pli_datetime(self):
from simtrace2_pysniff.server.decode import _decode_datetime
self.assertEqual(
_decode_datetime(bytes.fromhex('62802141030021')),
'2026-08-12 14:30:00 (UTC+03:00)')
self.assertEqual(
_decode_datetime(bytes.fromhex('628021410300FF')),
'2026-08-12 14:30:00 (UTCunknown)')
def test_datetime_invalid_bcd(self):
from simtrace2_pysniff.server.decode import _decode_datetime
self.assertIsNone(_decode_datetime(bytes.fromhex('A505326C333939')))
def test_datetime_invalid_semantics(self):
from simtrace2_pysniff.server.decode import _decode_datetime
self.assertIsNone(_decode_datetime(bytes.fromhex('62310100000000')))
def test_pli_language(self):
from simtrace2_pysniff.server.decode import _decode_local_info, PLI_LANGUAGE
self.assertEqual(_decode_local_info(PLI_LANGUAGE, b'en'),
{'label': 'Language', 'value': 'en'})
def test_pli_battery(self):
from simtrace2_pysniff.server.decode import _decode_local_info, PLI_BATTERY
self.assertEqual(_decode_local_info(PLI_BATTERY, b'\x05'),
{'label': 'Battery', 'value': 'full'})
def test_pli_access_tech(self):
from simtrace2_pysniff.server.decode import _decode_local_info, PLI_ACCESS_TECH
self.assertEqual(_decode_local_info(PLI_ACCESS_TECH, bytes([0x03, 0x07])),
{'label': 'Access technology', 'value': 'UTRAN, E-UTRAN'})
class TestAuthCommand(unittest.TestCase):
def test_3g_rand_autn(self):
r = decode_message(bytes.fromhex(
'0088008122104A75BA425D438C549EF35BA5E3DD53051065E04D80E4DC8000A5C7FD842F41395E6135'))
cmd = r['cmd']
self.assertNotIn('context', cmd)
self.assertEqual(cmd['specific_key'], True)
self.assertEqual(cmd['rand'], '4A75BA425D438C549EF35BA5E3DD5305')
self.assertEqual(cmd['autn'], '65E04D80E4DC8000A5C7FD842F41395E')
self.assertEqual(cmd['sqn_ak'], '65E04D80E4DC')
self.assertEqual(cmd['amf'], '8000')
self.assertEqual(cmd['mac'], 'A5C7FD842F41395E')
def test_auth_p2_context(self):
r = decode_message(bytes.fromhex(
'0088008122104A75BA425D438C549EF35BA5E3DD53051065E04D80E4DC8000A5C7FD842F41395E6135'))
p2 = r['p2']
self.assertEqual(p2['raw'], '81')
self.assertIn('3G/EPS/5G context', p2['bits'])
self.assertIn('Specific reference data', p2['bits'])
def test_gsm_rand_only(self):
from simtrace2_pysniff.server.decode import _decode_auth_cmd
r = _decode_auth_cmd(bytes.fromhex('10' + '00' * 16), 0x00)
self.assertNotIn('context', r)
self.assertNotIn('specific_key', r)
self.assertEqual(r['rand'], '00' * 16)
self.assertNotIn('autn', r)
def test_sync_fail_auts(self):
from simtrace2_pysniff.server.decode import _decode_auth
auts = bytes.fromhex('010203040506' + '0a0b0c0d0e0f1011')
r = _decode_auth(b'\xdc' + bytes([len(auts)]) + auts)
self.assertEqual(r['status'], 'sync fail')
self.assertEqual(r['auts'], auts.hex().upper())
self.assertEqual(r['sqn_ak'], '010203040506')
self.assertEqual(r['mac_s'], '0A0B0C0D0E0F1011')
class TestEventDownload(unittest.TestCase):
def test_location_status(self):
r = decode_message(bytes.fromhex(
'80C2000017D615190103020282811B0100130952F0991EC57A43C09F9000'))
cmd = r['cmd']
self.assertEqual(cmd['type'], 'EVENT DOWNLOAD')
self.assertEqual(cmd['events'], ['Location status'])
self.assertEqual(cmd['location_status'], 'Normal service')
def test_event_no_service(self):
r = decode_message(bytes.fromhex(
'80C200000CD60A190103020282811B01029000'))
self.assertEqual(r['cmd']['events'], ['Location status'])
self.assertEqual(r['cmd']['location_status'], 'No service')
def test_location_status_with_info(self):
r = decode_message(bytes.fromhex(
'80C2000017D615190103020282811B0100130952F02026ADBC51A16F9000'))
cmd = r['cmd']
self.assertEqual(cmd['location_status'], 'Normal service')
self.assertEqual(cmd['location_info'],
'MCC 250 MNC 02 · LAC 0x26AD · Cell 0xBC51A16F')
def test_mt_call(self):
r = decode_message(bytes.fromhex(
'80C2000014D612190100020283811C010586069121436587099000'))
cmd = r['cmd']
self.assertEqual(cmd['events'], ['MT call'])
self.assertEqual(cmd['transaction_id'], 5)
self.assertEqual(cmd['caller'], '+1234567890')
def test_cell_broadcast(self):
cbp = ('4C511002F211' +
'040000313A0103035E047FFB0F31303238322A333036343835363234' +
'0D' * 54)
r = decode_message(bytes.fromhex(
'80C2000060D25E020283810C58' + cbp + '9000'))
cmd = r['cmd']
self.assertEqual(cmd['type'], 'CELL BROADCAST DOWNLOAD')
self.assertEqual(cmd['cb_page']['serial'], '4C51')
self.assertEqual(cmd['cb_page']['message_id'], '0x1002')
self.assertEqual(cmd['cb_page']['page'], '1/1')
self.assertEqual(cmd['cb_page']['content'],
'040000313A0103035E047FFB0F31303238322A333036343835363234' + '0D' * 54)
class TestSmTpdu(unittest.TestCase):
def _bcd(self, number):
d = [int(c) for c in number]
out = bytearray()
for i in range(0, len(d), 2):
lo = d[i]
hi = d[i + 1] if i + 1 < len(d) else 0xF
out.append((hi << 4) | lo)
return bytes(out)
def test_deliver_gsm7(self):
from simtrace2_pysniff.server.decode import _decode_sm_tpdu
oa = b'\x91' + self._bcd('79031234567')
tpdu = (b'\x00' + bytes([11]) + oa + b'\x00\x00' + b'\x00' * 7 +
b'\x05' + bytes.fromhex('E8329BFD06'))
r = _decode_sm_tpdu(tpdu)
self.assertEqual(r['mti'], 'SMS-DELIVER')
self.assertEqual(r['oa'], '+79031234567')
self.assertEqual(r['encoding'], 'GSM 7-bit')
self.assertEqual(r['text'], 'hello')
def test_deliver_ucs2(self):
from simtrace2_pysniff.server.decode import _decode_sm_tpdu
oa = b'\x91' + self._bcd('79031234567')
ucs2 = '\u041f\u0440\u0438\u0432\u0435\u0442'.encode('utf-16-be')
tpdu = (b'\x00' + bytes([11]) + oa + b'\x00\x08' + b'\x00' * 7 +
bytes([len(ucs2)]) + ucs2)
r = _decode_sm_tpdu(tpdu)
self.assertEqual(r['encoding'], 'UCS2')
self.assertEqual(r['text'], '\u041f\u0440\u0438\u0432\u0435\u0442')
def test_8bit_no_text(self):
from simtrace2_pysniff.server.decode import _decode_sm_tpdu
oa = b'\x91' + self._bcd('79031234567')
# Mixed low/high bytes are treated as binary → no text decode.
payload = bytes.fromhex('00018081A0FF07')
tpdu = (b'\x00' + bytes([11]) + oa + b'\x00\x04' + b'\x00' * 7 +
bytes([len(payload)]) + payload)
r = _decode_sm_tpdu(tpdu)
self.assertEqual(r['encoding'], '8-bit data')
self.assertNotIn('text', r)
self.assertEqual(r['payload'], '00018081A0FF07')
def test_sim_data_download(self):
from simtrace2_pysniff.server.decode import _decode_sm_tpdu
oa = b'\x91' + self._bcd('79031234567')
ud = bytes.fromhex('02700000200d000000000000020000000000000010100102a2090804420435044104420600')
tpdu = (b'\x44' + bytes([11]) + oa + b'\x7f\x04' + b'\x00' * 7 +
bytes([len(ud)]) + ud)
r = _decode_sm_tpdu(tpdu)
self.assertEqual(r['pid_name'], 'SIM data download (secured packet)')
self.assertEqual(r['udh'][0]['name'], 'Command Packet Identifier (CPI)')
self.assertEqual(r['secured']['cpl'], 32)
self.assertEqual(r['secured']['chl'], 13)
self.assertEqual(r['secured']['spi']['counter'], 'no counter')
self.assertEqual(r['secured']['spi']['rc_cc_ds'], 'none')
self.assertEqual(r['secured']['tar'], '000002')
self.assertEqual(r['secured']['cntr'], '0000000000')
self.assertEqual(r['secured']['data'], '0010100102A2090804420435044104420600')
def test_response_packet(self):
from simtrace2_pysniff.server.decode import _decode_response_packet
body = bytes.fromhex('001f0a00000000000000500000ab12800101230d08a0000001510000000f9a9000')
r = _decode_response_packet(body)
self.assertEqual(r['rpl'], 31)
self.assertEqual(r['rhl'], 10)
self.assertEqual(r['tar'], '000000')
self.assertEqual(r['cntr'], '0000000050')
self.assertEqual(r['pcntr'], 0)
self.assertEqual(r['status'], {'code': '00', 'name': 'PoR OK'})
self.assertEqual(r['data'], 'AB12800101230D08A0000001510000000F9A9000')
def test_secured_packet_fallback(self):
from simtrace2_pysniff.server.decode import _decode_secured_packet
self.assertEqual(_decode_secured_packet(bytes.fromhex('0001')), {'raw': '0001'})
def test_send_sm_response_packet(self):
from simtrace2_pysniff.server.decode import _decode_sm_tpdu
tpdu = bytes.fromhex('41000481112200f624027100001f0a00000000000000500000ab12800101230d08a0000001510000000f9a9000')
r = _decode_sm_tpdu(tpdu)
self.assertEqual(r['mti'], 'SMS-SUBMIT')
self.assertEqual(r['da'], '1122')
self.assertEqual(r['udh'][0]['name'], 'Response Packet Identifier (RPI)')
self.assertEqual(r['response_packet']['rpl'], 31)
self.assertEqual(r['response_packet']['cntr'], '0000000050')
self.assertEqual(r['response_packet']['status'], {'code': '00', 'name': 'PoR OK'})
self.assertEqual(r['response_packet']['data'], 'AB12800101230D08A0000001510000000F9A9000')
def test_deliver_scts(self):
from simtrace2_pysniff.server.decode import _decode_sm_tpdu
oa = b'\x91' + self._bcd('79031234567')
scts = bytes.fromhex('62802141030021')
tpdu = (b'\x00' + bytes([11]) + oa + b'\x00\x00' + scts + b'\x00')
r = _decode_sm_tpdu(tpdu)
self.assertEqual(r['scts'], '2026-08-12 14:30:00 (UTC+03:00)')
def test_deliver_pid_name(self):
from simtrace2_pysniff.server.decode import _decode_sm_tpdu
oa = b'\x91' + self._bcd('79031234567')
tpdu = (b'\x00' + bytes([11]) + oa + b'\x48\x00' + b'\x00' * 7 + b'\x00')
r = _decode_sm_tpdu(tpdu)
self.assertEqual(r['pid_name'], 'Device Triggering Short Message')
def test_deliver_dcs_info(self):
from simtrace2_pysniff.server.decode import _decode_sm_tpdu
oa = b'\x91' + self._bcd('79031234567')
tpdu = (b'\x00' + bytes([11]) + oa + b'\x00\x11' + b'\x00' * 7 + b'\x00')
r = _decode_sm_tpdu(tpdu)
self.assertEqual(r['dcs_info']['group'], 'Message Marked for Automatic Deletion')
self.assertEqual(r['dcs_info']['has_class'], True)
self.assertEqual(r['dcs_info']['msg_class'], 1)
def test_dcs_mwi(self):
from simtrace2_pysniff.server.decode import _decode_dcs_full
d = _decode_dcs_full(0xD0)
self.assertEqual(d['group'], 'Message Waiting Indication')
self.assertEqual(d['action'], 'store')
self.assertEqual(d['sense'], 'inactive')
self.assertEqual(d['indication'], 'voicemail')
def test_udh_concat_8bit(self):
from simtrace2_pysniff.server.decode import _decode_udh
el = _decode_udh(bytes.fromhex('0003A10201'))
self.assertEqual(el[0]['name'], 'Concatenated short messages, 8-bit reference number')
self.assertEqual(el[0]['data'], {'reference': 0xA1, 'max': 2, 'seq': 1})
def test_udh_concat_16bit(self):
from simtrace2_pysniff.server.decode import _decode_udh
el = _decode_udh(bytes.fromhex('080403000201'))
self.assertEqual(el[0]['name'], 'Concatenated short message, 16-bit reference number')
self.assertEqual(el[0]['data'], {'reference': 0x0300, 'max': 2, 'seq': 1})
def test_udh_app_port_8bit(self):
from simtrace2_pysniff.server.decode import _decode_udh
el = _decode_udh(bytes.fromhex('0402B5A5'))
self.assertEqual(el[0]['data'], {'dest_port': 0xB5, 'orig_port': 0xA5})
def test_udh_app_port_16bit(self):
from simtrace2_pysniff.server.decode import _decode_udh
el = _decode_udh(bytes.fromhex('05040B8423F0'))
self.assertEqual(el[0]['data'], {'dest_port': 0x0B84, 'orig_port': 0x23F0})
def test_udh_special_sms(self):
from simtrace2_pysniff.server.decode import _decode_udh
el = _decode_udh(bytes.fromhex('01028102'))
self.assertEqual(el[0]['data'], {'store': True, 'indication': 'fax', 'count': 2})
def test_deliver_udh_ucs2(self):
from simtrace2_pysniff.server.decode import _decode_sm_tpdu
oa = b'\x91' + self._bcd('79031234567')
ud = bytes.fromhex('050003A10201') + 'Hi'.encode('utf-16-be')
tpdu = (b'\x40' + bytes([11]) + oa + b'\x00\x08' + b'\x00' * 7 +
bytes([len(ud)]) + ud)
r = _decode_sm_tpdu(tpdu)
self.assertEqual(r['udh'][0]['data'], {'reference': 0xA1, 'max': 2, 'seq': 1})
self.assertEqual(r['text'], 'Hi')
def test_submit_vp_relative(self):
from simtrace2_pysniff.server.decode import _decode_sm_tpdu
da = b'\x81' + self._bcd('999')
tpdu = b'\x09' + b'\x2A' + bytes([len(da)]) + da + b'\x00\x00' + b'\x8F' + b'\x00'
r = _decode_sm_tpdu(tpdu)
self.assertEqual(r['mti'], 'SMS-SUBMIT')
self.assertEqual(r['vp'], '720 min')
def test_submit_vp_absolute(self):
from simtrace2_pysniff.server.decode import _decode_sm_tpdu
da = b'\x81' + self._bcd('999')
scts = bytes.fromhex('62802141030021')
tpdu = b'\x11' + b'\x2A' + bytes([len(da)]) + da + b'\x00\x00' + scts + b'\x00'
r = _decode_sm_tpdu(tpdu)
self.assertEqual(r['vp'], '2026-08-12 14:30:00 (UTC+03:00)')
def test_submit_no_ud_encoding(self):
# Real capture: absolute VP with non-BCD bytes, no TP-UD.
from simtrace2_pysniff.server.decode import _decode_sm_tpdu
tpdu = bytes.fromhex('11FF038154F50004A505326C333939')
r = _decode_sm_tpdu(tpdu)
self.assertEqual(r['da'], '455')
self.assertEqual(r['encoding'], '8-bit data')
self.assertEqual(r['msg_class'], 0)
self.assertEqual(r['vp'], 'A505326C333939')
def test_8bit_ascii_text(self):
from simtrace2_pysniff.server.decode import _decode_sm_tpdu
oa = b'\x91' + self._bcd('79031234567')
payload = b'Hello World'
tpdu = (b'\x00' + bytes([11]) + oa + b'\x00\x04' + b'\x00' * 7 +
bytes([len(payload)]) + payload)
r = _decode_sm_tpdu(tpdu)
self.assertEqual(r['encoding'], '8-bit data')
self.assertEqual(r['text'], 'Hello World')
def test_8bit_ascii_ctrl_replaced(self):
from simtrace2_pysniff.server.decode import _decode_sm_tpdu
oa = b'\x91' + self._bcd('79031234567')
payload = b'Hi\x00There'
tpdu = (b'\x00' + bytes([11]) + oa + b'\x00\x04' + b'\x00' * 7 +
bytes([len(payload)]) + payload)
r = _decode_sm_tpdu(tpdu)
self.assertEqual(r['text'], 'Hi\u00b7There')
def test_8bit_high_text(self):
from simtrace2_pysniff.server.decode import _decode_sm_tpdu
oa = b'\x91' + self._bcd('79031234567')
payload = '\u00e9\u00fc\u00f1'.encode('latin-1')
tpdu = (b'\x00' + bytes([11]) + oa + b'\x00\x04' + b'\x00' * 7 +
bytes([len(payload)]) + payload)
r = _decode_sm_tpdu(tpdu)
self.assertEqual(r['text'], '\u00e9\u00fc\u00f1')
def test_8bit_mixed_no_text(self):
from simtrace2_pysniff.server.decode import _decode_sm_tpdu
oa = b'\x91' + self._bcd('79031234567')
payload = b'\x00\x01\xff\xfe'
tpdu = (b'\x00' + bytes([11]) + oa + b'\x00\x04' + b'\x00' * 7 +
bytes([len(payload)]) + payload)
r = _decode_sm_tpdu(tpdu)
self.assertNotIn('text', r)
self.assertEqual(r['payload'], '0001FFFE')
class TestBcdAddress(unittest.TestCase):
def test_international(self):
from simtrace2_pysniff.server.decode import _decode_bcd_address
self.assertEqual(_decode_bcd_address(bytes.fromhex('912143658709')), '+1234567890')
def test_national(self):
from simtrace2_pysniff.server.decode import _decode_bcd_address
self.assertEqual(_decode_bcd_address(bytes.fromhex('A12143658709')), '1234567890')
def test_odd_digits(self):
from simtrace2_pysniff.server.decode import _decode_bcd_address
self.assertEqual(_decode_bcd_address(bytes.fromhex('9121436587F9')), '+123456789')
class TestProactiveDecode(unittest.TestCase):
def test_setup_menu(self):
r = decode_message(bytes.fromhex(
'8012000030D02E810301250082028182050B416C6661204D6F62696C'
'658F16808112089DB0C1C2C0BEB9BAB82F53657474696E67739000'))
cmd = r['cmd']
self.assertEqual(cmd['type'], 'SET UP MENU')
self.assertEqual(cmd['title'], 'Alfa Mobile')
self.assertEqual(cmd['items'], [{'id': 0x80, 'text': 'Настройки/Settings'}])
def test_setup_event_list(self):
r = decode_message(bytes.fromhex(
'801200000FD00D810301050082028182990203129000'))
self.assertEqual(r['cmd']['events'], ['Location status', 'Network Rejection'])
def test_poll_interval_duration(self):
r = decode_message(bytes.fromhex(
'801200000FD00D8103010300820281820402011E9000'))
self.assertEqual(r['cmd']['duration'], {'value': 30, 'unit': 'seconds'})
def test_send_short_message_tpdu(self):
r = decode_message(bytes.fromhex(
'801200004AD04881030113008202818305000B3B11FF038199F90004A531'
'494D45492038363732333130353634323832383020494D53492032353031'
'3937373030313932373935204556454E5420319000'))
self.assertEqual(r['cmd']['type'], 'SEND SHORT MESSAGE')
self.assertEqual(r['cmd']['tpdu']['mti'], 'SMS-SUBMIT')
self.assertEqual(r['cmd']['tpdu']['da'], '999')
def test_send_short_message_rich_tpdu(self):
r = decode_message(bytes.fromhex(
'801200001AD01881030113008202818305000B0B0101038199F9000402'
'48699000'))
t = r['cmd']['tpdu']
self.assertEqual(r['cmd']['type'], 'SEND SHORT MESSAGE')
self.assertEqual(t['mti'], 'SMS-SUBMIT')
self.assertEqual(t['da'], '999')
self.assertEqual(t['pid_name'], 'SME-to-SME (implicit)')
self.assertEqual(t['encoding'], '8-bit data')
self.assertEqual(t['text'], 'Hi')
def test_setup_menu_long_form_length(self):
# Real Mi A1 startup trace: D0 length encoded as 0x81 0xBC (188 bytes).
r = decode_message(bytes.fromhex(
'80120000BFD081BC810301250082028182051580004D0065006700610046'
'006F006E00500052004F8F100180041C0435043304300424043E043D8F18'
'02800420043004370432043B043504470435043D0438044F8F0C03800421'
'043F043E044004428F100480041D043E0432043E0441044204388F100580'
'04240438043D0430043D0441044B8F1006800421043F044004300432043A'
'04308F100780041E043104490435043D043804358F188080041A0430043B'
'043504390434043E0441043A043E043F9000'))
self.assertEqual(r['cat_command'], 'SET UP MENU')
self.assertEqual(r['cmd']['title'], 'MegaFonPRO')
self.assertEqual(r['cmd']['items'][0], {'id': 1, 'text': 'МегаФон'})
class TestAnnexA(unittest.TestCase):
def test_gsm_default(self):
from simtrace2_pysniff.server.decode import _decode_annex_a
self.assertEqual(_decode_annex_a(b'Alfa Mobile'), 'Alfa Mobile')
def test_ucs2_variant1(self):
from simtrace2_pysniff.server.decode import _decode_annex_a
self.assertEqual(_decode_annex_a(b'\x80' + 'Привет'.encode('utf_16_be')), 'Привет')
def test_ucs2_variant2_base_ptr(self):
from simtrace2_pysniff.server.decode import _decode_annex_a
# 'Н' = U+041D, base 0x08<<7 = 0x400, offset 0x1D
self.assertEqual(_decode_annex_a(bytes.fromhex('8101089D')), 'Н')
def test_ucs2_variant_90(self):
from simtrace2_pysniff.server.decode import _decode_annex_a
self.assertEqual(_decode_annex_a(b'\x90' + 'Привет'.encode('utf_16_be')), 'Привет')
def test_gsm7_packed(self):
from simtrace2_pysniff.server.decode import _decode_annex_a
self.assertEqual(_decode_annex_a(bytes.fromhex('41b6390c')), 'Alfa')
class TestPnnSpn(unittest.TestCase):
def test_pnn_full_name(self):
from simtrace2_pysniff.server.decode import _decode_pnn
r = _decode_pnn(bytes.fromhex('43058441b6390c' + 'ff' * 13))
self.assertEqual(r['full'], 'Alfa')
def test_pnn_ucs2(self):
from simtrace2_pysniff.server.decode import _decode_pnn
v = b'\x90' + 'Привет'.encode('utf-16-be')
r = _decode_pnn(b'\x43' + bytes([len(v)]) + v)
self.assertEqual(r['full'], 'Привет')
def test_spn_ucs2_90(self):
from simtrace2_pysniff.server.decode import _decode_spn
r = _decode_spn(b'\x00' + b'\x90' + 'Привет'.encode('utf-16-be'))
self.assertEqual(r['name'], 'Привет')
def test_spn_gsm7(self):
from simtrace2_pysniff.server.decode import _decode_spn
r = _decode_spn(b'\x00' + bytes.fromhex('41b6390c'))
self.assertEqual(r['name'], 'Alfa')
class TestDcsText(unittest.TestCase):
def test_gsm7(self):
from simtrace2_pysniff.server.decode import _decode_dcs_text
self.assertEqual(_decode_dcs_text(b'\x00Alfa'), 'Alfa')
def test_ucs2(self):
from simtrace2_pysniff.server.decode import _decode_dcs_text
self.assertEqual(_decode_dcs_text(b'\x08' + 'OK'.encode('utf_16_be')), 'OK')
def test_latin1(self):
from simtrace2_pysniff.server.decode import _decode_dcs_text
self.assertEqual(_decode_dcs_text(b'\x04caf\xe9'), 'café')
class TestAtrPps(unittest.TestCase):
def test_atr_simple_t0_only(self):
from simtrace2_pysniff.server.decode import _decode_atr
# 3B 00: direct, T0=00, no interface bytes, 0 historical bytes, only T=0 → no TCK
r = _decode_atr(bytes.fromhex('3B00'))
self.assertEqual(r['convention'], 'direct')
self.assertEqual(r['t0'], '00')
self.assertEqual(r['historical_len'], 0)
self.assertNotIn('tck', r)
def test_atr_ta1(self):
from simtrace2_pysniff.server.decode import _decode_atr
# 3B 12 11 00 00: TA1=11 (Fi=1,Di=1), 2 historical bytes, T=0 → no TCK
r = _decode_atr(bytes.fromhex('3B12110000'))
self.assertEqual(r['convention'], 'direct')
self.assertEqual(r['interface'][0]['name'], 'TA1')
self.assertEqual(r['interface'][0]['f'], 372)
self.assertEqual(r['interface'][0]['d'], 1)
self.assertEqual(r['historical_len'], 2)
self.assertNotIn('tck', r)
def test_atr_td1_t1_has_tck(self):
from simtrace2_pysniff.server.decode import _decode_atr
# 3B 80 01 80 31 80 66 ... : construct T=1 with TCK
# T0=80 → TD1 present, 0 historical; TD1=01 (T=1, no more interface); then TCK
# bytes: 3B 80 01 xx where xx = XOR(80,01) = 81
r = _decode_atr(bytes.fromhex('3B800181'))
self.assertEqual(r['protocols'], ['T=1'])
self.assertIn('tck', r)
self.assertTrue(r['tck_valid'])
self.assertEqual(r['tck'], '81')
def test_atr_inverse_convention(self):
from simtrace2_pysniff.server.decode import _decode_atr
# Inverse: TS=3F, then inverted bytes. Take direct ATR "3B 00" and invert body.
# Inverted 0x00 is 0x00, so ATR = 3F 00 → convention inverse, T0=00.
r = _decode_atr(bytes.fromhex('3F00'))
self.assertEqual(r['convention'], 'inverse')
self.assertEqual(r['t0'], '00')
def test_atr_historical_life_cycle(self):
from simtrace2_pysniff.server.decode import _decode_atr
# T0 = 0x03 (3 historical bytes); historical = 80 00 05 (life cycle: operational)
r = _decode_atr(bytes.fromhex('3B03800005'))
self.assertEqual(r['historical_len'], 3)
self.assertEqual(r['historical']['category'], 'status information (life cycle)')
self.assertEqual(r['historical']['life_cycle'], 'operational state (activated)')
def test_pps(self):
from simtrace2_pysniff.server.decode import _decode_pps
# FF 10 11 xx: PPS0=10 (T=0, PPS1 present), PPS1=11 (Fi=1,Di=1), PCK=XOR(FF,10,11)=FE
r = _decode_pps(bytes.fromhex('FF1011FE'))
self.assertEqual(r['protocol'], 'T=0')
self.assertEqual(r['fi_di']['f'], 372)
self.assertEqual(r['fi_di']['d'], 1)
self.assertTrue(r['pck_valid'])
self.assertEqual(r['pck'], 'FE')
class TestSummary(unittest.TestCase):
def test_select_path_from_mf(self):
r = decode_message(bytes.fromhex('a0a40804022fe29000'))
self.assertEqual(r['summary'], 'Path from MF: 2FE2 (EF_ICCID), Return FCI template')
def test_select_by_file_id(self):
r = decode_message(bytes.fromhex('a0a40000023f009000'))
self.assertEqual(r['summary'], 'DF/EF/MF by file ID: 3F00 (MF)')
def test_read_record(self):
r = decode_message(bytes.fromhex('a0b2010400'))
self.assertEqual(r['summary'], 'Record number: 1, absolute mode (record number in P1)')
def test_verify_pin_no_leak(self):
# VERIFY PIN with P2 = 0x01 (PIN Appl 1): the PIN value must not leak.
r = decode_message(bytes.fromhex('0020000108' + '12' * 8))
self.assertEqual(r['summary'], 'PIN Appl 1')
self.assertNotIn('12', r['summary'])
def test_fetch_setup_menu(self):
r = decode_message(bytes.fromhex(
'8012000030D02E810301250082028182050B416C6661204D6F62696C'
'658F16808112089DB0C1C2C0BEB9BAB82F53657474696E67739000'))
self.assertEqual(r['summary'], 'Alfa Mobile, 1 item')
def test_envelope_sms_pp(self):
r = decode_message(bytes.fromhex(
'80C2000022D120020283810607919740430900F40B1104038154F50004'
'628021410300210248699000'))
self.assertEqual(r['summary'], 'SMSC +79043490004, SMS-DELIVER from 455 «Hi»')
def test_terminal_response(self):
r = decode_message(bytes.fromhex(
'80140000108103010300020282810301000402011E9000'))
self.assertEqual(r['summary'], '→ POLL INTERVAL, Command performed successfully')
def test_get_response_for(self):
r = decode_message(bytes.fromhex('a0c000000f'),
prev={'ins_name': 'SELECT', 'sw1': '61'})
self.assertEqual(r['summary'], 'response for SELECT')
class TestSelectPath(unittest.TestCase):
def test_path_adf_alias(self):
# 7FFF = current ADF; 6F05 after it → EF_LI (USIM).
r = decode_message(bytes.fromhex('a0a40804047fff6f059000'))
self.assertEqual(r['body']['note'], 'current ADF/EF_LI')
self.assertEqual(r['summary'],
'Path from MF: 7FFF6F05 (current ADF/EF_LI), Return FCI template')
def test_path_full(self):
r = decode_message(bytes.fromhex('a0a40804067f107f206f079000'))
self.assertEqual(r['body']['note'], 'DF_TELECOM/DF_GSM/EF_IMSI')
def test_path_current_df(self):
# P1 = 0x09 → 'Path from current DF' (no 7FFF/3F00 prefix).
r = decode_message(bytes.fromhex('a0a40904046f076f049000'))
self.assertEqual(r['p1']['name'], 'Path from current DF')
self.assertEqual(r['body']['note'], 'EF_IMSI/EF_IMPU')
def test_path_isim_adf_alias(self):
# 7FFF 6F04 → EF_IMPU (ISIM).
r = decode_message(bytes.fromhex('a0a40804047fff6f049000'))
self.assertEqual(r['body']['note'], 'current ADF/EF_IMPU')
def test_path_df_child(self):
# 7FFF 5F3B 4F20 → current ADF / DF_GSM_ACCESS / EF_Kc.
r = decode_message(bytes.fromhex('a0a40804067fff5f3b4f209000'))
self.assertEqual(r['body']['note'], 'current ADF/DF_GSM_ACCESS/EF_Kc')
def test_path_df_child_disambiguates_fid(self):
# 7F10 5F3B 4F47 → DF_TELECOM / DF_MULTIMEDIA / EF_MML (5F3B is
# DF_MULTIMEDIA here, not DF_GSM_ACCESS).
r = decode_message(bytes.fromhex('a0a40804067f105f3b4f479000'))
self.assertEqual(r['body']['note'], 'DF_TELECOM/DF_MULTIMEDIA/EF_MML')
def test_path_phonebook(self):
r = decode_message(bytes.fromhex('a0a40804067f105f3a4f229000'))
self.assertEqual(r['body']['note'], 'DF_TELECOM/DF_PHONEBOOK/EF_PSC')
def test_path_5gs(self):
r = decode_message(bytes.fromhex('a0a40804067fff5fc04f019000'))
self.assertEqual(r['body']['note'], 'current ADF/DF_5GS/EF_5GS3GPPLOCI')
class TestFileDecoders(unittest.TestCase):
def test_imsi(self):
from simtrace2_pysniff.server.decode import _decode_file_data
f = _decode_file_data('6f07', bytes.fromhex('082905102143658709'))
self.assertEqual(f['imsi'], '250011234567890')
def test_imsi_real_card(self):
# Real capture (Mi A1): 08 length byte + 8 bytes, first nibble is the
# TS 24.008 type+parity indicator (9 = IMSI, odd).
from simtrace2_pysniff.server.decode import _decode_file_data
f = _decode_file_data('6f07', bytes.fromhex('082905917700917259'))
self.assertEqual(f['imsi'], '250197700192795')
def test_iccid(self):
from simtrace2_pysniff.server.decode import _decode_file_data
f = _decode_file_data('2fe2', bytes.fromhex('98891020000000460012'))
self.assertEqual(f['iccid'], '89980102000000640021')
def test_li(self):
from simtrace2_pysniff.server.decode import _decode_file_data
f = _decode_file_data('6f05', b'enru')
self.assertEqual(f['languages'], ['en', 'ru'])
def test_adn(self):
from simtrace2_pysniff.server.decode import _decode_file_data
f = _decode_file_data('6f3a', bytes.fromhex(
'42204841203120536963FFFFFFFFFFFF06810628560810FFFFFFFFFFFFFF'))
self.assertEqual(f['name'], 'B HA 1 Sic')
self.assertEqual(f['number'], '6082658001')
def test_adn_ber_tlv_fallback(self):
# Non-ADN data (BER-TLV) must not be decoded as a name/number.
from simtrace2_pysniff.server.decode import _decode_file_data
raw = bytes.fromhex(
'A0348001078120E823FF53C3E271754A644ED63DEFCF24A916387E3C585F'
'0820CF3E27841852F7820400000000830400000000840102'
'FFFFFFFFFFFFFFFFFFFFFFFFFFFF')
f = _decode_file_data('6fc7', raw)
self.assertNotIn('name', f)
self.assertIn('raw', f)
def test_ust(self):
from simtrace2_pysniff.server.decode import _decode_file_data
f = _decode_file_data('6f38', bytes.fromhex('07'))
self.assertEqual([s['n'] for s in f['services']], [1, 2, 3])
self.assertEqual(f['services'][0]['name'], 'Local Phone Book')
def test_plmn_list(self):
from simtrace2_pysniff.server.decode import _decode_file_data
f = _decode_file_data('6f30', bytes.fromhex('42f095'))
self.assertEqual(len(f['plmns']), 1)
self.assertIn('mcc', f['plmns'][0])
self.assertIn('mnc', f['plmns'][0])
def test_dir(self):
from simtrace2_pysniff.server.decode import _decode_file_data
f = _decode_file_data('2f00', bytes.fromhex(
'612b4f10a0000000871002fffff00189000001ff50074d656761466f6e'
'730ea00c80011781025f408203454150ffffffffffffffffffffffffffffffffffffff'))
self.assertEqual(f['applications'][0]['label'], 'MegaFon')
self.assertEqual(f['applications'][0]['aid'], 'A0000000871002FFFFF00189000001FF')
eap = f['applications'][0]['eap']
self.assertEqual(eap['eap_types'], ['EAP-AKA'])
self.assertEqual(eap['dfs'], ['5f40'])
self.assertEqual(eap['label'], 'EAP')
def test_nai_empty_record(self):
from simtrace2_pysniff.server.decode import _decode_file_data
# Unused ISIM record (all FF) must be 'empty', not garbage text.
f = _decode_file_data('6f04', b'\xff' * 75)
self.assertTrue(f['empty'])
self.assertNotIn('text', f)
def test_nai(self):
from simtrace2_pysniff.server.decode import _decode_file_data
f = _decode_file_data('6f04', bytes.fromhex(
'80357369703a32353030323639333537373333363840696d732e6d6e63'
'3030322e6d63633235302e336770706e6574776f726b2e6f7267'))
self.assertEqual(f['text'], 'sip:250026935773368@ims.mnc002.mcc250.3gppnetwork.org')
def test_plmn_wact(self):
from simtrace2_pysniff.server.decode import _decode_file_data
# TS 51.011 §10.3.35: 5-byte entries (3 PLMN + 2 access tech).
f = _decode_file_data('6f60', bytes.fromhex('52f020400052f0208000'))
self.assertEqual(f['plmns'][0]['mcc'], '250')
self.assertEqual(f['plmns'][0]['mnc'], '02')
self.assertEqual(f['plmns'][0]['access_tech'], 'E-UTRAN NB-S1, E-UTRAN WB-S1')
self.assertEqual(f['plmns'][1]['access_tech'], 'UTRAN')
def test_sms_record(self):
from simtrace2_pysniff.server.decode import _decode_file_data
# status 0x07 (MO, to be sent) + empty SMSC (len 0) + SMS-SUBMIT TPDU
rec = bytes.fromhex('0700') + bytes.fromhex('01ff038199f90004024869')
f = _decode_file_data('6f3c', rec)
self.assertEqual(f['direction'], 'MO')
self.assertEqual(f['status'], 'message to be sent')
self.assertEqual(f['tpdu']['mti'], 'SMS-SUBMIT')
self.assertEqual(f['tpdu']['da'], '999')
self.assertEqual(f['tpdu']['text'], 'Hi')
def test_loci(self):
from simtrace2_pysniff.server.decode import _decode_file_data
# EF_LOCI (11 bytes): TMSI 4 + LAI 5 + TMSI_TIME 1 + status 1.
f = _decode_file_data('6f7e', bytes.fromhex('1e731e5752f09969c50000'))
self.assertEqual(f['tmsi'], '1E731E57')
self.assertEqual(f['mcc'], '250')
self.assertEqual(f['mnc'], '99')
self.assertEqual(f['lac'], '0x69C5')
self.assertEqual(f['location_update_status'], '0x00')
def test_epsloci(self):
from simtrace2_pysniff.server.decode import _decode_file_data
# EF_EPSLOCI (18 bytes): GUTI 12 + TAI 5 + status 1.
f = _decode_file_data('6fe3', bytes.fromhex('0bf652f0998001b0fb91192652f0991d9900'))
self.assertEqual(f['guti'], '0BF652F0998001B0FB911926')
self.assertEqual(f['eps_update_status'], '0x00')
def test_psloci(self):
from simtrace2_pysniff.server.decode import _decode_file_data
# EF_PSLOCI (14 bytes): P-TMSI 4 + sig 3 + RAI 6 + status 1.
f = _decode_file_data('6f73', bytes.fromhex('1111111122222252f0991d990001'))
self.assertEqual(f['p_tmsi'], '11111111')
self.assertEqual(f['p_tmsi_signature'], '222222')
self.assertEqual(f['mcc'], '250')
self.assertEqual(f['mnc'], '99')
self.assertEqual(f['lac'], '0x1D99')
self.assertEqual(f['rac'], '0x00')
self.assertEqual(f['update_status'], '0x01')
def test_epsnsc(self):
from simtrace2_pysniff.server.decode import _decode_file_data
# EF_EPSNSC record: A0 { 80 KSI_ASME, 81 K_ASME, 82/83 NAS counts, 84 algos }.
f = _decode_file_data('6fe4', bytes.fromhex(
'a0188001018104aabbccdd820400000003830400000004840102'))
self.assertEqual(f['ksi_asme'], '01')
self.assertEqual(f['k_asme'], 'AABBCCDD')
self.assertEqual(f['uplink_nas_count'], 3)
self.assertEqual(f['downlink_nas_count'], 4)
self.assertEqual(f['nas_algorithms'], '02')
def test_read_binary_direct(self):
# SELECT EF_IMSI then READ BINARY → file decoded in body.
r = decode_message(bytes.fromhex('00b0000009') +
bytes.fromhex('082905102143658709') + bytes.fromhex('9000'),
prev={'sel': {'fid': '6f07', 'name': 'EF_IMSI'}})
self.assertEqual(r['file']['imsi'], '250011234567890')
self.assertIn('IMSI 250011234567890', r['summary'])
def test_read_binary_offset_skip(self):
r = decode_message(bytes.fromhex('00b00100') +
bytes.fromhex('082905102143658709') + bytes.fromhex('9000'),
prev={'sel': {'fid': '6f07'}})
self.assertNotIn('file', r)
def test_read_binary_offset_form(self):
# b8 of P1 = 0 → offset = b7..b1 << 8 | P2.
r = decode_message(bytes.fromhex('00b0010000'), prev=None)
self.assertEqual(r['p1p2'], {'value': 0x0100, 'offset': 256})
self.assertIn('Offset: 0x0100', r['summary'])
def test_read_binary_sfi_form(self):
# b8 of P1 = 1 → SFI referencing: SFI = b5..b1, P2 = offset.
r = decode_message(bytes.fromhex('00d68b000b') +
bytes.fromhex('0000000000000000000000') + bytes.fromhex('9000'),
prev=None)
self.assertEqual(r['p1p2'], {'value': 0x8B00, 'sfi': 11, 'offset': 0})
self.assertIn('SFI 11', r['summary'])
self.assertNotIn('offset 0', r['summary'])
def test_read_binary_sfi_offset(self):
# SFI referencing with a non-zero offset (P2 = 5).
r = decode_message(bytes.fromhex('00b08b0500'), prev=None)
self.assertEqual(r['p1p2'], {'value': 0x8B05, 'sfi': 11, 'offset': 5})
self.assertIn('SFI 11, offset 5', r['summary'])
def test_read_via_get_response(self):
iccid = bytes.fromhex('98891020000000460012')
gr = bytes.fromhex('00c00000') + bytes([len(iccid)]) + iccid + bytes.fromhex('9000')
r = decode_message(gr, prev={'ins': 0xB0, 'ins_name': 'READ BINARY', 'sw1': '61',
'sel': {'fid': '2fe2'}, 'file_ok': True})
self.assertEqual(r['file']['iccid'], '89980102000000640021')
def test_get_response_file_ok_false(self):
iccid = bytes.fromhex('98891020000000460012')
gr = bytes.fromhex('00c00000') + bytes([len(iccid)]) + iccid + bytes.fromhex('9000')
r = decode_message(gr, prev={'ins': 0xB0, 'ins_name': 'READ BINARY', 'sw1': '61',
'sel': {'fid': '2fe2'}, 'file_ok': False})
self.assertNotIn('file', r)
def test_search_record_numbers(self):
# SEARCH RECORD returns 61xx; GET RESPONSE carries the record numbers.
numbers = bytes(range(3, 21)) # 3..20
gr = bytes.fromhex('00c00000') + bytes([len(numbers)]) + numbers + bytes.fromhex('9000')
r = decode_message(gr, prev={'ins': 0xA2, 'ins_name': 'SEARCH RECORD', 'sw1': '61',
'sel': {'fid': '6f3a'}, 'file_ok': True})
self.assertEqual(r['file']['record_numbers'], list(range(3, 21)))
self.assertIn('18 record(s)', r['summary'])
def test_select_target_fid(self):
from simtrace2_pysniff.server.decode import select_target_fid
self.assertEqual(select_target_fid({'p1': {'raw': '00'}, 'body': {'hex': '6f07'}}), '6f07')
self.assertEqual(select_target_fid({'p1': {'raw': '08'}, 'body': {'hex': '7fff6f05'}}), '6f05')
self.assertIsNone(select_target_fid({'p1': {'raw': '04'}, 'body': {'hex': 'a0000000871002'}}))
self.assertIsNone(select_target_fid({'p1': {'raw': '08'}, 'body': {'hex': '7fff'}}))
class TestSelectionTracking(unittest.TestCase):
def test_select_then_read(self):
import tempfile
from simtrace2_pysniff.server.database import Database
with tempfile.NamedTemporaryFile(suffix='.db') as tmp:
db = Database(tmp.name)
sid = db.create_session('capture')
db.insert_message(sid, 0.0, 'atr', b'\x3b\x00')
db.insert_message(sid, 0.1, 'tpdu', bytes.fromhex('a0a40804047fff6f079000'))
db.insert_message(sid, 0.2, 'tpdu',
bytes.fromhex('00b0000009') + bytes.fromhex('082905102143658709') + bytes.fromhex('9000'))
msgs = db.get_messages(sid)
read = msgs[2]
self.assertEqual(read['decoded']['file']['imsi'], '250011234567890')
def test_atr_resets_selection(self):
import tempfile
from simtrace2_pysniff.server.database import Database
with tempfile.NamedTemporaryFile(suffix='.db') as tmp:
db = Database(tmp.name)
sid = db.create_session('capture')
db.insert_message(sid, 0.0, 'tpdu', bytes.fromhex('a0a40804047fff6f079000'))
db.insert_message(sid, 0.1, 'atr', b'\x3b\x00')
db.insert_message(sid, 0.2, 'tpdu',
bytes.fromhex('00b0000009') + bytes.fromhex('082905102143658709') + bytes.fromhex('9000'))
msgs = db.get_messages(sid)
self.assertNotIn('file', msgs[2]['decoded'])
def test_timeout_wt_keeps_selection(self):
# A waiting-time timeout interrupts one TPDU but the card stays
# selected, so the selection must not be reset.
import tempfile
from simtrace2_pysniff.server.database import Database
with tempfile.NamedTemporaryFile(suffix='.db') as tmp:
db = Database(tmp.name)
sid = db.create_session('capture')
db.insert_message(sid, 0.0, 'tpdu', bytes.fromhex('a0a40804047fff6f079000'))
db.insert_message(sid, 0.1, 'change', b'', 1 << 4) # TIMEOUT_WT
db.insert_message(sid, 0.2, 'tpdu',
bytes.fromhex('00b0000009') + bytes.fromhex('082905102143658709') + bytes.fromhex('9000'))
msgs = db.get_messages(sid)
self.assertEqual(msgs[2]['decoded']['file']['imsi'], '250011234567890')
def test_reset_assert_resets_selection(self):
import tempfile
from simtrace2_pysniff.server.database import Database
with tempfile.NamedTemporaryFile(suffix='.db') as tmp:
db = Database(tmp.name)
sid = db.create_session('capture')
db.insert_message(sid, 0.0, 'tpdu', bytes.fromhex('a0a40804047fff6f079000'))
db.insert_message(sid, 0.1, 'change', b'', 1 << 2) # RESET_ASSERT
db.insert_message(sid, 0.2, 'tpdu',
bytes.fromhex('00b0000009') + bytes.fromhex('082905102143658709') + bytes.fromhex('9000'))
msgs = db.get_messages(sid)
self.assertNotIn('file', msgs[2]['decoded'])
def test_gap_resets_selection(self):
# A capture gap (device disconnected/reconnected) means messages may
# have been missed, so the selection must be reset.
import tempfile
from simtrace2_pysniff.server.database import Database
with tempfile.NamedTemporaryFile(suffix='.db') as tmp:
db = Database(tmp.name)
sid = db.create_session('capture')
db.insert_message(sid, 0.0, 'tpdu', bytes.fromhex('a0a40804047fff6f079000'))
db.insert_message(sid, 0.1, 'gap', b'', 0)
db.insert_message(sid, 0.2, 'tpdu',
bytes.fromhex('00b0000009') + bytes.fromhex('082905102143658709') + bytes.fromhex('9000'))
msgs = db.get_messages(sid)
self.assertNotIn('file', msgs[2]['decoded'])
def test_failed_select_keeps_selection(self):
# A SELECT that fails (6A82) must not change the current file.
import tempfile
from simtrace2_pysniff.server.database import Database
with tempfile.NamedTemporaryFile(suffix='.db') as tmp:
db = Database(tmp.name)
sid = db.create_session('capture')
db.insert_message(sid, 0.0, 'tpdu', bytes.fromhex('a0a40804047fff6f079000'))
db.insert_message(sid, 0.1, 'tpdu', bytes.fromhex('a0a40804047fff6f136a82'))
db.insert_message(sid, 0.2, 'tpdu',
bytes.fromhex('00b0000009') + bytes.fromhex('082905102143658709') + bytes.fromhex('9000'))
msgs = db.get_messages(sid)
read = msgs[2]
self.assertEqual(read['decoded']['file']['imsi'], '250011234567890')
def test_record_op_on_transparent_is_stale(self):
# SELECT a transparent EF, then READ RECORD → selection is stale, so
# we must not decode garbage.
import tempfile
from simtrace2_pysniff.server.database import Database
with tempfile.NamedTemporaryFile(suffix='.db') as tmp:
db = Database(tmp.name)
sid = db.create_session('capture')
db.insert_message(sid, 0.0, 'tpdu', bytes.fromhex('a0a40804047fff6f056123'))
db.insert_message(sid, 0.1, 'tpdu',
bytes.fromhex('00c000000a') + bytes.fromhex('62088202012183026f05') + bytes.fromhex('9000'))
db.insert_message(sid, 0.2, 'tpdu',
bytes.fromhex('00b2010408') + bytes.fromhex('800101a010a4068301019501') + bytes.fromhex('9000'))
msgs = db.get_messages(sid)
read = msgs[2]
self.assertTrue(read['decoded']['file']['stale'])
def test_sfi_update_resolves_eps_loci(self):
# SFI referencing must resolve the target EF (EF_EPSLOCI), not the
# last-selected file.
import tempfile
from simtrace2_pysniff.server.database import Database
with tempfile.NamedTemporaryFile(suffix='.db') as tmp:
db = Database(tmp.name)
sid = db.create_session('capture')
db.insert_message(sid, 0.0, 'atr', b'\x3b\x00')
db.insert_message(sid, 0.1, 'tpdu', bytes.fromhex('00a40804027fff612a'))
db.insert_message(sid, 0.2, 'tpdu',
bytes.fromhex('00d69e0012') +
bytes.fromhex('0bf652f0998001b0fb91192652f0991d9900') +
bytes.fromhex('9000'))
msgs = db.get_messages(sid)
up = msgs[2]
self.assertEqual(up['decoded']['file']['ef'], 'EF_EPSLOCI')
self.assertEqual(up['decoded']['file']['guti'], '0BF652F0998001B0FB911926')
def test_sfi_unknown_not_misattributed(self):
# An SFI not in the map must not fall back to the selected file.
import tempfile
from simtrace2_pysniff.server.database import Database
with tempfile.NamedTemporaryFile(suffix='.db') as tmp:
db = Database(tmp.name)
sid = db.create_session('capture')
db.insert_message(sid, 0.0, 'tpdu', bytes.fromhex('00a40804022fe2611e'))
db.insert_message(sid, 0.1, 'tpdu', bytes.fromhex('00d6890001079000'))
msgs = db.get_messages(sid)
up = msgs[1]
self.assertTrue(up['decoded']['file']['unknown'])
self.assertEqual(up['decoded']['file']['sfi'], 9)
def test_record_sfi_sets_current_ef(self):
# A record command with a valid SFI sets that file as the current EF,
# so a subsequent non-SFI record op targets it (TS 102 221 §11.1.2).
import tempfile
from simtrace2_pysniff.server.database import Database
with tempfile.NamedTemporaryFile(suffix='.db') as tmp:
db = Database(tmp.name)
sid = db.create_session('capture')
db.insert_message(sid, 0.0, 'atr', b'\x3b\x00')
db.insert_message(sid, 0.1, 'tpdu', bytes.fromhex('00a40804047fff6f7e611e'))
db.insert_message(sid, 0.2, 'tpdu', bytes.fromhex('00dc01c436') +
bytes.fromhex('a0348001018104aabbccdd820400000003830400000004840102') +
bytes.fromhex('9000'))
db.insert_message(sid, 0.3, 'tpdu', bytes.fromhex('00dc010436') +
bytes.fromhex('a0348001078104aabbccdd820400000004830400000003840102') +
bytes.fromhex('9000'))
msgs = db.get_messages(sid)
self.assertEqual(msgs[2]['decoded']['file']['ef'], 'EF_EPSNSC')
# 0.3 has P2=04 (no SFI) → must still target EF_EPSNSC.
self.assertEqual(msgs[3]['decoded']['file']['ef'], 'EF_EPSNSC')
def test_channel_selection_not_clobbered(self):
# A SELECT on channel 1 (CLA 01) must not change channel 0's selection.
import tempfile
from simtrace2_pysniff.server.database import Database
with tempfile.NamedTemporaryFile(suffix='.db') as tmp:
db = Database(tmp.name)
sid = db.create_session('capture')
db.insert_message(sid, 0.0, 'tpdu', bytes.fromhex('00a40804047fff6f409000'))
db.insert_message(sid, 0.1, 'tpdu', bytes.fromhex('01a40804047fff6f02611e'))
db.insert_message(sid, 0.2, 'tpdu',
bytes.fromhex('00b2010427') +
bytes.fromhex('8109089cbeb920bdbebcb5c0ffffffffffffffffffffffffff07919752738981f7ffffffffffff') +
bytes.fromhex('9000'))
msgs = db.get_messages(sid)
up = msgs[2]
# channel 0 still selects EF_MSISDN, not channel 1's EF_IMPI.
self.assertEqual(up['decoded']['file']['ef'], 'EF_MSISDN')
class TestSfiResolution(unittest.TestCase):
def test_sfi_resolved(self):
r = decode_message(
bytes.fromhex('00d69e00120bf652f0998001b0fb91192652f0991d99009000'),
prev={'sfi_map': {30: '6fe3'}})
self.assertEqual(r['file']['ef'], 'EF_EPSLOCI')
self.assertEqual(r['file']['guti'], '0BF652F0998001B0FB911926')
def test_sfi_unknown(self):
r = decode_message(bytes.fromhex('00d6880001079000'), prev={'sfi_map': {}})
self.assertTrue(r['file']['unknown'])
self.assertEqual(r['file']['sfi'], 8)
def test_sfi_read_resolves(self):
# READ BINARY via SFI needs no prev['sel']; only the SFI map.
r = decode_message(bytes.fromhex('00b08b000b') +
bytes.fromhex('1e731e5752f09969c50000') + bytes.fromhex('9000'),
prev={'sfi_map': {11: '6f7e'}})
self.assertEqual(r['file']['ef'], 'EF_LOCI')
self.assertEqual(r['file']['tmsi'], '1E731E57')
def test_selected_df_fid(self):
from simtrace2_pysniff.server.decode import selected_df_fid, sfi_table
self.assertEqual(selected_df_fid({'ins_hex': 'a4', 'p1': {'raw': '08'},
'body': {'hex': '3f007fff'}}), '7fff')
self.assertEqual(selected_df_fid({'ins_hex': 'a4', 'p1': {'raw': '00'},
'body': {'hex': '7f20'}}), '7f20')
self.assertIsNone(selected_df_fid({'ins_hex': 'a4', 'p1': {'raw': '00'},
'body': {'hex': '6f7e'}}))
self.assertIsNone(selected_df_fid({'ins_hex': 'b0'}))
self.assertEqual(sfi_table('7fff')[0x1e], '6fe3')
self.assertEqual(sfi_table('3f00')[0x1e], '2f00')
self.assertEqual(sfi_table('7fff')[0x17], '6f06')
self.assertEqual(sfi_table('7fff')[0x1a], '6fc6')
self.assertEqual(sfi_table('6f7e'), {})
def test_record_p2_sfi(self):
# READ RECORD P2 = 0xBC → SFI = 0xBC>>3 = 23, mode = 0x04.
r = decode_message(bytes.fromhex('00b204bc00'), prev={})
self.assertEqual(r['p2']['sfi'], 23)
self.assertIn('absolute mode', r['p2']['bits'][0])
def test_update_record_sfi_resolves(self):
# UPDATE RECORD P2 = 0xC4 → SFI 24 = EF_EPSNSC (record 54 B).
raw = bytes.fromhex('00dc01c436') + bytes.fromhex(
'a0348001078120ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
'8204ffffffff8304ffffffff840100') + bytes.fromhex('9000')
r = decode_message(raw, prev={'sfi_map': {24: '6fe4'}})
self.assertEqual(r['file']['ef'], 'EF_EPSNSC')
self.assertEqual(r['file']['ksi_asme'], '07')
class TestP1P2(unittest.TestCase):
def test_search_record_p2_modes(self):
r = decode_message(bytes.fromhex('00a2010508ffffffffffffffff9000'))
self.assertEqual(r['p2']['bits'], ['Simple search (backward)'])
r = decode_message(bytes.fromhex('00a2010708ffffffffffffffff9000'))
self.assertEqual(r['p2']['bits'], ['Proprietary search'])
def test_pin_p2_reference(self):
r = decode_message(bytes.fromhex('0020000108' + '12' * 8))
self.assertEqual(r['p1']['name'], 'No indication')
self.assertEqual(r['p2']['name'], 'PIN Appl 1')
r = decode_message(bytes.fromhex('0020008108' + '12' * 8))
self.assertEqual(r['p2']['name'], 'Second PIN Appl 1')
def test_manage_channel_p2(self):
r = decode_message(bytes.fromhex('0070000300'))
self.assertEqual(r['p1']['name'], 'Open channel')
self.assertEqual(r['p2']['name'], 'Channel 3')
def test_retrieve_data_p2(self):
r = decode_message(bytes.fromhex('80cb008004' + '4f' * 4))
self.assertEqual(r['p2']['bits'], ['First block'])
def test_get_data_tag(self):
r = decode_message(bytes.fromhex('80ca005000'))
self.assertEqual(r['p1p2'], {'label': 'Tag', 'value': 0x50})
def test_commands_always_have_p1p2(self):
# FETCH and GET RESPONSE have unused P1/P2 (always '00') — RFU.
r = decode_message(bytes.fromhex('801200000fd00d8103010300820281820402011e9000'))
self.assertEqual(r['p1p2'], {'unused': True, 'value': 0})
r = decode_message(bytes.fromhex('00c0000000'), prev={'ins': 0xA4, 'ins_name': 'SELECT', 'sw1': '61'})
self.assertEqual(r['p1p2'], {'unused': True, 'value': 0})
def test_increase_p1p2_unused(self):
r = decode_message(bytes.fromhex('80320000030100000000'))
self.assertEqual(r['p1p2'], {'unused': True, 'value': 0})
def test_deactivate_activate_file_p1(self):
r = decode_message(bytes.fromhex('00040000126f0a'))
self.assertEqual(r['p1']['name'], 'EF by file ID')
self.assertEqual(r['p2']['name'], 'No indication')
r = decode_message(bytes.fromhex('00040800123f002f00'))
self.assertEqual(r['p1']['name'], 'Path from MF')
r = decode_message(bytes.fromhex('00040900122f00'))
self.assertEqual(r['p1']['name'], 'Path from current DF')
class TestSwWrongLength(unittest.TestCase):
def test_6c(self):
from simtrace2_pysniff.server.decode import decode_sw
self.assertEqual(decode_sw(bytes.fromhex('6c38'))['name'],
'Wrong length (Le): correct length is 0x38 (56 bytes)')
def test_62_unknown_is_unnamed(self):
from simtrace2_pysniff.server.decode import decode_sw
self.assertIsNone(decode_sw(bytes.fromhex('6205'))['name'])
class TestStatusFcp(unittest.TestCase):
def test_status_body_is_fcp(self):
raw = bytes.fromhex(
'80f2000038'
'62368202782183027ff08410a0000000871002ff45ff0189020111008a01058b032f0608c60c90016083010183018183010a81040000039f'
'9000')
r = decode_message(raw)
self.assertEqual(r['ins_name'], 'STATUS')
self.assertEqual(r['sw']['sw1'], '90')
resp = r['response']
self.assertEqual(resp['template'], 'FCP')
self.assertEqual(resp['file_id'], '7FF0')
self.assertEqual(resp['df_name'], 'A0000000871002FF45FF018902011100')
self.assertEqual(resp['file_descriptor']['file_type'], 'DF or ADF')
self.assertEqual(resp['life_cycle'], 'operational state (activated)')
class TestLengthMismatch(unittest.TestCase):
def test_lc_truncated(self):
# ENVELOPE with Lc=21, body missing (only SW captured).
r = decode_message(bytes.fromhex('80c20000159000'))
self.assertEqual(r['sw']['sw1'], '90')
self.assertEqual(r['sw']['sw2'], '00')
self.assertNotIn('body', r)
self.assertEqual(r['length_mismatch'],
{'kind': 'truncated', 'expected': 21, 'actual': 0})
def test_lc_excessive(self):
# ENVELOPE with Lc=3 but 4 bytes of body + SW.
r = decode_message(bytes.fromhex('80c2000003d10102039000'))
self.assertEqual(r['length_mismatch'],
{'kind': 'excessive', 'expected': 3, 'actual': 4})
def test_select_well_formed(self):
r = decode_message(bytes.fromhex('a0a40000023f009000'))
self.assertNotIn('length_mismatch', r)
def test_le_short_response_not_flagged(self):
# READ BINARY (Le) returning an error SW with no data is not "truncated".
r = decode_message(bytes.fromhex('00b08f00066a82'))
self.assertEqual(r['sw']['sw1'], '6a')
self.assertNotIn('length_mismatch', r)
class TestRefresh(unittest.TestCase):
def test_uicc_reset(self):
r = decode_message(bytes.fromhex('801200000bd0098103010104820281829000'))
self.assertEqual(r['cat_command'], 'REFRESH')
self.assertEqual(r['cmd']['qualifier'], '0x04 UICC Reset')
self.assertNotIn('file_list', r['cmd'])
self.assertNotIn('aid', r['cmd'])
def test_file_change_notification(self):
r = decode_message(bytes.fromhex('8012000011d00f81030101018202818292046f076f209000'))
self.assertEqual(r['cmd']['qualifier'], '0x01 File Change Notification')
self.assertEqual(r['cmd']['file_list'], ['6F07', '6F20'])
if __name__ == '__main__':
unittest.main()