cat: properly decode IMEI/IMEISV IE

Currently BcdAdapter(GreedyBytes), i.e. a plain swapped nibble digit
string, which is wrong, the IMEI IE is not a bcd string.
See TS 124.008 10.5.1.4 figure 10.5.4 + table 10.5.4

It currently
 - decodes framing nibble as digit
 - encodes a 15 digit IMEI as 7 bytes, wrong len, dropped last digit

Fix this, while at it add the 8.74 IMEISV as well + tests for the framing
because the current round trip missed all of this.

Change-Id: Iad7aea77513d413c338edc18ef1bcc37488ae849
This commit is contained in:
Eric Wild
2026-09-23 17:51:04 +02:00
parent df8de1a69a
commit 0de8274e99
3 changed files with 117 additions and 4 deletions
+56 -3
View File
@@ -22,7 +22,7 @@ from typing import List
from bidict import bidict
from construct import Int8ub, Int16ub, Byte, BitsInteger
from construct import Struct, Enum, BitStruct, this
from construct import Switch, GreedyRange, FlagsEnum
from construct import Switch, GreedyRange, FlagsEnum, Adapter
from osmocom.tlv import TLV_IE, COMPR_TLV_IE, BER_TLV_IE, TLV_IE_Collection
from osmocom.construct import PlmnAdapter, BcdAdapter, GsmStringAdapter, TonNpi, GsmString, Bytes, GreedyBytes
from osmocom.utils import b2h, h2b
@@ -320,9 +320,54 @@ class FileList(COMPR_TLV_IE, tag=0x92):
class LocationInformation(COMPR_TLV_IE, tag=0x93):
pass
# TS 102 223 Section 8.20
class MobileIdentityAdapter(Adapter):
"""TS 124.008 section 10.5.1.4 figure 10.5.4 + table 10.5.4
NOT a plain BCD string:
- bits 1-3 type of identity + odd/even bit 4
- digit 1 in bits 5-8, following octets contain 2 digits, low nibble first
- if even length: high nibble of last octet 1111
So IMEI IE of 8 bytes is 15 digits + framing nibble."""
# Table 10.5.4 bits 321
TYPE_IMSI = 1
TYPE_IMEI = 2
TYPE_IMEISV = 3
def __init__(self, subcon, type_of_identity: int):
super().__init__(subcon)
self.type_of_identity = type_of_identity
def _decode(self, obj, context, path):
data = bytes(obj)
if not data:
return ''
# TS 24.008 figure 10.5.4: octet 3 holds type of identity (b1-3), odd/even (b4) and
# digit 1 in its high nibble, the remaining digits follow BCD swapped from octet 4
odd = bool(data[0] & 0x08) # bit 4: 1 = odd number of digits
digits = '%x' % (data[0] >> 4) # bits 5-8: digit 1
for octet in data[1:]:
digits += '%x%x' % (octet & 0x0f, octet >> 4)
if not odd:
digits = digits[:-1] # drop the 1111 end mark
return digits
def _encode(self, obj, context, path):
digits = str(obj)
odd = len(digits) % 2
first = (int(digits[0], 16) << 4) | (0x08 if odd else 0x00) | self.type_of_identity
rest = digits[1:] if odd else digits[1:] + 'f'
return bytes([first]) + bytes((int(rest[i+1], 16) << 4) | int(rest[i], 16)
for i in range(0, len(rest), 2))
# TS 102 223 Section 8.20, len is fixed at 8: "The IMEI is coded [..] as the
# value part of the Mobile Identity IE as specified in TS 124 008", and the
# IMEI itself is the 15 digits of TS 123 003.
class IMEI(COMPR_TLV_IE, tag=0x94):
_construct = BcdAdapter(GreedyBytes)
_test_de_encode = [
( '94081a32547698103254', '123456789012345' ),
]
_construct = MobileIdentityAdapter(GreedyBytes, MobileIdentityAdapter.TYPE_IMEI)
# TS 102 223 Section 8.21
class HelpRequest(COMPR_TLV_IE, tag=0x95):
@@ -596,6 +641,14 @@ class UtranEutranMeasurementQualifier(COMPR_TLV_IE, tag=0xE9):
eutran_inter_rat_utran=0x08,
eutran_inter_rat_nr=0x09)
# TS 102 223 Section 8.74, length is not fixed, because IMEISV is 16 digits per TS 123.003
# -> even count needs the '1111' end mark and is 9 bytes long
class IMEISV(COMPR_TLV_IE, tag=0xE2):
_test_de_encode = [
( 'e2091332547698103254f6', '1234567890123456' ),
]
_construct = MobileIdentityAdapter(GreedyBytes, MobileIdentityAdapter.TYPE_IMEISV)
# TS 102 223 Section 8.75
class NetworkSearchMode(COMPR_TLV_IE, tag=0xE5):
_construct = Enum(Int8ub, manual=0, automatic=1)
+60 -1
View File
@@ -26,7 +26,66 @@ import unittest
from osmocom.utils import b2h, h2b
from pySim.cat import SupportedRadioAccessTechnologies
from pySim.cat import IMEI, IMEISV, AccessTechnology, SupportedRadioAccessTechnologies
class IMEI_Test(unittest.TestCase):
"""TS 102 223 8.20: the IMEI IE is 8 bytes, coded as valie part of Mobile Identity IE from 124 008"""
IMEI_15 = '123456789012345'
ENCODED = '94081a32547698103254'
def test_encode_is_eight_bytes(self):
"""15 digits in 8 byte: 16 nibbles, one is type/parity framing."""
tlv = IMEI(decoded=self.IMEI_15).to_tlv()
self.assertEqual(b2h(tlv), self.ENCODED)
self.assertEqual(tlv[1], 0x08) # spec len 8
self.assertEqual(len(tlv) - 2, 8)
def test_first_octet_framing(self):
"""TS 24.008 table 10.5.4"""
octet1 = IMEI(decoded=self.IMEI_15).to_tlv()[2]
self.assertEqual(octet1 & 0x07, 2) # IMEI
self.assertEqual((octet1 >> 3) & 0x01, 1) # odd
self.assertEqual(octet1 >> 4, 1) # digit 1
def test_decodes_to_the_raw_imei(self):
"""strip framing nibble"""
ie = IMEI()
ie.from_tlv(h2b(self.ENCODED))
self.assertEqual(ie.decoded, self.IMEI_15)
def test_even_digit_count_uses_the_end_mark(self):
""""end marker, IMEISV case"""
ie = IMEI(decoded='1234567890123456')
tlv = ie.to_tlv()
self.assertEqual(tlv[2] >> 3 & 0x01, 0) # even
self.assertEqual(tlv[-1] >> 4, 0x0f) # end mark
back = IMEI()
back.from_tlv(tlv)
self.assertEqual(back.decoded, '1234567890123456')
class IMEISV_Test(unittest.TestCase):
"""TS 102 223 8.74, no fixed len, end marker"""
IMEISV_16 = '1234567890123456'
ENCODED = 'e2091332547698103254f6'
def test_encode(self):
self.assertEqual(b2h(IMEISV(decoded=self.IMEISV_16).to_tlv()), self.ENCODED)
def test_type_of_identity_and_end_mark(self):
value = IMEISV(decoded=self.IMEISV_16).to_tlv()[2:]
self.assertEqual(value[0] & 0x07, 3) # IMEISV
self.assertEqual((value[0] >> 3) & 0x01, 0) # even
self.assertEqual(value[-1] >> 4, 0x0f) # end mark
self.assertEqual(len(value), 9)
def test_decode(self):
ie = IMEISV()
ie.from_tlv(h2b(self.ENCODED))
self.assertEqual(ie.decoded, self.IMEISV_16)
class SupportedRadioAccessTechnologies_Test(unittest.TestCase):
+1
View File
@@ -21,6 +21,7 @@ import logging
from osmocom.utils import b2h, h2b, all_subclasses
from osmocom.tlv import *
import pySim.cat
import pySim.iso7816_4
import pySim.ts_102_221
import pySim.ts_102_222