4 Commits

Author SHA1 Message Date
Philipp Maier
e5f56dd35f pySim/transport: fix GET RESPONSE behaviour
The current behavior we implement in the method __send_apdu_T0 is
incomplete. Some details discussed in ETSI TS 102 221,
section 7.3.1.1.4, clause 4 seem to be not fully implemented. We
may also end up sending a GET RESPONSE in other APDU cases than
case 4 (the only case that uses the GET RESPONSE command).

Related: OS#6970
Change-Id: I26f0566af0cdd61dcc97f5f502479dc76adc37cc
2026-03-10 16:58:27 +01:00
Philipp Maier
c3edcf7294 pySim-prog/pySim-read: add pySimLogger and verbose cmdline argument
pySim-prog and pySim-read do not integrate the pySimLogger yet. As we
may add more debug output that should not be visible on normal use, we
should ensure that the pySimLogger is correctly set up.

Change-Id: Ia2fa535fd9ce4ffa301c3f5d6f98c1f7a4716c74
2026-03-10 16:58:27 +01:00
Philipp Maier
858c9eb421 pySim-shell/cosmetic: remove semicolon
Change-Id: I629bacd432491211b939fcd2bed554b44ef441bc
2026-03-10 16:58:27 +01:00
Philipp Maier
a48b9e565a PySimLogger: add parameter to set initial log-level/verbosity
When we initialize a new PySimLogger, we always call the setup method
first and then use the set_verbose and set_level method to configure
the initial log level and the initial log verbosity. However, we
initialize the PySimLogger in all our programs the same way and we
end up with the same boilerplate code every time. Let's add a keyword
parameter to the setup method where we can pass our opts.verbose (bool)
parameter so that the setup method can do the work for the main program.

In case the caller wants a different default configuration he still can
call set_verbose and set_level methods as needed.

Change-Id: I4b8ef1e203186878910c9614a1d900d5759236a8
2026-03-10 16:58:27 +01:00
25 changed files with 120 additions and 112 deletions

View File

@@ -285,10 +285,7 @@ if __name__ == '__main__':
option_parser.add_argument("--admin", action='store_true', help="perform action as admin", default=False) option_parser.add_argument("--admin", action='store_true', help="perform action as admin", default=False)
opts = option_parser.parse_args() opts = option_parser.parse_args()
PySimLogger.setup(print, {logging.WARN: "\033[33m"}) PySimLogger.setup(print, {logging.WARN: "\033[33m"}, opts.verbose)
if (opts.verbose):
PySimLogger.set_verbose(True)
PySimLogger.set_level(logging.DEBUG)
# Open CSV file # Open CSV file
cr = open_csv(opts) cr = open_csv(opts)

View File

@@ -44,6 +44,11 @@ from pySim.legacy.ts_51_011 import EF
from pySim.card_handler import * from pySim.card_handler import *
from pySim.utils import * from pySim.utils import *
from pathlib import Path
import logging
from pySim.log import PySimLogger
log = PySimLogger.get(Path(__file__).stem)
def parse_options(): def parse_options():
@@ -185,6 +190,7 @@ def parse_options():
default=False, action="store_true") default=False, action="store_true")
parser.add_argument("--card_handler", dest="card_handler_config", metavar="FILE", parser.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
help="Use automatic card handling machine") help="Use automatic card handling machine")
parser.add_argument("--verbose", help="Enable verbose logging", action='store_true', default=False)
options = parser.parse_args() options = parser.parse_args()
@@ -770,6 +776,9 @@ if __name__ == '__main__':
# Parse options # Parse options
opts = parse_options() opts = parse_options()
# Setup logger
PySimLogger.setup(print, {logging.WARN: "\033[33m"}, opts.verbose)
# Init card reader driver # Init card reader driver
sl = init_reader(opts) sl = init_reader(opts)

View File

@@ -46,11 +46,17 @@ from pySim.utils import dec_imsi, dec_iccid
from pySim.legacy.utils import format_xplmn_w_act, dec_st, dec_msisdn from pySim.legacy.utils import format_xplmn_w_act, dec_st, dec_msisdn
from pySim.ts_51_011 import EF_SMSP from pySim.ts_51_011 import EF_SMSP
from pathlib import Path
import logging
from pySim.log import PySimLogger
log = PySimLogger.get(Path(__file__).stem)
option_parser = argparse.ArgumentParser(description='Legacy tool for reading some parts of a SIM card', option_parser = argparse.ArgumentParser(description='Legacy tool for reading some parts of a SIM card',
formatter_class=argparse.ArgumentDefaultsHelpFormatter) formatter_class=argparse.ArgumentDefaultsHelpFormatter)
option_parser.add_argument("--verbose", help="Enable verbose logging", action='store_true', default=False)
argparse_add_reader_args(option_parser) argparse_add_reader_args(option_parser)
def select_app(adf: str, card: SimCard): def select_app(adf: str, card: SimCard):
"""Select application by its AID""" """Select application by its AID"""
sw = 0 sw = 0
@@ -75,6 +81,9 @@ if __name__ == '__main__':
# Parse options # Parse options
opts = option_parser.parse_args() opts = option_parser.parse_args()
# Setup logger
PySimLogger.setup(print, {logging.WARN: "\033[33m"}, opts.verbose)
# Init card reader driver # Init card reader driver
sl = init_reader(opts) sl = init_reader(opts)

