Flag ciphered SCP80 fields: on-wire CNTR/PCNTR/RC are ciphertext

User-reported: the decoded CNTR (0x1830A44E41 ≈ 104 billion) cannot
be the real counter — the OTA platform shows ~200 for that card.

Root cause: per TS 102 225 Table 2 note 1, when SPI indicates
ciphering the whole block from CNTR through the secured data
(CNTR, PCNTR, RC/CC/DS, secured data) is ENCRYPTED. Only CPI, CPL,
CHI/CHL, SPI, KIc, KID and TAR travel in the clear. Our decoder
parsed the ciphertext bytes as if they were the real CNTR/PCNTR/RC
and presented them as such — misleading (the true values are only
recoverable after 3DES decryption with the card's keys).

Fix:
- _decode_secured_packet: emit 'ciphered': True when SPI1.b3 is set
  (wire hex values kept for reference); docstring documents the
  ciphered block.
- _decode_response_packet: accepts ciphered (command SPI2.b5, PoR
  ciphering) and flags the response likewise; database context now
  carries por_ciphered from the ENVELOPE so the GET RESPONSE PoR
  inherits it. Standalone SMS-SUBMIT PoRs carry no SPI — state
  unknowable, defaults to False (documented).
- Frontend: CNTR/PCNTR/RC-CC-DS labels get "(encrypted)" when the
  respective packet is ciphered (command, response, and ENVELOPE
  response renderings).

Tests: +4 TestSecuredCiphering (ciphered flagged, unciphered plain
CNTR e.g. 200, response flag, PoR-via-context). 348 total. Version
1.18.0, SW cache simtrace-analyser-v26.
This commit is contained in:
catarrh committed 2026-08-24 19:51:40 +03:00
1 parent 14445ae38f
commit 840e233baf
8 files changed
+93 -23

No files matched your search

+48
View File
@@ -2080,6 +2080,54 @@ class TestSwUiccSpecific(unittest.TestCase):
self.assertIsNone(self._sw('6205')['name'])
class TestSecuredCiphering(unittest.TestCase):
"""Ciphered SCP80 packets: on-wire CNTR/PCNTR/RC are ciphertext
(TS 102 225 Table 2 note 1) — flagged, not shown as real values."""
def test_ciphered_packet_flagged(self):
# stk_test1-style SMS-PP DOWNLOAD secured packet (ciphering on):
# CNTR on the wire is ciphertext; the card's real counter (~200)
# is only recoverable after 3DES decryption.
from simtrace2_pysniff.server.decode import _decode_secured_packet
sec = bytes.fromhex('003815160115 15b000011830a44e4191ad45d0ec'
'2a291315986468a539d248fcdce7952c31cd635baec'
'7e93a5d567b59f6283316c0dad8603bf3'.replace(' ', ''))
r = _decode_secured_packet(sec)
self.assertTrue(r['ciphered'])
self.assertTrue(r['spi']['ciphering'])
self.assertEqual(r['cntr'], '1830A44E41') # wire bytes kept, flagged
def test_unciphered_packet_plain_cntr(self):
# SPI1 = 0x10: counter present, NO ciphering, RC/CC/DS none →
# CNTR is genuine plaintext (e.g. 200 = 0x00000000C8).
from simtrace2_pysniff.server.decode import _decode_secured_packet
sec = bytes.fromhex('00100d10011515b0000100000000c800aabb')
r = _decode_secured_packet(sec)
self.assertFalse(r['ciphered'])
self.assertEqual(r['cntr'], '00000000C8')
self.assertNotIn('rc_cc_ds', r)
def test_response_packet_ciphered_flag(self):
from simtrace2_pysniff.server.decode import _decode_response_packet
body = bytes.fromhex('001f0a00000000000000500000ab12800101230d08a0000001510000000f9a9000')
r = _decode_response_packet(body, ciphered=True)
self.assertTrue(r['ciphered'])
r2 = _decode_response_packet(body)
self.assertFalse(r2['ciphered'])
def test_por_ciphered_via_context(self):
# GET RESPONSE after a ciphered ENVELOPE: the response packet's
# ciphered flag flows from the command's SPI2.b5 via prev context.
prev = {'ins': 0xC2, 'ins_name': 'ENVELOPE', 'sw1': '61',
'por_ciphered': True}
raw = bytes.fromhex('80c0000019' +
'0271' +
'001f0a00000000000000500000ab12800101230d08a0000001510000000f9a9000'[:0x19 * 2] +
'9000')
r = decode_message(raw, prev=prev)
self.assertTrue(r['response']['ciphered'])
class TestSpiBits(unittest.TestCase):
"""TS 102 225 §5.1.1: SPI1 b3 = ciphering, SPI2 b5 = PoR ciphering."""