diff --git a/pySim-shell.py b/pySim-shell.py index 128c0eaa..0519ec4f 100755 --- a/pySim-shell.py +++ b/pySim-shell.py @@ -45,8 +45,10 @@ from pySim.utils import dec_st, sanitize_pin_adm, tabulate_str_list, is_hex, box from pySim.card_handler import CardHandler, CardHandlerAuto from pySim.filesystem import CardMF, RuntimeState, CardDF, CardADF, CardModel +from pySim.profile import CardProfile from pySim.ts_51_011 import CardProfileSIM, DF_TELECOM, DF_GSM from pySim.ts_102_221 import CardProfileUICC +from pySim.ts_102_221 import CardProfileUICCSIM from pySim.ts_31_102 import CardApplicationUSIM from pySim.ts_31_103 import CardApplicationISIM from pySim.ara_m import CardApplicationARAM @@ -80,19 +82,32 @@ def init_card(sl): card = card_detect("auto", scc) if card is None: - print("Could not detect card type!") + print("Warning: Could not detect card type - assuming a generic card type...") + card = SimCard(scc) + + profile = CardProfile.pick(scc) + if profile is None: + print("Unsupported card type!") return None, None + print("Info: Card is of type: %s" % str(profile)) + + # FIXME: This shouln't be here, the profile should add the applications, + # however, we cannot simply put his into ts_102_221.py since we would + # have to e.g. import CardApplicationUSIM from ts_31_102.py, which already + # imports from ts_102_221.py. This means we will end up with a circular + # import, which needs to be resolved first. + if isinstance(profile, CardProfileUICC): + profile.add_application(CardApplicationUSIM()) + profile.add_application(CardApplicationISIM()) + profile.add_application(CardApplicationARAM()) + # Create runtime state with card profile - profile = CardProfileUICC() - profile.add_application(CardApplicationUSIM()) - profile.add_application(CardApplicationISIM()) - profile.add_application(CardApplicationARAM()) rs = RuntimeState(card, profile) - # FIXME: do this dynamically - rs.mf.add_file(DF_TELECOM()) - rs.mf.add_file(DF_GSM()) + # FIXME: This is an GSM-R related file, it needs to be added throughout, + # the profile. At the moment we add it for all cards, this won't hurt, + # but regular SIM and UICC will not have it and fail to select it. rs.mf.add_file(DF_EIRENE()) CardModel.apply_matching_models(scc, rs) diff --git a/pySim/filesystem.py b/pySim/filesystem.py index 0238c971..6b20db5a 100644 --- a/pySim/filesystem.py +++ b/pySim/filesystem.py @@ -1443,65 +1443,6 @@ class CardApplication(object): """ return interpret_sw(self.sw, sw) -class CardProfile(object): - """A Card Profile describes a card, it's filesystem hierarchy, an [initial] list of - applications as well as profile-specific SW and shell commands. Every card has - one card profile, but there may be multiple applications within that profile.""" - def __init__(self, name, **kw): - """ - Args: - desc (str) : Description - files_in_mf : List of CardEF instances present in MF - applications : List of CardApplications present on card - sw : List of status word definitions - shell_cmdsets : List of cmd2 shell command sets of profile-specific commands - cla : class byte that should be used with cards of this profile - sel_ctrl : selection control bytes class byte that should be used with cards of this profile - """ - self.name = name - self.desc = kw.get("desc", None) - self.files_in_mf = kw.get("files_in_mf", []) - self.sw = kw.get("sw", {}) - self.applications = kw.get("applications", []) - self.shell_cmdsets = kw.get("shell_cmdsets", []) - self.cla = kw.get("cla", "00") - self.sel_ctrl = kw.get("sel_ctrl", "0004") - - def __str__(self): - return self.name - - def add_application(self, app:CardApplication): - """Add an application to a card profile. - - Args: - app : CardApplication instance to be added to profile - """ - self.applications.append(app) - - def interpret_sw(self, sw:str): - """Interpret a given status word within the profile. - - Args: - sw : Status word as string of 4 hex digits - - Returns: - Tuple of two strings - """ - return interpret_sw(self.sw, sw) - - def decode_select_response(self, data_hex:str) -> Any: - """Decode the response to a SELECT command. - - This is the fall-back method which doesn't perform any decoding. It mostly - exists so specific derived classes can overload it for actual decoding. - This method is implemented in the profile and is only used when application - specific decoding cannot be performed (no ADF is selected). - - Args: - data_hex: Hex string of the select response - """ - return data_hex - class CardModel(abc.ABC): """A specific card model, typically having some additional vendor-specific files. All diff --git a/pySim/profile.py b/pySim/profile.py new file mode 100644 index 00000000..f068d7cd --- /dev/null +++ b/pySim/profile.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- + +""" pySim: tell old 2G SIMs apart from UICC +""" + +# +# (C) 2021 by Sysmocom s.f.m.c. GmbH +# All Rights Reserved +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from pySim.commands import SimCardCommands +from pySim.filesystem import CardApplication, interpret_sw +from pySim.utils import all_subclasses +from typing import Any +import abc +import operator + +def _mf_select_test(scc:SimCardCommands, cla_byte:str, sel_ctrl:str) -> bool: + cla_byte_bak = scc.cla_byte + sel_ctrl_bak = scc.sel_ctrl + scc.reset_card() + + scc.cla_byte = cla_byte + scc.sel_ctrl = sel_ctrl + rc = True + try: + scc.select_file('3f00') + except: + rc = False + + scc.reset_card() + scc.cla_byte = cla_byte_bak + scc.sel_ctrl = sel_ctrl_bak + return rc + +def match_uicc(scc:SimCardCommands) -> bool: + """ Try to access MF via UICC APDUs (3GPP TS 102.221), if this works, the + card is considered a UICC card. + """ + return _mf_select_test(scc, "00", "0004") + +def match_sim(scc:SimCardCommands) -> bool: + """ Try to access MF via 2G APDUs (3GPP TS 11.11), if this works, the card + is also a simcard. This will be the case for most UICC cards, but there may + also be plain UICC cards without 2G support as well. + """ + return _mf_select_test(scc, "a0", "0000") + +class CardProfile(object): + """A Card Profile describes a card, it's filesystem hierarchy, an [initial] list of + applications as well as profile-specific SW and shell commands. Every card has + one card profile, but there may be multiple applications within that profile.""" + + def __init__(self, name, **kw): + """ + Args: + desc (str) : Description + files_in_mf : List of CardEF instances present in MF + applications : List of CardApplications present on card + sw : List of status word definitions + shell_cmdsets : List of cmd2 shell command sets of profile-specific commands + cla : class byte that should be used with cards of this profile + sel_ctrl : selection control bytes class byte that should be used with cards of this profile + """ + self.name = name + self.desc = kw.get("desc", None) + self.files_in_mf = kw.get("files_in_mf", []) + self.sw = kw.get("sw", {}) + self.applications = kw.get("applications", []) + self.shell_cmdsets = kw.get("shell_cmdsets", []) + self.cla = kw.get("cla", "00") + self.sel_ctrl = kw.get("sel_ctrl", "0004") + + def __str__(self): + return self.name + + def add_application(self, app:CardApplication): + """Add an application to a card profile. + + Args: + app : CardApplication instance to be added to profile + """ + self.applications.append(app) + + def interpret_sw(self, sw:str): + """Interpret a given status word within the profile. + + Args: + sw : Status word as string of 4 hex digits + + Returns: + Tuple of two strings + """ + return interpret_sw(self.sw, sw) + + def decode_select_response(self, data_hex:str) -> Any: + """Decode the response to a SELECT command. + + This is the fall-back method which doesn't perform any decoding. It mostly + exists so specific derived classes can overload it for actual decoding. + This method is implemented in the profile and is only used when application + specific decoding cannot be performed (no ADF is selected). + + Args: + data_hex: Hex string of the select response + """ + return data_hex + + @staticmethod + @abc.abstractmethod + def match_with_card(scc:SimCardCommands) -> bool: + """Check if the specific profile matches the card. This method is a + placeholder that is overloaded by specific dirived classes. The method + actively probes the card to make sure the profile class matches the + physical card. This usually also means that the card is reset during + the process, so this method must not be called at random times. It may + only be called on startup. + + Args: + scc: SimCardCommands class + Returns: + match = True, no match = False + """ + return False + + @staticmethod + def pick(scc:SimCardCommands): + profiles = list(all_subclasses(CardProfile)) + profiles.sort(key=operator.attrgetter('ORDER')) + + for p in profiles: + if p.match_with_card(scc): + return p() + + return None diff --git a/pySim/ts_102_221.py b/pySim/ts_102_221.py index 61e236e2..53cd1180 100644 --- a/pySim/ts_102_221.py +++ b/pySim/ts_102_221.py @@ -23,6 +23,13 @@ from pySim.construct import * from pySim.utils import * from pySim.filesystem import * from bidict import bidict +from pySim.profile import CardProfile +from pySim.profile import match_uicc +from pySim.profile import match_sim + +# A UICC will usually also support 2G functionality. If this is the case, we +# need to add DF_GSM and DF_TELECOM along with the UICC related files +from pySim.ts_51_011 import DF_GSM, DF_TELECOM ts_102_22x_cmdset = CardCommandSet('TS 102 22x', [ # TS 102 221 Section 10.1.2 Table 10.5 "Coding of Instruction Byte" @@ -603,9 +610,11 @@ class EF_UMPC(TransparentEF): addl_info = FlagsEnum(Byte, req_inc_idle_current=1, support_uicc_suspend=2) self._construct = Struct('max_current_mA'/Int8ub, 't_op_s'/Int8ub, 'addl_info'/addl_info) - class CardProfileUICC(CardProfile): - def __init__(self): + + ORDER = 1 + + def __init__(self, name = 'UICC'): files = [ EF_DIR(), EF_ICCID(), @@ -683,7 +692,27 @@ class CardProfileUICC(CardProfile): }, } - super().__init__('UICC', desc='ETSI TS 102 221', cla="00", sel_ctrl="0004", files_in_mf=files, sw=sw) + super().__init__(name, desc='ETSI TS 102 221', cla="00", sel_ctrl="0004", files_in_mf=files, sw=sw) def decode_select_response(self, data_hex:str) -> Any: return decode_select_response(data_hex) + + @staticmethod + def match_with_card(scc:SimCardCommands) -> bool: + return match_uicc(scc) + +class CardProfileUICCSIM(CardProfileUICC): + """Same as above, but including 2G SIM support""" + + ORDER = 0 + + def __init__(self): + super().__init__('UICC-SIM') + + # Add GSM specific files + self.files_in_mf.append(DF_TELECOM()) + self.files_in_mf.append(DF_GSM()) + + @staticmethod + def match_with_card(scc:SimCardCommands) -> bool: + return match_uicc(scc) and match_sim(scc) diff --git a/pySim/ts_51_011.py b/pySim/ts_51_011.py index 73d569ea..393277bc 100644 --- a/pySim/ts_51_011.py +++ b/pySim/ts_51_011.py @@ -332,6 +332,8 @@ from pySim.construct import * import enum from pySim.filesystem import * +from pySim.profile import CardProfile +from pySim.profile import match_sim ###################################################################### # DF.TELECOM @@ -975,6 +977,9 @@ def _decode_select_response(resp_hex): return ret class CardProfileSIM(CardProfile): + + ORDER = 2 + def __init__(self): sw = { 'Normal': { @@ -1016,3 +1021,7 @@ class CardProfileSIM(CardProfile): super().__init__('SIM', desc='GSM SIM Card', cla="a0", sel_ctrl="0000", files_in_mf=[DF_TELECOM(), DF_GSM()], sw=sw) def decode_select_response(self, data_hex:str) -> Any: return _decode_select_response(data_hex) + + @staticmethod + def match_with_card(scc:SimCardCommands) -> bool: + return match_sim(scc) diff --git a/pySim/utils.py b/pySim/utils.py index 521abd68..e30c970e 100644 --- a/pySim/utils.py +++ b/pySim/utils.py @@ -1530,3 +1530,7 @@ 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)])