Add more documentation to the classes/methods

* add type annotations in-line with PEP484
* convert existing documentation to follow the
  "Google Python Style Guide" format understood by
  the sphinx.ext.napoleon' extension
* add much more documentation all over the code base

Change-Id: I6ac88e0662cf3c56ae32d86d50b18a8b4150571a
This commit is contained in:
Harald Welte
2021-04-02 13:00:18 +02:00
parent 082d4e0956
commit ee3501fc62
9 changed files with 635 additions and 185 deletions

View File

@@ -65,7 +65,7 @@ class SimCardCommands(object):
# from what SIMs responds. See also: # from what SIMs responds. See also:
# USIM: ETSI TS 102 221, chapter 11.1.1.3 Response Data # USIM: ETSI TS 102 221, chapter 11.1.1.3 Response Data
# SIM: GSM 11.11, chapter 9.2.1 SELECT # SIM: GSM 11.11, chapter 9.2.1 SELECT
def __record_len(self, r): def __record_len(self, r) -> int:
if self.sel_ctrl == "0004": if self.sel_ctrl == "0004":
tlv_parsed = self.__parse_fcp(r[-1]) tlv_parsed = self.__parse_fcp(r[-1])
file_descriptor = tlv_parsed['82'] file_descriptor = tlv_parsed['82']
@@ -76,14 +76,15 @@ class SimCardCommands(object):
# Tell the length of a binary file. See also comment # Tell the length of a binary file. See also comment
# above. # above.
def __len(self, r): def __len(self, r) -> int:
if self.sel_ctrl == "0004": if self.sel_ctrl == "0004":
tlv_parsed = self.__parse_fcp(r[-1]) tlv_parsed = self.__parse_fcp(r[-1])
return int(tlv_parsed['80'], 16) return int(tlv_parsed['80'], 16)
else: else:
return int(r[-1][4:8], 16) return int(r[-1][4:8], 16)
def get_atr(self): def get_atr(self) -> str:
"""Return the ATR of the currently inserted card."""
return self._tp.get_atr() return self._tp.get_atr()
@property @property
@@ -101,6 +102,7 @@ class SimCardCommands(object):
self._sel_ctrl = value self._sel_ctrl = value
def try_select_path(self, dir_list): def try_select_path(self, dir_list):
""" Try to select a specified path given as list of hex-string FIDs"""
rv = [] rv = []
if type(dir_list) is not list: if type(dir_list) is not list:
dir_list = [dir_list] dir_list = [dir_list]
@@ -112,6 +114,14 @@ class SimCardCommands(object):
return rv return rv
def select_path(self, dir_list): def select_path(self, dir_list):
"""Execute SELECT for an entire list/path of FIDs.
Args:
dir_list: list of FIDs representing the path to select
Returns:
list of return values (FCP in hex encoding) for each element of the path
"""
rv = [] rv = []
if type(dir_list) is not list: if type(dir_list) is not list:
dir_list = [dir_list] dir_list = [dir_list]
@@ -120,14 +130,23 @@ class SimCardCommands(object):
rv.append(data) rv.append(data)
return rv return rv
def select_file(self, fid): def select_file(self, fid:str):
"""Execute SELECT a given file by FID."""
return self._tp.send_apdu_checksw(self.cla_byte + "a4" + self.sel_ctrl + "02" + fid) return self._tp.send_apdu_checksw(self.cla_byte + "a4" + self.sel_ctrl + "02" + fid)
def select_adf(self, aid): def select_adf(self, aid:str):
"""Execute SELECT a given Applicaiton ADF."""
aidlen = ("0" + format(len(aid) // 2, 'x'))[-2:] aidlen = ("0" + format(len(aid) // 2, 'x'))[-2:]
return self._tp.send_apdu_checksw(self.cla_byte + "a4" + "0404" + aidlen + aid) return self._tp.send_apdu_checksw(self.cla_byte + "a4" + "0404" + aidlen + aid)
def read_binary(self, ef, length=None, offset=0): def read_binary(self, ef, length:int=None, offset:int=0):
"""Execute READD BINARY.
Args:
ef : string or list of strings indicating name or path of transparent EF
length : number of bytes to read
offset : byte offset in file from which to start reading
"""
r = self.select_path(ef) r = self.select_path(ef)
if len(r[-1]) == 0: if len(r[-1]) == 0:
return (None, None) return (None, None)
@@ -145,7 +164,15 @@ class SimCardCommands(object):
raise ValueError('Failed to read (offset %d)' % (offset)) raise ValueError('Failed to read (offset %d)' % (offset))
return total_data, sw return total_data, sw
def update_binary(self, ef, data, offset=0, verify=False, conserve=False): def update_binary(self, ef, data:str, offset:int=0, verify:bool=False, conserve:bool=False):
"""Execute UPDATE BINARY.
Args:
ef : string or list of strings indicating name or path of transparent EF
data : hex string of data to be written
offset : byte offset in file from which to start writing
verify : Whether or not to verify data after write
"""
data_length = len(data) // 2 data_length = len(data) // 2
# Save write cycles by reading+comparing before write # Save write cycles by reading+comparing before write
@@ -161,18 +188,32 @@ class SimCardCommands(object):
self.verify_binary(ef, data, offset) self.verify_binary(ef, data, offset)
return res return res
def verify_binary(self, ef, data, offset=0): def verify_binary(self, ef, data:str, offset:int=0):
"""Verify contents of transparent EF.
Args:
ef : string or list of strings indicating name or path of transparent EF
data : hex string of expected data
offset : byte offset in file from which to start verifying
"""
res = self.read_binary(ef, len(data) // 2, offset) res = self.read_binary(ef, len(data) // 2, offset)
if res[0].lower() != data.lower(): if res[0].lower() != data.lower():
raise ValueError('Binary verification failed (expected %s, got %s)' % (data.lower(), res[0].lower())) raise ValueError('Binary verification failed (expected %s, got %s)' % (data.lower(), res[0].lower()))
def read_record(self, ef, rec_no): def read_record(self, ef, rec_no:int):
"""Execute READ RECORD.
Args:
ef : string or list of strings indicating name or path of linear fixed EF
rec_no : record number to read
"""
r = self.select_path(ef) r = self.select_path(ef)
rec_length = self.__record_len(r) rec_length = self.__record_len(r)
pdu = self.cla_byte + 'b2%02x04%02x' % (rec_no, rec_length) pdu = self.cla_byte + 'b2%02x04%02x' % (rec_no, rec_length)
return self._tp.send_apdu(pdu) return self._tp.send_apdu(pdu)
def update_record(self, ef, rec_no, data, force_len=False, verify=False, conserve=False): def update_record(self, ef, rec_no:int, data:str, force_len:bool=False, verify:bool=False,
conserve:bool=False):
r = self.select_path(ef) r = self.select_path(ef)
if not force_len: if not force_len:
rec_length = self.__record_len(r) rec_length = self.__record_len(r)
@@ -194,30 +235,47 @@ class SimCardCommands(object):
self.verify_record(ef, rec_no, data) self.verify_record(ef, rec_no, data)
return res return res
def verify_record(self, ef, rec_no, data): def verify_record(self, ef, rec_no:int, data:str):
res = self.read_record(ef, rec_no) res = self.read_record(ef, rec_no)
if res[0].lower() != data.lower(): if res[0].lower() != data.lower():
raise ValueError('Record verification failed (expected %s, got %s)' % (data.lower(), res[0].lower())) raise ValueError('Record verification failed (expected %s, got %s)' % (data.lower(), res[0].lower()))
def record_size(self, ef): def record_size(self, ef):
"""Determine the record size of given file.
Args:
ef : string or list of strings indicating name or path of linear fixed EF
"""
r = self.select_path(ef) r = self.select_path(ef)
return self.__record_len(r) return self.__record_len(r)
def record_count(self, ef): def record_count(self, ef):
"""Determine the number of records in given file.
Args:
ef : string or list of strings indicating name or path of linear fixed EF
"""
r = self.select_path(ef) r = self.select_path(ef)
return self.__len(r) // self.__record_len(r) return self.__len(r) // self.__record_len(r)
def binary_size(self, ef): def binary_size(self, ef):
"""Determine the size of given transparent file.
Args:
ef : string or list of strings indicating name or path of transparent EF
"""
r = self.select_path(ef) r = self.select_path(ef)
return self.__len(r) return self.__len(r)
def run_gsm(self, rand): def run_gsm(self, rand:str):
"""Execute RUN GSM ALGORITHM."""
if len(rand) != 32: if len(rand) != 32:
raise ValueError('Invalid rand') raise ValueError('Invalid rand')
self.select_path(['3f00', '7f20']) self.select_path(['3f00', '7f20'])
return self._tp.send_apdu(self.cla_byte + '88000010' + rand) return self._tp.send_apdu(self.cla_byte + '88000010' + rand)
def reset_card(self): def reset_card(self):
"""Physically reset the card"""
return self._tp.reset_card() return self._tp.reset_card()
def _chv_process_sw(self, op_name, chv_no, pin_code, sw): def _chv_process_sw(self, op_name, chv_no, pin_code, sw):
@@ -227,31 +285,36 @@ class SimCardCommands(object):
elif (sw != '9000'): elif (sw != '9000'):
raise SwMatchError(sw, '9000') raise SwMatchError(sw, '9000')
def verify_chv(self, chv_no, pin_code): def verify_chv(self, chv_no:int, code:str):
fc = rpad(b2h(pin_code), 16) """Verify a given CHV (Card Holder Verification == PIN)"""
fc = rpad(b2h(code), 16)
data, sw = self._tp.send_apdu(self.cla_byte + '2000' + ('%02X' % chv_no) + '08' + fc) data, sw = self._tp.send_apdu(self.cla_byte + '2000' + ('%02X' % chv_no) + '08' + fc)
self._chv_process_sw('verify', chv_no, pin_code, sw) self._chv_process_sw('verify', chv_no, code, sw)
return (data, sw) return (data, sw)
def unblock_chv(self, chv_no, puk_code, pin_code): def unblock_chv(self, chv_no:int, puk_code:str, pin_code:str):
"""Unblock a given CHV (Card Holder Verification == PIN)"""
fc = rpad(b2h(puk_code), 16) + rpad(b2h(pin_code), 16) fc = rpad(b2h(puk_code), 16) + rpad(b2h(pin_code), 16)
data, sw = self._tp.send_apdu(self.cla_byte + '2C00' + ('%02X' % chv_no) + '10' + fc) data, sw = self._tp.send_apdu(self.cla_byte + '2C00' + ('%02X' % chv_no) + '10' + fc)
self._chv_process_sw('unblock', chv_no, pin_code, sw) self._chv_process_sw('unblock', chv_no, pin_code, sw)
return (data, sw) return (data, sw)
def change_chv(self, chv_no, pin_code, new_pin_code): def change_chv(self, chv_no:int, pin_code:str, new_pin_code:str):
"""Change a given CHV (Card Holder Verification == PIN)"""
fc = rpad(b2h(pin_code), 16) + rpad(b2h(new_pin_code), 16) fc = rpad(b2h(pin_code), 16) + rpad(b2h(new_pin_code), 16)
data, sw = self._tp.send_apdu(self.cla_byte + '2400' + ('%02X' % chv_no) + '10' + fc) data, sw = self._tp.send_apdu(self.cla_byte + '2400' + ('%02X' % chv_no) + '10' + fc)
self._chv_process_sw('change', chv_no, pin_code, sw) self._chv_process_sw('change', chv_no, pin_code, sw)
return (data, sw) return (data, sw)
def disable_chv(self, chv_no, pin_code): def disable_chv(self, chv_no:int, pin_code:str):
"""Disable a given CHV (Card Holder Verification == PIN)"""
fc = rpad(b2h(pin_code), 16) fc = rpad(b2h(pin_code), 16)
data, sw = self._tp.send_apdu(self.cla_byte + '2600' + ('%02X' % chv_no) + '08' + fc) data, sw = self._tp.send_apdu(self.cla_byte + '2600' + ('%02X' % chv_no) + '08' + fc)
self._chv_process_sw('disable', chv_no, pin_code, sw) self._chv_process_sw('disable', chv_no, pin_code, sw)
return (data, sw) return (data, sw)
def enable_chv(self, chv_no, pin_code): def enable_chv(self, chv_no:int, pin_code:str):
"""Enable a given CHV (Card Holder Verification == PIN)"""
fc = rpad(b2h(pin_code), 16) fc = rpad(b2h(pin_code), 16)
data, sw = self._tp.send_apdu(self.cla_byte + '2800' + ('%02X' % chv_no) + '08' + fc) data, sw = self._tp.send_apdu(self.cla_byte + '2800' + ('%02X' % chv_no) + '08' + fc)
self._chv_process_sw('enable', chv_no, pin_code, sw) self._chv_process_sw('enable', chv_no, pin_code, sw)

View File

@@ -22,18 +22,27 @@
# #
class NoCardError(Exception): class NoCardError(Exception):
"""No card was found in the reader."""
pass pass
class ProtocolError(Exception): class ProtocolError(Exception):
"""Some kind of protocol level error interfacing with the card."""
pass pass
class ReaderError(Exception): class ReaderError(Exception):
"""Some kind of general error with the card reader."""
pass pass
class SwMatchError(Exception): class SwMatchError(Exception):
"""Raised when an operation specifies an expected SW but the actual SW from """Raised when an operation specifies an expected SW but the actual SW from
the card doesn't match.""" the card doesn't match."""
def __init__(self, sw_actual, sw_expected, rs=None): def __init__(self, sw_actual:str, sw_expected:str, rs=None):
"""
Args:
sw_actual : the SW we actually received from the card (4 hex digits)
sw_expected : the SW we expected to receive from the card (4 hex digits)
rs : interpreter class to convert SW to string
"""
self.sw_actual = sw_actual self.sw_actual = sw_actual
self.sw_expected = sw_expected self.sw_expected = sw_expected
self.rs = rs self.rs = rs

View File

@@ -31,6 +31,8 @@ import cmd2
from cmd2 import CommandSet, with_default_category, with_argparser from cmd2 import CommandSet, with_default_category, with_argparser
import argparse import argparse
from typing import Optional, Iterable, List, Any, Tuple
from pySim.utils import sw_match, h2b, b2h, is_hex from pySim.utils import sw_match, h2b, b2h, is_hex
from pySim.exceptions import * from pySim.exceptions import *
@@ -41,7 +43,16 @@ class CardFile(object):
RESERVED_NAMES = ['..', '.', '/', 'MF'] RESERVED_NAMES = ['..', '.', '/', 'MF']
RESERVED_FIDS = ['3f00'] RESERVED_FIDS = ['3f00']
def __init__(self, fid=None, sfid=None, name=None, desc=None, parent=None): def __init__(self, fid:str=None, sfid:str=None, name:str=None, desc:str=None,
parent:Optional['CardDF']=None):
"""
Args:
fid : File Identifier (4 hex digits)
sfid : Short File Identifier (2 hex digits, optional)
name : Brief name of the file, lik EF_ICCID
desc : Descriptoin of the file
parent : Parent CardFile object within filesystem hierarchy
"""
if not isinstance(self, CardADF) and fid == None: if not isinstance(self, CardADF) and fid == None:
raise ValueError("fid is mandatory") raise ValueError("fid is mandatory")
if fid: if fid:
@@ -53,7 +64,7 @@ class CardFile(object):
self.parent = parent self.parent = parent
if self.parent and self.parent != self and self.fid: if self.parent and self.parent != self and self.fid:
self.parent.add_file(self) self.parent.add_file(self)
self.shell_commands = [] self.shell_commands: List[CommandSet] = []
# Note: the basic properties (fid, name, ect.) are verified when # Note: the basic properties (fid, name, ect.) are verified when
# the file is attached to a parent file. See method add_file() in # the file is attached to a parent file. See method add_file() in
@@ -65,14 +76,18 @@ class CardFile(object):
else: else:
return self.fid return self.fid
def _path_element(self, prefer_name): def _path_element(self, prefer_name:bool) -> Optional[str]:
if prefer_name and self.name: if prefer_name and self.name:
return self.name return self.name
else: else:
return self.fid return self.fid
def fully_qualified_path(self, prefer_name=True): def fully_qualified_path(self, prefer_name:bool=True):
"""Return fully qualified path to file as list of FID or name strings.""" """Return fully qualified path to file as list of FID or name strings.
Args:
prefer_name : Preferably build path of names; fall-back to FIDs as required
"""
if self.parent != self: if self.parent != self:
ret = self.parent.fully_qualified_path(prefer_name) ret = self.parent.fully_qualified_path(prefer_name)
else: else:
@@ -80,7 +95,7 @@ class CardFile(object):
ret.append(self._path_element(prefer_name)) ret.append(self._path_element(prefer_name))
return ret return ret
def get_mf(self): def get_mf(self) -> Optional['CardMF']:
"""Return the MF (root) of the file system.""" """Return the MF (root) of the file system."""
if self.parent == None: if self.parent == None:
return None return None
@@ -90,8 +105,16 @@ class CardFile(object):
node = node.parent node = node.parent
return node return node
def _get_self_selectables(self, alias=None, flags = []): def _get_self_selectables(self, alias:str=None, flags = []) -> dict:
"""Return a dict of {'identifier': self} tuples""" """Return a dict of {'identifier': self} tuples.
Args:
alias : Add an alias with given name to 'self'
flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
If not specified, all selectables will be returned.
Returns:
dict containing reference to 'self' for all identifiers.
"""
sels = {} sels = {}
if alias: if alias:
sels.update({alias: self}) sels.update({alias: self})
@@ -101,8 +124,16 @@ class CardFile(object):
sels.update({self.name: self}) sels.update({self.name: self})
return sels return sels
def get_selectables(self, flags = []): def get_selectables(self, flags = []) -> dict:
"""Return a dict of {'identifier': File} that is selectable from the current file.""" """Return a dict of {'identifier': File} that is selectable from the current file.
Args:
flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
If not specified, all selectables will be returned.
Returns:
dict containing all selectable items. Key is identifier (string), value
a reference to a CardFile (or derived class) instance.
"""
sels = {} sels = {}
# we can always select ourself # we can always select ourself
if flags == [] or 'SELF' in flags: if flags == [] or 'SELF' in flags:
@@ -118,12 +149,20 @@ class CardFile(object):
sels.update(mf.get_app_selectables(flags = flags)) sels.update(mf.get_app_selectables(flags = flags))
return sels return sels
def get_selectable_names(self, flags = []): def get_selectable_names(self, flags = []) -> dict:
"""Return a list of strings for all identifiers that are selectable from the current file.""" """Return a dict of {'identifier': File} that is selectable from the current file.
Args:
flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
If not specified, all selectables will be returned.
Returns:
dict containing all selectable items. Key is identifier (string), value
a reference to a CardFile (or derived class) instance.
"""
sels = self.get_selectables(flags) sels = self.get_selectables(flags)
return sels.keys() return sels.keys()
def decode_select_response(self, data_hex): def decode_select_response(self, data_hex:str):
"""Decode the response to a SELECT command.""" """Decode the response to a SELECT command."""
return self.parent.decode_select_response(data_hex) return self.parent.decode_select_response(data_hex)
@@ -147,8 +186,12 @@ class CardDF(CardFile):
def __str__(self): def __str__(self):
return "DF(%s)" % (super().__str__()) return "DF(%s)" % (super().__str__())
def add_file(self, child, ignore_existing=False): def add_file(self, child:CardFile, ignore_existing:bool=False):
"""Add a child (DF/EF) to this DF""" """Add a child (DF/EF) to this DF.
Args:
child: The new DF/EF to be added
ignore_existing: Ignore, if file with given FID already exists. Old one will be kept.
"""
if not isinstance(child, CardFile): if not isinstance(child, CardFile):
raise TypeError("Expected a File instance") raise TypeError("Expected a File instance")
if not is_hex(child.fid, minlen = 4, maxlen = 4): if not is_hex(child.fid, minlen = 4, maxlen = 4):
@@ -170,13 +213,26 @@ class CardDF(CardFile):
self.children[child.fid] = child self.children[child.fid] = child
child.parent = self child.parent = self
def add_files(self, children, ignore_existing=False): def add_files(self, children:Iterable[CardFile], ignore_existing:bool=False):
"""Add a list of child (DF/EF) to this DF""" """Add a list of child (DF/EF) to this DF
Args:
children: List of new DF/EFs to be added
ignore_existing: Ignore, if file[s] with given FID already exists. Old one[s] will be kept.
"""
for child in children: for child in children:
self.add_file(child, ignore_existing) self.add_file(child, ignore_existing)
def get_selectables(self, flags = []): def get_selectables(self, flags = []) -> dict:
"""Get selectable (DF/EF names) from current DF""" """Return a dict of {'identifier': File} that is selectable from the current DF.
Args:
flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
If not specified, all selectables will be returned.
Returns:
dict containing all selectable items. Key is identifier (string), value
a reference to a CardFile (or derived class) instance.
"""
# global selectables + our children # global selectables + our children
sels = super().get_selectables(flags) sels = super().get_selectables(flags)
if flags == [] or 'FIDS' in flags: if flags == [] or 'FIDS' in flags:
@@ -185,7 +241,8 @@ class CardDF(CardFile):
sels.update({x.name: x for x in self.children.values() if x.name}) sels.update({x.name: x for x in self.children.values() if x.name})
return sels return sels
def lookup_file_by_name(self, name): def lookup_file_by_name(self, name:str) -> Optional[CardFile]:
"""Find a file with given name within current DF."""
if name == None: if name == None:
return None return None
for i in self.children.values(): for i in self.children.values():
@@ -193,7 +250,8 @@ class CardDF(CardFile):
return i return i
return None return None
def lookup_file_by_sfid(self, sfid): def lookup_file_by_sfid(self, sfid:str) -> Optional[CardFile]:
"""Find a file with given short file ID within current DF."""
if sfid == None: if sfid == None:
return None return None
for i in self.children.values(): for i in self.children.values():
@@ -201,7 +259,8 @@ class CardDF(CardFile):
return i return i
return None return None
def lookup_file_by_fid(self, fid): def lookup_file_by_fid(self, fid:str) -> Optional[CardFile]:
"""Find a file with given file ID within current DF."""
if fid in self.children: if fid in self.children:
return self.children[fid] return self.children[fid]
return None return None
@@ -222,7 +281,7 @@ class CardMF(CardDF):
def __str__(self): def __str__(self):
return "MF(%s)" % (self.fid) return "MF(%s)" % (self.fid)
def add_application(self, app): def add_application(self, app:'CardADF'):
"""Add an ADF (Application Dedicated File) to the MF""" """Add an ADF (Application Dedicated File) to the MF"""
if not isinstance(app, CardADF): if not isinstance(app, CardADF):
raise TypeError("Expected an ADF instance") raise TypeError("Expected an ADF instance")
@@ -235,13 +294,21 @@ class CardMF(CardDF):
"""Get list of completions (AID names)""" """Get list of completions (AID names)"""
return [x.name for x in self.applications] return [x.name for x in self.applications]
def get_selectables(self, flags = []): def get_selectables(self, flags = []) -> dict:
"""Get list of completions (DF/EF/ADF names) from current DF""" """Return a dict of {'identifier': File} that is selectable from the current DF.
Args:
flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
If not specified, all selectables will be returned.
Returns:
dict containing all selectable items. Key is identifier (string), value
a reference to a CardFile (or derived class) instance.
"""
sels = super().get_selectables(flags) sels = super().get_selectables(flags)
sels.update(self.get_app_selectables(flags)) sels.update(self.get_app_selectables(flags))
return sels return sels
def get_app_selectables(self, flags = []): def get_app_selectables(self, flags = []) -> dict:
"""Get applications by AID + name""" """Get applications by AID + name"""
sels = {} sels = {}
if flags == [] or 'AIDS' in flags: if flags == [] or 'AIDS' in flags:
@@ -250,15 +317,19 @@ class CardMF(CardDF):
sels.update({x.name: x for x in self.applications.values() if x.name}) sels.update({x.name: x for x in self.applications.values() if x.name})
return sels return sels
def decode_select_response(self, data_hex): def decode_select_response(self, data_hex:str) -> Any:
"""Decode the response to a SELECT command.""" """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.
"""
return data_hex return data_hex
class CardADF(CardDF): class CardADF(CardDF):
"""ADF (Application Dedicated File) in the smart card filesystem""" """ADF (Application Dedicated File) in the smart card filesystem"""
def __init__(self, aid, **kwargs): def __init__(self, aid:str, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.aid = aid # Application Identifier self.aid = aid # Application Identifier
if self.parent: if self.parent:
@@ -267,7 +338,7 @@ class CardADF(CardDF):
def __str__(self): def __str__(self):
return "ADF(%s)" % (self.aid) return "ADF(%s)" % (self.aid)
def _path_element(self, prefer_name): def _path_element(self, prefer_name:bool):
if self.name and prefer_name: if self.name and prefer_name:
return self.name return self.name
else: else:
@@ -283,8 +354,16 @@ class CardEF(CardFile):
def __str__(self): def __str__(self):
return "EF(%s)" % (super().__str__()) return "EF(%s)" % (super().__str__())
def get_selectables(self, flags = []): def get_selectables(self, flags = []) -> dict:
"""Get list of completions (EF names) from current DF""" """Return a dict of {'identifier': File} that is selectable from the current DF.
Args:
flags : Specify which selectables to return 'FIDS' and/or 'NAMES';
If not specified, all selectables will be returned.
Returns:
dict containing all selectable items. Key is identifier (string), value
a reference to a CardFile (or derived class) instance.
"""
#global selectable names + those of the parent DF #global selectable names + those of the parent DF
sels = super().get_selectables(flags) sels = super().get_selectables(flags)
sels.update({x.name:x for x in self.parent.children.values() if x != self}) sels.update({x.name:x for x in self.parent.children.values() if x != self})
@@ -292,10 +371,14 @@ class CardEF(CardFile):
class TransparentEF(CardEF): class TransparentEF(CardEF):
"""Transparent EF (Entry File) in the smart card filesystem""" """Transparent EF (Entry File) in the smart card filesystem.
A Transparent EF is a binary file with no formal structure. This is contrary to
Record based EFs which have [fixed size] records that can be individually read/updated."""
@with_default_category('Transparent EF Commands') @with_default_category('Transparent EF Commands')
class ShellCommands(CommandSet): class ShellCommands(CommandSet):
"""Shell commands specific for Trransparent EFs."""
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@@ -333,13 +416,33 @@ class TransparentEF(CardEF):
if data: if data:
self._cmd.poutput(json.dumps(data, indent=4)) self._cmd.poutput(json.dumps(data, indent=4))
def __init__(self, fid, sfid=None, name=None, desc=None, parent=None, size={1,None}): def __init__(self, fid:str, sfid:str=None, name:str=None, desc:str=None, parent:CardDF=None,
size={1,None}):
"""
Args:
fid : File Identifier (4 hex digits)
sfid : Short File Identifier (2 hex digits, optional)
name : Brief name of the file, lik EF_ICCID
desc : Descriptoin of the file
parent : Parent CardFile object within filesystem hierarchy
size : tuple of (minimum_size, recommended_size)
"""
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent) super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent)
self.size = size self.size = size
self.shell_commands = [self.ShellCommands()] self.shell_commands = [self.ShellCommands()]
def decode_bin(self, raw_bin_data): def decode_bin(self, raw_bin_data:bytearray) -> dict:
"""Decode raw (binary) data into abstract representation. Overloaded by specific classes.""" """Decode raw (binary) data into abstract representation.
A derived class would typically provide a _decode_bin() or _decode_hex() method
for implementing this specifically for the given file. This function checks which
of the method exists, add calls them (with conversion, as needed).
Args:
raw_bin_data : binary encoded data
Returns:
abstract_data; dict representing the decoded data
"""
method = getattr(self, '_decode_bin', None) method = getattr(self, '_decode_bin', None)
if callable(method): if callable(method):
return method(raw_bin_data) return method(raw_bin_data)
@@ -348,8 +451,18 @@ class TransparentEF(CardEF):
return method(b2h(raw_bin_data)) return method(b2h(raw_bin_data))
return {'raw': raw_bin_data.hex()} return {'raw': raw_bin_data.hex()}
def decode_hex(self, raw_hex_data): def decode_hex(self, raw_hex_data:str) -> dict:
"""Decode raw (hex string) data into abstract representation. Overloaded by specific classes.""" """Decode raw (hex string) data into abstract representation.
A derived class would typically provide a _decode_bin() or _decode_hex() method
for implementing this specifically for the given file. This function checks which
of the method exists, add calls them (with conversion, as needed).
Args:
raw_hex_data : hex-encoded data
Returns:
abstract_data; dict representing the decoded data
"""
method = getattr(self, '_decode_hex', None) method = getattr(self, '_decode_hex', None)
if callable(method): if callable(method):
return method(raw_hex_data) return method(raw_hex_data)
@@ -359,8 +472,18 @@ class TransparentEF(CardEF):
return method(raw_bin_data) return method(raw_bin_data)
return {'raw': raw_bin_data.hex()} return {'raw': raw_bin_data.hex()}
def encode_bin(self, abstract_data): def encode_bin(self, abstract_data:dict) -> bytearray:
"""Encode abstract representation into raw (binary) data. Overloaded by specific classes.""" """Encode abstract representation into raw (binary) data.
A derived class would typically provide an _encode_bin() or _encode_hex() method
for implementing this specifically for the given file. This function checks which
of the method exists, add calls them (with conversion, as needed).
Args:
abstract_data : dict representing the decoded data
Returns:
binary encoded data
"""
method = getattr(self, '_encode_bin', None) method = getattr(self, '_encode_bin', None)
if callable(method): if callable(method):
return method(abstract_data) return method(abstract_data)
@@ -369,8 +492,18 @@ class TransparentEF(CardEF):
return h2b(method(abstract_data)) return h2b(method(abstract_data))
raise NotImplementedError raise NotImplementedError
def encode_hex(self, abstract_data): def encode_hex(self, abstract_data:dict) -> str:
"""Encode abstract representation into raw (hex string) data. Overloaded by specific classes.""" """Encode abstract representation into raw (hex string) data.
A derived class would typically provide an _encode_bin() or _encode_hex() method
for implementing this specifically for the given file. This function checks which
of the method exists, add calls them (with conversion, as needed).
Args:
abstract_data : dict representing the decoded data
Returns:
hex string encoded data
"""
method = getattr(self, '_encode_hex', None) method = getattr(self, '_encode_hex', None)
if callable(method): if callable(method):
return method(abstract_data) return method(abstract_data)
@@ -382,10 +515,14 @@ class TransparentEF(CardEF):
class LinFixedEF(CardEF): class LinFixedEF(CardEF):
"""Linear Fixed EF (Entry File) in the smart card filesystem""" """Linear Fixed EF (Entry File) in the smart card filesystem.
Linear Fixed EFs are record oriented files. They consist of a number of fixed-size
records. The records can be individually read/updated."""
@with_default_category('Linear Fixed EF Commands') @with_default_category('Linear Fixed EF Commands')
class ShellCommands(CommandSet): class ShellCommands(CommandSet):
"""Shell commands specific for Linear Fixed EFs."""
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@@ -432,13 +569,33 @@ class LinFixedEF(CardEF):
if data: if data:
self._cmd.poutput(data) self._cmd.poutput(data)
def __init__(self, fid, sfid=None, name=None, desc=None, parent=None, rec_len={1,None}): def __init__(self, fid:str, sfid:str=None, name:str=None, desc:str=None,
parent:Optional[CardDF]=None, rec_len={1,None}):
"""
Args:
fid : File Identifier (4 hex digits)
sfid : Short File Identifier (2 hex digits, optional)
name : Brief name of the file, lik EF_ICCID
desc : Descriptoin of the file
parent : Parent CardFile object within filesystem hierarchy
rec_len : tuple of (minimum_length, recommended_length)
"""
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent) super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent)
self.rec_len = rec_len self.rec_len = rec_len
self.shell_commands = [self.ShellCommands()] self.shell_commands = [self.ShellCommands()]
def decode_record_hex(self, raw_hex_data): def decode_record_hex(self, raw_hex_data:str) -> dict:
"""Decode raw (hex string) data into abstract representation. Overloaded by specific classes.""" """Decode raw (hex string) data into abstract representation.
A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
method for implementing this specifically for the given file. This function checks which
of the method exists, add calls them (with conversion, as needed).
Args:
raw_hex_data : hex-encoded data
Returns:
abstract_data; dict representing the decoded data
"""
method = getattr(self, '_decode_record_hex', None) method = getattr(self, '_decode_record_hex', None)
if callable(method): if callable(method):
return method(raw_hex_data) return method(raw_hex_data)
@@ -448,8 +605,18 @@ class LinFixedEF(CardEF):
return method(raw_bin_data) return method(raw_bin_data)
return {'raw': raw_bin_data.hex()} return {'raw': raw_bin_data.hex()}
def decode_record_bin(self, raw_bin_data): def decode_record_bin(self, raw_bin_data:bytearray) -> dict:
"""Decode raw (binary) data into abstract representation. Overloaded by specific classes.""" """Decode raw (binary) data into abstract representation.
A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
method for implementing this specifically for the given file. This function checks which
of the method exists, add calls them (with conversion, as needed).
Args:
raw_bin_data : binary encoded data
Returns:
abstract_data; dict representing the decoded data
"""
method = getattr(self, '_decode_record_bin', None) method = getattr(self, '_decode_record_bin', None)
if callable(method): if callable(method):
return method(raw_bin_data) return method(raw_bin_data)
@@ -459,47 +626,90 @@ class LinFixedEF(CardEF):
return method(raw_hex_data) return method(raw_hex_data)
return {'raw': raw_hex_data} return {'raw': raw_hex_data}
def encode_record_hex(self, abstract_data): def encode_record_hex(self, abstract_data:dict) -> str:
"""Encode abstract representation into raw (hex string) data. Overloaded by specific classes.""" """Encode abstract representation into raw (hex string) data.
A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
method for implementing this specifically for the given file. This function checks which
of the method exists, add calls them (with conversion, as needed).
Args:
abstract_data : dict representing the decoded data
Returns:
hex string encoded data
"""
method = getattr(self, '_encode_record_hex', None) method = getattr(self, '_encode_record_hex', None)
if callable(method): if callable(method):
return method(abstract_data) return method(abstract_data)
method = getattr(self, '_encode_record_bin', None) method = getattr(self, '_encode_record_bin', None)
if callable(method): if callable(method):
raw_bin_data = method(abstract_data) raw_bin_data = method(abstract_data)
return b2h(raww_bin_data) return h2b(raw_bin_data)
raise NotImplementedError raise NotImplementedError
def encode_record_bin(self, abstract_data): def encode_record_bin(self, abstract_data:dict) -> bytearray:
"""Encode abstract representation into raw (binary) data. Overloaded by specific classes.""" """Encode abstract representation into raw (binary) data.
A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
method for implementing this specifically for the given file. This function checks which
of the method exists, add calls them (with conversion, as needed).
Args:
abstract_data : dict representing the decoded data
Returns:
binary encoded data
"""
method = getattr(self, '_encode_record_bin', None) method = getattr(self, '_encode_record_bin', None)
if callable(method): if callable(method):
return method(abstract_data) return method(abstract_data)
method = getattr(self, '_encode_record_hex', None) method = getattr(self, '_encode_record_hex', None)
if callable(method): if callable(method):
return b2h(method(abstract_data)) return h2b(method(abstract_data))
raise NotImplementedError raise NotImplementedError
class CyclicEF(LinFixedEF): class CyclicEF(LinFixedEF):
"""Cyclic EF (Entry File) in the smart card filesystem""" """Cyclic EF (Entry File) in the smart card filesystem"""
# we don't really have any special support for those; just recycling LinFixedEF here # we don't really have any special support for those; just recycling LinFixedEF here
def __init__(self, fid, sfid=None, name=None, desc=None, parent=None, rec_len={1,None}): def __init__(self, fid:str, sfid:str=None, name:str=None, desc:str=None, parent:CardDF=None,
rec_len={1,None}):
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, rec_len=rec_len) super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, rec_len=rec_len)
class TransRecEF(TransparentEF): class TransRecEF(TransparentEF):
"""Transparent EF (Entry File) containing fixed-size records. """Transparent EF (Entry File) containing fixed-size records.
These are the real odd-balls and mostly look like mistakes in the specification: These are the real odd-balls and mostly look like mistakes in the specification:
Specified as 'transparent' EF, but actually containing several fixed-length records Specified as 'transparent' EF, but actually containing several fixed-length records
inside. inside.
We add a special class for those, so the user only has to provide encoder/decoder functions We add a special class for those, so the user only has to provide encoder/decoder functions
for a record, while this class takes care of split / merge of records. for a record, while this class takes care of split / merge of records.
""" """
def __init__(self, fid, sfid=None, name=None, desc=None, parent=None, rec_len=None, size={1,None}): def __init__(self, fid:str, sfid:str=None, name:str=None, desc:str=None,
parent:Optional[CardDF]=None, rec_len:int=None, size={1,None}):
"""
Args:
fid : File Identifier (4 hex digits)
sfid : Short File Identifier (2 hex digits, optional)
name : Brief name of the file, lik EF_ICCID
desc : Descriptoin of the file
parent : Parent CardFile object within filesystem hierarchy
rec_len : Length of the fixed-length records within transparent EF
size : tuple of (minimum_size, recommended_size)
"""
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, size=size) super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, size=size)
self.rec_len = rec_len self.rec_len = rec_len
def decode_record_hex(self, raw_hex_data): def decode_record_hex(self, raw_hex_data:str) -> dict:
"""Decode raw (hex string) data into abstract representation. Overloaded by specific classes.""" """Decode raw (hex string) data into abstract representation.
A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
method for implementing this specifically for the given file. This function checks which
of the method exists, add calls them (with conversion, as needed).
Args:
raw_hex_data : hex-encoded data
Returns:
abstract_data; dict representing the decoded data
"""
method = getattr(self, '_decode_record_hex', None) method = getattr(self, '_decode_record_hex', None)
if callable(method): if callable(method):
return method(raw_hex_data) return method(raw_hex_data)
@@ -509,8 +719,18 @@ class TransRecEF(TransparentEF):
return method(raw_bin_data) return method(raw_bin_data)
return {'raw': raw_hex_data} return {'raw': raw_hex_data}
def decode_record_bin(self, raw_bin_data): def decode_record_bin(self, raw_bin_data:bytearray) -> dict:
"""Decode raw (hex string) data into abstract representation. Overloaded by specific classes.""" """Decode raw (binary) data into abstract representation.
A derived class would typically provide a _decode_record_bin() or _decode_record_hex()
method for implementing this specifically for the given file. This function checks which
of the method exists, add calls them (with conversion, as needed).
Args:
raw_bin_data : binary encoded data
Returns:
abstract_data; dict representing the decoded data
"""
method = getattr(self, '_decode_record_bin', None) method = getattr(self, '_decode_record_bin', None)
if callable(method): if callable(method):
return method(raw_bin_data) return method(raw_bin_data)
@@ -520,8 +740,18 @@ class TransRecEF(TransparentEF):
return method(raw_hex_data) return method(raw_hex_data)
return {'raw': raw_hex_data} return {'raw': raw_hex_data}
def encode_record_hex(self, abstract_data): def encode_record_hex(self, abstract_data:dict) -> str:
"""Encode abstract representation into raw (hex string) data. Overloaded by specific classes.""" """Encode abstract representation into raw (hex string) data.
A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
method for implementing this specifically for the given file. This function checks which
of the method exists, add calls them (with conversion, as needed).
Args:
abstract_data : dict representing the decoded data
Returns:
hex string encoded data
"""
method = getattr(self, '_encode_record_hex', None) method = getattr(self, '_encode_record_hex', None)
if callable(method): if callable(method):
return method(abstract_data) return method(abstract_data)
@@ -530,8 +760,18 @@ class TransRecEF(TransparentEF):
return h2b(method(abstract_data)) return h2b(method(abstract_data))
raise NotImplementedError raise NotImplementedError
def encode_record_bin(self, abstract_data): def encode_record_bin(self, abstract_data:dict) -> bytearray:
"""Encode abstract representation into raw (binary) data. Overloaded by specific classes.""" """Encode abstract representation into raw (binary) data.
A derived class would typically provide an _encode_record_bin() or _encode_record_hex()
method for implementing this specifically for the given file. This function checks which
of the method exists, add calls them (with conversion, as needed).
Args:
abstract_data : dict representing the decoded data
Returns:
binary encoded data
"""
method = getattr(self, '_encode_record_bin', None) method = getattr(self, '_encode_record_bin', None)
if callable(method): if callable(method):
return method(abstract_data) return method(abstract_data)
@@ -540,11 +780,11 @@ class TransRecEF(TransparentEF):
return h2b(method(abstract_data)) return h2b(method(abstract_data))
raise NotImplementedError raise NotImplementedError
def _decode_bin(self, raw_bin_data): def _decode_bin(self, raw_bin_data:bytearray):
chunks = [raw_bin_data[i:i+self.rec_len] for i in range(0, len(raw_bin_data), self.rec_len)] chunks = [raw_bin_data[i:i+self.rec_len] for i in range(0, len(raw_bin_data), self.rec_len)]
return [self.decode_record_bin(x) for x in chunks] return [self.decode_record_bin(x) for x in chunks]
def _encode_bin(self, abstract_data): def _encode_bin(self, abstract_data) -> bytes:
chunks = [self.encode_record_bin(x) for x in abstract_data] chunks = [self.encode_record_bin(x) for x in abstract_data]
# FIXME: pad to file size # FIXME: pad to file size
return b''.join(chunks) return b''.join(chunks)
@@ -555,7 +795,12 @@ class TransRecEF(TransparentEF):
class RuntimeState(object): class RuntimeState(object):
"""Represent the runtime state of a session with a card.""" """Represent the runtime state of a session with a card."""
def __init__(self, card, profile): def __init__(self, card, profile:'CardProfile'):
"""
Args:
card : pysim.cards.Card instance
profile : CardProfile instance
"""
self.mf = CardMF() self.mf = CardMF()
self.card = card self.card = card
self.selected_file = self.mf self.selected_file = self.mf
@@ -589,15 +834,22 @@ class RuntimeState(object):
print("error: could not determine card applications") print("error: could not determine card applications")
return apps_taken return apps_taken
def get_cwd(self): def get_cwd(self) -> CardDF:
"""Obtain the current working directory.""" """Obtain the current working directory.
Returns:
CardDF instance
"""
if isinstance(self.selected_file, CardDF): if isinstance(self.selected_file, CardDF):
return self.selected_file return self.selected_file
else: else:
return self.selected_file.parent return self.selected_file.parent
def get_application(self): def get_application(self) -> Optional[CardADF]:
"""Obtain the currently selected application (if any).""" """Obtain the currently selected application (if any).
Returns:
CardADF() instance or None"""
# iterate upwards from selected file; check if any is an ADF # iterate upwards from selected file; check if any is an ADF
node = self.selected_file node = self.selected_file
while node.parent != node: while node.parent != node:
@@ -606,9 +858,16 @@ class RuntimeState(object):
node = node.parent node = node.parent
return None return None
def interpret_sw(self, sw): def interpret_sw(self, sw:str):
"""Interpret the given SW relative to the currently selected Application """Interpret a given status word relative to the currently selected application
or the underlying profile.""" or the underlying card profile.
Args:
sw : Status word as string of 4 hexd digits
Returns:
Tuple of two strings
"""
app = self.get_application() app = self.get_application()
if app: if app:
# The application either comes with its own interpret_sw # The application either comes with its own interpret_sw
@@ -622,11 +881,9 @@ class RuntimeState(object):
else: else:
return self.profile.interpret_sw(sw) return self.profile.interpret_sw(sw)
def probe_file(self, fid, cmd_app=None): def probe_file(self, fid:str, cmd_app=None):
""" """Blindly try to select a file and automatically add a matching file
blindly try to select a file and automatically add a matching file object if the file actually exists."""
object if the file actually exists
"""
if not is_hex(fid, 4, 4): if not is_hex(fid, 4, 4):
raise ValueError("Cannot select unknown file by name %s, only hexadecimal 4 digit FID is allowed" % fid) raise ValueError("Cannot select unknown file by name %s, only hexadecimal 4 digit FID is allowed" % fid)
@@ -651,8 +908,13 @@ class RuntimeState(object):
self.selected_file = f self.selected_file = f
return select_resp return select_resp
def select(self, name, cmd_app=None): def select(self, name:str, cmd_app=None):
"""Change current directory""" """Select a file (EF, DF, ADF, MF, ...).
Args:
name : Name of file to select
cmd_app : Command Application State (for unregistering old file commands)
"""
sels = self.selected_file.get_selectables() sels = self.selected_file.get_selectables()
if is_hex(name): if is_hex(name):
name = name.lower() name = name.lower()
@@ -686,43 +948,98 @@ class RuntimeState(object):
return select_resp return select_resp
def read_binary(self, length=None, offset=0): 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): if not isinstance(self.selected_file, TransparentEF):
raise TypeError("Only works with TransparentEF") raise TypeError("Only works with TransparentEF")
return self.card._scc.read_binary(self.selected_file.fid, length, offset) return self.card._scc.read_binary(self.selected_file.fid, length, offset)
def read_binary_dec(self): def read_binary_dec(self) -> dict:
"""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() (data, sw) = self.read_binary()
dec_data = self.selected_file.decode_hex(data) dec_data = self.selected_file.decode_hex(data)
print("%s: %s -> %s" % (sw, data, dec_data)) print("%s: %s -> %s" % (sw, data, dec_data))
return (dec_data, sw) return (dec_data, sw)
def update_binary(self, data_hex, offset=0): 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): if not isinstance(self.selected_file, TransparentEF):
raise TypeError("Only works with 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.card._scc.update_binary(self.selected_file.fid, data_hex, offset, conserve=self.conserve_write)
def update_binary_dec(self, data): 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) data_hex = self.selected_file.encode_hex(data)
print("%s -> %s" % (data, data_hex)) print("%s -> %s" % (data, data_hex))
return self.update_binary(data_hex) return self.update_binary(data_hex)
def read_record(self, rec_nr=0): 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): if not isinstance(self.selected_file, LinFixedEF):
raise TypeError("Only works with Linear Fixed EF") raise TypeError("Only works with Linear Fixed EF")
# returns a string of hex nibbles # returns a string of hex nibbles
return self.card._scc.read_record(self.selected_file.fid, rec_nr) return self.card._scc.read_record(self.selected_file.fid, rec_nr)
def read_record_dec(self, rec_nr=0): 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) (data, sw) = self.read_record(rec_nr)
return (self.selected_file.decode_record_hex(data), sw) return (self.selected_file.decode_record_hex(data), sw)
def update_record(self, rec_nr, data_hex): 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): if not isinstance(self.selected_file, LinFixedEF):
raise TypeError("Only works with Linear Fixed EF") 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.card._scc.update_record(self.selected_file.fid, rec_nr, data_hex, conserve=self.conserve_write)
def update_record_dec(self, rec_nr, data): 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
"""
hex_data = self.selected_file.encode_record_hex(data) hex_data = self.selected_file.encode_record_hex(data)
return self.update_record(self, rec_nr, data_hex) return self.update_record(self, rec_nr, data_hex)
@@ -735,9 +1052,15 @@ class FileData(object):
self.fcp = None self.fcp = None
def interpret_sw(sw_data, sw): def interpret_sw(sw_data:dict, sw:str):
"""Interpret a given status word within the profile. Returns tuple of """Interpret a given status word.
two strings"""
Args:
sw_data : Hierarchical dict of status word matches
sw : status word to match (string of 4 hex digits)
Returns:
tuple of two strings (class_string, description)
"""
for class_str, swdict in sw_data.items(): for class_str, swdict in sw_data.items():
# first try direct match # first try direct match
if sw in swdict: if sw in swdict:
@@ -751,7 +1074,12 @@ def interpret_sw(sw_data, sw):
class CardApplication(object): class CardApplication(object):
"""A card application is represented by an ADF (with contained hierarchy) and optionally """A card application is represented by an ADF (with contained hierarchy) and optionally
some SW definitions.""" some SW definitions."""
def __init__(self, name, adf=None, sw=None): def __init__(self, name, adf:str=None, sw:dict=None):
"""
Args:
adf : ADF name
sw : Dict of status word conversions
"""
self.name = name self.name = name
self.adf = adf self.adf = adf
self.sw = sw or dict() self.sw = sw or dict()
@@ -760,8 +1088,14 @@ class CardApplication(object):
return "APP(%s)" % (self.name) return "APP(%s)" % (self.name)
def interpret_sw(self, sw): def interpret_sw(self, sw):
"""Interpret a given status word within the application. Returns tuple of """Interpret a given status word within the application.
two strings"""
Args:
sw : Status word as string of 4 hexd digits
Returns:
Tuple of two strings
"""
return interpret_sw(self.sw, sw) return interpret_sw(self.sw, sw)
class CardProfile(object): class CardProfile(object):
@@ -769,6 +1103,14 @@ class CardProfile(object):
applications as well as profile-specific SW and shell commands. Every card has 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.""" one card profile, but there may be multiple applications within that profile."""
def __init__(self, name, **kw): 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
"""
self.name = name self.name = name
self.desc = kw.get("desc", None) self.desc = kw.get("desc", None)
self.files_in_mf = kw.get("files_in_mf", []) self.files_in_mf = kw.get("files_in_mf", [])
@@ -779,10 +1121,21 @@ class CardProfile(object):
def __str__(self): def __str__(self):
return self.name return self.name
def add_application(self, app): 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) self.applications.append(app)
def interpret_sw(self, sw): def interpret_sw(self, sw:str):
"""Interpret a given status word within the profile. Returns tuple of """Interpret a given status word within the profile.
two strings"""
Args:
sw : Status word as string of 4 hexd digits
Returns:
Tuple of two strings
"""
return interpret_sw(self.sw, sw) return interpret_sw(self.sw, sw)

