From 58af838e72b3f1cf3ca20c0eb5302df962dd3e4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Sun, 16 Aug 2026 22:15:20 +0300 Subject: [PATCH] Decode P1/P2 for all commands; fix PIN/SEARCH RECORD references - Always emit raw P1/P2 in decode_message (with name/bits when the spec provides them), so FETCH/ENVELOPE/GET RESPONSE/etc. show P1/P2. - Add PIN key-reference table (TS 102 221 Table 9.3) and move PIN reference from P1 to P2 for VERIFY/CHANGE/DISABLE/ENABLE/UNBLOCK PIN. - Fix SEARCH RECORD P2 mode bits (forward/backward/enhanced/proprietary). - Add MANAGE CHANNEL P2 (channel number), RETRIEVE/SET DATA block mode, GET/PUT DATA tag, STATUS/DELETE FILE P2. - Frontend renders raw-only P1/P2 cleanly. --- frontend/index.html | 6 +-- simtrace2_pysniff/server/decode.py | 61 +++++++++++++++++++++--------- tests/test_decode.py | 47 +++++++++++++++++++++-- 3 files changed, 89 insertions(+), 25 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 863aa87..422aca7 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -529,7 +529,7 @@ function responseRows(resp) { if (resp.life_cycle) rows.push(['Life cycle', resp.life_cycle]); if (resp.short_ef_id) rows.push(['Short EF ID', resp.short_ef_id]); - if (resp.type === 'GSM' || resp.type === '3G') { + if (resp.type === 'GSM' || resp.type === '3G/EPS/5G') { rows.push(['Auth', resp.type + (resp.status ? ' (' + resp.status + ')' : '')]); if (resp.sres) rows.push(['SRES', resp.sres]); if (resp.res) rows.push(['RES', resp.res]); @@ -827,12 +827,12 @@ function showDetail(msg, entry) { let p1 = ''; if (decoded.p1) { const n = decoded.p1.name || (decoded.p1.label && decoded.p1.value != null ? `${decoded.p1.label}: ${decoded.p1.value}` : ''); - p1 = `P10x${(decoded.p1.raw || '').toUpperCase()} — ${n}`; + p1 = `P10x${(decoded.p1.raw || '').toUpperCase()}${n ? ` — ${n}` : ''}`; } let p2 = ''; if (decoded.p2) { const n = decoded.p2.name || (decoded.p2.bits ? decoded.p2.bits.join(', ') : ''); - p2 = `P20x${(decoded.p2.raw || '').toUpperCase()} — ${n}`; + p2 = `P20x${(decoded.p2.raw || '').toUpperCase()}${n ? ` — ${n}` : ''}`; } let sw = ''; diff --git a/simtrace2_pysniff/server/decode.py b/simtrace2_pysniff/server/decode.py index 859852b..8c83a10 100644 --- a/simtrace2_pysniff/server/decode.py +++ b/simtrace2_pysniff/server/decode.py @@ -439,6 +439,17 @@ def select_target_fid(d): # ──────────────────── Per-INS specifications ──────────────────── +# TS 102 221 Table 9.3 — PIN mapping into key references (P2 of the PIN commands) +PIN_KEY_REFS = { + 0x01: 'PIN Appl 1', 0x02: 'PIN Appl 2', 0x03: 'PIN Appl 3', 0x04: 'PIN Appl 4', + 0x05: 'PIN Appl 5', 0x06: 'PIN Appl 6', 0x07: 'PIN Appl 7', 0x08: 'PIN Appl 8', + 0x0A: 'ADM1', 0x0B: 'ADM2', 0x0C: 'ADM3', 0x0D: 'ADM4', 0x0E: 'ADM5', + 0x11: 'Universal PIN', + 0x81: 'Second PIN Appl 1', 0x82: 'Second PIN Appl 2', 0x83: 'Second PIN Appl 3', + 0x84: 'Second PIN Appl 4', 0x85: 'Second PIN Appl 5', 0x86: 'Second PIN Appl 6', + 0x87: 'Second PIN Appl 7', 0x88: 'Second PIN Appl 8', +} + APDU_SPEC = { 0xA4: { 'name': 'SELECT', @@ -518,10 +529,8 @@ APDU_SPEC = { }, 0x20: { 'name': 'VERIFY PIN', - 'p1': {0x00: 'Verify PIN1', 0x01: 'Verify PIN2', 0x02: 'Verify ADM2', - 0x03: 'Verify ADM3', 0x04: 'Verify ADM4', 0x80: 'Verify PUK', - 0x81: 'Verify ADM5'}, - 'p2': {0x00: 'No indication', 0x04: 'Reset PIN'}, + 'p1': {0x00: 'No indication'}, + 'p2': PIN_KEY_REFS, 'body': {'label': 'PIN value'}, }, 0x21: { @@ -530,24 +539,26 @@ APDU_SPEC = { }, 0x24: { 'name': 'CHANGE PIN', - 'p1': {0x00: 'Change PIN1', 0x01: 'Change PIN2', 0x02: 'Change ADM2', - 0x03: 'Change ADM3', 0x04: 'Change ADM4', 0x80: 'Change PUK', - 0x81: 'Change ADM5'}, + 'p1': {0x00: 'No indication'}, + 'p2': PIN_KEY_REFS, 'body': {'label': 'Old+new PIN'}, }, 0x26: { 'name': 'DISABLE PIN', + 'p1': {0x00: 'No indication'}, + 'p2': PIN_KEY_REFS, 'body': {'label': 'PIN value'}, }, 0x28: { 'name': 'ENABLE PIN', + 'p1': {0x00: 'No indication'}, + 'p2': PIN_KEY_REFS, 'body': {'label': 'PIN value'}, }, 0x2C: { 'name': 'UNBLOCK PIN', - 'p1': {0x00: 'Unblock PIN1', 0x01: 'Unblock PIN2', 0x02: 'Unblock ADM2', - 0x03: 'Unblock ADM3', 0x04: 'Unblock ADM4', 0x80: 'Unblock PUK', - 0x81: 'Unblock ADM5'}, + 'p1': {0x00: 'No indication'}, + 'p2': PIN_KEY_REFS, 'body': {'label': 'PUK + new PIN'}, }, 0x88: { @@ -573,6 +584,7 @@ APDU_SPEC = { 0x70: { 'name': 'MANAGE CHANNEL', 'p1': {0x00: 'Open channel', 0x80: 'Close channel'}, + 'p2': {0x00: 'Auto-assign channel', **{n: f'Channel {n}' for n in range(1, 20)}}, 'body': None, }, 0xC0: { @@ -610,6 +622,7 @@ APDU_SPEC = { 'name': 'STATUS', 'p1': {0x00: 'No indication', 0x01: 'Current DF', 0x02: 'EF under current DF', 0x04: 'DF name', 0x0d: 'Applet status'}, + 'p2': {0x00: 'No indication'}, 'body': None, }, 0xE0: { @@ -619,6 +632,7 @@ APDU_SPEC = { 0xE4: { 'name': 'DELETE FILE', 'p1': {0x00: 'Delete EF/DF', 0x0c: 'Delete EF', 0x0d: 'Delete DF'}, + 'p2': {0x00: 'No indication'}, 'body': None, }, 0xAA: { @@ -644,18 +658,29 @@ APDU_SPEC = { 'label': 'Mode', 'bits': { 0x04: 'Simple search (forward)', - 0x02: 'Backward search', - 0x01: 'Enhanced search', + 0x05: 'Simple search (backward)', + 0x06: 'Enhanced search', + 0x07: 'Proprietary search', }, }, 'body': {'label': 'Search pattern'}, }, 0xCB: { 'name': 'RETRIEVE DATA', + 'p1': {0x00: 'No indication'}, + 'p2': {'label': 'Mode', 'bits': { + 0x80: 'First block', + 0x40: 'Retransmit previous block', + }}, 'body': {'label': 'TLV data'}, }, 0xDB: { 'name': 'SET DATA', + 'p1': {0x00: 'No indication'}, + 'p2': {'label': 'Mode', 'bits': { + 0x80: 'First block', + 0x40: 'Retransmit previous block', + }}, 'body': {'label': 'TLV data'}, }, 0x10: { @@ -668,10 +693,12 @@ APDU_SPEC = { }, 0xCA: { 'name': 'GET DATA', + 'p1p2': {'fmt': 'uint16be', 'label': 'Tag'}, 'body': {'label': 'TLV data'}, }, 0xDA: { 'name': 'PUT DATA', + 'p1p2': {'fmt': 'uint16be', 'label': 'Tag'}, 'body': {'label': 'TLV data'}, }, 0xE2: { @@ -1137,14 +1164,14 @@ def _decode_auth_3g(data): auts = data[2:2 + auts_len] else: auts = data[1:] - result = {'type': '3G', 'status': 'sync fail', 'auts': auts.hex().upper()} + result = {'type': '3G/EPS/5G', 'status': 'sync fail', 'auts': auts.hex().upper()} # AUTS = SQN_MS⊕AK (6) || MAC-S (8) (TS 33.102) if len(auts) >= 14: result['sqn_ak'] = auts[0:6].hex().upper() result['mac_s'] = auts[6:14].hex().upper() return result # tag 0xDB: success → length-prefixed RES, CK, IK, (KC) - result = {'type': '3G', 'status': 'success'} + result = {'type': '3G/EPS/5G', 'status': 'success'} i = 1 for name in ('res', 'ck', 'ik', 'kc'): if i >= len(data): @@ -2546,10 +2573,8 @@ def decode_message(raw_data, prev=None): offset = (p1 << 8) | p2 result['p1p2'] = {'label': spec['p1p2']['label'], 'value': offset} else: - if 'p1' in spec: - result['p1'] = _decode_field(spec['p1'], p1) - if 'p2' in spec: - result['p2'] = _decode_field(spec['p2'], p2) + result['p1'] = _decode_field(spec.get('p1', {}), p1) + result['p2'] = _decode_field(spec.get('p2', {}), p2) # Body: at least P3 bytes from byte 5; extra bytes may be SW remaining = raw_data[5:] diff --git a/tests/test_decode.py b/tests/test_decode.py index 4ec0e6e..8f465db 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -266,7 +266,7 @@ class TestAuthResponse(unittest.TestCase): from simtrace2_pysniff.server.decode import _decode_auth data = bytes.fromhex('DB084CFA9017FD0DD85A101A2622F60E8ABD2C2497B9A8EFAF55E510CAA393329FF97868B9537369D5266A4F084D417B05ABFAFAEE') r = _decode_auth(data) - self.assertEqual(r['type'], '3G') + self.assertEqual(r['type'], '3G/EPS/5G') self.assertEqual(r['status'], 'success') self.assertEqual(r['res'], '4CFA9017FD0DD85A') self.assertEqual(r['ck'], '1A2622F60E8ABD2C2497B9A8EFAF55E5') @@ -276,7 +276,7 @@ class TestAuthResponse(unittest.TestCase): from simtrace2_pysniff.server.decode import _decode_auth data = bytes.fromhex('DC0E' + '00' * 14) r = _decode_auth(data) - self.assertEqual(r['type'], '3G') + self.assertEqual(r['type'], '3G/EPS/5G') self.assertEqual(r['status'], 'sync fail') self.assertEqual(r['auts'], '00' * 14) @@ -799,8 +799,10 @@ class TestSummary(unittest.TestCase): self.assertEqual(r['summary'], 'Record number: 1, use record number from P1') def test_verify_pin_no_leak(self): - r = decode_message(bytes.fromhex('0020000000')) - self.assertEqual(r['summary'], 'Verify PIN1') + # 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( @@ -1063,5 +1065,42 @@ class TestSelectionTracking(unittest.TestCase): self.assertTrue(read['decoded']['file']['stale']) +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 P1/P2 (raw '00') even without a spec. + r = decode_message(bytes.fromhex('801200000fd00d8103010300820281820402011e9000')) + self.assertEqual(r['p1'], {'raw': '00'}) + self.assertEqual(r['p2'], {'raw': '00'}) + r = decode_message(bytes.fromhex('00c0000000'), prev={'ins': 0xA4, 'ins_name': 'SELECT', 'sw1': '61'}) + self.assertEqual(r['p1'], {'raw': '00'}) + self.assertEqual(r['p2'], {'raw': '00'}) + + if __name__ == '__main__': unittest.main()