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)