mirror of
https://gitea.osmocom.org/sim-card/pysim.git
synced 2026-03-22 05:18:33 +03:00
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:
486
pySim/utils.py
486
pySim/utils.py
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user