View File

@@ -107,12 +107,12 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
kwargs = {'include_ipy': True} kwargs = {'include_ipy': True}
self.verbose = verbose self.verbose = verbose
self._onchange_verbose('verbose', False, self.verbose); PySimLogger.setup(self.poutput, {logging.WARN: YELLOW})
self._onchange_verbose('verbose', False, self.verbose)
# pylint: disable=unexpected-keyword-arg # pylint: disable=unexpected-keyword-arg
super().__init__(persistent_history_file='~/.pysim_shell_history', allow_cli_args=False, super().__init__(persistent_history_file='~/.pysim_shell_history', allow_cli_args=False,
auto_load_commands=False, startup_script=script, **kwargs) auto_load_commands=False, startup_script=script, **kwargs)
PySimLogger.setup(self.poutput, {logging.WARN: YELLOW})
self.intro = style(self.BANNER, fg=RED) self.intro = style(self.BANNER, fg=RED)
self.default_category = 'pySim-shell built-in commands' self.default_category = 'pySim-shell built-in commands'
self.card = None self.card = None
@@ -1175,13 +1175,7 @@ if __name__ == '__main__':
opts = option_parser.parse_args() opts = option_parser.parse_args()
# Ensure that we are able to print formatted warnings from the beginning. # Ensure that we are able to print formatted warnings from the beginning.
PySimLogger.setup(print, {logging.WARN: YELLOW}) PySimLogger.setup(print, {logging.WARN: YELLOW}, opts.verbose)
if opts.verbose:
PySimLogger.set_verbose(True)
PySimLogger.set_level(logging.DEBUG)
else:
PySimLogger.set_verbose(False)
PySimLogger.set_level(logging.INFO)
# Register csv-file as card data provider, either from specified CSV # Register csv-file as card data provider, either from specified CSV
# or from CSV file in home directory # or from CSV file in home directory

View File

@@ -72,10 +72,10 @@ class ApduArDO(BER_TLV_IE, tag=0xd0):
if do[0] == 0x01: if do[0] == 0x01:
self.decoded = {'generic_access_rule': 'always'} self.decoded = {'generic_access_rule': 'always'}
return self.decoded return self.decoded
raise ValueError('Invalid 1-byte generic APDU access rule') return ValueError('Invalid 1-byte generic APDU access rule')
else: else:
if len(do) % 8: if len(do) % 8:
raise ValueError('Invalid non-modulo-8 length of APDU filter: %d' % len(do)) return ValueError('Invalid non-modulo-8 length of APDU filter: %d' % len(do))
self.decoded = {'apdu_filter': []} self.decoded = {'apdu_filter': []}
offset = 0 offset = 0
while offset < len(do): while offset < len(do):
@@ -90,19 +90,19 @@ class ApduArDO(BER_TLV_IE, tag=0xd0):
return b'\x00' return b'\x00'
if self.decoded['generic_access_rule'] == 'always': if self.decoded['generic_access_rule'] == 'always':
return b'\x01' return b'\x01'
raise ValueError('Invalid 1-byte generic APDU access rule') return ValueError('Invalid 1-byte generic APDU access rule')
else: else:
if not 'apdu_filter' in self.decoded: if not 'apdu_filter' in self.decoded:
raise ValueError('Invalid APDU AR DO') return ValueError('Invalid APDU AR DO')
filters = self.decoded['apdu_filter'] filters = self.decoded['apdu_filter']
res = b'' res = b''
for f in filters: for f in filters:
if not 'header' in f or not 'mask' in f: if not 'header' in f or not 'mask' in f:
raise ValueError('APDU filter must contain header and mask') return ValueError('APDU filter must contain header and mask')
header_b = h2b(f['header']) header_b = h2b(f['header'])
mask_b = h2b(f['mask']) mask_b = h2b(f['mask'])
if len(header_b) != 4 or len(mask_b) != 4: if len(header_b) != 4 or len(mask_b) != 4:
raise ValueError('APDU filter header and mask must each be 4 bytes') return ValueError('APDU filter header and mask must each be 4 bytes')
res += header_b + mask_b res += header_b + mask_b
return res return res
@@ -269,7 +269,7 @@ class ADF_ARAM(CardADF):
cmd_do_enc = cmd_do.to_ie() cmd_do_enc = cmd_do.to_ie()
cmd_do_len = len(cmd_do_enc) cmd_do_len = len(cmd_do_enc)
if cmd_do_len > 255: if cmd_do_len > 255:
raise ValueError('DO > 255 bytes not supported yet') return ValueError('DO > 255 bytes not supported yet')
else: else:
cmd_do_enc = b'' cmd_do_enc = b''
cmd_do_len = 0 cmd_do_len = 0
@@ -361,7 +361,7 @@ class ADF_ARAM(CardADF):
ar_do_content += [{'apdu_ar_do': {'generic_access_rule': 'always'}}] ar_do_content += [{'apdu_ar_do': {'generic_access_rule': 'always'}}]
elif opts.apdu_filter: elif opts.apdu_filter:
if len(opts.apdu_filter) % 16: if len(opts.apdu_filter) % 16:
raise ValueError(f'Invalid non-modulo-16 length of APDU filter: {len(opts.apdu_filter)}') return ValueError('Invalid non-modulo-16 length of APDU filter: %d' % len(do))
offset = 0 offset = 0
apdu_filter = [] apdu_filter = []
while offset < len(opts.apdu_filter): while offset < len(opts.apdu_filter):

