This is merely a historical archive of years 2008-2021, before the migration to mailman3.
A maintained and still updated list archive can be found at https://lists.osmocom.org/hyperkitty/list/gerrit-log@lists.osmocom.org/.
laforge gerrit-no-reply at lists.osmocom.orglaforge has uploaded this change for review. ( https://gerrit.osmocom.org/c/pysim/+/23577 ) Change subject: Add more documentation to the classes/methods ...................................................................... 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 --- M pySim/commands.py M pySim/exceptions.py M pySim/filesystem.py M pySim/transport/__init__.py M pySim/transport/calypso.py M pySim/transport/modem_atcmd.py M pySim/transport/pcsc.py M pySim/transport/serial.py M pySim/utils.py 9 files changed, 600 insertions(+), 155 deletions(-) git pull ssh://gerrit.osmocom.org:29418/pysim refs/changes/77/23577/1 diff --git a/pySim/commands.py b/pySim/commands.py index 9aed588..9f4d450 100644 --- a/pySim/commands.py +++ b/pySim/commands.py @@ -64,7 +64,7 @@ # from what SIMs responds. See also: # USIM: ETSI TS 102 221, chapter 11.1.1.3 Response Data # 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": tlv_parsed = self.__parse_fcp(r[-1]) file_descriptor = tlv_parsed['82'] @@ -75,14 +75,15 @@ # Tell the length of a binary file. See also comment # above. - def __len(self, r): + def __len(self, r) -> int: if self.sel_ctrl == "0004": tlv_parsed = self.__parse_fcp(r[-1]) return int(tlv_parsed['80'], 16) else: 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() @property @@ -100,6 +101,7 @@ self._sel_ctrl = value def try_select_path(self, dir_list): + """ Try to select a specified path given as list of hex-string FIDs""" rv = [] if type(dir_list) is not list: dir_list = [dir_list] @@ -111,6 +113,14 @@ return rv 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 = [] if type(dir_list) is not list: dir_list = [dir_list] @@ -119,14 +129,23 @@ rv.append(data) 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) - 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:] 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) if len(r[-1]) == 0: return (None, None) @@ -144,7 +163,15 @@ raise ValueError('Failed to read (offset %d)' % (offset)) return total_data, sw - def update_binary(self, ef, data, offset=0, verify=False): + def update_binary(self, ef, data:str, offset:int=0, verify: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 + """ self.select_path(ef) pdu = self.cla_byte + 'd6%04x%02x' % (offset, len(data) // 2) + data res = self._tp.send_apdu_checksw(pdu) @@ -152,18 +179,31 @@ self.verify_binary(ef, data, offset) 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) if res[0].lower() != data.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) rec_length = self.__record_len(r) pdu = self.cla_byte + 'b2%02x04%02x' % (rec_no, rec_length) return self._tp.send_apdu(pdu) - def update_record(self, ef, rec_no, data, force_len=False, verify=False): + def update_record(self, ef, rec_no:int, data:str, force_len:bool=False, verify:bool=False): r = self.select_path(ef) if not force_len: rec_length = self.__record_len(r) @@ -177,33 +217,51 @@ self.verify_record(ef, rec_no, data) 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) if res[0].lower() != data.lower(): raise ValueError('Record verification failed (expected %s, got %s)' % (data.lower(), res[0].lower())) 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) return self.__record_len(r) 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) return self.__len(r) // self.__record_len(r) 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) return self.__len(r) - def run_gsm(self, rand): + def run_gsm(self, rand:str): + """Execute RUN GSM ALGORITHM.""" if len(rand) != 32: raise ValueError('Invalid rand') self.select_path(['3f00', '7f20']) return self._tp.send_apdu(self.cla_byte + '88000010' + rand) def reset_card(self): + """Physically reset the card""" return self._tp.reset_card() - def verify_chv(self, chv_no, code): + def verify_chv(self, chv_no:int, code:str): + """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) if (sw != '9000'): diff --git a/pySim/exceptions.py b/pySim/exceptions.py index 4fb8f72..f1d1a18 100644 --- a/pySim/exceptions.py +++ b/pySim/exceptions.py @@ -22,18 +22,27 @@ # class NoCardError(Exception): + """No card was found in the reader.""" pass class ProtocolError(Exception): + """Some kind of protocol level error interfacing with the card.""" pass class ReaderError(Exception): + """Some kind of general error with the card reader.""" pass class SwMatchError(Exception): """Raised when an operation specifies an expected SW but the actual SW from 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_expected = sw_expected self.rs = rs diff --git a/pySim/filesystem.py b/pySim/filesystem.py index cb39b94..ead21f9 100644 --- a/pySim/filesystem.py +++ b/pySim/filesystem.py @@ -41,7 +41,16 @@ RESERVED_NAMES = ['..', '.', '/', 'MF'] 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=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: raise ValueError("fid is mandatory") if fid: @@ -72,7 +81,11 @@ return self.fid def fully_qualified_path(self, prefer_name=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: ret = self.parent.fully_qualified_path(prefer_name) else: @@ -90,8 +103,16 @@ node = node.parent return node - def _get_self_selectables(self, alias=None, flags = []): - """Return a dict of {'identifier': self} tuples""" + def _get_self_selectables(self, alias:str=None, flags = []): + """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 = {} if alias: sels.update({alias: self}) @@ -102,7 +123,15 @@ return sels def get_selectables(self, flags = []): - """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 = {} # we can always select ourself if flags == [] or 'SELF' in flags: @@ -119,7 +148,15 @@ return sels def get_selectable_names(self, flags = []): - """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) return sels.keys() @@ -140,8 +177,12 @@ def __str__(self): return "DF(%s)" % (super().__str__()) - def add_file(self, child, ignore_existing=False): - """Add a child (DF/EF) to this DF""" + def add_file(self, child:CardFile, ignore_existing:bool=False): + """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): raise TypeError("Expected a File instance") if not is_hex(child.fid, minlen = 4, maxlen = 4): @@ -164,12 +205,25 @@ child.parent = self def add_files(self, children, ignore_existing=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: self.add_file(child, ignore_existing) - def get_selectables(self, flags = []): - """Get selectable (DF/EF names) from current DF""" + def get_selectables(self, flags = []) -> dict: + """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 sels = super().get_selectables(flags) if flags == [] or 'FIDS' in flags: @@ -178,7 +232,8 @@ sels.update({x.name: x for x in self.children.values() if x.name}) return sels - def lookup_file_by_name(self, name): + def lookup_file_by_name(self, name:str) -> CardFile: + """Find a file with given name within current DF.""" if name == None: return None for i in self.children.values(): @@ -186,7 +241,8 @@ return i return None - def lookup_file_by_sfid(self, sfid): + def lookup_file_by_sfid(self, sfid:str) -> CardFile: + """Find a file with given short file ID within current DF.""" if sfid == None: return None for i in self.children.values(): @@ -194,7 +250,8 @@ return i return None - def lookup_file_by_fid(self, fid): + def lookup_file_by_fid(self, fid:str) -> CardFile: + """Find a file with given file ID within current DF.""" if fid in self.children: return self.children[fid] return None @@ -228,13 +285,21 @@ """Get list of completions (AID names)""" return [x.name for x in self.applications] - def get_selectables(self, flags = []): - """Get list of completions (DF/EF/ADF names) from current DF""" + def get_selectables(self, flags = []) -> dict: + """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.update(self.get_app_selectables(flags)) return sels - def get_app_selectables(self, flags = []): + def get_app_selectables(self, flags = []) -> dict: """Get applications by AID + name""" sels = {} if flags == [] or 'AIDS' in flags: @@ -243,15 +308,19 @@ sels.update({x.name: x for x in self.applications.values() if x.name}) return sels - def decode_select_response(self, data_hex): - """Decode the response to a SELECT command.""" + def decode_select_response(self, data_hex:str) -> str: + """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 class CardADF(CardDF): """ADF (Application Dedicated File) in the smart card filesystem""" - def __init__(self, aid, **kwargs): + def __init__(self, aid:str, **kwargs): super().__init__(**kwargs) self.aid = aid # Application Identifier if self.parent: @@ -276,8 +345,16 @@ def __str__(self): return "EF(%s)" % (super().__str__()) - def get_selectables(self, flags = []): - """Get list of completions (EF names) from current DF""" + def get_selectables(self, flags = []) -> dict: + """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 sels = super().get_selectables(flags) sels.update({x.name:x for x in self.parent.children.values() if x != self}) @@ -285,10 +362,14 @@ 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') class ShellCommands(CommandSet): + """Shell commands specific for Trransparent EFs.""" def __init__(self): super().__init__() @@ -326,13 +407,33 @@ if data: 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) self.size = size self.shell_commands = [self.ShellCommands()] - def decode_bin(self, raw_bin_data): - """Decode raw (binary) data into abstract representation. Overloaded by specific classes.""" + def decode_bin(self, raw_bin_data:bytearray): + """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) if callable(method): return method(raw_bin_data) @@ -341,8 +442,18 @@ return method(b2h(raw_bin_data)) return {'raw': raw_bin_data.hex()} - def decode_hex(self, raw_hex_data): - """Decode raw (hex string) data into abstract representation. Overloaded by specific classes.""" + def decode_hex(self, raw_hex_data:str): + """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) if callable(method): return method(raw_hex_data) @@ -352,8 +463,18 @@ return method(raw_bin_data) return {'raw': raw_bin_data.hex()} - def encode_bin(self, abstract_data): - """Encode abstract representation into raw (binary) data. Overloaded by specific classes.""" + def encode_bin(self, abstract_data) -> bytearray: + """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) if callable(method): return method(abstract_data) @@ -362,8 +483,18 @@ return h2b(method(abstract_data)) raise NotImplementedError - def encode_hex(self, abstract_data): - """Encode abstract representation into raw (hex string) data. Overloaded by specific classes.""" + def encode_hex(self, abstract_data) -> str: + """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) if callable(method): return method(abstract_data) @@ -375,10 +506,14 @@ 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') class ShellCommands(CommandSet): + """Shell commands specific for Linear Fixed EFs.""" def __init__(self): super().__init__() @@ -425,13 +560,33 @@ if 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: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) self.rec_len = rec_len self.shell_commands = [self.ShellCommands()] - def decode_record_hex(self, raw_hex_data): - """Decode raw (hex string) data into abstract representation. Overloaded by specific classes.""" + def decode_record_hex(self, raw_hex_data:str): + """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) if callable(method): return method(raw_hex_data) @@ -441,8 +596,18 @@ return method(raw_bin_data) return {'raw': raw_bin_data.hex()} - def decode_record_bin(self, raw_bin_data): - """Decode raw (binary) data into abstract representation. Overloaded by specific classes.""" + def decode_record_bin(self, raw_bin_data:bytearray): + """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) if callable(method): return method(raw_bin_data) @@ -452,8 +617,18 @@ return method(raw_hex_data) return {'raw': raw_hex_data} - def encode_record_hex(self, abstract_data): - """Encode abstract representation into raw (hex string) data. Overloaded by specific classes.""" + def encode_record_hex(self, abstract_data) -> str: + """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) if callable(method): return method(abstract_data) @@ -463,8 +638,18 @@ return b2h(raww_bin_data) raise NotImplementedError - def encode_record_bin(self, abstract_data): - """Encode abstract representation into raw (binary) data. Overloaded by specific classes.""" + def encode_record_bin(self, abstract_data) -> bytearray: + """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) if callable(method): return method(abstract_data) @@ -476,23 +661,46 @@ class CyclicEF(LinFixedEF): """Cyclic EF (Entry File) in the smart card filesystem""" # 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) class TransRecEF(TransparentEF): """Transparent EF (Entry File) containing fixed-size records. + 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 inside. 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. """ - 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:CardDF=None, + rec_len=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) self.rec_len = rec_len def decode_record_hex(self, raw_hex_data): - """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) if callable(method): return method(raw_hex_data) @@ -502,8 +710,18 @@ return method(raw_bin_data) return {'raw': raw_hex_data} - def decode_record_bin(self, raw_bin_data): - """Decode raw (hex string) data into abstract representation. Overloaded by specific classes.""" + def decode_record_bin(self, raw_bin_data:bytearray): + """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) if callable(method): return method(raw_bin_data) @@ -513,8 +731,18 @@ return method(raw_hex_data) return {'raw': raw_hex_data} - def encode_record_hex(self, abstract_data): - """Encode abstract representation into raw (hex string) data. Overloaded by specific classes.""" + def encode_record_hex(self, abstract_data) -> str: + """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) if callable(method): return method(abstract_data) @@ -523,8 +751,18 @@ return h2b(method(abstract_data)) raise NotImplementedError - def encode_record_bin(self, abstract_data): - """Encode abstract representation into raw (binary) data. Overloaded by specific classes.""" + def encode_record_bin(self, abstract_data) -> bytearray: + """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) if callable(method): return method(abstract_data) @@ -549,6 +787,11 @@ class RuntimeState(object): """Represent the runtime state of a session with a card.""" def __init__(self, card, profile): + """ + Args: + card : pysim.cards.Card instance + profile : CardProfile instance + """ self.mf = CardMF() self.card = card self.selected_file = self.mf @@ -581,15 +824,22 @@ print("error: could not determine card applications") return apps_taken - def get_cwd(self): - """Obtain the current working directory.""" + 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(self): - """Obtain the currently selected application (if any).""" + def get_application(self) -> CardADF: + """Obtain the currently selected application (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: @@ -598,9 +848,16 @@ node = node.parent return None - def interpret_sw(self, sw): - """Interpret the given SW relative to the currently selected Application - or the underlying profile.""" + 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 hexd digits + + Returns: + Tuple of two strings + """ app = self.get_application() if app: # The application either comes with its own interpret_sw @@ -614,8 +871,13 @@ else: return self.profile.interpret_sw(sw) - def select(self, name, cmd_app=None): - """Change current directory""" + 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) + """ sels = self.selected_file.get_selectables() if is_hex(name): name = name.lower() @@ -645,43 +907,98 @@ else: raise ValueError("Cannot select unknown %s" % (name)) - 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): raise TypeError("Only works with TransparentEF") return self.card._scc.read_binary(self.selected_file.fid, length, offset) def read_binary_dec(self): + """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) print("%s: %s -> %s" % (sw, data, dec_data)) 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): raise TypeError("Only works with TransparentEF") return self.card._scc.update_binary(self.selected_file.fid, data_hex, offset) def update_binary_dec(self, data): + """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) print("%s -> %s" % (data, 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): 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) - def read_record_dec(self, rec_nr=0): + def read_record_dec(self, rec_nr:int=0): + """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), sw) - def update_record(self, rec_nr, data_hex): + def update_record(self, rec_nr:int, data_hex): + """Update a record with given binary data + + Args: + rec_nr : Record number to read + data_hex : Hex string binary data to be written + """ if not isinstance(self.selected_file, LinFixedEF): raise TypeError("Only works with Linear Fixed EF") return self.card._scc.update_record(self.selected_file.fid, rec_nr, data_hex) - def update_record_dec(self, rec_nr, data): + def update_record_dec(self, rec_nr:int, data): + """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) return self.update_record(self, rec_nr, data_hex) @@ -694,9 +1011,15 @@ self.fcp = None -def interpret_sw(sw_data, sw): - """Interpret a given status word within the profile. Returns tuple of - two strings""" +def interpret_sw(sw_data:dict, sw:str): + """Interpret a given status word. + + 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(): # first try direct match if sw in swdict: @@ -710,7 +1033,12 @@ class CardApplication(object): """A card application is represented by an ADF (with contained hierarchy) and optionally 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.adf = adf self.sw = sw or dict() @@ -719,8 +1047,14 @@ return "APP(%s)" % (self.name) def interpret_sw(self, sw): - """Interpret a given status word within the application. Returns tuple of - two strings""" + """Interpret a given status word within the application. + + Args: + sw : Status word as string of 4 hexd digits + + Returns: + Tuple of two strings + """ return interpret_sw(self.sw, sw) class CardProfile(object): @@ -728,6 +1062,14 @@ applications as well as profile-specific SW and shell commands. Every card has one card profile, but there may be multiple applications within that profile.""" def __init__(self, name, **kw): + """ + Args: + desc (str) : Description + files_in_mf : List of CardEF instances present in MF + applications : List of CardApplications present on card + sw : List of status word definitions + shell_cmdsets : List of cmd2 shell command sets of profile-specific commands + """ self.name = name self.desc = kw.get("desc", None) self.files_in_mf = kw.get("files_in_mf", []) @@ -739,9 +1081,20 @@ return self.name def add_application(self, app): + """Add an application to a card profile. + + Args: + app : CardApplication instance to be added to profile + """ self.applications.append(app) - def interpret_sw(self, sw): - """Interpret a given status word within the profile. Returns tuple of - two strings""" + def interpret_sw(self, sw:str): + """Interpret a given status word within the profile. + + Args: + sw : Status word as string of 4 hexd digits + + Returns: + Tuple of two strings + """ return interpret_sw(self.sw, sw) diff --git a/pySim/transport/__init__.py b/pySim/transport/__init__.py index d720259..f946af8 100644 --- a/pySim/transport/__init__.py +++ b/pySim/transport/__init__.py @@ -24,48 +24,53 @@ # class LinkBase(object): + """Base class for link/transport to card.""" - def wait_for_card(self, timeout=None, newcardonly=False): - """wait_for_card(): Wait for a card and connect to it + def wait_for_card(self, timeout:int=None, newcardonly:bool=False): + """Wait for a card and connect to it - timeout : Maximum wait time (None=no timeout) - newcardonly : Should we wait for a new card, or an already - inserted one ? + Args: + timeout : Maximum wait time in seconds (None=no timeout) + newcardonly : Should we wait for a new card, or an already inserted one ? """ pass def connect(self): - """connect(): Connect to a card immediately + """Connect to a card immediately """ pass def disconnect(self): - """disconnect(): Disconnect from card + """Disconnect from card """ pass def reset_card(self): - """reset_card(): Resets the card (power down/up) + """Resets the card (power down/up) """ pass - def send_apdu_raw(self, pdu): - """send_apdu_raw(pdu): Sends an APDU with minimal processing + def send_apdu_raw(self, pdu:str): + """Sends an APDU with minimal processing - pdu : string of hexadecimal characters (ex. "A0A40000023F00") - return : tuple(data, sw), where - data : string (in hex) of returned data (ex. "074F4EFFFF") - sw : string (in hex) of status word (ex. "9000") + Args: + pdu : string of hexadecimal characters (ex. "A0A40000023F00") + Returns: + tuple(data, sw), where + data : string (in hex) of returned data (ex. "074F4EFFFF") + sw : string (in hex) of status word (ex. "9000") """ pass 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") - return : tuple(data, sw), where - data : string (in hex) of returned data (ex. "074F4EFFFF") - sw : string (in hex) of status word (ex. "9000") + Args: + pdu : string of hexadecimal characters (ex. "A0A40000023F00") + Returns: + 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) @@ -82,15 +87,16 @@ return data, sw 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") - sw : string of 4 hexadecimal characters (ex. "9000"). The - user may mask out certain digits using a '?' to add some - ambiguity if needed. - return : tuple(data, sw), where - data : string (in hex) of returned data (ex. "074F4EFFFF") - sw : string (in hex) of status word (ex. "9000") + Args: + pdu : string of hexadecimal characters (ex. "A0A40000023F00") + sw : string of 4 hexadecimal characters (ex. "9000"). The user may mask out certain + digits using a '?' to add some ambiguity if needed. + Returns: + tuple(data, sw), where + data : string (in hex) of returned data (ex. "074F4EFFFF") + sw : string (in hex) of status word (ex. "9000") """ rv = self.send_apdu(pdu) diff --git a/pySim/transport/calypso.py b/pySim/transport/calypso.py index 7f99d21..467d5ee 100644 --- a/pySim/transport/calypso.py +++ b/pySim/transport/calypso.py @@ -1,9 +1,5 @@ # -*- coding: utf-8 -*- -""" pySim: Transport Link for Calypso bases phones -""" - -# # Copyright (C) 2018 Vadim Yanitskiy <axilirator at gmail.com> # # This program is free software: you can redistribute it and/or modify @@ -73,8 +69,9 @@ self.data += pdu 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 if not os.path.exists(sock_path): raise ReaderError("There is no such ('%s') UNIX socket" % sock_path) @@ -119,7 +116,6 @@ pass # Nothing to do really ... def send_apdu_raw(self, pdu): - """see LinkBase.send_apdu_raw""" # Request FULL reset req_msg = L1CTLMessageSIM(h2b(pdu)) diff --git a/pySim/transport/modem_atcmd.py b/pySim/transport/modem_atcmd.py index 86d4443..fccd388 100644 --- a/pySim/transport/modem_atcmd.py +++ b/pySim/transport/modem_atcmd.py @@ -1,8 +1,5 @@ # -*- coding: utf-8 -*- -""" pySim: Transport Link for 3GPP TS 27.007 compliant modems -""" - # Copyright (C) 2020 Vadim Yanitskiy <axilirator at gmail.com> # # This program is free software: you can redistribute it and/or modify @@ -31,7 +28,8 @@ # log.root.setLevel(log.DEBUG) 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._device = device self._atr = None diff --git a/pySim/transport/pcsc.py b/pySim/transport/pcsc.py index 2c2cbb9..f08f71a 100644 --- a/pySim/transport/pcsc.py +++ b/pySim/transport/pcsc.py @@ -1,9 +1,5 @@ # -*- coding: utf-8 -*- -""" pySim: PCSC reader transport link -""" - -# # Copyright (C) 2009-2010 Sylvain Munaut <tnt at 246tNt.com> # Copyright (C) 2010 Harald Welte <laforge at gnumonks.org> # @@ -32,8 +28,9 @@ class PcscSimLink(LinkBase): + """ pySim: PCSC reader transport link.""" - def __init__(self, reader_number=0): + def __init__(self, reader_number:int=0): r = readers() self._reader = r[reader_number] self._con = self._reader.createConnection() @@ -46,7 +43,7 @@ pass 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) try: cr.waitforcard() @@ -75,7 +72,6 @@ return 1 def send_apdu_raw(self, pdu): - """see LinkBase.send_apdu_raw""" apdu = h2i(pdu) diff --git a/pySim/transport/serial.py b/pySim/transport/serial.py index 03d3f38..6d39303 100644 --- a/pySim/transport/serial.py +++ b/pySim/transport/serial.py @@ -1,9 +1,5 @@ # -*- coding: utf-8 -*- -""" pySim: Transport Link for serial (RS232) based readers included with simcard -""" - -# # Copyright (C) 2009-2010 Sylvain Munaut <tnt at 246tNt.com> # # This program is free software: you can redistribute it and/or modify @@ -30,8 +26,10 @@ 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): raise ValueError("device file %s does not exist -- abort" % device) self._sl = serial.Serial( @@ -183,7 +181,6 @@ return self._sl.read() def send_apdu_raw(self, pdu): - """see LinkBase.send_apdu_raw""" pdu = h2b(pdu) data_len = ord(pdu[4]) # P3 diff --git a/pySim/utils.py b/pySim/utils.py index 0848b01..607526c 100644 --- a/pySim/utils.py +++ b/pySim/utils.py @@ -21,43 +21,65 @@ # -def h2b(s): +def h2b(s: str) -> bytearray: """convert from a string of hex nibbles to a sequence of bytes""" return bytearray.fromhex(s) -def b2h(b): +def b2h(b: bytearray) -> str: """convert from a sequence of bytes to a string of hex nibbles""" 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])] -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]) -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]) 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.extend(map(ord, s)) return b2h(b) # 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]) -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])]) -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)) -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 -def half_round_up(n): +def half_round_up(n:int) -> int: return (n + 1)//2 # IMSI encoded format: @@ -75,8 +97,8 @@ # 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. -def enc_imsi(imsi): - """Converts a string imsi into the value of the EF""" +def enc_imsi(imsi:str): + """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 oe = len(imsi) & 1 # Odd (1) / Even (0) ei = '%02x' % l + swap_nibbles('%01x%s' % ((oe<<3)|1, rpad(imsi, 15))) @@ -781,7 +803,7 @@ return None -def sw_match(sw, pattern): +def sw_match(sw:str, pattern:str) -> str: """Match given SW against given pattern.""" # Create a masked version of the returned status word sw_lower = sw.lower() @@ -796,8 +818,18 @@ # Compare the masked version against the pattern return sw_masked == pattern -def tabulate_str_list(str_list, width = 79, hspace = 2, lspace = 1, align_left = True): - """Pretty print a list of strings into a tabulated form""" +def tabulate_str_list(str_list, width:int = 79, hspace:int = 2, lspace:int = 1, + 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: return "" if len(str_list) <= 0: -- To view, visit https://gerrit.osmocom.org/c/pysim/+/23577 To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings Gerrit-Project: pysim Gerrit-Branch: master Gerrit-Change-Id: I6ac88e0662cf3c56ae32d86d50b18a8b4150571a Gerrit-Change-Number: 23577 Gerrit-PatchSet: 1 Gerrit-Owner: laforge <laforge at osmocom.org> Gerrit-MessageType: newchange -------------- next part -------------- An HTML attachment was scrubbed... URL: <http://lists.osmocom.org/pipermail/gerrit-log/attachments/20210402/55ccbf25/attachment.htm>