Migrate over to using pyosmocom

We're creating a 'pyosmocom' pypi module which contains a number of core
Osmocom libraries / interfaces that are not specific to SIM card stuff
contained here.

The main modules moved in this initial step are pySim.tlv, pySim.utils
and pySim.construct. utils is split, not all of the contents is
unrelated to SIM Cards.  The other two are moved completely.

Change-Id: I4b63e45bcb0c9ba2424dacf85e0222aee735f411
This commit is contained in:
Harald Welte
2024-08-30 12:07:08 +02:00
parent a437d11135
commit a3962b2076
71 changed files with 157 additions and 2057 deletions

View File

@@ -10,6 +10,8 @@ import datetime
import argparse
from io import BytesIO
from typing import Optional, List, Dict, Any, Tuple, NewType, Union
from osmocom.utils import *
from osmocom.tlv import bertlv_encode_tag, bertlv_encode_len
# Copyright (C) 2009-2010 Sylvain Munaut <tnt@246tNt.com>
# Copyright (C) 2021 Harald Welte <laforge@osmocom.org>
@@ -28,400 +30,6 @@ from typing import Optional, List, Dict, Any, Tuple, NewType, Union
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# just to differentiate strings of hex nibbles from everything else
Hexstr = NewType('Hexstr', str)
SwHexstr = NewType('SwHexstr', str)
SwMatchstr = NewType('SwMatchstr', str)
ResTuple = Tuple[Hexstr, SwHexstr]
def h2b(s: Hexstr) -> bytearray:
"""convert from a string of hex nibbles to a sequence of bytes"""
return bytearray.fromhex(s)
def b2h(b: bytearray) -> Hexstr:
"""convert from a sequence of bytes to a string of hex nibbles"""
return ''.join(['%02x' % (x) for x in b])
def h2i(s: Hexstr) -> List[int]:
"""convert from a string of hex nibbles to a list of integers"""
return [(int(x, 16) << 4)+int(y, 16) for x, y in zip(s[0::2], s[1::2])]
def i2h(s: List[int]) -> Hexstr:
"""convert from a list of integers to a string of hex nibbles"""
return ''.join(['%02x' % (x) for x in s])
def h2s(s: Hexstr) -> str:
"""convert from a string of hex nibbles to an ASCII string"""
return ''.join([chr((int(x, 16) << 4)+int(y, 16)) for x, y in zip(s[0::2], s[1::2])
if int(x + y, 16) != 0xff])
def s2h(s: str) -> Hexstr:
"""convert from an ASCII string to a string of hex nibbles"""
b = bytearray()
b.extend(map(ord, s))
return b2h(b)
def i2s(s: List[int]) -> str:
"""convert from a list of integers to an ASCII string"""
return ''.join([chr(x) for x in s])
def swap_nibbles(s: Hexstr) -> Hexstr:
"""swap the nibbles in a hex string"""
return ''.join([x+y for x, y in zip(s[1::2], s[0::2])])
def rpad(s: str, l: int, c='f') -> str:
"""pad string on the right side.
Args:
s : string to pad
l : total length to pad to
c : padding character
Returns:
String 's' padded with as many 'c' as needed to reach total length of 'l'
"""
return s + c * (l - len(s))
def lpad(s: str, l: int, c='f') -> str:
"""pad string on the left side.
Args:
s : string to pad
l : total length to pad to
c : padding character
Returns:
String 's' padded with as many 'c' as needed to reach total length of 'l'
"""
return c * (l - len(s)) + s
def half_round_up(n: int) -> int:
return (n + 1)//2
def str_sanitize(s: str) -> str:
"""replace all non printable chars, line breaks and whitespaces, with ' ', make sure that
there are no whitespaces at the end and at the beginning of the string.
Args:
s : string to sanitize
Returns:
filtered result of string 's'
"""
chars_to_keep = string.digits + string.ascii_letters + string.punctuation
res = ''.join([c if c in chars_to_keep else ' ' for c in s])
return res.strip()
#########################################################################
# poor man's COMPREHENSION-TLV decoder.
#########################################################################
def comprehensiontlv_parse_tag_raw(binary: bytes) -> Tuple[int, bytes]:
"""Parse a single Tag according to ETSI TS 101 220 Section 7.1.1"""
if binary[0] in [0x00, 0x80, 0xff]:
raise ValueError("Found illegal value 0x%02x in %s" %
(binary[0], binary))
if binary[0] == 0x7f:
# three-byte tag
tag = binary[0] << 16 | binary[1] << 8 | binary[2]
return (tag, binary[3:])
elif binary[0] == 0xff:
return None, binary
else:
# single byte tag
tag = binary[0]
return (tag, binary[1:])
def comprehensiontlv_parse_tag(binary: bytes) -> Tuple[dict, bytes]:
"""Parse a single Tag according to ETSI TS 101 220 Section 7.1.1"""
if binary[0] in [0x00, 0x80, 0xff]:
raise ValueError("Found illegal value 0x%02x in %s" %
(binary[0], binary))
if binary[0] == 0x7f:
# three-byte tag
tag = (binary[1] & 0x7f) << 8
tag |= binary[2]
compr = bool(binary[1] & 0x80)
return ({'comprehension': compr, 'tag': tag}, binary[3:])
else:
# single byte tag
tag = binary[0] & 0x7f
compr = bool(binary[0] & 0x80)
return ({'comprehension': compr, 'tag': tag}, binary[1:])
def comprehensiontlv_encode_tag(tag) -> bytes:
"""Encode a single Tag according to ETSI TS 101 220 Section 7.1.1"""
# permit caller to specify tag also as integer value
if isinstance(tag, int):
compr = bool(tag < 0xff and tag & 0x80)
tag = {'tag': tag, 'comprehension': compr}
compr = tag.get('comprehension', False)
if tag['tag'] in [0x00, 0x80, 0xff] or tag['tag'] > 0xff:
# 3-byte format
byte3 = tag['tag'] & 0xff
byte2 = (tag['tag'] >> 8) & 0x7f
if compr:
byte2 |= 0x80
return b'\x7f' + byte2.to_bytes(1, 'big') + byte3.to_bytes(1, 'big')
else:
# 1-byte format
ret = tag['tag']
if compr:
ret |= 0x80
return ret.to_bytes(1, 'big')
# length value coding is equal to BER-TLV
def comprehensiontlv_parse_one(binary: bytes) -> Tuple[dict, int, bytes, bytes]:
"""Parse a single TLV IE at the start of the given binary data.
Args:
binary : binary input data of BER-TLV length field
Returns:
Tuple of (tag:dict, len:int, remainder:bytes)
"""
(tagdict, remainder) = comprehensiontlv_parse_tag(binary)
(length, remainder) = bertlv_parse_len(remainder)
value = remainder[:length]
remainder = remainder[length:]
return (tagdict, length, value, remainder)
#########################################################################
# poor man's BER-TLV decoder. To be a more sophisticated OO library later
#########################################################################
def bertlv_parse_tag_raw(binary: bytes) -> Tuple[int, bytes]:
"""Get a single raw Tag from start of input according to ITU-T X.690 8.1.2
Args:
binary : binary input data of BER-TLV length field
Returns:
Tuple of (tag:int, remainder:bytes)
"""
# check for FF padding at the end, as customary in SIM card files
if binary[0] == 0xff and len(binary) == 1 or binary[0] == 0xff and binary[1] == 0xff:
return None, binary
tag = binary[0] & 0x1f
if tag <= 30:
return binary[0], binary[1:]
else: # multi-byte tag
tag = binary[0]
i = 1
last = False
while not last:
last = not bool(binary[i] & 0x80)
tag <<= 8
tag |= binary[i]
i += 1
return tag, binary[i:]
def bertlv_parse_tag(binary: bytes) -> Tuple[dict, bytes]:
"""Parse a single Tag value according to ITU-T X.690 8.1.2
Args:
binary : binary input data of BER-TLV length field
Returns:
Tuple of ({class:int, constructed:bool, tag:int}, remainder:bytes)
"""
cls = binary[0] >> 6
constructed = bool(binary[0] & 0x20)
tag = binary[0] & 0x1f
if tag <= 30:
return ({'class': cls, 'constructed': constructed, 'tag': tag}, binary[1:])
else: # multi-byte tag
tag = 0
i = 1
last = False
while not last:
last = not bool(binary[i] & 0x80)
tag <<= 7
tag |= binary[i] & 0x7f
i += 1
return ({'class': cls, 'constructed': constructed, 'tag': tag}, binary[i:])
def bertlv_encode_tag(t) -> bytes:
"""Encode a single Tag value according to ITU-T X.690 8.1.2
"""
def get_top7_bits(inp: int) -> Tuple[int, int]:
"""Get top 7 bits of integer. Returns those 7 bits as integer and the remaining LSBs."""
remain_bits = inp.bit_length()
if remain_bits >= 7:
bitcnt = 7
else:
bitcnt = remain_bits
outp = inp >> (remain_bits - bitcnt)
remainder = inp & ~ (inp << (remain_bits - bitcnt))
return outp, remainder
def count_int_bytes(inp: int) -> int:
"""count the number of bytes require to represent the given integer."""
i = 1
inp = inp >> 8
while inp:
i += 1
inp = inp >> 8
return i
if isinstance(t, int):
# first convert to a dict representation
tag_size = count_int_bytes(t)
t, _remainder = bertlv_parse_tag(t.to_bytes(tag_size, 'big'))
tag = t['tag']
constructed = t['constructed']
cls = t['class']
if tag <= 30:
t = tag & 0x1f
if constructed:
t |= 0x20
t |= (cls & 3) << 6
return bytes([t])
else: # multi-byte tag
t = 0x1f
if constructed:
t |= 0x20
t |= (cls & 3) << 6
tag_bytes = bytes([t])
remain = tag
while True:
t, remain = get_top7_bits(remain)
if remain:
t |= 0x80
tag_bytes += bytes([t])
if not remain:
break
return tag_bytes
def bertlv_parse_len(binary: bytes) -> Tuple[int, bytes]:
"""Parse a single Length value according to ITU-T X.690 8.1.3;
only the definite form is supported here.
Args:
binary : binary input data of BER-TLV length field
Returns:
Tuple of (length, remainder)
"""
if binary[0] < 0x80:
return (binary[0], binary[1:])
else:
num_len_oct = binary[0] & 0x7f
length = 0
if len(binary) < num_len_oct + 1:
return (0, b'')
for i in range(1, 1+num_len_oct):
length <<= 8
length |= binary[i]
return (length, binary[1+num_len_oct:])
def bertlv_encode_len(length: int) -> bytes:
"""Encode a single Length value according to ITU-T X.690 8.1.3;
only the definite form is supported here.
Args:
length : length value to be encoded
Returns:
binary output data of BER-TLV length field
"""
if length < 0x80:
return length.to_bytes(1, 'big')
elif length <= 0xff:
return b'\x81' + length.to_bytes(1, 'big')
elif length <= 0xffff:
return b'\x82' + length.to_bytes(2, 'big')
elif length <= 0xffffff:
return b'\x83' + length.to_bytes(3, 'big')
elif length <= 0xffffffff:
return b'\x84' + length.to_bytes(4, 'big')
else:
raise ValueError("Length > 32bits not supported")
def bertlv_parse_one(binary: bytes) -> Tuple[dict, int, bytes, bytes]:
"""Parse a single TLV IE at the start of the given binary data.
Args:
binary : binary input data of BER-TLV length field
Returns:
Tuple of (tag:dict, len:int, remainder:bytes)
"""
(tagdict, remainder) = bertlv_parse_tag(binary)
(length, remainder) = bertlv_parse_len(remainder)
value = remainder[:length]
remainder = remainder[length:]
return (tagdict, length, value, remainder)
def bertlv_parse_one_rawtag(binary: bytes) -> Tuple[int, int, bytes, bytes]:
"""Parse a single TLV IE at the start of the given binary data; return tag as raw integer.
Args:
binary : binary input data of BER-TLV length field
Returns:
Tuple of (tag:int, len:int, remainder:bytes)
"""
(tag, remainder) = bertlv_parse_tag_raw(binary)
(length, remainder) = bertlv_parse_len(remainder)
value = remainder[:length]
remainder = remainder[length:]
return (tag, length, value, remainder)
def bertlv_return_one_rawtlv(binary: bytes) -> Tuple[int, int, bytes, bytes]:
"""Return one single [encoded] TLV IE at the start of the given binary data.
Args:
binary : binary input data of BER-TLV length field
Returns:
Tuple of (tag:int, len:int, tlv:bytes, remainder:bytes)
"""
(tag, remainder) = bertlv_parse_tag_raw(binary)
(length, remainder) = bertlv_parse_len(remainder)
tl_length = len(binary) - len(remainder)
value = binary[:tl_length] + remainder[:length]
remainder = remainder[length:]
return (tag, length, value, remainder)
def dgi_parse_tag_raw(binary: bytes) -> Tuple[int, bytes]:
# In absence of any clear spec guidance we assume it's always 16 bit
return int.from_bytes(binary[:2], 'big'), binary[2:]
def dgi_encode_tag(t: int) -> bytes:
return t.to_bytes(2, 'big')
def dgi_encode_len(length: int) -> bytes:
"""Encode a single Length value according to GlobalPlatform Systems Scripting Language
Specification v1.1.0 Annex B.
Args:
length : length value to be encoded
Returns:
binary output data of encoded length field
"""
if length < 255:
return length.to_bytes(1, 'big')
elif length <= 0xffff:
return b'\xff' + length.to_bytes(2, 'big')
else:
raise ValueError("Length > 32bits not supported")
def dgi_parse_len(binary: bytes) -> Tuple[int, bytes]:
"""Parse a single Length value according to GlobalPlatform Systems Scripting Language
Specification v1.1.0 Annex B.
Args:
binary : binary input data of BER-TLV length field
Returns:
Tuple of (length, remainder)
"""
if binary[0] == 255:
assert len(binary) >= 3
return ((binary[1] << 8) | binary[2]), binary[3:]
else:
return binary[0], binary[1:]
# IMSI encoded format:
# For IMSI 0123456789ABCDE:
#
@@ -437,6 +45,10 @@ def dgi_parse_len(binary: bytes) -> Tuple[int, bytes]:
# Because of this, an odd length IMSI fits exactly into len(imsi) + 1 // 2 bytes, whereas an
# even length IMSI only uses half of the last byte.
SwHexstr = NewType('SwHexstr', str)
SwMatchstr = NewType('SwMatchstr', str)
ResTuple = Tuple[Hexstr, SwHexstr]
def enc_imsi(imsi: str):
"""Converts a string IMSI into the encoded value of the EF"""
l = half_round_up(
@@ -809,29 +421,6 @@ def enc_msisdn(msisdn: str, npi: int = 0x01, ton: int = 0x03) -> Hexstr:
return ('%02x' % bcd_len) + ('%02x' % npi_ton) + bcd + ("ff" * 2)
def is_hex(string: str, minlen: int = 2, maxlen: Optional[int] = None) -> bool:
"""
Check if a string is a valid hexstring
"""
# Filter obviously bad strings
if not string:
return False
if len(string) < minlen or minlen < 2:
return False
if len(string) % 2:
return False
if maxlen and len(string) > maxlen:
return False
# Try actual encoding to be sure
try:
_try_encode = h2b(string)
return True
except:
return False
def sanitize_pin_adm(pin_adm, pin_adm_hex=None) -> Hexstr:
"""
The ADM pin can be supplied either in its hexadecimal form or as
@@ -963,26 +552,6 @@ def tabulate_str_list(str_list, width: int = 79, hspace: int = 2, lspace: int =
return '\n'.join(table)
def auto_int(x):
"""Helper function for argparse to accept hexadecimal integers."""
return int(x, 0)
def _auto_uint(x, max_val: int):
"""Helper function for argparse to accept hexadecimal or decimal integers."""
ret = int(x, 0)
if ret < 0 or ret > max_val:
raise argparse.ArgumentTypeError('Number exceeds permited value range (0, %u)' % max_val)
return ret
def auto_uint7(x):
return _auto_uint(x, 127)
def auto_uint8(x):
return _auto_uint(x, 255)
def auto_uint16(x):
return _auto_uint(x, 65535)
def expand_hex(hexstring, length):
"""Expand a given hexstring to a specified length by replacing "." or ".."
with a filler that is derived from the neighboring nibbles respective
@@ -1037,17 +606,6 @@ def expand_hex(hexstring, length):
return hexstring
class JsonEncoder(json.JSONEncoder):
"""Extend the standard library JSONEncoder with support for more types."""
def default(self, o):
if isinstance(o, (BytesIO, bytes, bytearray)):
return b2h(o)
elif isinstance(o, datetime.datetime):
return o.isoformat()
return json.JSONEncoder.default(self, o)
def boxed_heading_str(heading, width=80):
"""Generate a string that contains a boxed heading."""
# Auto-enlarge box if heading exceeds length
@@ -1456,35 +1014,3 @@ class CardCommandSet:
if cla and not cmd.match_cla(cla):
return None
return cmd
def all_subclasses(cls) -> set:
"""Recursively get all subclasses of a specified class"""
return set(cls.__subclasses__()).union([s for c in cls.__subclasses__() for s in all_subclasses(c)])
def is_hexstr_or_decimal(instr: str) -> str:
"""Method that can be used as 'type' in argparse.add_argument() to validate the value consists of
[hexa]decimal digits only."""
if instr.isdecimal():
return instr
if not all(c in string.hexdigits for c in instr):
raise ValueError('Input must be [hexa]decimal')
if len(instr) & 1:
raise ValueError('Input has un-even number of hex digits')
return instr
def is_hexstr(instr: str) -> str:
"""Method that can be used as 'type' in argparse.add_argument() to validate the value consists of
an even sequence of hexadecimal digits only."""
if not all(c in string.hexdigits for c in instr):
raise ValueError('Input must be hexadecimal')
if len(instr) & 1:
raise ValueError('Input has un-even number of hex digits')
return instr
def is_decimal(instr: str) -> str:
"""Method that can be used as 'type' in argparse.add_argument() to validate the value consists of
an even sequence of decimal digits only."""
if not instr.isdecimal():
raise ValueError('Input must decimal')
return instr