View File

@@ -24,48 +24,53 @@ from pySim.utils import sw_match
# #
class LinkBase(object): class LinkBase(object):
"""Base class for link/transport to card."""
def wait_for_card(self, timeout=None, newcardonly=False): def wait_for_card(self, timeout:int=None, newcardonly:bool=False):
"""wait_for_card(): Wait for a card and connect to it """Wait for a card and connect to it
timeout : Maximum wait time (None=no timeout) Args:
newcardonly : Should we wait for a new card, or an already timeout : Maximum wait time in seconds (None=no timeout)
inserted one ? newcardonly : Should we wait for a new card, or an already inserted one ?
""" """
pass pass
def connect(self): def connect(self):
"""connect(): Connect to a card immediately """Connect to a card immediately
""" """
pass pass
def disconnect(self): def disconnect(self):
"""disconnect(): Disconnect from card """Disconnect from card
""" """
pass pass
def reset_card(self): def reset_card(self):
"""reset_card(): Resets the card (power down/up) """Resets the card (power down/up)
""" """
pass pass
def send_apdu_raw(self, pdu): def send_apdu_raw(self, pdu:str):
"""send_apdu_raw(pdu): Sends an APDU with minimal processing """Sends an APDU with minimal processing
pdu : string of hexadecimal characters (ex. "A0A40000023F00") Args:
return : tuple(data, sw), where pdu : string of hexadecimal characters (ex. "A0A40000023F00")
data : string (in hex) of returned data (ex. "074F4EFFFF") Returns:
sw : string (in hex) of status word (ex. "9000") tuple(data, sw), where
data : string (in hex) of returned data (ex. "074F4EFFFF")
sw : string (in hex) of status word (ex. "9000")
""" """
pass pass
def send_apdu(self, pdu): def send_apdu(self, pdu):
"""send_apdu(pdu): Sends an APDU and auto fetch response data """Sends an APDU and auto fetch response data
pdu : string of hexadecimal characters (ex. "A0A40000023F00") Args:
return : tuple(data, sw), where pdu : string of hexadecimal characters (ex. "A0A40000023F00")
data : string (in hex) of returned data (ex. "074F4EFFFF") Returns:
sw : string (in hex) of status word (ex. "9000") tuple(data, sw), where
data : string (in hex) of returned data (ex. "074F4EFFFF")
sw : string (in hex) of status word (ex. "9000")
""" """
data, sw = self.send_apdu_raw(pdu) data, sw = self.send_apdu_raw(pdu)
@@ -82,15 +87,16 @@ class LinkBase(object):
return data, sw return data, sw
def send_apdu_checksw(self, pdu, sw="9000"): def send_apdu_checksw(self, pdu, sw="9000"):
"""send_apdu_checksw(pdu,sw): Sends an APDU and check returned SW """Sends an APDU and check returned SW
pdu : string of hexadecimal characters (ex. "A0A40000023F00") Args:
sw : string of 4 hexadecimal characters (ex. "9000"). The pdu : string of hexadecimal characters (ex. "A0A40000023F00")
user may mask out certain digits using a '?' to add some sw : string of 4 hexadecimal characters (ex. "9000"). The user may mask out certain
ambiguity if needed. digits using a '?' to add some ambiguity if needed.
return : tuple(data, sw), where Returns:
data : string (in hex) of returned data (ex. "074F4EFFFF") tuple(data, sw), where
sw : string (in hex) of status word (ex. "9000") data : string (in hex) of returned data (ex. "074F4EFFFF")
sw : string (in hex) of status word (ex. "9000")
""" """
rv = self.send_apdu(pdu) rv = self.send_apdu(pdu)

