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.
This commit is contained in:
1 parent
0fdaed503e
commit
58af838e72
3 files changed
+89
-25
No files matched your search
+3
-3
@@ -529,7 +529,7 @@ function responseRows(resp) {
|
|||||||
if (resp.life_cycle) rows.push(['Life cycle', resp.life_cycle]);
|
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.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 + ')' : '')]);
|
rows.push(['Auth', resp.type + (resp.status ? ' (' + resp.status + ')' : '')]);
|
||||||
if (resp.sres) rows.push(['SRES', resp.sres]);
|
if (resp.sres) rows.push(['SRES', resp.sres]);
|
||||||
if (resp.res) rows.push(['RES', resp.res]);
|
if (resp.res) rows.push(['RES', resp.res]);
|
||||||
@@ -827,12 +827,12 @@ function showDetail(msg, entry) {
|
|||||||
let p1 = '';
|
let p1 = '';
|
||||||
if (decoded.p1) {
|
if (decoded.p1) {
|
||||||
const n = decoded.p1.name || (decoded.p1.label && decoded.p1.value != null ? `${decoded.p1.label}: ${decoded.p1.value}` : '');
|
const n = decoded.p1.name || (decoded.p1.label && decoded.p1.value != null ? `${decoded.p1.label}: ${decoded.p1.value}` : '');
|
||||||
p1 = `<tr><td class="label">P1</td><td class="value">0x${(decoded.p1.raw || '').toUpperCase()} — <span class="name">${n}</span></td></tr>`;
|
p1 = `<tr><td class="label">P1</td><td class="value">0x${(decoded.p1.raw || '').toUpperCase()}${n ? ` — <span class="name">${n}</span>` : ''}</td></tr>`;
|
||||||
}
|
}
|
||||||
let p2 = '';
|
let p2 = '';
|
||||||
if (decoded.p2) {
|
if (decoded.p2) {
|
||||||
const n = decoded.p2.name || (decoded.p2.bits ? decoded.p2.bits.join(', ') : '');
|
const n = decoded.p2.name || (decoded.p2.bits ? decoded.p2.bits.join(', ') : '');
|
||||||
p2 = `<tr><td class="label">P2</td><td class="value">0x${(decoded.p2.raw || '').toUpperCase()} — <span class="name">${n}</span></td></tr>`;
|
p2 = `<tr><td class="label">P2</td><td class="value">0x${(decoded.p2.raw || '').toUpperCase()}${n ? ` — <span class="name">${n}</span>` : ''}</td></tr>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let sw = '';
|
let sw = '';
|
||||||
|
|||||||
@@ -439,6 +439,17 @@ def select_target_fid(d):
|
|||||||
|
|
||||||
# ──────────────────── Per-INS specifications ────────────────────
|
# ──────────────────── 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 = {
|
APDU_SPEC = {
|
||||||
0xA4: {
|
0xA4: {
|
||||||
'name': 'SELECT',
|
'name': 'SELECT',
|
||||||
@@ -518,10 +529,8 @@ APDU_SPEC = {
|
|||||||
},
|
},
|
||||||
0x20: {
|
0x20: {
|
||||||
'name': 'VERIFY PIN',
|
'name': 'VERIFY PIN',
|
||||||
'p1': {0x00: 'Verify PIN1', 0x01: 'Verify PIN2', 0x02: 'Verify ADM2',
|
'p1': {0x00: 'No indication'},
|
||||||
0x03: 'Verify ADM3', 0x04: 'Verify ADM4', 0x80: 'Verify PUK',
|
'p2': PIN_KEY_REFS,
|
||||||
0x81: 'Verify ADM5'},
|
|
||||||
'p2': {0x00: 'No indication', 0x04: 'Reset PIN'},
|
|
||||||
'body': {'label': 'PIN value'},
|
'body': {'label': 'PIN value'},
|
||||||
},
|
},
|
||||||
0x21: {
|
0x21: {
|
||||||
@@ -530,24 +539,26 @@ APDU_SPEC = {
|
|||||||
},
|
},
|
||||||
0x24: {
|
0x24: {
|
||||||
'name': 'CHANGE PIN',
|
'name': 'CHANGE PIN',
|
||||||
'p1': {0x00: 'Change PIN1', 0x01: 'Change PIN2', 0x02: 'Change ADM2',
|
'p1': {0x00: 'No indication'},
|
||||||
0x03: 'Change ADM3', 0x04: 'Change ADM4', 0x80: 'Change PUK',
|
'p2': PIN_KEY_REFS,
|
||||||
0x81: 'Change ADM5'},
|
|
||||||
'body': {'label': 'Old+new PIN'},
|
'body': {'label': 'Old+new PIN'},
|
||||||
},
|
},
|
||||||
0x26: {
|
0x26: {
|
||||||
'name': 'DISABLE PIN',
|
'name': 'DISABLE PIN',
|
||||||
|
'p1': {0x00: 'No indication'},
|
||||||
|
'p2': PIN_KEY_REFS,
|
||||||
'body': {'label': 'PIN value'},
|
'body': {'label': 'PIN value'},
|
||||||
},
|
},
|
||||||
0x28: {
|
0x28: {
|
||||||
'name': 'ENABLE PIN',
|
'name': 'ENABLE PIN',
|
||||||
|
'p1': {0x00: 'No indication'},
|
||||||
|
'p2': PIN_KEY_REFS,
|
||||||
'body': {'label': 'PIN value'},
|
'body': {'label': 'PIN value'},
|
||||||
},
|
},
|
||||||
0x2C: {
|
0x2C: {
|
||||||
'name': 'UNBLOCK PIN',
|
'name': 'UNBLOCK PIN',
|
||||||
'p1': {0x00: 'Unblock PIN1', 0x01: 'Unblock PIN2', 0x02: 'Unblock ADM2',
|
'p1': {0x00: 'No indication'},
|
||||||
0x03: 'Unblock ADM3', 0x04: 'Unblock ADM4', 0x80: 'Unblock PUK',
|
'p2': PIN_KEY_REFS,
|
||||||
0x81: 'Unblock ADM5'},
|
|
||||||
'body': {'label': 'PUK + new PIN'},
|
'body': {'label': 'PUK + new PIN'},
|
||||||
},
|
},
|
||||||
0x88: {
|
0x88: {
|
||||||
@@ -573,6 +584,7 @@ APDU_SPEC = {
|
|||||||
0x70: {
|
0x70: {
|
||||||
'name': 'MANAGE CHANNEL',
|
'name': 'MANAGE CHANNEL',
|
||||||
'p1': {0x00: 'Open channel', 0x80: 'Close 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,
|
'body': None,
|
||||||
},
|
},
|
||||||
0xC0: {
|
0xC0: {
|
||||||
@@ -610,6 +622,7 @@ APDU_SPEC = {
|
|||||||
'name': 'STATUS',
|
'name': 'STATUS',
|
||||||
'p1': {0x00: 'No indication', 0x01: 'Current DF', 0x02: 'EF under current DF',
|
'p1': {0x00: 'No indication', 0x01: 'Current DF', 0x02: 'EF under current DF',
|
||||||
0x04: 'DF name', 0x0d: 'Applet status'},
|
0x04: 'DF name', 0x0d: 'Applet status'},
|
||||||
|
'p2': {0x00: 'No indication'},
|
||||||
'body': None,
|
'body': None,
|
||||||
},
|
},
|
||||||
0xE0: {
|
0xE0: {
|
||||||
@@ -619,6 +632,7 @@ APDU_SPEC = {
|
|||||||
0xE4: {
|
0xE4: {
|
||||||
'name': 'DELETE FILE',
|
'name': 'DELETE FILE',
|
||||||
'p1': {0x00: 'Delete EF/DF', 0x0c: 'Delete EF', 0x0d: 'Delete DF'},
|
'p1': {0x00: 'Delete EF/DF', 0x0c: 'Delete EF', 0x0d: 'Delete DF'},
|
||||||
|
'p2': {0x00: 'No indication'},
|
||||||
'body': None,
|
'body': None,
|
||||||
},
|
},
|
||||||
0xAA: {
|
0xAA: {
|
||||||
@@ -644,18 +658,29 @@ APDU_SPEC = {
|
|||||||
'label': 'Mode',
|
'label': 'Mode',
|
||||||
'bits': {
|
'bits': {
|
||||||
0x04: 'Simple search (forward)',
|
0x04: 'Simple search (forward)',
|
||||||
0x02: 'Backward search',
|
0x05: 'Simple search (backward)',
|
||||||
0x01: 'Enhanced search',
|
0x06: 'Enhanced search',
|
||||||
|
0x07: 'Proprietary search',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'body': {'label': 'Search pattern'},
|
'body': {'label': 'Search pattern'},
|
||||||
},
|
},
|
||||||
0xCB: {
|
0xCB: {
|
||||||
'name': 'RETRIEVE DATA',
|
'name': 'RETRIEVE DATA',
|
||||||
|
'p1': {0x00: 'No indication'},
|
||||||
|
'p2': {'label': 'Mode', 'bits': {
|
||||||
|
0x80: 'First block',
|
||||||
|
0x40: 'Retransmit previous block',
|
||||||
|
}},
|
||||||
'body': {'label': 'TLV data'},
|
'body': {'label': 'TLV data'},
|
||||||
},
|
},
|
||||||
0xDB: {
|
0xDB: {
|
||||||
'name': 'SET DATA',
|
'name': 'SET DATA',
|
||||||
|
'p1': {0x00: 'No indication'},
|
||||||
|
'p2': {'label': 'Mode', 'bits': {
|
||||||
|
0x80: 'First block',
|
||||||
|
0x40: 'Retransmit previous block',
|
||||||
|
}},
|
||||||
'body': {'label': 'TLV data'},
|
'body': {'label': 'TLV data'},
|
||||||
},
|
},
|
||||||
0x10: {
|
0x10: {
|
||||||
@@ -668,10 +693,12 @@ APDU_SPEC = {
|
|||||||
},
|
},
|
||||||
0xCA: {
|
0xCA: {
|
||||||
'name': 'GET DATA',
|
'name': 'GET DATA',
|
||||||
|
'p1p2': {'fmt': 'uint16be', 'label': 'Tag'},
|
||||||
'body': {'label': 'TLV data'},
|
'body': {'label': 'TLV data'},
|
||||||
},
|
},
|
||||||
0xDA: {
|
0xDA: {
|
||||||
'name': 'PUT DATA',
|
'name': 'PUT DATA',
|
||||||
|
'p1p2': {'fmt': 'uint16be', 'label': 'Tag'},
|
||||||
'body': {'label': 'TLV data'},
|
'body': {'label': 'TLV data'},
|
||||||
},
|
},
|
||||||
0xE2: {
|
0xE2: {
|
||||||
@@ -1137,14 +1164,14 @@ def _decode_auth_3g(data):
|
|||||||
auts = data[2:2 + auts_len]
|
auts = data[2:2 + auts_len]
|
||||||
else:
|
else:
|
||||||
auts = data[1:]
|
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)
|
# AUTS = SQN_MS⊕AK (6) || MAC-S (8) (TS 33.102)
|
||||||
if len(auts) >= 14:
|
if len(auts) >= 14:
|
||||||
result['sqn_ak'] = auts[0:6].hex().upper()
|
result['sqn_ak'] = auts[0:6].hex().upper()
|
||||||
result['mac_s'] = auts[6:14].hex().upper()
|
result['mac_s'] = auts[6:14].hex().upper()
|
||||||
return result
|
return result
|
||||||
# tag 0xDB: success → length-prefixed RES, CK, IK, (KC)
|
# tag 0xDB: success → length-prefixed RES, CK, IK, (KC)
|
||||||
result = {'type': '3G', 'status': 'success'}
|
result = {'type': '3G/EPS/5G', 'status': 'success'}
|
||||||
i = 1
|
i = 1
|
||||||
for name in ('res', 'ck', 'ik', 'kc'):
|
for name in ('res', 'ck', 'ik', 'kc'):
|
||||||
if i >= len(data):
|
if i >= len(data):
|
||||||
@@ -2546,10 +2573,8 @@ def decode_message(raw_data, prev=None):
|
|||||||
offset = (p1 << 8) | p2
|
offset = (p1 << 8) | p2
|
||||||
result['p1p2'] = {'label': spec['p1p2']['label'], 'value': offset}
|
result['p1p2'] = {'label': spec['p1p2']['label'], 'value': offset}
|
||||||
else:
|
else:
|
||||||
if 'p1' in spec:
|
result['p1'] = _decode_field(spec.get('p1', {}), p1)
|
||||||
result['p1'] = _decode_field(spec['p1'], p1)
|
result['p2'] = _decode_field(spec.get('p2', {}), p2)
|
||||||
if 'p2' in spec:
|
|
||||||
result['p2'] = _decode_field(spec['p2'], p2)
|
|
||||||
|
|
||||||
# Body: at least P3 bytes from byte 5; extra bytes may be SW
|
# Body: at least P3 bytes from byte 5; extra bytes may be SW
|
||||||
remaining = raw_data[5:]
|
remaining = raw_data[5:]
|
||||||
|
|||||||
+43
-4
@@ -266,7 +266,7 @@ class TestAuthResponse(unittest.TestCase):
|
|||||||
from simtrace2_pysniff.server.decode import _decode_auth
|
from simtrace2_pysniff.server.decode import _decode_auth
|
||||||
data = bytes.fromhex('DB084CFA9017FD0DD85A101A2622F60E8ABD2C2497B9A8EFAF55E510CAA393329FF97868B9537369D5266A4F084D417B05ABFAFAEE')
|
data = bytes.fromhex('DB084CFA9017FD0DD85A101A2622F60E8ABD2C2497B9A8EFAF55E510CAA393329FF97868B9537369D5266A4F084D417B05ABFAFAEE')
|
||||||
r = _decode_auth(data)
|
r = _decode_auth(data)
|
||||||
self.assertEqual(r['type'], '3G')
|
self.assertEqual(r['type'], '3G/EPS/5G')
|
||||||
self.assertEqual(r['status'], 'success')
|
self.assertEqual(r['status'], 'success')
|
||||||
self.assertEqual(r['res'], '4CFA9017FD0DD85A')
|
self.assertEqual(r['res'], '4CFA9017FD0DD85A')
|
||||||
self.assertEqual(r['ck'], '1A2622F60E8ABD2C2497B9A8EFAF55E5')
|
self.assertEqual(r['ck'], '1A2622F60E8ABD2C2497B9A8EFAF55E5')
|
||||||
@@ -276,7 +276,7 @@ class TestAuthResponse(unittest.TestCase):
|
|||||||
from simtrace2_pysniff.server.decode import _decode_auth
|
from simtrace2_pysniff.server.decode import _decode_auth
|
||||||
data = bytes.fromhex('DC0E' + '00' * 14)
|
data = bytes.fromhex('DC0E' + '00' * 14)
|
||||||
r = _decode_auth(data)
|
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['status'], 'sync fail')
|
||||||
self.assertEqual(r['auts'], '00' * 14)
|
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')
|
self.assertEqual(r['summary'], 'Record number: 1, use record number from P1')
|
||||||
|
|
||||||
def test_verify_pin_no_leak(self):
|
def test_verify_pin_no_leak(self):
|
||||||
r = decode_message(bytes.fromhex('0020000000'))
|
# VERIFY PIN with P2 = 0x01 (PIN Appl 1): the PIN value must not leak.
|
||||||
self.assertEqual(r['summary'], 'Verify PIN1')
|
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):
|
def test_fetch_setup_menu(self):
|
||||||
r = decode_message(bytes.fromhex(
|
r = decode_message(bytes.fromhex(
|
||||||
@@ -1063,5 +1065,42 @@ class TestSelectionTracking(unittest.TestCase):
|
|||||||
self.assertTrue(read['decoded']['file']['stale'])
|
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__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
Reference in new issue
Block a user