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):