View File

@@ -1,9 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" pySim: Transport Link for Calypso bases phones
"""
#
# Copyright (C) 2018 Vadim Yanitskiy <axilirator@gmail.com> # Copyright (C) 2018 Vadim Yanitskiy <axilirator@gmail.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
@@ -73,8 +69,9 @@ class L1CTLMessageSIM(L1CTLMessage):
self.data += pdu self.data += pdu
class CalypsoSimLink(LinkBase): class CalypsoSimLink(LinkBase):
"""Transport Link for Calypso based phones."""
def __init__(self, sock_path = "/tmp/osmocom_l2"): def __init__(self, sock_path:str = "/tmp/osmocom_l2"):
# Make sure that a given socket path exists # Make sure that a given socket path exists
if not os.path.exists(sock_path): if not os.path.exists(sock_path):
raise ReaderError("There is no such ('%s') UNIX socket" % sock_path) raise ReaderError("There is no such ('%s') UNIX socket" % sock_path)
@@ -119,7 +116,6 @@ class CalypsoSimLink(LinkBase):
pass # Nothing to do really ... pass # Nothing to do really ...
def send_apdu_raw(self, pdu): def send_apdu_raw(self, pdu):
"""see LinkBase.send_apdu_raw"""
# Request FULL reset # Request FULL reset
req_msg = L1CTLMessageSIM(h2b(pdu)) req_msg = L1CTLMessageSIM(h2b(pdu))

View File

@@ -1,8 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" pySim: Transport Link for 3GPP TS 27.007 compliant modems
"""
# Copyright (C) 2020 Vadim Yanitskiy <axilirator@gmail.com> # Copyright (C) 2020 Vadim Yanitskiy <axilirator@gmail.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
@@ -31,7 +28,8 @@ from pySim.exceptions import *
# log.root.setLevel(log.DEBUG) # log.root.setLevel(log.DEBUG)
class ModemATCommandLink(LinkBase): class ModemATCommandLink(LinkBase):
def __init__(self, device='/dev/ttyUSB0', baudrate=115200): """Transport Link for 3GPP TS 27.007 compliant modems."""
def __init__(self, device:str='/dev/ttyUSB0', baudrate:int=115200):
self._sl = serial.Serial(device, baudrate, timeout=5) self._sl = serial.Serial(device, baudrate, timeout=5)
self._device = device self._device = device
self._atr = None self._atr = None

