laforge has uploaded this change for review.

View Change

move Runtime{State,Lchan} from pySim.filesystem to new pySim.runtime

Those two are really separate concepts, so let's keep them in separate
source code files.

Change-Id: I9ec54304dd8f4a4cba9487054a8eb8d265c2d340
---
M pySim-shell.py
M pySim-trace.py
M pySim/apdu/__init__.py
M pySim/apdu/ts_102_221.py
M pySim/filesystem.py
A pySim/runtime.py
M pySim/sysmocom_sja2.py
7 files changed, 537 insertions(+), 497 deletions(-)

git pull ssh://gerrit.osmocom.org:29418/pysim refs/changes/02/33702/1
diff --git a/pySim-shell.py b/pySim-shell.py
index 5281eab..213f057 100755
--- a/pySim-shell.py
+++ b/pySim-shell.py
@@ -55,7 +55,8 @@
from pySim.utils import sanitize_pin_adm, tabulate_str_list, boxed_heading_str, Hexstr
from pySim.card_handler import CardHandler, CardHandlerAuto

-from pySim.filesystem import RuntimeState, CardDF, CardADF, CardModel, CardApplication
+from pySim.filesystem import CardDF, CardADF, CardModel, CardApplication
+from pySim.runtime import RuntimeState
from pySim.profile import CardProfile
from pySim.cdma_ruim import CardProfileRUIM
from pySim.ts_102_221 import CardProfileUICC
diff --git a/pySim-trace.py b/pySim-trace.py
index 4c8696d..0bb69b0 100755
--- a/pySim-trace.py
+++ b/pySim-trace.py
@@ -6,7 +6,7 @@
from pprint import pprint as pp

from pySim.apdu import *
-from pySim.filesystem import RuntimeState
+from pySim.runtime import RuntimeState

from pySim.cards import UiccCardBase
from pySim.commands import SimCardCommands
diff --git a/pySim/apdu/__init__.py b/pySim/apdu/__init__.py
index 1d1174d..cc0f701 100644
--- a/pySim/apdu/__init__.py
+++ b/pySim/apdu/__init__.py
@@ -35,7 +35,7 @@
from construct import Optional as COptional
from pySim.construct import *
from pySim.utils import *
-from pySim.filesystem import RuntimeLchan, RuntimeState, lchan_nr_from_cla
+from pySim.runtime import RuntimeLchan, RuntimeState, lchan_nr_from_cla
from pySim.filesystem import CardADF, CardFile, TransparentEF, LinFixedEF

"""There are multiple levels of decode:
diff --git a/pySim/apdu/ts_102_221.py b/pySim/apdu/ts_102_221.py
index cd246b1..3780c8a 100644
--- a/pySim/apdu/ts_102_221.py
+++ b/pySim/apdu/ts_102_221.py
@@ -20,6 +20,7 @@
import logging
from pySim.construct import *
from pySim.filesystem import *
+from pySim.runtime import RuntimeLchan
from pySim.apdu import ApduCommand, ApduCommandSet
from typing import Optional, Dict, Tuple

diff --git a/pySim/filesystem.py b/pySim/filesystem.py
index 2bcd363..9f3ee17 100644
--- a/pySim/filesystem.py
+++ b/pySim/filesystem.py
@@ -51,18 +51,6 @@

Size = Tuple[int, Optional[int]]

-def lchan_nr_from_cla(cla: int) -> int:
- """Resolve the logical channel number from the CLA byte."""
- # TS 102 221 10.1.1 Coding of Class Byte
- if cla >> 4 in [0x0, 0xA, 0x8]:
- # Table 10.3
- return cla & 0x03
- elif cla & 0xD0 in [0x40, 0xC0]:
- # Table 10.4a
- return 4 + (cla & 0x0F)
- else:
- raise ValueError('Could not determine logical channel for CLA=%2X' % cla)
-
class CardFile:
"""Base class for all objects in the smart card filesystem.
Serve as a common ancestor to all other file types; rarely used directly.
@@ -1285,486 +1273,6 @@
self.size = size
self.shell_commands = [self.ShellCommands()]

