diff --git a/frontend/index.html b/frontend/index.html index 209d2c7..f21df42 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1881,7 +1881,7 @@ function onFilterChange() { } // ──────────────────── Versioning ──────────────────── -const PWA_VERSION = '1.16.0'; +const PWA_VERSION = '1.16.1'; const MIN_SERVER_VERSION = '1.0.0'; function parseVersion(v) { diff --git a/frontend/sw.js b/frontend/sw.js index d5f8cd5..f6ba6c8 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simtrace-analyser-v22'; +const CACHE = 'simtrace-analyser-v23'; const PRECACHE = [ 'index.html', diff --git a/pyproject.toml b/pyproject.toml index 2b20a1e..5c5de61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend" [project] name = "simtrace2-pysniff" -version = "1.16.0" +version = "1.16.1" description = "Python-based sniffing tool for SIMtrace2 hardware (SIM card communication sniffer)" readme = {file = "README.md", content-type = "text/markdown"} license = {text = "GPL-2.0-or-later"} diff --git a/simtrace2_pysniff/__init__.py b/simtrace2_pysniff/__init__.py index 2682846..5b297ff 100644 --- a/simtrace2_pysniff/__init__.py +++ b/simtrace2_pysniff/__init__.py @@ -6,7 +6,7 @@ from .gsmtap import GsmtapSender, GsmtapReceiver from .pcap import PcapWriter from .dump import FileDumper, format_message -__version__ = '1.16.0' +__version__ = '1.16.1' __all__ = [ 'SniffSession', 'DeviceDisconnected', diff --git a/simtrace2_pysniff/server/decode.py b/simtrace2_pysniff/server/decode.py index 6e06b7d..98d9125 100644 --- a/simtrace2_pysniff/server/decode.py +++ b/simtrace2_pysniff/server/decode.py @@ -3858,13 +3858,22 @@ def decode_message(raw_data, prev=None): result['file'] = {'sfi': rec_sfi, 'unknown': True, 'ef': f'SFI {rec_sfi}'} elif ins in (0xB0, 0xB2, 0xD6, 0xDC) and prev and prev.get('sel'): offset = p1p2.get('offset', 0) - if ins in (0xB2, 0xDC) or offset == 0: - if ins in (0xB2, 0xDC) and not _is_record_file(prev['sel']): - result['file'] = _stale_file_note(prev['sel']) - else: + if ins in (0xB2, 0xDC) and not _is_record_file(prev['sel']): + result['file'] = _stale_file_note(prev['sel']) + else: + file_dec = None + if ins in (0xB2, 0xDC) or offset == 0: file_dec = _decode_file_data(prev['sel']['fid'], body, p1=p1) - if file_dec: - result['file'] = file_dec + if file_dec: + result['file'] = file_dec + else: + # Known file, no content decoder (or non-zero offset) — + # still attribute the operation to the selected file. + result['file'] = { + 'fid': prev['sel']['fid'], + 'ef': KNOWN_FIDS.get(prev['sel']['fid'], + prev['sel']['fid'].upper()), + } # SW if sw_bytes: @@ -3884,6 +3893,13 @@ def decode_message(raw_data, prev=None): file_dec = _decode_file_data(prev['sel']['fid'], remaining[:cmd_body_len]) if file_dec: result['file'] = file_dec + else: + # Known file, no content decoder — attribute anyway. + fid = prev['sel']['fid'] + result['file'] = { + 'fid': fid, + 'ef': KNOWN_FIDS.get(fid, fid.upper()), + } elif prev_ins == 0xA2 and prev.get('sel') and prev.get('file_ok'): if not _is_record_file(prev['sel']): result['file'] = _stale_file_note(prev['sel']) diff --git a/tests/test_decode.py b/tests/test_decode.py index 98a5f0e..7cad17b 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -1143,10 +1143,13 @@ class TestFileDecoders(unittest.TestCase): self.assertIn('IMSI 250011234567890', r['summary']) def test_read_binary_offset_skip(self): + # Non-zero offset: content decode is skipped (data starts mid-file) + # but the operation is still attributed to the selected file. r = decode_message(bytes.fromhex('00b00100') + bytes.fromhex('082905102143658709') + bytes.fromhex('9000'), prev={'sel': {'fid': '6f07'}}) - self.assertNotIn('file', r) + self.assertEqual(r['file'], {'fid': '6f07', 'ef': 'EF_IMSI'}) + self.assertNotIn('imsi', r['file']) def test_read_binary_offset_form(self): # b8 of P1 = 0 → offset = b7..b1 << 8 | P2. @@ -2129,6 +2132,47 @@ class TestApduSpecCoverage(unittest.TestCase): self.assertEqual(r, 'MANAGE LSI') +class TestFileAttribution(unittest.TestCase): + """Non-SFI READ/UPDATE attribute to the last file selected in the + channel — even when the file has no content decoder (mobileid + session 63 regression: UPDATE BINARY after a proactive group).""" + + SEL_SMSS = {'sel': {'fid': '6f43', 'name': 'EF_SMSS', 'structure': 'transparent'}} + + def _upd(self, prev): + raw = bytes.fromhex('00d6000004aabbccdd' + '9000') + return decode_message(raw, prev=prev) + + def test_update_atributed_without_content_decoder(self): + # EF_SMSS (6F43) has no FILE_DECODERS entry — attribution must + # still carry the file name. + r = self._upd(dict(self.SEL_SMSS, ins=0xB0, ins_name='SELECT', sw1='61')) + self.assertEqual(r['file'], {'fid': '6f43', 'ef': 'EF_SMSS'}) + + def test_update_with_content_decoder_wins(self): + prev = dict(self.SEL_SMSS, ins=0xB0, ins_name='SELECT', sw1='61') + prev['sel'] = {'fid': '6f7e', 'name': 'EF_LOCI', 'structure': 'transparent'} + raw = bytes.fromhex('00d600000b' + '00' * 11 + '9000') + r = decode_message(raw, prev=prev) + self.assertEqual(r['file']['ef'], 'EF_LOCI') + self.assertIn('mcc', r['file']) + + def test_update_nonzero_offset_attributed(self): + # Content decode skipped for non-zero offset, attribution kept. + raw = bytes.fromhex('00d6001004aabbccdd' + '9000') + r = decode_message(raw, prev=dict(self.SEL_SMSS, ins=0xB0, + ins_name='SELECT', sw1='61')) + self.assertEqual(r['file']['ef'], 'EF_SMSS') + + def test_get_response_attribution(self): + # READ BINARY (61 XX) → GET RESPONSE on a decoder-less file + prev = dict(self.SEL_SMSS, ins=0xB0, ins_name='READ BINARY', sw1='61', + file_ok=True) + raw = bytes.fromhex('00c0000002' + 'cafe' + '9000') + r = decode_message(raw, prev=prev) + self.assertEqual(r['file'], {'fid': '6f43', 'ef': 'EF_SMSS'}) + + class TestClaDecode(unittest.TestCase): """ISO 7816-4 §5.1.1 first/further interindustry CLA coding.""" diff --git a/tests/test_pcap_import.py b/tests/test_pcap_import.py index 7fae3de..d5df490 100644 --- a/tests/test_pcap_import.py +++ b/tests/test_pcap_import.py @@ -197,7 +197,7 @@ class TestImportEndpoint(unittest.TestCase): class TestVersion(unittest.TestCase): def test_package_version(self): from simtrace2_pysniff import __version__ - self.assertEqual(__version__, '1.16.0') + self.assertEqual(__version__, '1.16.1') def test_status_includes_version(self): import os