View File

@@ -128,10 +128,10 @@ class EF_AD(TransparentEF):
cell_test = 0x04 cell_test = 0x04
def __init__(self, fid='6f43', sfid=None, name='EF.AD', def __init__(self, fid='6f43', sfid=None, name='EF.AD',
desc='Administrative Data', size=(3, None), **kwargs): desc='Service Provider Name', size=(3, None), **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs) super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
self._construct = Struct( self._construct = Struct(
# Byte 1: MS operation mode # Byte 1: Display Condition
'ms_operation_mode'/Enum(Byte, self.OP_MODE), 'ms_operation_mode'/Enum(Byte, self.OP_MODE),
# Bytes 2-3: Additional information # Bytes 2-3: Additional information
'additional_info'/Bytes(2), 'additional_info'/Bytes(2),

View File

@@ -19,7 +19,7 @@ import abc
import requests import requests
import logging import logging
import json import json
from typing import Optional, Tuple from typing import Optional
import base64 import base64
from twisted.web.server import Request from twisted.web.server import Request
@@ -180,7 +180,7 @@ class JsonHttpApiFunction(abc.ABC):
# receives from the a requesting client. The same applies vice versa to class variables that have an "output_" # receives from the a requesting client. The same applies vice versa to class variables that have an "output_"
# prefix. # prefix.
# path of the API function (e.g. '/gsma/rsp2/es2plus/confirmOrder', see also method rewrite_url). # path of the API function (e.g. '/gsma/rsp2/es2plus/confirmOrder')
path = None path = None
# dictionary of input parameters. key is parameter name, value is ApiParam class # dictionary of input parameters. key is parameter name, value is ApiParam class
@@ -336,22 +336,6 @@ class JsonHttpApiFunction(abc.ABC):
output[p] = p_class.decode(v) output[p] = p_class.decode(v)
return output return output
def rewrite_url(self, data: dict, url: str) -> Tuple[dict, str]:
"""
Rewrite a static URL using information passed in the data dict. This method may be overloaded by a derived
class to allow fully dynamic URLs. The input parameters required for the URL rewriting may be passed using
data parameter. In case those parameters are additional parameters that are not intended to be passed to
the encode_client method later, they must be removed explcitly.
Args:
data: (see JsonHttpApiClient and JsonHttpApiServer)
url: statically generated URL string (see comment in JsonHttpApiClient)
"""
# This implementation is a placeholder in which we do not perform any URL rewriting. We just pass through data
# and url unmodified.
return data, url
class JsonHttpApiClient(): class JsonHttpApiClient():
def __init__(self, api_func: JsonHttpApiFunction, url_prefix: str, func_req_id: Optional[str], def __init__(self, api_func: JsonHttpApiFunction, url_prefix: str, func_req_id: Optional[str],
session: requests.Session): session: requests.Session):
@@ -368,16 +352,8 @@ class JsonHttpApiClient():
self.session = session self.session = session
def call(self, data: dict, func_call_id: Optional[str] = None, timeout=10) -> Optional[dict]: def call(self, data: dict, func_call_id: Optional[str] = None, timeout=10) -> Optional[dict]:
""" """Make an API call to the HTTP API endpoint represented by this object. Input data is passed in `data` as
Make an API call to the HTTP API endpoint represented by this object. Input data is passed in `data` as json-serializable dict. Output data is returned as json-deserialized dict."""
json-serializable fields. `data` may also contain additional parameters required for URL rewriting (see
rewrite_url in class JsonHttpApiFunction). Output data is returned as json-deserialized dict.
Args:
data: Input data required to perform the request.
func_call_id: Function Call Identifier, if present a header field is generated automatically.
timeout: Maximum amount of time to wait for the request to complete.
"""
# In case a function caller ID is supplied, use it together with the stored function requestor ID to generate # In case a function caller ID is supplied, use it together with the stored function requestor ID to generate
# and prepend the header field according to SGP.22, section 6.5.1.1 and 6.5.1.3. (the presence of the header # and prepend the header field according to SGP.22, section 6.5.1.1 and 6.5.1.3. (the presence of the header
@@ -386,11 +362,6 @@ class JsonHttpApiClient():
data = {'header' : {'functionRequesterIdentifier': self.func_req_id, data = {'header' : {'functionRequesterIdentifier': self.func_req_id,
'functionCallIdentifier': func_call_id}} | data 'functionCallIdentifier': func_call_id}} | data
# The URL used for the HTTP request (see below) normally consists of the initially given url_prefix
# concatenated with the path defined by the JsonHttpApiFunction definition. This static URL path may be
# rewritten by rewrite_url method defined in the JsonHttpApiFunction.
data, url = self.api_func.rewrite_url(data, self.url_prefix + self.api_func.path)
# Encode the message (the presence of mandatory fields is checked during encoding) # Encode the message (the presence of mandatory fields is checked during encoding)
encoded = json.dumps(self.api_func.encode_client(data)) encoded = json.dumps(self.api_func.encode_client(data))
@@ -402,6 +373,7 @@ class JsonHttpApiClient():
req_headers.update(self.api_func.extra_http_req_headers) req_headers.update(self.api_func.extra_http_req_headers)
# Perform HTTP request # Perform HTTP request
url = self.url_prefix + self.api_func.path
logger.debug("HTTP REQ %s - hdr: %s '%s'" % (url, req_headers, encoded)) logger.debug("HTTP REQ %s - hdr: %s '%s'" % (url, req_headers, encoded))
response = self.session.request(self.api_func.http_method, url, data=encoded, headers=req_headers, timeout=timeout) response = self.session.request(self.api_func.http_method, url, data=encoded, headers=req_headers, timeout=timeout)
logger.debug("HTTP RSP-STS: [%u] hdr: %s" % (response.status_code, response.headers)) logger.debug("HTTP RSP-STS: [%u] hdr: %s" % (response.status_code, response.headers))

View File

@@ -151,8 +151,6 @@ class File:
self.df_name = None self.df_name = None
self.fill_pattern = None self.fill_pattern = None
self.fill_pattern_repeat = False self.fill_pattern_repeat = False
self.pstdo = None # pinStatusTemplateDO, mandatory for DF/ADF
self.lcsi = None # optional life cycle status indicator
# apply some defaults from profile # apply some defaults from profile
if self.template: if self.template:
self.from_template(self.template) self.from_template(self.template)
@@ -280,8 +278,6 @@ class File:
elif self.file_type in ['MF', 'DF', 'ADF']: elif self.file_type in ['MF', 'DF', 'ADF']:
fdb_dec['file_type'] = 'df' fdb_dec['file_type'] = 'df'
fdb_dec['structure'] = 'no_info_given' fdb_dec['structure'] = 'no_info_given'
# pinStatusTemplateDO is mandatory for DF/ADF
fileDescriptor['pinStatusTemplateDO'] = self.pstdo
# build file descriptor based on above input data # build file descriptor based on above input data
fd_dict = {} fd_dict = {}
if len(fdb_dec): if len(fdb_dec):
@@ -308,8 +304,6 @@ class File:
# desired fill or repeat pattern in the "proprietaryEFInfo" element for the EF in Profiles # desired fill or repeat pattern in the "proprietaryEFInfo" element for the EF in Profiles
# downloaded to a V2.2 or earlier eUICC. # downloaded to a V2.2 or earlier eUICC.
fileDescriptor['proprietaryEFInfo'] = pefi fileDescriptor['proprietaryEFInfo'] = pefi
if self.lcsi:
fileDescriptor['lcsi'] = self.lcsi
logger.debug("%s: to_fileDescriptor(%s)" % (self, fileDescriptor)) logger.debug("%s: to_fileDescriptor(%s)" % (self, fileDescriptor))
return fileDescriptor return fileDescriptor
@@ -329,8 +323,6 @@ class File:
if efFileSize: if efFileSize:
self._file_size = self._decode_file_size(efFileSize) self._file_size = self._decode_file_size(efFileSize)
self.pstdo = fileDescriptor.get('pinStatusTemplateDO', None)
self.lcsi = fileDescriptor.get('lcsi', None)
pefi = fileDescriptor.get('proprietaryEFInfo', {}) pefi = fileDescriptor.get('proprietaryEFInfo', {})
securityAttributesReferenced = fileDescriptor.get('securityAttributesReferenced', None) securityAttributesReferenced = fileDescriptor.get('securityAttributesReferenced', None)
if securityAttributesReferenced: if securityAttributesReferenced:
@@ -441,7 +433,7 @@ class File:
elif k == 'fillFileContent': elif k == 'fillFileContent':
stream.write(v) stream.write(v)
else: else:
raise ValueError("Unknown key '%s' in tuple list" % k) return ValueError("Unknown key '%s' in tuple list" % k)
return stream.getvalue() return stream.getvalue()
def file_content_to_tuples(self, optimize:bool = False) -> List[Tuple]: def file_content_to_tuples(self, optimize:bool = False) -> List[Tuple]:

View File

@@ -276,7 +276,7 @@ class ListOfSupportedOptions(BER_TLV_IE, tag=0x81):
class SupportedKeysForScp03(BER_TLV_IE, tag=0x82): class SupportedKeysForScp03(BER_TLV_IE, tag=0x82):
_construct = FlagsEnum(Byte, aes128=0x01, aes192=0x02, aes256=0x04) _construct = FlagsEnum(Byte, aes128=0x01, aes192=0x02, aes256=0x04)
class SupportedTlsCipherSuitesForScp81(BER_TLV_IE, tag=0x83): class SupportedTlsCipherSuitesForScp81(BER_TLV_IE, tag=0x83):
_construct = GreedyRange(Int16ub) _consuruct = GreedyRange(Int16ub)
class ScpInformation(BER_TLV_IE, tag=0xa0, nested=[ScpType, ListOfSupportedOptions, SupportedKeysForScp03, class ScpInformation(BER_TLV_IE, tag=0xa0, nested=[ScpType, ListOfSupportedOptions, SupportedKeysForScp03,
SupportedTlsCipherSuitesForScp81]): SupportedTlsCipherSuitesForScp81]):
pass pass
@@ -319,7 +319,7 @@ class CurrentSecurityLevel(BER_TLV_IE, tag=0xd3):
# GlobalPlatform v2.3.1 Section 11.3.3.1.3 # GlobalPlatform v2.3.1 Section 11.3.3.1.3
class ApplicationAID(BER_TLV_IE, tag=0x4f): class ApplicationAID(BER_TLV_IE, tag=0x4f):
_construct = GreedyBytes _construct = GreedyBytes
class ApplicationTemplate(BER_TLV_IE, tag=0x61, nested=[ApplicationAID]): class ApplicationTemplate(BER_TLV_IE, tag=0x61, ntested=[ApplicationAID]):
pass pass
class ListOfApplications(BER_TLV_IE, tag=0x2f00, nested=[ApplicationTemplate]): class ListOfApplications(BER_TLV_IE, tag=0x2f00, nested=[ApplicationTemplate]):
pass pass
@@ -562,14 +562,14 @@ class ADF_SD(CardADF):
@cmd2.with_argparser(store_data_parser) @cmd2.with_argparser(store_data_parser)
def do_store_data(self, opts): def do_store_data(self, opts):
"""Perform the GlobalPlatform STORE DATA command in order to store some card-specific data. """Perform the GlobalPlatform GET DATA command in order to store some card-specific data.
See GlobalPlatform CardSpecification v2.3 Section 11.11 for details.""" See GlobalPlatform CardSpecification v2.3Section 11.11 for details."""
response_permitted = opts.response == 'may_be_returned' response_permitted = opts.response == 'may_be_returned'
self.store_data(h2b(opts.DATA), opts.data_structure, opts.encryption, response_permitted) self.store_data(h2b(opts.DATA), opts.data_structure, opts.encryption, response_permitted)
def store_data(self, data: bytes, structure:str = 'none', encryption:str = 'none', response_permitted: bool = False) -> bytes: def store_data(self, data: bytes, structure:str = 'none', encryption:str = 'none', response_permitted: bool = False) -> bytes:
"""Perform the GlobalPlatform STORE DATA command in order to store some card-specific data. """Perform the GlobalPlatform GET DATA command in order to store some card-specific data.
See GlobalPlatform CardSpecification v2.3 Section 11.11 for details.""" See GlobalPlatform CardSpecification v2.3Section 11.11 for details."""
max_cmd_len = self._cmd.lchan.scc.max_cmd_len max_cmd_len = self._cmd.lchan.scc.max_cmd_len
# Table 11-89 of GP Card Specification v2.3 # Table 11-89 of GP Card Specification v2.3
remainder = data remainder = data
@@ -585,7 +585,7 @@ class ADF_SD(CardADF):
data, _sw = self._cmd.lchan.scc.send_apdu_checksw(hdr + b2h(chunk) + "00") data, _sw = self._cmd.lchan.scc.send_apdu_checksw(hdr + b2h(chunk) + "00")
block_nr += 1 block_nr += 1
response += data response += data
return h2b(response) return data
put_key_parser = argparse.ArgumentParser() put_key_parser = argparse.ArgumentParser()
put_key_parser.add_argument('--old-key-version-nr', type=auto_uint8, default=0, help='Old Key Version Number') put_key_parser.add_argument('--old-key-version-nr', type=auto_uint8, default=0, help='Old Key Version Number')

View File

@@ -438,7 +438,7 @@ class Scp03SessionKeys:
"""Obtain the ICV value computed as described in 6.2.6. """Obtain the ICV value computed as described in 6.2.6.
This method has two modes: This method has two modes:
* is_response=False for computing the ICV for C-ENC. Will pre-increment the counter. * is_response=False for computing the ICV for C-ENC. Will pre-increment the counter.
* is_response=True for computing the ICV for R-DEC.""" * is_response=False for computing the ICV for R-DEC."""
if not is_response: if not is_response:
self.block_nr += 1 self.block_nr += 1
# The binary value of this number SHALL be left padded with zeroes to form a full block. # The binary value of this number SHALL be left padded with zeroes to form a full block.

View File

@@ -63,7 +63,7 @@ class PySimLogger:
raise RuntimeError('static class, do not instantiate') raise RuntimeError('static class, do not instantiate')
@staticmethod @staticmethod
def setup(print_callback = None, colors:dict = {}): def setup(print_callback = None, colors:dict = {}, verbose_debug:bool = False):
""" """
Set a print callback function and color scheme. This function call is optional. In case this method is not Set a print callback function and color scheme. This function call is optional. In case this method is not
called, default settings apply. called, default settings apply.
@@ -72,10 +72,20 @@ class PySimLogger:
have the following format: print_callback(message:str) have the following format: print_callback(message:str)
colors : An optional dict through which certain log levels can be assigned a color. colors : An optional dict through which certain log levels can be assigned a color.
(e.g. {logging.WARN: YELLOW}) (e.g. {logging.WARN: YELLOW})
verbose_debug: Enable verbose logging and set the loglevel DEBUG when set to true. Otherwise the
non-verbose logging is used and the loglevel is set to INFO. This setting can be changed
using the set_verbose and set_level methods at any time.
""" """
PySimLogger.print_callback = print_callback PySimLogger.print_callback = print_callback
PySimLogger.colors = colors PySimLogger.colors = colors
if (verbose_debug):
PySimLogger.set_verbose(True)
PySimLogger.set_level(logging.DEBUG)
else:
PySimLogger.set_verbose(False)
PySimLogger.set_level(logging.INFO)
@staticmethod @staticmethod
def set_verbose(verbose:bool = False): def set_verbose(verbose:bool = False):
""" """

View File

@@ -221,12 +221,12 @@ class OtaAlgoCrypt(OtaAlgo, abc.ABC):
for subc in cls.__subclasses__(): for subc in cls.__subclasses__():
if subc.enum_name == otak.algo_crypt: if subc.enum_name == otak.algo_crypt:
return subc(otak) return subc(otak)
raise ValueError('No implementation for crypt algorithm %s' % otak.algo_crypt) raise ValueError('No implementation for crypt algorithm %s' % otak.algo_auth)
class OtaAlgoAuth(OtaAlgo, abc.ABC): class OtaAlgoAuth(OtaAlgo, abc.ABC):
def __init__(self, otak: OtaKeyset): def __init__(self, otak: OtaKeyset):
if self.enum_name != otak.algo_auth: if self.enum_name != otak.algo_auth:
raise ValueError('Cannot use algorithm %s with key for %s' % (self.enum_name, otak.algo_auth)) raise ValueError('Cannot use algorithm %s with key for %s' % (self.enum_name, otak.algo_crypt))
super().__init__(otak) super().__init__(otak)
def sign(self, data:bytes) -> bytes: def sign(self, data:bytes) -> bytes:

View File

@@ -169,14 +169,8 @@ class SMS_TPDU(abc.ABC):
class SMS_DELIVER(SMS_TPDU): class SMS_DELIVER(SMS_TPDU):
"""Representation of a SMS-DELIVER T-PDU. This is the Network to MS/UE (downlink) direction.""" """Representation of a SMS-DELIVER T-PDU. This is the Network to MS/UE (downlink) direction."""
flags_construct = BitStruct('tp_rp'/Flag, flags_construct = BitStruct('tp_rp'/Flag, 'tp_udhi'/Flag, 'tp_rp'/Flag, 'tp_sri'/Flag,
'tp_udhi'/Flag, Padding(1), 'tp_mms'/Flag, 'tp_mti'/BitsInteger(2))
'tp_sri'/Flag,
Padding(1),
'tp_lp'/Flag,
'tp_mms'/Flag,
'tp_mti'/BitsInteger(2))
def __init__(self, **kwargs): def __init__(self, **kwargs):
kwargs['tp_mti'] = 0 kwargs['tp_mti'] = 0
super().__init__(**kwargs) super().__init__(**kwargs)

View File

@@ -301,24 +301,53 @@ class LinkBaseTpdu(LinkBase):
prev_tpdu = tpdu prev_tpdu = tpdu
data, sw = self.send_tpdu(tpdu) data, sw = self.send_tpdu(tpdu)
log.debug("T0: case #%u TPDU: %s => %s %s", case, tpdu, data or "(no data)", sw or "(no status word)")
# When we have sent the first APDU, the SW may indicate that there are response bytes # After sending the APDU/TPDU the UICC/eUICC or SIM may response with a status word that indicates that further
# available. There are two SWs commonly used for this 9fxx (sim) and 61xx (usim), where # TPDUs have to be sent in order to complete the task.
# xx is the number of response bytes available.
# See also:
if sw is not None: if sw is not None:
while (sw[0:2] in ['9f', '61', '62', '63']): if case == 4 or self.apdu_strict == False:
# SW1=9F: 3GPP TS 51.011 9.4.1, Responses to commands which are correctly executed # In case the APDU is a case #4 APDU, the UICC/eUICC/SIM may indicate that there is response data
# SW1=61: ISO/IEC 7816-4, Table 5 — General meaning of the interindustry values of SW1-SW2 # available which has to be retrieved using a GET RESPONSE command TPDU.
# SW1=62: ETSI TS 102 221 7.3.1.1.4 Clause 4b): 62xx, 63xx, 9xxx != 9000 #
tpdu_gr = tpdu[0:2] + 'c00000' + sw[2:4] # ETSI TS 102 221, section 7.3.1.1.4 is very cleare about the fact that the GET RESPONSE mechanism
prev_tpdu = tpdu_gr # shall only apply on case #4 APDUs but unfortunately it is impossible to distinguish between case #3
d, sw = self.send_tpdu(tpdu_gr) # and case #4 when the APDU format is not strictly followed. In order to be able to detect case #4
data += d # correctly the Le byte (usually 0x00) must be present, is often forgotten. To avoid problems with
# legacy scripts that use raw APDU strings, we will still loosely apply GET RESPONSE based on what
# the status word indicates. Unless the user explicitly enables the strict mode (set apdu_strict true)
while True:
if sw in ['9000', '9100']:
# A status word of 9000 (or 9100 in case there is pending data from a proactive SIM command)
# indicates that either no response data was returnd or all response data has been retrieved
# successfully. We may discontinue the processing at this point.
break;
if sw[0:2] in ['61', '9f']:
# A status word of 61xx or 9fxx indicates that there is (still) response data available. We
# send a GET RESPONSE command with the length value indicated in the second byte of the status
# word. (see also ETSI TS 102 221, section 7.3.1.1.4, clause 4a and 3GPP TS 51.011 9.4.1 and
# ISO/IEC 7816-4, Table 5)
le_gr = sw[2:4]
elif sw[0:2] in ['62', '63']:
# There are corner cases (status word is 62xx or 63xx) where the UICC/eUICC/SIM asks us
# to send a dummy GET RESPONSE command. We send a GET RESPONSE command with a length of 0.
# (see also ETSI TS 102 221, section 7.3.1.1.4, clause 4b and ETSI TS 151 011, section 9.4.1)
le_gr = '00'
else:
# A status word other then the ones covered by the above logic may indicate an error. In this
# case we will discontinue the processing as well.
# (see also ETSI TS 102 221, section 7.3.1.1.4, clause 4c)
break
tpdu_gr = tpdu[0:2] + 'c00000' + le_gr
prev_tpdu = tpdu_gr
data_gr, sw = self.send_tpdu(tpdu_gr)
log.debug("T0: GET RESPONSE TPDU: %s => %s %s", tpdu_gr, data_gr or "(no data)", sw or "(no status word)")
data += data_gr
if sw[0:2] == '6c': if sw[0:2] == '6c':
# SW1=6C: ETSI TS 102 221 Table 7.1: Procedure byte coding # SW1=6C: ETSI TS 102 221 Table 7.1: Procedure byte coding
tpdu_gr = prev_tpdu[0:8] + sw[2:4] tpdu_gr = prev_tpdu[0:8] + sw[2:4]
data, sw = self.send_tpdu(tpdu_gr) data, sw = self.send_tpdu(tpdu_gr)
log.debug("T0: repated case #%u TPDU: %s => %s %s", case, tpdu_gr, data or "(no data)", sw or "(no status word)")
return data, sw return data, sw

View File

@@ -80,7 +80,8 @@ class PcscSimLink(LinkBaseTpdu):
def connect(self): def connect(self):
try: try:
# To avoid leakage of resources, make sure the reader is disconnected # To avoid leakage of resources, make sure the reader
# is disconnected
self.disconnect() self.disconnect()
# Make card connection and select a suitable communication protocol # Make card connection and select a suitable communication protocol

View File

@@ -1058,7 +1058,7 @@ class EF_OCSGL(LinFixedEF):
# TS 31.102 Section 4.4.11.2 (Rel 15) # TS 31.102 Section 4.4.11.2 (Rel 15)
class EF_5GS3GPPLOCI(TransparentEF): class EF_5GS3GPPLOCI(TransparentEF):
def __init__(self, fid='4f01', sfid=0x01, name='EF.5GS3GPPLOCI', size=(20, 20), def __init__(self, fid='4f01', sfid=0x01, name='EF.5GS3GPPLOCI', size=(20, 20),
desc='5GS 3GPP location information', **kwargs): desc='5S 3GP location information', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs) super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
upd_status_constr = Enum( upd_status_constr = Enum(
Byte, updated=0, not_updated=1, roaming_not_allowed=2) Byte, updated=0, not_updated=1, roaming_not_allowed=2)
@@ -1326,7 +1326,7 @@ class EF_5G_PROSE_UIR(TransparentEF):
pass pass
class FiveGDdnmfCtfAddrForUploading(BER_TLV_IE, tag=0x97): class FiveGDdnmfCtfAddrForUploading(BER_TLV_IE, tag=0x97):
pass pass
class ProSeConfigDataForUsageInfoReporting(BER_TLV_IE, tag=0xa0, class ProSeConfigDataForUeToNetworkRelayUE(BER_TLV_IE, tag=0xa0,
nested=[EF_5G_PROSE_DD.ValidityTimer, nested=[EF_5G_PROSE_DD.ValidityTimer,
CollectionPeriod, ReportingWindow, CollectionPeriod, ReportingWindow,
ReportingIndicators, ReportingIndicators,
@@ -1336,7 +1336,7 @@ class EF_5G_PROSE_UIR(TransparentEF):
desc='5G ProSe configuration data for usage information reporting', **kwargs): desc='5G ProSe configuration data for usage information reporting', **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, **kwargs) super().__init__(fid, sfid=sfid, name=name, desc=desc, **kwargs)
# contains TLV structure despite being TransparentEF, not BER-TLV ?!? # contains TLV structure despite being TransparentEF, not BER-TLV ?!?
self._tlv = EF_5G_PROSE_UIR.ProSeConfigDataForUsageInfoReporting self._tlv = EF_5G_PROSE_UIR.ProSeConfigDataForUeToNetworkRelayUE
# TS 31.102 Section 4.4.13.8 (Rel 18) # TS 31.102 Section 4.4.13.8 (Rel 18)
class EF_5G_PROSE_U2URU(TransparentEF): class EF_5G_PROSE_U2URU(TransparentEF):

View File

@@ -309,6 +309,7 @@ class EF_SMSP(LinFixedEF):
'tp_dest_addr'/Flag)), 'tp_dest_addr'/Flag)),
'tp_dest_addr'/ScAddr, 'tp_dest_addr'/ScAddr,
'tp_sc_addr'/ScAddr, 'tp_sc_addr'/ScAddr,
'tp_pid'/Bytes(1), 'tp_pid'/Bytes(1),
'tp_dcs'/Bytes(1), 'tp_dcs'/Bytes(1),
'tp_vp_minutes'/EF_SMSP.ValidityPeriodAdapter(Byte)) 'tp_vp_minutes'/EF_SMSP.ValidityPeriodAdapter(Byte))
@@ -1116,8 +1117,8 @@ class DF_GSM(CardDF):
EF_MBI(), EF_MBI(),
EF_MWIS(), EF_MWIS(),
EF_CFIS(), EF_CFIS(),
EF_EXT('6fc8', None, 'EF.EXT6', desc='Extension6 (MBDN)'), EF_EXT('6fc8', None, 'EF.EXT6', desc='Externsion6 (MBDN)'),
EF_EXT('6fcc', None, 'EF.EXT7', desc='Extension7 (CFIS)'), EF_EXT('6fcc', None, 'EF.EXT7', desc='Externsion7 (CFIS)'),
EF_SPDI(), EF_SPDI(),
EF_MMSN(), EF_MMSN(),
EF_EXT('6fcf', None, 'EF.EXT8', desc='Extension8 (MMSN)'), EF_EXT('6fcf', None, 'EF.EXT8', desc='Extension8 (MMSN)'),