View File

@@ -1,9 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" pySim: PCSC reader transport link
"""
#
# Copyright (C) 2009-2010 Sylvain Munaut <tnt@246tNt.com> # Copyright (C) 2009-2010 Sylvain Munaut <tnt@246tNt.com>
# Copyright (C) 2010 Harald Welte <laforge@gnumonks.org> # Copyright (C) 2010 Harald Welte <laforge@gnumonks.org>
# #
@@ -32,8 +28,9 @@ from pySim.utils import h2i, i2h
class PcscSimLink(LinkBase): class PcscSimLink(LinkBase):
""" pySim: PCSC reader transport link."""
def __init__(self, reader_number=0): def __init__(self, reader_number:int=0):
r = readers() r = readers()
self._reader = r[reader_number] self._reader = r[reader_number]
self._con = self._reader.createConnection() self._con = self._reader.createConnection()
@@ -46,7 +43,7 @@ class PcscSimLink(LinkBase):
pass pass
return return
def wait_for_card(self, timeout=None, newcardonly=False): def wait_for_card(self, timeout:int=None, newcardonly:bool=False):
cr = CardRequest(readers=[self._reader], timeout=timeout, newcardonly=newcardonly) cr = CardRequest(readers=[self._reader], timeout=timeout, newcardonly=newcardonly)
try: try:
cr.waitforcard() cr.waitforcard()
@@ -75,7 +72,6 @@ class PcscSimLink(LinkBase):
return 1 return 1
def send_apdu_raw(self, pdu): def send_apdu_raw(self, pdu):
"""see LinkBase.send_apdu_raw"""
apdu = h2i(pdu) apdu = h2i(pdu)

View File

@@ -1,9 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" pySim: Transport Link for serial (RS232) based readers included with simcard
"""
#
# Copyright (C) 2009-2010 Sylvain Munaut <tnt@246tNt.com> # Copyright (C) 2009-2010 Sylvain Munaut <tnt@246tNt.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
@@ -30,8 +26,10 @@ from pySim.utils import h2b, b2h
class SerialSimLink(LinkBase): class SerialSimLink(LinkBase):
""" pySim: Transport Link for serial (RS232) based readers included with simcard"""
def __init__(self, device='/dev/ttyUSB0', baudrate=9600, rst='-rts', debug=False): def __init__(self, device:str='/dev/ttyUSB0', baudrate:int=9600, rst:str='-rts',
debug:bool=False):
if not os.path.exists(device): if not os.path.exists(device):
raise ValueError("device file %s does not exist -- abort" % device) raise ValueError("device file %s does not exist -- abort" % device)
self._sl = serial.Serial( self._sl = serial.Serial(
@@ -183,7 +181,6 @@ class SerialSimLink(LinkBase):
return self._sl.read() return self._sl.read()
def send_apdu_raw(self, pdu): def send_apdu_raw(self, pdu):
"""see LinkBase.send_apdu_raw"""
pdu = h2b(pdu) pdu = h2b(pdu)
data_len = ord(pdu[4]) # P3 data_len = ord(pdu[4]) # P3

View File

@@ -21,43 +21,65 @@
# #
def h2b(s): def h2b(s: str) -> bytearray:
"""convert from a string of hex nibbles to a sequence of bytes""" """convert from a string of hex nibbles to a sequence of bytes"""
return bytearray.fromhex(s) return bytearray.fromhex(s)
def b2h(b): def b2h(b: bytearray) -> str:
"""convert from a sequence of bytes to a string of hex nibbles""" """convert from a sequence of bytes to a string of hex nibbles"""
return ''.join(['%02x'%(x) for x in b]) return ''.join(['%02x'%(x) for x in b])
def h2i(s): def h2i(s:str):
"""convert from a string of hex nibbles to a list of integers"""
return [(int(x,16)<<4)+int(y,16) for x,y in zip(s[0::2], s[1::2])] return [(int(x,16)<<4)+int(y,16) for x,y in zip(s[0::2], s[1::2])]
def i2h(s): def i2h(s) -> str:
"""convert from a list of integers to a string of hex nibbles"""
return ''.join(['%02x'%(x) for x in s]) return ''.join(['%02x'%(x) for x in s])
def h2s(s): def h2s(s:str) -> str:
"""convert from a string of hex nibbles to an ASCII string"""
return ''.join([chr((int(x,16)<<4)+int(y,16)) for x,y in zip(s[0::2], s[1::2]) return ''.join([chr((int(x,16)<<4)+int(y,16)) for x,y in zip(s[0::2], s[1::2])
if int(x + y, 16) != 0xff]) if int(x + y, 16) != 0xff])
def s2h(s): def s2h(s:str) -> str:
"""convert from an ASCII string to a string of hex nibbles"""
b = bytearray() b = bytearray()
b.extend(map(ord, s)) b.extend(map(ord, s))
return b2h(b) return b2h(b)
# List of bytes to string # List of bytes to string
def i2s(s): def i2s(s) -> str:
"""convert from a list of integers to an ASCII string"""
return ''.join([chr(x) for x in s]) return ''.join([chr(x) for x in s])
def swap_nibbles(s): def swap_nibbles(s:str) -> str:
"""swap the nibbles in a hex string"""
return ''.join([x+y for x,y in zip(s[1::2], s[0::2])]) return ''.join([x+y for x,y in zip(s[1::2], s[0::2])])
def rpad(s, l, c='f'): def rpad(s:str, l:int, c='f') -> str:
"""pad string on the right side.
Args:
s : string to pad
l : total length to pad to
c : padding character
Returns:
String 's' padded with as many 'c' as needed to reach total length of 'l'
"""
return s + c * (l - len(s)) return s + c * (l - len(s))
def lpad(s, l, c='f'): def lpad(s:str, l:int, c='f') -> str:
"""pad string on the left side.
Args:
s : string to pad
l : total length to pad to
c : padding character
Returns:
String 's' padded with as many 'c' as needed to reach total length of 'l'
"""
return c * (l - len(s)) + s return c * (l - len(s)) + s
def half_round_up(n): def half_round_up(n:int) -> int:
return (n + 1)//2 return (n + 1)//2
# IMSI encoded format: # IMSI encoded format:
@@ -75,8 +97,8 @@ def half_round_up(n):
# Because of this, an odd length IMSI fits exactly into len(imsi) + 1 // 2 bytes, whereas an # Because of this, an odd length IMSI fits exactly into len(imsi) + 1 // 2 bytes, whereas an
# even length IMSI only uses half of the last byte. # even length IMSI only uses half of the last byte.
def enc_imsi(imsi): def enc_imsi(imsi:str):
"""Converts a string imsi into the value of the EF""" """Converts a string IMSI into the encoded value of the EF"""
l = half_round_up(len(imsi) + 1) # Required bytes - include space for odd/even indicator l = half_round_up(len(imsi) + 1) # Required bytes - include space for odd/even indicator
oe = len(imsi) & 1 # Odd (1) / Even (0) oe = len(imsi) & 1 # Odd (1) / Even (0)
ei = '%02x' % l + swap_nibbles('%01x%s' % ((oe<<3)|1, rpad(imsi, 15))) ei = '%02x' % l + swap_nibbles('%01x%s' % ((oe<<3)|1, rpad(imsi, 15)))
@@ -781,7 +803,7 @@ def get_addr_type(addr):
return None return None
def sw_match(sw, pattern): def sw_match(sw:str, pattern:str) -> str:
"""Match given SW against given pattern.""" """Match given SW against given pattern."""
# Create a masked version of the returned status word # Create a masked version of the returned status word
sw_lower = sw.lower() sw_lower = sw.lower()
@@ -796,8 +818,18 @@ def sw_match(sw, pattern):
# Compare the masked version against the pattern # Compare the masked version against the pattern
return sw_masked == pattern return sw_masked == pattern
def tabulate_str_list(str_list, width = 79, hspace = 2, lspace = 1, align_left = True): def tabulate_str_list(str_list, width:int = 79, hspace:int = 2, lspace:int = 1,
"""Pretty print a list of strings into a tabulated form""" align_left:bool = True):
"""Pretty print a list of strings into a tabulated form.
Args:
width : total width in characters per line
space : horizontal space between cells
lspace : number of spaces before row
align_lef : Align text to the left side
Returns:
multi-line string containing formatted table
"""
if str_list == None: if str_list == None:
return "" return ""
if len(str_list) <= 0: if len(str_list) <= 0: