filesystem: Introduce the basic notion of 'logical channels'

cards can have multiple logical channels; each logical channel
has its own state of what is the current selected file + application.

Let's split the RuntimeState class into the global RuntimeState and the
per-lchan-specific RuntimeLchan class.

This code doesn't actually introduce any code that uses lchans other
than the basic logical channel (0), but just modifies the data model
to accomodate those in the future.

Change-Id: I7aa994b625467d4e46a2edd8123240b930305360
This commit is contained in:
Harald Welte
2022-07-17 14:23:17 +02:00
parent de4c14c0dc
commit a6c0f880da
7 changed files with 181 additions and 118 deletions

View File

@@ -49,6 +49,18 @@ from pySim.commands import SimCardCommands
# tuple: logical-and of the listed services requires this file
CardFileService = Union[int, List[int], Tuple[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.
@@ -545,7 +557,7 @@ class TransparentEF(CardEF):
@cmd2.with_argparser(dec_hex_parser)
def do_decode_hex(self, opts):
"""Decode command-line provided hex-string as if it was read from the file."""
data = self._cmd.rs.selected_file.decode_hex(opts.HEXSTR)
data = self._cmd.lchan.selected_file.decode_hex(opts.HEXSTR)
self._cmd.poutput_json(data, opts.oneline)
read_bin_parser = argparse.ArgumentParser()
@@ -557,7 +569,7 @@ class TransparentEF(CardEF):
@cmd2.with_argparser(read_bin_parser)
def do_read_binary(self, opts):
"""Read binary data from a transparent EF"""
(data, sw) = self._cmd.rs.read_binary(opts.length, opts.offset)
(data, sw) = self._cmd.lchan.read_binary(opts.length, opts.offset)
self._cmd.poutput(data)
read_bin_dec_parser = argparse.ArgumentParser()
@@ -567,7 +579,7 @@ class TransparentEF(CardEF):
@cmd2.with_argparser(read_bin_dec_parser)
def do_read_binary_decoded(self, opts):
"""Read + decode data from a transparent EF"""
(data, sw) = self._cmd.rs.read_binary_dec()
(data, sw) = self._cmd.lchan.read_binary_dec()
self._cmd.poutput_json(data, opts.oneline)
upd_bin_parser = argparse.ArgumentParser()
@@ -579,7 +591,7 @@ class TransparentEF(CardEF):
@cmd2.with_argparser(upd_bin_parser)
def do_update_binary(self, opts):
"""Update (Write) data of a transparent EF"""
(data, sw) = self._cmd.rs.update_binary(opts.data, opts.offset)
(data, sw) = self._cmd.lchan.update_binary(opts.data, opts.offset)
if data:
self._cmd.poutput(data)
@@ -593,18 +605,18 @@ class TransparentEF(CardEF):
def do_update_binary_decoded(self, opts):
"""Encode + Update (Write) data of a transparent EF"""
if opts.json_path:
(data_json, sw) = self._cmd.rs.read_binary_dec()
(data_json, sw) = self._cmd.lchan.read_binary_dec()
js_path_modify(data_json, opts.json_path,
json.loads(opts.data))
else:
data_json = json.loads(opts.data)
(data, sw) = self._cmd.rs.update_binary_dec(data_json)
(data, sw) = self._cmd.lchan.update_binary_dec(data_json)
if data:
self._cmd.poutput_json(data)
def do_edit_binary_decoded(self, opts):
"""Edit the JSON representation of the EF contents in an editor."""
(orig_json, sw) = self._cmd.rs.read_binary_dec()
(orig_json, sw) = self._cmd.lchan.read_binary_dec()
with tempfile.TemporaryDirectory(prefix='pysim_') as dirname:
filename = '%s/file' % dirname
# write existing data as JSON to file
@@ -617,7 +629,7 @@ class TransparentEF(CardEF):
if edited_json == orig_json:
self._cmd.poutput("Data not modified, skipping write")
else:
(data, sw) = self._cmd.rs.update_binary_dec(edited_json)
(data, sw) = self._cmd.lchan.update_binary_dec(edited_json)
if data:
self._cmd.poutput_json(data)
@@ -768,7 +780,7 @@ class LinFixedEF(CardEF):
@cmd2.with_argparser(dec_hex_parser)
def do_decode_hex(self, opts):
"""Decode command-line provided hex-string as if it was read from the file."""
data = self._cmd.rs.selected_file.decode_record_hex(opts.HEXSTR)
data = self._cmd.lchan.selected_file.decode_record_hex(opts.HEXSTR)
self._cmd.poutput_json(data, opts.oneline)
read_rec_parser = argparse.ArgumentParser()
@@ -782,7 +794,7 @@ class LinFixedEF(CardEF):
"""Read one or multiple records from a record-oriented EF"""
for r in range(opts.count):
recnr = opts.record_nr + r
(data, sw) = self._cmd.rs.read_record(recnr)
(data, sw) = self._cmd.lchan.read_record(recnr)
if (len(data) > 0):
recstr = str(data)
else:
@@ -798,7 +810,7 @@ class LinFixedEF(CardEF):
@cmd2.with_argparser(read_rec_dec_parser)
def do_read_record_decoded(self, opts):
"""Read + decode a record from a record-oriented EF"""
(data, sw) = self._cmd.rs.read_record_dec(opts.record_nr)
(data, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
self._cmd.poutput_json(data, opts.oneline)
read_recs_parser = argparse.ArgumentParser()
@@ -806,9 +818,9 @@ class LinFixedEF(CardEF):
@cmd2.with_argparser(read_recs_parser)
def do_read_records(self, opts):
"""Read all records from a record-oriented EF"""
num_of_rec = self._cmd.rs.selected_file_num_of_rec()
num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
for recnr in range(1, 1 + num_of_rec):
(data, sw) = self._cmd.rs.read_record(recnr)
(data, sw) = self._cmd.lchan.read_record(recnr)
if (len(data) > 0):
recstr = str(data)
else:
@@ -822,11 +834,11 @@ class LinFixedEF(CardEF):
@cmd2.with_argparser(read_recs_dec_parser)
def do_read_records_decoded(self, opts):
"""Read + decode all records from a record-oriented EF"""
num_of_rec = self._cmd.rs.selected_file_num_of_rec()
num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
# collect all results in list so they are rendered as JSON list when printing
data_list = []
for recnr in range(1, 1 + num_of_rec):
(data, sw) = self._cmd.rs.read_record_dec(recnr)
(data, sw) = self._cmd.lchan.read_record_dec(recnr)
data_list.append(data)
self._cmd.poutput_json(data_list, opts.oneline)
@@ -839,7 +851,7 @@ class LinFixedEF(CardEF):
@cmd2.with_argparser(upd_rec_parser)
def do_update_record(self, opts):
"""Update (write) data to a record-oriented EF"""
(data, sw) = self._cmd.rs.update_record(opts.record_nr, opts.data)
(data, sw) = self._cmd.lchan.update_record(opts.record_nr, opts.data)
if data:
self._cmd.poutput(data)
@@ -855,12 +867,12 @@ class LinFixedEF(CardEF):
def do_update_record_decoded(self, opts):
"""Encode + Update (write) data to a record-oriented EF"""
if opts.json_path:
(data_json, sw) = self._cmd.rs.read_record_dec(opts.record_nr)
(data_json, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
js_path_modify(data_json, opts.json_path,
json.loads(opts.data))
else:
data_json = json.loads(opts.data)
(data, sw) = self._cmd.rs.update_record_dec(
(data, sw) = self._cmd.lchan.update_record_dec(
opts.record_nr, data_json)
if data:
self._cmd.poutput(data)
@@ -872,7 +884,7 @@ class LinFixedEF(CardEF):
@cmd2.with_argparser(edit_rec_dec_parser)
def do_edit_record_decoded(self, opts):
"""Edit the JSON representation of one record in an editor."""
(orig_json, sw) = self._cmd.rs.read_record_dec(opts.record_nr)
(orig_json, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
with tempfile.TemporaryDirectory(prefix='pysim_') as dirname:
filename = '%s/file' % dirname
# write existing data as JSON to file
@@ -885,7 +897,7 @@ class LinFixedEF(CardEF):
if edited_json == orig_json:
self._cmd.poutput("Data not modified, skipping write")
else:
(data, sw) = self._cmd.rs.update_record_dec(
(data, sw) = self._cmd.lchan.update_record_dec(
opts.record_nr, edited_json)
if data:
self._cmd.poutput_json(data)
@@ -1192,12 +1204,12 @@ class BerTlvEF(CardEF):
@cmd2.with_argparser(retrieve_data_parser)
def do_retrieve_data(self, opts):
"""Retrieve (Read) data from a BER-TLV EF"""
(data, sw) = self._cmd.rs.retrieve_data(opts.tag)
(data, sw) = self._cmd.lchan.retrieve_data(opts.tag)
self._cmd.poutput(data)
def do_retrieve_tags(self, opts):
"""List tags available in a given BER-TLV EF"""
tags = self._cmd.rs.retrieve_tags()
tags = self._cmd.lchan.retrieve_tags()
self._cmd.poutput(tags)
set_data_parser = argparse.ArgumentParser()
@@ -1209,7 +1221,7 @@ class BerTlvEF(CardEF):
@cmd2.with_argparser(set_data_parser)
def do_set_data(self, opts):
"""Set (Write) data for a given tag in a BER-TLV EF"""
(data, sw) = self._cmd.rs.set_data(opts.tag, opts.data)
(data, sw) = self._cmd.lchan.set_data(opts.tag, opts.data)
if data:
self._cmd.poutput(data)
@@ -1220,7 +1232,7 @@ class BerTlvEF(CardEF):
@cmd2.with_argparser(del_data_parser)
def do_delete_data(self, opts):
"""Delete data for a given tag in a BER-TLV EF"""
(data, sw) = self._cmd.rs.set_data(opts.tag, None)
(data, sw) = self._cmd.lchan.set_data(opts.tag, None)
if data:
self._cmd.poutput(data)
@@ -1252,11 +1264,10 @@ class RuntimeState:
"""
self.mf = CardMF(profile=profile)
self.card = card
self.selected_file = self.mf # type: CardDF
self.selected_adf = None
self.profile = profile
self.selected_file_fcp = None
self.selected_file_fcp_hex = None
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
@@ -1315,6 +1326,66 @@ class RuntimeState:
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']
@@ -1330,17 +1401,6 @@ class RuntimeState:
def selected_file_num_of_rec(self) -> Optional[int]:
return self.selected_file_fcp['file_descriptor'].get('num_of_rec')
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)
"""
atr = i2h(self.card.reset())
# select MF to reset internal state and to verify card really works
self.select('MF', cmd_app)
self.selected_adf = None
return atr
def get_cwd(self) -> CardDF:
"""Obtain the current working directory.
@@ -1384,7 +1444,7 @@ class RuntimeState:
# card profile.
if app and hasattr(app, "interpret_sw"):
res = app.interpret_sw(sw)
return res or self.profile.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
@@ -1394,7 +1454,7 @@ class RuntimeState:
"Cannot select unknown file by name %s, only hexadecimal 4 digit FID is allowed" % fid)
try:
(data, sw) = self.card._scc.select_file(fid)
(data, sw) = self.rs.card._scc.select_file(fid)
except SwMatchError as swm:
k = self.interpret_sw(swm.sw_actual)
if not k:
@@ -1446,10 +1506,10 @@ class RuntimeState:
for p in inter_path:
try:
if isinstance(p, CardADF):
(data, sw) = self.card.select_adf_by_aid(p.aid)
(data, sw) = self.rs.card.select_adf_by_aid(p.aid)
self.selected_adf = p
else:
(data, sw) = self.card._scc.select_file(p.fid)
(data, sw) = self.rs.card._scc.select_file(p.fid)
self.selected_file = p
except SwMatchError as swm:
self._select_post(cmd_app)
@@ -1490,9 +1550,9 @@ class RuntimeState:
f = sels[name]
try:
if isinstance(f, CardADF):
(data, sw) = self.card.select_adf_by_aid(f.aid)
(data, sw) = self.rs.card.select_adf_by_aid(f.aid)
else:
(data, sw) = self.card._scc.select_file(f.fid)
(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)
@@ -1512,7 +1572,7 @@ class RuntimeState:
def status(self):
"""Request STATUS (current selected file FCP) from card."""
(data, sw) = self.card._scc.status()
(data, sw) = self.rs.card._scc.status()
return self.selected_file.decode_select_response(data)
def get_file_for_selectable(self, name: str):
@@ -1523,7 +1583,7 @@ class RuntimeState:
"""Request ACTIVATE FILE of specified file."""
sels = self.selected_file.get_selectables()
f = sels[name]
data, sw = self.card._scc.activate_file(f.fid)
data, sw = self.rs.card._scc.activate_file(f.fid)
return data, sw
def read_binary(self, length: int = None, offset: int = 0):
@@ -1537,7 +1597,7 @@ class RuntimeState:
"""
if not isinstance(self.selected_file, TransparentEF):
raise TypeError("Only works with TransparentEF")
return self.card._scc.read_binary(self.selected_file.fid, length, offset)
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.
@@ -1561,7 +1621,7 @@ class RuntimeState:
"""
if not isinstance(self.selected_file, TransparentEF):
raise TypeError("Only works with TransparentEF")
return self.card._scc.update_binary(self.selected_file.fid, data_hex, offset, conserve=self.conserve_write)
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
@@ -1584,7 +1644,7 @@ class RuntimeState:
if not isinstance(self.selected_file, LinFixedEF):
raise TypeError("Only works with Linear Fixed EF")
# returns a string of hex nibbles
return self.card._scc.read_record(self.selected_file.fid, rec_nr)
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.
@@ -1606,7 +1666,7 @@ class RuntimeState:
"""
if not isinstance(self.selected_file, LinFixedEF):
raise TypeError("Only works with Linear Fixed EF")
return self.card._scc.update_record(self.selected_file.fid, rec_nr, data_hex, conserve=self.conserve_write)
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
@@ -1630,7 +1690,7 @@ class RuntimeState:
if not isinstance(self.selected_file, BerTlvEF):
raise TypeError("Only works with BER-TLV EF")
# returns a string of hex nibbles
return self.card._scc.retrieve_data(self.selected_file.fid, tag)
return self.rs.card._scc.retrieve_data(self.selected_file.fid, tag)
def retrieve_tags(self):
"""Retrieve tags available on BER-TLV EF.
@@ -1640,7 +1700,7 @@ class RuntimeState:
"""
if not isinstance(self.selected_file, BerTlvEF):
raise TypeError("Only works with BER-TLV EF")
data, sw = self.card._scc.retrieve_data(self.selected_file.fid, 0x5c)
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)
@@ -1653,7 +1713,7 @@ class RuntimeState:
"""
if not isinstance(self.selected_file, BerTlvEF):
raise TypeError("Only works with BER-TLV EF")
return self.card._scc.set_data(self.selected_file.fid, tag, data_hex, conserve=self.conserve_write)
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."""