mirror of
https://gitea.osmocom.org/sim-card/pysim.git
synced 2026-03-17 02:48:34 +03:00
At the moment we use random identifiers as names when we create a new logger for pySimLogger. Let's switch to consistently use the module name here. For the top level modules let's use the program name so that it will show up in the log instead of __init__. Change-Id: I49a9beb98845f66247edd42ed548980c97a7151a
686 lines
28 KiB
Python
686 lines
28 KiB
Python
# 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 osmocom.utils import h2b, i2h, is_hex, Hexstr
|
|
from osmocom.tlv import bertlv_parse_one
|
|
|
|
from pySim.exceptions import *
|
|
from pySim.filesystem import *
|
|
from pySim.log import PySimLogger
|
|
|
|
log = PySimLogger.get(__name__)
|
|
|
|
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
|
|
if cla & 0xD0 in [0x40, 0xC0]:
|
|
# Table 10.4a
|
|
return 4 + (cla & 0x0F)
|
|
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: 'CardBase', 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)
|
|
# this is a dict of card identities which different parts of the code might populate,
|
|
# typically with something like ICCID, EID, ATR, ...
|
|
self.identity = {}
|
|
self.adm_verified = False
|
|
|
|
# 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)
|
|
|
|
# make sure MF is selected before probing for Addons
|
|
self.lchan[0].select('MF')
|
|
|
|
for addon_cls in self.profile.addons:
|
|
addon = addon_cls()
|
|
if addon.probe(self.card):
|
|
log.info("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.lchan[0].select('MF')
|
|
|
|
# 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 = []
|
|
log.info("AIDs on card:")
|
|
for a in aids_card:
|
|
for f in apps_profile:
|
|
if f.aid in a:
|
|
log.info(" %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:
|
|
log.info(" unknown: %s (EF.DIR)" % a)
|
|
else:
|
|
log.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 sorted(set(apps_profile) - set(apps_taken), key=str):
|
|
try:
|
|
# we can not use the lchan provided methods select, or select_file
|
|
# since those method work on an already finished file model. At
|
|
# this point we are still in the initialization process, so it is
|
|
# no problem when we access the card object directly without caring
|
|
# about updating other states. For normal selects at runtime, the
|
|
# caller must use the lchan provided methods select or select_file!
|
|
_data, sw = self.card.select_adf_by_aid(f.aid)
|
|
self.selected_adf = f
|
|
if sw == "9000":
|
|
log.info(" %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 list(self.lchan.keys()):
|
|
self.lchan[lchan_nr].scc.scp = None
|
|
if lchan_nr == 0:
|
|
continue
|
|
del self.lchan[lchan_nr]
|
|
self.adm_verified = False
|
|
atr = self.card.reset()
|
|
if cmd_app:
|
|
cmd_app.lchan = self.lchan[0]
|
|
# 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
|
|
# store ATR as part of our card identities dict
|
|
self.identity['ATR'] = atr
|
|
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.scc = self.rs.card._scc.fork_lchan(lchan_nr)
|
|
|
|
# File reference data
|
|
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 selected_file_record_len(self) -> Optional[int]:
|
|
return self.selected_file_fcp['file_descriptor'].get('record_len')
|
|
|
|
def selected_file_size(self) -> Optional[int]:
|
|
return self.selected_file_fcp.get('file_size')
|
|
|
|
def selected_file_reserved_file_size(self) -> Optional[int]:
|
|
return self.selected_file_fcp['proprietary_information'].get('reserved_file_size')
|
|
|
|
def selected_file_maximum_file_size(self) -> Optional[int]:
|
|
return self.selected_file_fcp['proprietary_information'].get('maximum_file_size')
|
|
|
|
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 get_file_by_name(self, name: str) -> CardFile:
|
|
"""Obtain the file object from the file system tree by its name without actually selecting the file.
|
|
|
|
Returns:
|
|
CardFile() instance or None"""
|
|
|
|
# handling of entire paths with multiple directories/elements
|
|
if '/' in name:
|
|
pathlist = name.split('/')
|
|
# treat /DF.GSM/foo like MF/DF.GSM/foo
|
|
if pathlist[0] == '':
|
|
pathlist[0] = 'MF'
|
|
else:
|
|
pathlist = [name]
|
|
|
|
# start in the current working directory (we can still
|
|
# select any ADF and the MF from here, so those will be
|
|
# among the selectables).
|
|
file = self.get_cwd()
|
|
|
|
for p in pathlist:
|
|
# Look for the next file in the path list
|
|
selectables = file.get_selectables()
|
|
file = None
|
|
for selectable in selectables:
|
|
if selectable == p:
|
|
file = selectables[selectable]
|
|
break
|
|
|
|
# When we hit none, then the given path must be invalid
|
|
if file is None:
|
|
return None
|
|
|
|
# Return the file object found at the tip of the path
|
|
return file
|
|
|
|
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)
|
|
|
|
# unregister commands of old file
|
|
self.unregister_cmds(cmd_app)
|
|
|
|
try:
|
|
# We access the card through the select_file method of the scc object.
|
|
# If we succeed, we know that the file exists on the card and we may
|
|
# proceed with creating a new CardEF object in the local file model at
|
|
# run time. In case the file does not exist on the card, we just abort.
|
|
# The state on the card (selected file/application) won't be changed,
|
|
# so we do not have to update any state in that case.
|
|
(data, _sw) = self.scc.select_file(fid)
|
|
except SwMatchError as swm:
|
|
self._select_post(cmd_app)
|
|
k = self.interpret_sw(swm.sw_actual)
|
|
if not k:
|
|
raise swm
|
|
raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1])) from swm
|
|
|
|
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._select_post(cmd_app, f, data)
|
|
|
|
def _select_post(self, cmd_app, file:Optional[CardFile] = None, select_resp_data = None):
|
|
# we store some reference data (see above) about the currently selected file.
|
|
# This data must be updated after every select.
|
|
if file:
|
|
self.selected_file = file
|
|
if isinstance(file, CardADF):
|
|
self.selected_adf = file
|
|
if select_resp_data:
|
|
self.selected_file_fcp_hex = select_resp_data
|
|
self.selected_file_fcp = self.selected_file.decode_select_response(select_resp_data)
|
|
else:
|
|
self.selected_file_fcp_hex = None
|
|
self.selected_file_fcp = None
|
|
|
|
# register commands of new file
|
|
self.register_cmds(cmd_app)
|
|
|
|
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)
|
|
"""
|
|
|
|
if not isinstance(file, CardADF) and self.selected_adf and self.selected_adf.has_fs == False:
|
|
# Not every application that may be present on a GlobalPlatform card will support the SELECT
|
|
# command as we know it from ETSI TS 102 221, section 11.1.1. In fact the only subset of
|
|
# SELECT we may rely on is the OPEN SELECT command as specified in GlobalPlatform Card
|
|
# Specification, section 11.9. Unfortunately the OPEN SELECT command only supports the
|
|
# "select by name" method, which means we can only select an application and not a file.
|
|
# The consequence of this is that we may get trapped in an application that does not have
|
|
# ISIM/USIM like file system support and the only way to leave that application is to select
|
|
# an ISIM/USIM application in order to get the file system access back.
|
|
#
|
|
# To automate this escape-route we will first select an arbitrary ADF that has file system support first
|
|
# and then continue normally.
|
|
for selectable in self.rs.mf.get_selectables().items():
|
|
if isinstance(selectable[1], CardADF) and selectable[1].has_fs == True:
|
|
self.select(selectable[1].name, cmd_app)
|
|
break
|
|
|
|
# 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))
|
|
|
|
# unregister commands of old file
|
|
self.unregister_cmds(cmd_app)
|
|
|
|
# be sure the variables that we pass to _select_post contain valid values.
|
|
selected_file = self.selected_file
|
|
data = self.selected_file_fcp_hex
|
|
|
|
for f in inter_path:
|
|
try:
|
|
# We now directly accessing the card to perform the selection. This
|
|
# will change the state of the card, so we must take care to update
|
|
# the local state (lchan) as well. This is done in the method
|
|
# _select_post. It should be noted that the caller must always use
|
|
# the methods select_file or select. The caller must not access the
|
|
# card directly since this would lead into an incoherence of the
|
|
# card state and the state of the lchan.
|
|
if isinstance(f, CardADF):
|
|
(data, _sw) = self.rs.card.select_adf_by_aid(f.aid, scc=self.scc)
|
|
else:
|
|
(data, _sw) = self.scc.select_file(f.fid)
|
|
selected_file = f
|
|
except SwMatchError as swm:
|
|
self._select_post(cmd_app, selected_file, data)
|
|
raise swm
|
|
|
|
self._select_post(cmd_app, f, data)
|
|
|
|
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)
|
|
"""
|
|
# if any intermediate step fails, we must be able to go back where we were
|
|
prev_sel_file = self.selected_file
|
|
|
|
# handling of entire paths with multiple directories/elements
|
|
if '/' in name:
|
|
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 self.selected_file_fcp
|
|
except Exception as e:
|
|
self.select_file(prev_sel_file, cmd_app)
|
|
raise e
|
|
|
|
# we are now in the directory where the target file is located
|
|
# so we can now refer to the get_selectables() method to get the
|
|
# file object and select it using select_file()
|
|
sels = self.selected_file.get_selectables()
|
|
if is_hex(name):
|
|
name = name.lower()
|
|
|
|
try:
|
|
if name in sels:
|
|
self.select_file(sels[name], cmd_app)
|
|
else:
|
|
self.probe_file(name, cmd_app)
|
|
except Exception as e:
|
|
self.select_file(prev_sel_file, cmd_app)
|
|
raise e
|
|
|
|
return self.selected_file_fcp
|
|
|
|
def status(self):
|
|
"""Request STATUS (current selected file FCP) from card."""
|
|
(data, _sw) = self.scc.status()
|
|
return self.selected_file.decode_select_response(data)
|
|
|
|
def get_file_for_filename(self, name: str):
|
|
"""Get the related CardFile object for a specified filename."""
|
|
if is_hex(name):
|
|
name = name.lower()
|
|
sels = self.selected_file.get_selectables()
|
|
return sels[name]
|
|
|
|
def activate_file(self, name: str):
|
|
"""Request ACTIVATE FILE of specified file."""
|
|
if is_hex(name):
|
|
name = name.lower()
|
|
sels = self.selected_file.get_selectables()
|
|
f = sels[name]
|
|
data, sw = self.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, but %s is %s" % (self.selected_file,
|
|
self.selected_file.__class__.__mro__))
|
|
return self.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 __get_writeable_size(self):
|
|
""" Determine the writable size (file or record) using the cached FCP parameters of the currently selected
|
|
file. Return None in case the writeable size cannot be determined (no FCP available, FCP lacks size
|
|
information).
|
|
"""
|
|
fcp = self.selected_file_fcp
|
|
if not fcp:
|
|
return None
|
|
|
|
structure = fcp.get('file_descriptor', {}).get('file_descriptor_byte', {}).get('structure')
|
|
if not structure:
|
|
return None
|
|
|
|
if structure == 'transparent':
|
|
return fcp.get('file_size')
|
|
elif structure == 'linear_fixed':
|
|
return fcp.get('file_descriptor', {}).get('record_len')
|
|
else:
|
|
return None
|
|
|
|
def __check_writeable_size(self, data_len):
|
|
""" Guard against unsuccessful writes caused by attempts to write data that exceeds the file limits. """
|
|
|
|
writeable_size = self.__get_writeable_size()
|
|
if not writeable_size:
|
|
return
|
|
|
|
if isinstance(self.selected_file, TransparentEF):
|
|
writeable_name = "file"
|
|
elif isinstance(self.selected_file, LinFixedEF):
|
|
writeable_name = "record"
|
|
else:
|
|
writeable_name = "object"
|
|
|
|
if data_len > writeable_size:
|
|
raise TypeError("Data length (%u) exceeds %s size (%u) by %u bytes" %
|
|
(data_len, writeable_name, writeable_size, data_len - writeable_size))
|
|
elif data_len < writeable_size:
|
|
log.warning("Data length (%u) less than %s size (%u), leaving %u unwritten bytes at the end of the %s" %
|
|
(data_len, writeable_name, writeable_size, writeable_size - data_len, writeable_name))
|
|
|
|
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, but %s is %s" % (self.selected_file,
|
|
self.selected_file.__class__.__mro__))
|
|
self.__check_writeable_size(len(data_hex) // 2 + offset)
|
|
return self.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, self.selected_file_size())
|
|
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, but %s is %s" % (self.selected_file,
|
|
self.selected_file.__class__.__mro__))
|
|
# returns a string of hex nibbles
|
|
return self.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, but %s is %s" % (self.selected_file,
|
|
self.selected_file.__class__.__mro__))
|
|
self.__check_writeable_size(len(data_hex) // 2)
|
|
return self.scc.update_record(self.selected_file.fid, rec_nr, data_hex,
|
|
conserve=self.rs.conserve_write,
|
|
leftpad=self.selected_file.leftpad)
|
|
|
|
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, self.selected_file_record_len())
|
|
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.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, but %s is %s" % (self.selected_file,
|
|
self.selected_file.__class__.__mro__))
|
|
data, _sw = self.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, but %s is %s" % (self.selected_file,
|
|
self.selected_file.__class__.__mro__))
|
|
return self.scc.set_data(self.selected_file.fid, tag, data_hex, conserve=self.rs.conserve_write)
|
|
|
|
def register_cmds(self, cmd_app=None):
|
|
"""Register command set that is associated with the currently selected 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 unregister_cmds(self, cmd_app=None):
|
|
"""Unregister command set that is associated with the currently selected file"""
|
|
if cmd_app and self.selected_file.shell_commands:
|
|
for c in self.selected_file.shell_commands:
|
|
cmd_app.unregister_command_set(c)
|