Hoernchen has uploaded this change for review.
cat: properly decode IMEI/IMEISV IE
Currently BcdAdapter(GreedyBytes), a plain
swapped nibble digit string, which is wrong, it 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
---
M pySim/cat.py
M tests/unittests/test_cat.py
M tests/unittests/test_tlvs.py
3 files changed, 115 insertions(+), 4 deletions(-)
git pull ssh://gerrit.osmocom.org:29418/pysim refs/changes/61/43561/1
diff --git a/pySim/cat.py b/pySim/cat.py
index 991dd40..8e072d8 100644
--- a/pySim/cat.py
+++ b/pySim/cat.py
@@ -22,7 +22,7 @@
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,52 @@
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 ''
+ 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 +639,14 @@
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)
diff --git a/tests/unittests/test_cat.py b/tests/unittests/test_cat.py
index 4e0450c..c6324c2 100644
--- a/tests/unittests/test_cat.py
+++ b/tests/unittests/test_cat.py
@@ -25,7 +25,66 @@
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):
diff --git a/tests/unittests/test_tlvs.py b/tests/unittests/test_tlvs.py
index 7f1b72b..71eaa05 100755
--- a/tests/unittests/test_tlvs.py
+++ b/tests/unittests/test_tlvs.py
@@ -21,6 +21,7 @@
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
To view, visit change 43561. To unsubscribe, or for help writing mail filters, visit settings.