View File

@@ -139,6 +139,7 @@ def enc_plmn(mcc: Hexstr, mnc: Hexstr) -> Hexstr:
def dec_plmn(threehexbytes: Hexstr) -> dict: def dec_plmn(threehexbytes: Hexstr) -> dict:
res = {'mcc': "0", 'mnc': "0"} res = {'mcc': "0", 'mnc': "0"}
dec_mcc_from_plmn_str(threehexbytes)
res['mcc'] = dec_mcc_from_plmn_str(threehexbytes) res['mcc'] = dec_mcc_from_plmn_str(threehexbytes)
res['mnc'] = dec_mnc_from_plmn_str(threehexbytes) res['mnc'] = dec_mnc_from_plmn_str(threehexbytes)
return res return res
@@ -910,8 +911,7 @@ class DataObjectCollection:
def encode(self, decoded) -> bytes: def encode(self, decoded) -> bytes:
res = bytearray() res = bytearray()
for i in decoded: for i in decoded:
name = i[0] obj = self.members_by_name(i[0])
obj = self.members_by_name[name]
res.append(obj.to_tlv()) res.append(obj.to_tlv())
return res return res

View File

@@ -1,4 +1,4 @@
Using PC/SC reader interface INFO: Using PC/SC reader interface
Reading ... Reading ...
Autodetected card type: Fairwaves-SIM Autodetected card type: Fairwaves-SIM
ICCID: 8988219000000117833 ICCID: 8988219000000117833

