diff --git a/frontend/index.html b/frontend/index.html index 17b7f78..863aa87 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -454,7 +454,7 @@ function cmd_description(m) { function sw_text(m) { const d = m.decoded; if (!d || !d.sw) return ''; - return d.sw.sw1 + ' ' + d.sw.sw2; + return (d.sw.sw1 + ' ' + d.sw.sw2).toUpperCase(); } function sw_class(m) { @@ -536,7 +536,11 @@ function responseRows(resp) { if (resp.ck) rows.push(['CK', resp.ck]); if (resp.ik) rows.push(['IK', resp.ik]); if (resp.kc) rows.push(['Kc', resp.kc]); - if (resp.auts) rows.push(['AUTS', resp.auts]); + if (resp.auts) { + rows.push(['AUTS', resp.auts]); + if (resp.sqn_ak) rows.push(['SQN⊕AK', resp.sqn_ak + ' (concealed)']); + if (resp.mac_s) rows.push(['MAC-S', resp.mac_s]); + } } if (resp.code !== undefined) { @@ -564,10 +568,15 @@ function cmdRows(cmd) { const rows = []; // AUTHENTICATE command body - if (cmd.context !== undefined) { - rows.push(['Auth context', cmd.context]); + if (cmd.rand !== undefined || cmd.autn !== undefined || cmd.specific_key) { + if (cmd.specific_key) rows.push(['Key', 'specific reference data']); if (cmd.rand) rows.push(['RAND', cmd.rand]); - if (cmd.autn) rows.push(['AUTN', cmd.autn]); + if (cmd.autn) { + rows.push(['AUTN', cmd.autn]); + if (cmd.sqn_ak) rows.push(['SQN⊕AK', cmd.sqn_ak + ' (concealed)']); + if (cmd.amf) rows.push(['AMF', cmd.amf]); + if (cmd.mac) rows.push(['MAC', cmd.mac]); + } } // ENVELOPE fields diff --git a/simtrace2_pysniff/server/decode.py b/simtrace2_pysniff/server/decode.py index deed7c9..859852b 100644 --- a/simtrace2_pysniff/server/decode.py +++ b/simtrace2_pysniff/server/decode.py @@ -552,7 +552,14 @@ APDU_SPEC = { }, 0x88: { 'name': 'AUTHENTICATE', - 'p1': {0x00: 'Run GSM algorithm', 0x80: 'Run 3G algo (resynchronisation)'}, + 'p1': {0x00: 'No indication'}, + 'p2': {'label': 'Auth context', 'bits': { + 0x00: 'GSM context', + 0x01: '3G/EPS/5G context', + 0x02: 'VGCS/VBS context', + 0x04: 'GBA context', + 0x80: 'Specific reference data', + }}, 'body': {'label': 'Challenge/session key'}, }, 0x89: { @@ -1127,9 +1134,15 @@ def _decode_auth_3g(data): if tag == 0xDC: # synchronisation failure → length byte + AUTS if len(data) >= 2: auts_len = data[1] - return {'type': '3G', 'status': 'sync fail', - 'auts': data[2:2 + auts_len].hex().upper()} - return {'type': '3G', 'status': 'sync fail', 'auts': data[1:].hex().upper()} + auts = data[2:2 + auts_len] + else: + auts = data[1:] + result = {'type': '3G', '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'} i = 1 @@ -1170,18 +1183,12 @@ def _decode_response_for(ins, data): # ──────────────────── Command-body decoders ──────────────────── -AUTH_CONTEXTS = { - 0: 'GSM', - 1: '3G (UMTS)', - 2: 'VGC/VBS', - 4: 'GBA', -} - - def _decode_auth_cmd(data, p2): """Decode an AUTHENTICATE command body (RAND / RAND+AUTN).""" ctx = p2 & 0x07 - result = {'context': AUTH_CONTEXTS.get(ctx, f'unknown ({ctx})')} + result = {} + if p2 & 0x80: + result['specific_key'] = True if ctx in (0, 1) and data: i = 0 if i < len(data): @@ -1189,10 +1196,16 @@ def _decode_auth_cmd(data, p2): i += 1 result['rand'] = data[i:i + rlen].hex().upper() i += rlen - if ctx == 1 and i < len(data): # 3G → AUTN + if ctx == 1 and i < len(data): # 3G/EPS/5G → AUTN alen = data[i] i += 1 - result['autn'] = data[i:i + alen].hex().upper() + autn = data[i:i + alen] + result['autn'] = autn.hex().upper() + # AUTN = SQN⊕AK (6) || AMF (2) || MAC-A (8) (TS 33.102) + if len(autn) >= 16: + result['sqn_ak'] = autn[0:6].hex().upper() + result['amf'] = autn[6:8].hex().upper() + result['mac'] = autn[8:16].hex().upper() return result diff --git a/tests/test_decode.py b/tests/test_decode.py index 569deb0..4ec0e6e 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -361,17 +361,39 @@ class TestAuthCommand(unittest.TestCase): r = decode_message(bytes.fromhex( '0088008122104A75BA425D438C549EF35BA5E3DD53051065E04D80E4DC8000A5C7FD842F41395E6135')) cmd = r['cmd'] - self.assertEqual(cmd['context'], '3G (UMTS)') + 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.assertEqual(r['context'], 'GSM') + 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):