-
-class RuntimeState:
- """Represent the runtime state of a session with a card."""
-
- def __init__(self, card, profile: 'CardProfile'):
- """
- Args:
- card : pysim.cards.Card instance
- profile : CardProfile instance
- """
- self.mf = CardMF(profile=profile)
- self.card = card
- self.profile = profile
- self.lchan = {}
- # the basic logical channel always exists
- self.lchan[0] = RuntimeLchan(0, self)
-
- # make sure the class and selection control bytes, which are specified
- # by the card profile are used
- self.card.set_apdu_parameter(
- cla=self.profile.cla, sel_ctrl=self.profile.sel_ctrl)
-
- for addon_cls in self.profile.addons:
- addon = addon_cls()
- if addon.probe(self.card):
- print("Detected %s Add-on \"%s\"" % (self.profile, addon))
- for f in addon.files_in_mf:
- self.mf.add_file(f)
-
- # go back to MF before the next steps (addon probing might have changed DF)
- self.card._scc.select_file('3F00')
-
- # add application ADFs + MF-files from profile
- apps = self._match_applications()
- for a in apps:
- if a.adf:
- self.mf.add_application_df(a.adf)
- for f in self.profile.files_in_mf:
- self.mf.add_file(f)
- self.conserve_write = True
-
- # make sure that when the runtime state is created, the card is also
- # in a defined state.
- self.reset()
-
- def _match_applications(self):
- """match the applications from the profile with applications on the card"""
- apps_profile = self.profile.applications
-
- # When the profile does not feature any applications, then we are done already
- if not apps_profile:
- return []
-
- # Read AIDs from card and match them against the applications defined by the
- # card profile
- aids_card = self.card.read_aids()
- apps_taken = []
- if aids_card:
- aids_taken = []
- print("AIDs on card:")
- for a in aids_card:
- for f in apps_profile:
- if f.aid in a:
- print(" %s: %s (EF.DIR)" % (f.name, a))
- aids_taken.append(a)
- apps_taken.append(f)
- aids_unknown = set(aids_card) - set(aids_taken)
- for a in aids_unknown:
- print(" unknown: %s (EF.DIR)" % a)
- else:
- print("warning: EF.DIR seems to be empty!")
-
- # Some card applications may not be registered in EF.DIR, we will actively
- # probe for those applications
- for f in set(apps_profile) - set(apps_taken):
- try:
- data, sw = self.card.select_adf_by_aid(f.aid)
- if sw == "9000":
- print(" %s: %s" % (f.name, f.aid))
- apps_taken.append(f)
- except (SwMatchError, ProtocolError):
- pass
- return apps_taken
-
- def reset(self, cmd_app=None) -> Hexstr:
- """Perform physical card reset and obtain ATR.
- Args:
- cmd_app : Command Application State (for unregistering old file commands)
- """
- # delete all lchan != 0 (basic lchan)
- for lchan_nr in self.lchan.keys():
- if lchan_nr == 0:
- continue
- del self.lchan[lchan_nr]
- atr = i2h(self.card.reset())
- # select MF to reset internal state and to verify card really works
- self.lchan[0].select('MF', cmd_app)
- self.lchan[0].selected_adf = None
- return atr
-
- def add_lchan(self, lchan_nr: int) -> 'RuntimeLchan':
- """Add a logical channel to the runtime state. You shouldn't call this
- directly but always go through RuntimeLchan.add_lchan()."""
- if lchan_nr in self.lchan.keys():
- raise ValueError('Cannot create already-existing lchan %d' % lchan_nr)
- self.lchan[lchan_nr] = RuntimeLchan(lchan_nr, self)
- return self.lchan[lchan_nr]
-
- def del_lchan(self, lchan_nr: int):
- if lchan_nr in self.lchan.keys():
- del self.lchan[lchan_nr]
- return True
- else:
- return False
-
- def get_lchan_by_cla(self, cla) -> Optional['RuntimeLchan']:
- lchan_nr = lchan_nr_from_cla(cla)
- if lchan_nr in self.lchan.keys():
- return self.lchan[lchan_nr]
- else:
- return None
-
-
-class RuntimeLchan:
- """Represent the runtime state of a logical channel with a card."""
-
- def __init__(self, lchan_nr: int, rs: RuntimeState):
- self.lchan_nr = lchan_nr
- self.rs = rs
- self.selected_file = self.rs.mf
- self.selected_adf = None
- self.selected_file_fcp = None
- self.selected_file_fcp_hex = None
-
- def add_lchan(self, lchan_nr: int) -> 'RuntimeLchan':
- """Add a new logical channel from the current logical channel. Just affects
- internal state, doesn't actually open a channel with the UICC."""
- new_lchan = self.rs.add_lchan(lchan_nr)
- # See TS 102 221 Table 8.3
- if self.lchan_nr != 0:
- new_lchan.selected_file = self.get_cwd()
- new_lchan.selected_adf = self.selected_adf
- return new_lchan
-
- def selected_file_descriptor_byte(self) -> dict:
- return self.selected_file_fcp['file_descriptor']['file_descriptor_byte']
-
- def selected_file_shareable(self) -> bool:
- return self.selected_file_descriptor_byte()['shareable']
-
- def selected_file_structure(self) -> str:
- return self.selected_file_descriptor_byte()['structure']
-
- def selected_file_type(self) -> str:
- return self.selected_file_descriptor_byte()['file_type']
-
- def selected_file_num_of_rec(self) -> Optional[int]:
- return self.selected_file_fcp['file_descriptor'].get('num_of_rec')
-
- def get_cwd(self) -> CardDF:
- """Obtain the current working directory.
-
- Returns:
- CardDF instance
- """
- if isinstance(self.selected_file, CardDF):
- return self.selected_file
- else:
- return self.selected_file.parent
-
- def get_application_df(self) -> Optional[CardADF]:
- """Obtain the currently selected application DF (if any).
-
- Returns:
- CardADF() instance or None"""
- # iterate upwards from selected file; check if any is an ADF
- node = self.selected_file
- while node.parent != node:
- if isinstance(node, CardADF):
- return node
- node = node.parent
- return None
-
- def interpret_sw(self, sw: str):
- """Interpret a given status word relative to the currently selected application
- or the underlying card profile.
-
- Args:
- sw : Status word as string of 4 hex digits
-
- Returns:
- Tuple of two strings
- """
- res = None
- adf = self.get_application_df()
- if adf:
- app = adf.application
- # The application either comes with its own interpret_sw
- # method or we will use the interpret_sw method from the
- # card profile.
- if app and hasattr(app, "interpret_sw"):
- res = app.interpret_sw(sw)
- return res or self.rs.profile.interpret_sw(sw)
-
- def probe_file(self, fid: str, cmd_app=None):
- """Blindly try to select a file and automatically add a matching file
- object if the file actually exists."""
- if not is_hex(fid, 4, 4):
- raise ValueError(
- "Cannot select unknown file by name %s, only hexadecimal 4 digit FID is allowed" % fid)
-
- try:
- (data, sw) = self.rs.card._scc.select_file(fid)
- except SwMatchError as swm:
- k = self.interpret_sw(swm.sw_actual)
- if not k:
- raise(swm)
- raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1]))
-
- select_resp = self.selected_file.decode_select_response(data)
- if (select_resp['file_descriptor']['file_descriptor_byte']['file_type'] == 'df'):
- f = CardDF(fid=fid, sfid=None, name="DF." + str(fid).upper(),
- desc="dedicated file, manually added at runtime")
- else:
- if (select_resp['file_descriptor']['file_descriptor_byte']['structure'] == 'transparent'):
- f = TransparentEF(fid=fid, sfid=None, name="EF." + str(fid).upper(),
- desc="elementary file, manually added at runtime")
- else:
- f = LinFixedEF(fid=fid, sfid=None, name="EF." + str(fid).upper(),
- desc="elementary file, manually added at runtime")
-
- self.selected_file.add_files([f])
- self.selected_file = f
- return select_resp, data
-
- def _select_pre(self, cmd_app):
- # unregister commands of old file
- if cmd_app and self.selected_file.shell_commands:
- for c in self.selected_file.shell_commands:
- cmd_app.unregister_command_set(c)
-
- def _select_post(self, cmd_app):
- # register commands of new file
- if cmd_app and self.selected_file.shell_commands:
- for c in self.selected_file.shell_commands:
- cmd_app.register_command_set(c)
-
- def select_file(self, file: CardFile, cmd_app=None):
- """Select a file (EF, DF, ADF, MF, ...).
-
- Args:
- file : CardFile [or derived class] instance
- cmd_app : Command Application State (for unregistering old file commands)
- """
- # we need to find a path from our self.selected_file to the destination
- inter_path = self.selected_file.build_select_path_to(file)
- if not inter_path:
- raise RuntimeError('Cannot determine path from %s to %s' % (self.selected_file, file))
-
- self._select_pre(cmd_app)
-
- for p in inter_path:
- try:
- if isinstance(p, CardADF):
- (data, sw) = self.rs.card.select_adf_by_aid(p.aid)
- self.selected_adf = p
- else:
- (data, sw) = self.rs.card._scc.select_file(p.fid)
- self.selected_file = p
- except SwMatchError as swm:
- self._select_post(cmd_app)
- raise(swm)
-
- self._select_post(cmd_app)
-
- def select(self, name: str, cmd_app=None):
- """Select a file (EF, DF, ADF, MF, ...).
-
- Args:
- name : Name of file to select
- cmd_app : Command Application State (for unregistering old file commands)
- """
- # handling of entire paths with multiple directories/elements
- if '/' in name:
- prev_sel_file = self.selected_file
- pathlist = name.split('/')
- # treat /DF.GSM/foo like MF/DF.GSM/foo
- if pathlist[0] == '':
- pathlist[0] = 'MF'
- try:
- for p in pathlist:
- self.select(p, cmd_app)
- return
- except Exception as e:
- # if any intermediate step fails, go back to where we were
- self.select_file(prev_sel_file, cmd_app)
- raise e
-
- sels = self.selected_file.get_selectables()
- if is_hex(name):
- name = name.lower()
-
- self._select_pre(cmd_app)
-
- if name in sels:
- f = sels[name]
- try:
- if isinstance(f, CardADF):
- (data, sw) = self.rs.card.select_adf_by_aid(f.aid)
- else:
- (data, sw) = self.rs.card._scc.select_file(f.fid)
- self.selected_file = f
- except SwMatchError as swm:
- k = self.interpret_sw(swm.sw_actual)
- if not k:
- raise(swm)
- raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1]))
- select_resp = f.decode_select_response(data)
- else:
- (select_resp, data) = self.probe_file(name, cmd_app)
-
- # store the raw + decoded FCP for later reference
- self.selected_file_fcp_hex = data
- self.selected_file_fcp = select_resp
-
- self._select_post(cmd_app)
- return select_resp
-
- def status(self):
- """Request STATUS (current selected file FCP) from card."""
- (data, sw) = self.rs.card._scc.status()
- return self.selected_file.decode_select_response(data)
-
- def get_file_for_selectable(self, name: str):
- sels = self.selected_file.get_selectables()
- return sels[name]
-
- def activate_file(self, name: str):
- """Request ACTIVATE FILE of specified file."""
- sels = self.selected_file.get_selectables()
- f = sels[name]
- data, sw = self.rs.card._scc.activate_file(f.fid)
- return data, sw
-
- def read_binary(self, length: int = None, offset: int = 0):
- """Read [part of] a transparent EF binary data.
-
- Args:
- length : Amount of data to read (None: as much as possible)
- offset : Offset into the file from which to read 'length' bytes
- Returns:
- binary data read from the file
- """
- if not isinstance(self.selected_file, TransparentEF):
- raise TypeError("Only works with TransparentEF")
- return self.rs.card._scc.read_binary(self.selected_file.fid, length, offset)
-
- def read_binary_dec(self) -> Tuple[dict, str]:
- """Read [part of] a transparent EF binary data and decode it.
-
- Args:
- length : Amount of data to read (None: as much as possible)
- offset : Offset into the file from which to read 'length' bytes
- Returns:
- abstract decode data read from the file
- """
- (data, sw) = self.read_binary()
- dec_data = self.selected_file.decode_hex(data)
- return (dec_data, sw)
-
- def update_binary(self, data_hex: str, offset: int = 0):
- """Update transparent EF binary data.
-
- Args:
- data_hex : hex string of data to be written
- offset : Offset into the file from which to write 'data_hex'
- """
- if not isinstance(self.selected_file, TransparentEF):
- raise TypeError("Only works with TransparentEF")
- return self.rs.card._scc.update_binary(self.selected_file.fid, data_hex, offset, conserve=self.rs.conserve_write)
-
- def update_binary_dec(self, data: dict):
- """Update transparent EF from abstract data. Encodes the data to binary and
- then updates the EF with it.
-
- Args:
- data : abstract data which is to be encoded and written
- """
- data_hex = self.selected_file.encode_hex(data)
- return self.update_binary(data_hex)
-
- def read_record(self, rec_nr: int = 0):
- """Read a record as binary data.
-
- Args:
- rec_nr : Record number to read
- Returns:
- hex string of binary data contained in record
- """
- if not isinstance(self.selected_file, LinFixedEF):
- raise TypeError("Only works with Linear Fixed EF")
- # returns a string of hex nibbles
- return self.rs.card._scc.read_record(self.selected_file.fid, rec_nr)
-
- def read_record_dec(self, rec_nr: int = 0) -> Tuple[dict, str]:
- """Read a record and decode it to abstract data.
-
- Args:
- rec_nr : Record number to read
- Returns:
- abstract data contained in record
- """
- (data, sw) = self.read_record(rec_nr)
- return (self.selected_file.decode_record_hex(data, rec_nr), sw)
-
- def update_record(self, rec_nr: int, data_hex: str):
- """Update a record with given binary data
-
- Args:
- rec_nr : Record number to read
- data_hex : Hex string binary data to be written
- """
- if not isinstance(self.selected_file, LinFixedEF):
- raise TypeError("Only works with Linear Fixed EF")
- return self.rs.card._scc.update_record(self.selected_file.fid, rec_nr, data_hex, conserve=self.rs.conserve_write)
-
- def update_record_dec(self, rec_nr: int, data: dict):
- """Update a record with given abstract data. Will encode abstract to binary data
- and then write it to the given record on the card.
-
- Args:
- rec_nr : Record number to read
- data_hex : Abstract data to be written
- """
- data_hex = self.selected_file.encode_record_hex(data, rec_nr)
- return self.update_record(rec_nr, data_hex)
-
- def retrieve_data(self, tag: int = 0):
- """Read a DO/TLV as binary data.
-
- Args:
- tag : Tag of TLV/DO to read
- Returns:
- hex string of full BER-TLV DO including Tag and Length
- """
- if not isinstance(self.selected_file, BerTlvEF):
- raise TypeError("Only works with BER-TLV EF")
- # returns a string of hex nibbles
- return self.rs.card._scc.retrieve_data(self.selected_file.fid, tag)
-
- def retrieve_tags(self):
- """Retrieve tags available on BER-TLV EF.
-
- Returns:
- list of integer tags contained in EF
- """
- if not isinstance(self.selected_file, BerTlvEF):
- raise TypeError("Only works with BER-TLV EF")
- data, sw = self.rs.card._scc.retrieve_data(self.selected_file.fid, 0x5c)
- tag, length, value, remainder = bertlv_parse_one(h2b(data))
- return list(value)
-
- def set_data(self, tag: int, data_hex: str):
- """Update a TLV/DO with given binary data
-
- Args:
- tag : Tag of TLV/DO to be written
- data_hex : Hex string binary data to be written (value portion)
- """
- if not isinstance(self.selected_file, BerTlvEF):
- raise TypeError("Only works with BER-TLV EF")
- return self.rs.card._scc.set_data(self.selected_file.fid, tag, data_hex, conserve=self.rs.conserve_write)
-
- def unregister_cmds(self, cmd_app=None):
- """Unregister all file specific commands."""
- if cmd_app and self.selected_file.shell_commands:
- for c in self.selected_file.shell_commands:
- cmd_app.unregister_command_set(c)
-
-
def interpret_sw(sw_data: dict, sw: str):
"""Interpret a given status word.

@@ -1828,7 +1336,7 @@

@classmethod
@abc.abstractmethod
- def add_files(cls, rs: RuntimeState):
+ def add_files(cls, rs: 'RuntimeState'):
"""Add model specific files to given RuntimeState."""

@classmethod
@@ -1843,7 +1351,7 @@
return False

@staticmethod
- def apply_matching_models(scc: SimCardCommands, rs: RuntimeState):
+ def apply_matching_models(scc: SimCardCommands, rs: 'RuntimeState'):
"""Check if any of the CardModel sub-classes 'match' the currently inserted card
(by ATR or overriding the 'match' method). If so, call their 'add_files'
method."""
diff --git a/pySim/runtime.py b/pySim/runtime.py
new file mode 100644
index 0000000..5d181e8
--- /dev/null
+++ b/pySim/runtime.py
@@ -0,0 +1,517 @@
+# coding=utf-8
+"""Representation of the runtime state of an application like pySim-shell.
+"""
+
+# (C) 2021 by Harald Welte <laforge@osmocom.org>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+from typing import Optional, Tuple
+
+from pySim.utils import sw_match, h2b, i2h, is_hex, bertlv_parse_one, Hexstr
+from pySim.exceptions import *
+from pySim.filesystem import *
+
+def lchan_nr_from_cla(cla: int) -> int:
+ """Resolve the logical channel number from the CLA byte."""
+ # TS 102 221 10.1.1 Coding of Class Byte
+ if cla >> 4 in [0x0, 0xA, 0x8]:
+ # Table 10.3
+ return cla & 0x03
+ elif cla & 0xD0 in [0x40, 0xC0]:
+ # Table 10.4a
+ return 4 + (cla & 0x0F)
+ else:
+ raise ValueError('Could not determine logical channel for CLA=%2X' % cla)
+
+class RuntimeState:
+ """Represent the runtime state of a session with a card."""
+
+ def __init__(self, card, profile: 'CardProfile'):
+ """
+ Args:
+ card : pysim.cards.Card instance
+ profile : CardProfile instance
+ """
+ self.mf = CardMF(profile=profile)
+ self.card = card
+ self.profile = profile
+ self.lchan = {}
+ # the basic logical channel always exists
+ self.lchan[0] = RuntimeLchan(0, self)
+
+ # make sure the class and selection control bytes, which are specified
+ # by the card profile are used
+ self.card.set_apdu_parameter(
+ cla=self.profile.cla, sel_ctrl=self.profile.sel_ctrl)
+
+ for addon_cls in self.profile.addons:
+ addon = addon_cls()
+ if addon.probe(self.card):
+ print("Detected %s Add-on \"%s\"" % (self.profile, addon))
+ for f in addon.files_in_mf:
+ self.mf.add_file(f)
+
+ # go back to MF before the next steps (addon probing might have changed DF)
+ self.card._scc.select_file('3F00')
+
+ # add application ADFs + MF-files from profile
+ apps = self._match_applications()
+ for a in apps:
+ if a.adf:
+ self.mf.add_application_df(a.adf)
+ for f in self.profile.files_in_mf:
+ self.mf.add_file(f)
+ self.conserve_write = True
+
+ # make sure that when the runtime state is created, the card is also
+ # in a defined state.
+ self.reset()
+
+ def _match_applications(self):
+ """match the applications from the profile with applications on the card"""
+ apps_profile = self.profile.applications
+
+ # When the profile does not feature any applications, then we are done already
+ if not apps_profile:
+ return []
+
+ # Read AIDs from card and match them against the applications defined by the
+ # card profile
+ aids_card = self.card.read_aids()
+ apps_taken = []
+ if aids_card:
+ aids_taken = []
+ print("AIDs on card:")
+ for a in aids_card:
+ for f in apps_profile:
+ if f.aid in a:
+ print(" %s: %s (EF.DIR)" % (f.name, a))
+ aids_taken.append(a)
+ apps_taken.append(f)
+ aids_unknown = set(aids_card) - set(aids_taken)
+ for a in aids_unknown:
+ print(" unknown: %s (EF.DIR)" % a)
+ else:
+ print("warning: EF.DIR seems to be empty!")
+
+ # Some card applications may not be registered in EF.DIR, we will actively
+ # probe for those applications
+ for f in set(apps_profile) - set(apps_taken):
+ try:
+ data, sw = self.card.select_adf_by_aid(f.aid)
+ if sw == "9000":
+ print(" %s: %s" % (f.name, f.aid))
+ apps_taken.append(f)
+ except (SwMatchError, ProtocolError):
+ pass
+ return apps_taken
+
+ def reset(self, cmd_app=None) -> Hexstr:
+ """Perform physical card reset and obtain ATR.
+ Args:
+ cmd_app : Command Application State (for unregistering old file commands)
+ """
+ # delete all lchan != 0 (basic lchan)
+ for lchan_nr in self.lchan.keys():
+ if lchan_nr == 0:
+ continue
+ del self.lchan[lchan_nr]
+ atr = i2h(self.card.reset())
+ # select MF to reset internal state and to verify card really works
+ self.lchan[0].select('MF', cmd_app)
+ self.lchan[0].selected_adf = None
+ return atr
+
+ def add_lchan(self, lchan_nr: int) -> 'RuntimeLchan':
+ """Add a logical channel to the runtime state. You shouldn't call this
+ directly but always go through RuntimeLchan.add_lchan()."""
+ if lchan_nr in self.lchan.keys():
+ raise ValueError('Cannot create already-existing lchan %d' % lchan_nr)
+ self.lchan[lchan_nr] = RuntimeLchan(lchan_nr, self)
+ return self.lchan[lchan_nr]
+
+ def del_lchan(self, lchan_nr: int):
+ if lchan_nr in self.lchan.keys():
+ del self.lchan[lchan_nr]
+ return True
+ else:
+ return False
+
+ def get_lchan_by_cla(self, cla) -> Optional['RuntimeLchan']:
+ lchan_nr = lchan_nr_from_cla(cla)
+ if lchan_nr in self.lchan.keys():
+ return self.lchan[lchan_nr]
+ else:
+ return None
+
+
+class RuntimeLchan:
+ """Represent the runtime state of a logical channel with a card."""
+
+ def __init__(self, lchan_nr: int, rs: RuntimeState):
+ self.lchan_nr = lchan_nr
+ self.rs = rs
+ self.selected_file = self.rs.mf
+ self.selected_adf = None
+ self.selected_file_fcp = None
+ self.selected_file_fcp_hex = None
+
+ def add_lchan(self, lchan_nr: int) -> 'RuntimeLchan':
+ """Add a new logical channel from the current logical channel. Just affects
+ internal state, doesn't actually open a channel with the UICC."""
+ new_lchan = self.rs.add_lchan(lchan_nr)
+ # See TS 102 221 Table 8.3
+ if self.lchan_nr != 0:
+ new_lchan.selected_file = self.get_cwd()
+ new_lchan.selected_adf = self.selected_adf
+ return new_lchan
+
+ def selected_file_descriptor_byte(self) -> dict:
+ return self.selected_file_fcp['file_descriptor']['file_descriptor_byte']
+
+ def selected_file_shareable(self) -> bool:
+ return self.selected_file_descriptor_byte()['shareable']
+
+ def selected_file_structure(self) -> str:
+ return self.selected_file_descriptor_byte()['structure']
+
+ def selected_file_type(self) -> str:
+ return self.selected_file_descriptor_byte()['file_type']
+
+ def selected_file_num_of_rec(self) -> Optional[int]:
+ return self.selected_file_fcp['file_descriptor'].get('num_of_rec')
+
+ def get_cwd(self) -> CardDF:
+ """Obtain the current working directory.
+
+ Returns:
+ CardDF instance
+ """
+ if isinstance(self.selected_file, CardDF):
+ return self.selected_file
+ else:
+ return self.selected_file.parent
+
+ def get_application_df(self) -> Optional[CardADF]:
+ """Obtain the currently selected application DF (if any).
+
+ Returns:
+ CardADF() instance or None"""
+ # iterate upwards from selected file; check if any is an ADF
+ node = self.selected_file
+ while node.parent != node:
+ if isinstance(node, CardADF):
+ return node
+ node = node.parent
+ return None
+
+ def interpret_sw(self, sw: str):
+ """Interpret a given status word relative to the currently selected application
+ or the underlying card profile.
+
+ Args:
+ sw : Status word as string of 4 hex digits
+
+ Returns:
+ Tuple of two strings
+ """
+ res = None
+ adf = self.get_application_df()
+ if adf:
+ app = adf.application
+ # The application either comes with its own interpret_sw
+ # method or we will use the interpret_sw method from the
+ # card profile.
+ if app and hasattr(app, "interpret_sw"):
+ res = app.interpret_sw(sw)
+ return res or self.rs.profile.interpret_sw(sw)
+
+ def probe_file(self, fid: str, cmd_app=None):
+ """Blindly try to select a file and automatically add a matching file
+ object if the file actually exists."""
+ if not is_hex(fid, 4, 4):
+ raise ValueError(
+ "Cannot select unknown file by name %s, only hexadecimal 4 digit FID is allowed" % fid)
+
+ try:
+ (data, sw) = self.rs.card._scc.select_file(fid)
+ except SwMatchError as swm:
+ k = self.interpret_sw(swm.sw_actual)
+ if not k:
+ raise(swm)
+ raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1]))
+
+ select_resp = self.selected_file.decode_select_response(data)
+ if (select_resp['file_descriptor']['file_descriptor_byte']['file_type'] == 'df'):
+ f = CardDF(fid=fid, sfid=None, name="DF." + str(fid).upper(),
+ desc="dedicated file, manually added at runtime")
+ else:
+ if (select_resp['file_descriptor']['file_descriptor_byte']['structure'] == 'transparent'):
+ f = TransparentEF(fid=fid, sfid=None, name="EF." + str(fid).upper(),
+ desc="elementary file, manually added at runtime")
+ else:
+ f = LinFixedEF(fid=fid, sfid=None, name="EF." + str(fid).upper(),
+ desc="elementary file, manually added at runtime")
+
+ self.selected_file.add_files([f])
+ self.selected_file = f
+ return select_resp, data
+
+ def _select_pre(self, cmd_app):
+ # unregister commands of old file
+ if cmd_app and self.selected_file.shell_commands:
+ for c in self.selected_file.shell_commands:
+ cmd_app.unregister_command_set(c)
+
+ def _select_post(self, cmd_app):
+ # register commands of new file
+ if cmd_app and self.selected_file.shell_commands:
+ for c in self.selected_file.shell_commands:
+ cmd_app.register_command_set(c)
+
+ def select_file(self, file: CardFile, cmd_app=None):
+ """Select a file (EF, DF, ADF, MF, ...).
+
+ Args:
+ file : CardFile [or derived class] instance
+ cmd_app : Command Application State (for unregistering old file commands)
+ """
+ # we need to find a path from our self.selected_file to the destination
+ inter_path = self.selected_file.build_select_path_to(file)
+ if not inter_path:
+ raise RuntimeError('Cannot determine path from %s to %s' % (self.selected_file, file))
+
+ self._select_pre(cmd_app)
+
+ for p in inter_path:
+ try:
+ if isinstance(p, CardADF):
+ (data, sw) = self.rs.card.select_adf_by_aid(p.aid)
+ self.selected_adf = p
+ else:
+ (data, sw) = self.rs.card._scc.select_file(p.fid)
+ self.selected_file = p
+ except SwMatchError as swm:
+ self._select_post(cmd_app)
+ raise(swm)
+
+ self._select_post(cmd_app)
+
+ def select(self, name: str, cmd_app=None):
+ """Select a file (EF, DF, ADF, MF, ...).
+
+ Args:
+ name : Name of file to select
+ cmd_app : Command Application State (for unregistering old file commands)
+ """
+ # handling of entire paths with multiple directories/elements
+ if '/' in name:
+ prev_sel_file = self.selected_file
+ pathlist = name.split('/')
+ # treat /DF.GSM/foo like MF/DF.GSM/foo
+ if pathlist[0] == '':
+ pathlist[0] = 'MF'
+ try:
+ for p in pathlist:
+ self.select(p, cmd_app)
+ return
+ except Exception as e:
+ # if any intermediate step fails, go back to where we were
+ self.select_file(prev_sel_file, cmd_app)
+ raise e
+
+ sels = self.selected_file.get_selectables()
+ if is_hex(name):
+ name = name.lower()
+
+ self._select_pre(cmd_app)
+
+ if name in sels:
+ f = sels[name]
+ try:
+ if isinstance(f, CardADF):
+ (data, sw) = self.rs.card.select_adf_by_aid(f.aid)
+ else:
+ (data, sw) = self.rs.card._scc.select_file(f.fid)
+ self.selected_file = f
+ except SwMatchError as swm:
+ k = self.interpret_sw(swm.sw_actual)
+ if not k:
+ raise(swm)
+ raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1]))
+ select_resp = f.decode_select_response(data)
+ else:
+ (select_resp, data) = self.probe_file(name, cmd_app)
+
+ # store the raw + decoded FCP for later reference
+ self.selected_file_fcp_hex = data
+ self.selected_file_fcp = select_resp
+
+ self._select_post(cmd_app)
+ return select_resp
+
+ def status(self):
+ """Request STATUS (current selected file FCP) from card."""
+ (data, sw) = self.rs.card._scc.status()
+ return self.selected_file.decode_select_response(data)
+
+ def get_file_for_selectable(self, name: str):
+ sels = self.selected_file.get_selectables()
+ return sels[name]
+
+ def activate_file(self, name: str):
+ """Request ACTIVATE FILE of specified file."""
+ sels = self.selected_file.get_selectables()
+ f = sels[name]
+ data, sw = self.rs.card._scc.activate_file(f.fid)
+ return data, sw
+
+ def read_binary(self, length: int = None, offset: int = 0):
+ """Read [part of] a transparent EF binary data.
+
+ Args:
+ length : Amount of data to read (None: as much as possible)
+ offset : Offset into the file from which to read 'length' bytes
+ Returns:
+ binary data read from the file
+ """
+ if not isinstance(self.selected_file, TransparentEF):
+ raise TypeError("Only works with TransparentEF")
+ return self.rs.card._scc.read_binary(self.selected_file.fid, length, offset)
+
+ def read_binary_dec(self) -> Tuple[dict, str]:
+ """Read [part of] a transparent EF binary data and decode it.
+
+ Args:
+ length : Amount of data to read (None: as much as possible)
+ offset : Offset into the file from which to read 'length' bytes
+ Returns:
+ abstract decode data read from the file
+ """
+ (data, sw) = self.read_binary()
+ dec_data = self.selected_file.decode_hex(data)
+ return (dec_data, sw)
+
+ def update_binary(self, data_hex: str, offset: int = 0):
+ """Update transparent EF binary data.
+
+ Args:
+ data_hex : hex string of data to be written
+ offset : Offset into the file from which to write 'data_hex'
+ """
+ if not isinstance(self.selected_file, TransparentEF):
+ raise TypeError("Only works with TransparentEF")
+ return self.rs.card._scc.update_binary(self.selected_file.fid, data_hex, offset, conserve=self.rs.conserve_write)
+
+ def update_binary_dec(self, data: dict):
+ """Update transparent EF from abstract data. Encodes the data to binary and
+ then updates the EF with it.
+
+ Args:
+ data : abstract data which is to be encoded and written
+ """
+ data_hex = self.selected_file.encode_hex(data)
+ return self.update_binary(data_hex)
+
+ def read_record(self, rec_nr: int = 0):
+ """Read a record as binary data.
+
+ Args:
+ rec_nr : Record number to read
+ Returns:
+ hex string of binary data contained in record
+ """
+ if not isinstance(self.selected_file, LinFixedEF):
+ raise TypeError("Only works with Linear Fixed EF")
+ # returns a string of hex nibbles
+ return self.rs.card._scc.read_record(self.selected_file.fid, rec_nr)
+
+ def read_record_dec(self, rec_nr: int = 0) -> Tuple[dict, str]:
+ """Read a record and decode it to abstract data.
+
+ Args:
+ rec_nr : Record number to read
+ Returns:
+ abstract data contained in record
+ """
+ (data, sw) = self.read_record(rec_nr)
+ return (self.selected_file.decode_record_hex(data, rec_nr), sw)
+
+ def update_record(self, rec_nr: int, data_hex: str):
+ """Update a record with given binary data
+
+ Args:
+ rec_nr : Record number to read
+ data_hex : Hex string binary data to be written
+ """
+ if not isinstance(self.selected_file, LinFixedEF):
+ raise TypeError("Only works with Linear Fixed EF")
+ return self.rs.card._scc.update_record(self.selected_file.fid, rec_nr, data_hex, conserve=self.rs.conserve_write)
+
+ def update_record_dec(self, rec_nr: int, data: dict):
+ """Update a record with given abstract data. Will encode abstract to binary data
+ and then write it to the given record on the card.
+
+ Args:
+ rec_nr : Record number to read
+ data_hex : Abstract data to be written
+ """
+ data_hex = self.selected_file.encode_record_hex(data, rec_nr)
+ return self.update_record(rec_nr, data_hex)
+
+ def retrieve_data(self, tag: int = 0):
+ """Read a DO/TLV as binary data.
+
+ Args:
+ tag : Tag of TLV/DO to read
+ Returns:
+ hex string of full BER-TLV DO including Tag and Length
+ """
+ if not isinstance(self.selected_file, BerTlvEF):
+ raise TypeError("Only works with BER-TLV EF")
+ # returns a string of hex nibbles
+ return self.rs.card._scc.retrieve_data(self.selected_file.fid, tag)
+
+ def retrieve_tags(self):
+ """Retrieve tags available on BER-TLV EF.
+
+ Returns:
+ list of integer tags contained in EF
+ """
+ if not isinstance(self.selected_file, BerTlvEF):
+ raise TypeError("Only works with BER-TLV EF")
+ data, sw = self.rs.card._scc.retrieve_data(self.selected_file.fid, 0x5c)
+ tag, length, value, remainder = bertlv_parse_one(h2b(data))
+ return list(value)
+
+ def set_data(self, tag: int, data_hex: str):
+ """Update a TLV/DO with given binary data
+
+ Args:
+ tag : Tag of TLV/DO to be written
+ data_hex : Hex string binary data to be written (value portion)
+ """
+ if not isinstance(self.selected_file, BerTlvEF):
+ raise TypeError("Only works with BER-TLV EF")
+ return self.rs.card._scc.set_data(self.selected_file.fid, tag, data_hex, conserve=self.rs.conserve_write)
+
+ def unregister_cmds(self, cmd_app=None):
+ """Unregister all file specific commands."""
+ if cmd_app and self.selected_file.shell_commands:
+ for c in self.selected_file.shell_commands:
+ cmd_app.unregister_command_set(c)
+
+
+
diff --git a/pySim/sysmocom_sja2.py b/pySim/sysmocom_sja2.py
index 1bd4a8a..bfd0ff5 100644
--- a/pySim/sysmocom_sja2.py
+++ b/pySim/sysmocom_sja2.py
@@ -21,6 +21,7 @@
from struct import pack, unpack
from pySim.utils import *
from pySim.filesystem import *
+from pySim.runtime import RuntimeState
from pySim.ts_102_221 import CardProfileUICC
from pySim.construct import *
from construct import *

To view, visit change 33702. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: pysim
Gerrit-Branch: master
Gerrit-Change-Id: I9ec54304dd8f4a4cba9487054a8eb8d265c2d340
Gerrit-Change-Number: 33702
Gerrit-PatchSet: 1
Gerrit-Owner: laforge <laforge@osmocom.org>
Gerrit-MessageType: newchange