View File

@@ -1,4 +1,4 @@
Using PC/SC reader interface INFO: Using PC/SC reader interface
Reading ... Reading ...
Autodetected card type: Wavemobile-SIM Autodetected card type: Wavemobile-SIM
ICCID: 89445310150011013678 ICCID: 89445310150011013678

View File

@@ -1,4 +1,4 @@
Using PC/SC reader interface INFO: Using PC/SC reader interface
Reading ... Reading ...
Autodetected card type: fakemagicsim Autodetected card type: fakemagicsim
ICCID: 1122334455667788990 ICCID: 1122334455667788990

View File

@@ -1,4 +1,4 @@
Using PC/SC reader interface INFO: Using PC/SC reader interface
Reading ... Reading ...
Autodetected card type: sysmoISIM-SJA2 Autodetected card type: sysmoISIM-SJA2
ICCID: 8988211000000467343 ICCID: 8988211000000467343

View File

@@ -1,4 +1,4 @@
Using PC/SC reader interface INFO: Using PC/SC reader interface
Reading ... Reading ...
Autodetected card type: sysmoISIM-SJA5 Autodetected card type: sysmoISIM-SJA5
ICCID: 8949440000001155314 ICCID: 8949440000001155314

View File

@@ -1,4 +1,4 @@
Using PC/SC reader interface INFO: Using PC/SC reader interface
Reading ... Reading ...
Autodetected card type: sysmoUSIM-SJS1 Autodetected card type: sysmoUSIM-SJS1
ICCID: 8988211320300000028 ICCID: 8988211320300000028

View File

@@ -1,4 +1,4 @@
Using PC/SC reader interface INFO: Using PC/SC reader interface
Reading ... Reading ...
Autodetected card type: sysmosim-gr1 Autodetected card type: sysmosim-gr1
ICCID: 2222334455667788990 ICCID: 2222334455667788990