diff --git a/docs/shell.rst b/docs/shell.rst index 4cdf9e02..51709996 100644 --- a/docs/shell.rst +++ b/docs/shell.rst @@ -334,6 +334,43 @@ to the SIM card. This allows for easy interactive modification of file contents. + +BER-TLV EF commands +------------------- + +BER-TLV EFs are files that contain BER-TLV structured data. Every file can contain any number +of variable-length IEs (DOs). The tag within a BER-TLV EF must be unique within the file. + +The commands below become enabled only when your currently selected file is of *BER-TLV EF* type. + +retrieve_tags +~~~~~~~~~~~~~ + +Retrieve a list of all tags present in the currently selected file. + + +retrieve_data +~~~~~~~~~~~~~ +.. argparse:: + :module: pySim.filesystem + :func: BerTlvEF.ShellCommands.retrieve_data_parser + + +set_data +~~~~~~~~ +.. argparse:: + :module: pySim.filesystem + :func: BerTlvEF.ShellCommands.set_data_parser + + +del_data +~~~~~~~~ +.. argparse:: + :module: pySim.filesystem + :func: BerTlvEF.ShellCommands.del_data_parser + + + USIM commands ------------- diff --git a/pySim-shell.py b/pySim-shell.py index 659c12c0..5b5768a0 100755 --- a/pySim-shell.py +++ b/pySim-shell.py @@ -38,7 +38,7 @@ from pySim.exceptions import * from pySim.commands import SimCardCommands from pySim.transport import init_reader, ApduTracer, argparse_add_reader_args from pySim.cards import card_detect, Card -from pySim.utils import h2b, swap_nibbles, rpad, h2s, JsonEncoder +from pySim.utils import h2b, swap_nibbles, rpad, b2h, h2s, JsonEncoder, bertlv_parse_one from pySim.utils import dec_st, sanitize_pin_adm, tabulate_str_list, is_hex, boxed_heading_str from pySim.card_handler import card_handler @@ -256,11 +256,19 @@ class PySimCommands(CommandSet): if structure == 'transparent': result = self._cmd.rs.read_binary() self._cmd.poutput("update_binary " + str(result[0])) - if structure == 'cyclic' or structure == 'linear_fixed': + elif structure == 'cyclic' or structure == 'linear_fixed': num_of_rec = fd['num_of_rec'] for r in range(1, num_of_rec + 1): result = self._cmd.rs.read_record(r) self._cmd.poutput("update_record %d %s" % (r, str(result[0]))) + elif structure == 'ber_tlv': + tags = self._cmd.rs.retrieve_tags() + for t in tags: + result = self._cmd.rs.retrieve_data(t) + (tag, l, val) = bertlv_parse_one(h2b(result[0])) + self._cmd.poutput("set_data 0x%02x %s" % (t, b2h(val))) + else: + raise RuntimeError('Unsupported structure "%s" of file "%s"' % (structure, filename)) except Exception as e: bad_file_str = '/'.join(df_path_list) + "/" + str(filename) + ", " + str(e) self._cmd.poutput("# bad file: %s" % bad_file_str) diff --git a/pySim/commands.py b/pySim/commands.py index 33aec125..0b3d9b61 100644 --- a/pySim/commands.py +++ b/pySim/commands.py @@ -5,7 +5,7 @@ # # Copyright (C) 2009-2010 Sylvain Munaut -# Copyright (C) 2010 Harald Welte +# Copyright (C) 2010-2021 Harald Welte # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -23,7 +23,7 @@ from construct import * from pySim.construct import LV -from pySim.utils import rpad, b2h, sw_match +from pySim.utils import rpad, b2h, h2b, sw_match, bertlv_encode_len from pySim.exceptions import SwMatchError class SimCardCommands(object): @@ -269,6 +269,76 @@ class SimCardCommands(object): r = self.select_path(ef) return self.__len(r) + # TS 102 221 Section 11.3.1 low-level helper + def _retrieve_data(self, tag:int, first:bool=True): + if first: + pdu = '80cb008001%02x' % (tag) + else: + pdu = '80cb000000' + return self._tp.send_apdu_checksw(pdu) + + # TS 102 221 Section 11.3.1 + def retrieve_data(self, ef, tag:int): + """Execute RETRIEVE DATA. + + Args + ef : string or list of strings indicating name or path of transparent EF + tag : BER-TLV Tag of value to be retrieved + """ + r = self.select_path(ef) + if len(r[-1]) == 0: + return (None, None) + total_data = '' + # retrieve first block + data, sw = self._retrieve_data(tag, first=True) + total_data += data + while sw == '62f1' or sw == '62f2': + data, sw = self._retrieve_data(tag, first=False) + total_data += data + return total_data, sw + + # TS 102 221 Section 11.3.2 low-level helper + def _set_data(self, data:str, first:bool=True): + if first: + p1 = 0x80 + else: + p1 = 0x00 + if isinstance(data, bytes) or isinstance(data, bytearray): + data = b2h(data) + pdu = '80db00%02x%02x%s' % (p1, len(data)//2, data) + return self._tp.send_apdu_checksw(pdu) + + def set_data(self, ef, tag:int, value:str, verify:bool=False, conserve:bool=False): + """Execute SET DATA. + + Args + ef : string or list of strings indicating name or path of transparent EF + tag : BER-TLV Tag of value to be stored + value : BER-TLV value to be stored + """ + r = self.select_path(ef) + if len(r[-1]) == 0: + return (None, None) + + # in case of deleting the data, we only have 'tag' but no 'value' + if not value: + return self._set_data('%02x' % tag, first=True) + + # FIXME: proper BER-TLV encode + tl = '%02x%s' % (tag, b2h(bertlv_encode_len(len(value)//2))) + tlv = tl + value + tlv_bin = h2b(tlv) + + first = True + total_len = len(tlv_bin) + remaining = tlv_bin + while len(remaining) > 0: + fragment = remaining[:255] + rdata, sw = self._set_data(fragment, first=first) + first = False + remaining = remaining[255:] + return rdata, sw + def run_gsm(self, rand:str): """Execute RUN GSM ALGORITHM.""" if len(rand) != 32: diff --git a/pySim/filesystem.py b/pySim/filesystem.py index edfe85dc..b3e28efd 100644 --- a/pySim/filesystem.py +++ b/pySim/filesystem.py @@ -34,7 +34,7 @@ import argparse from typing import cast, Optional, Iterable, List, Any, Dict, Tuple -from pySim.utils import sw_match, h2b, b2h, is_hex +from pySim.utils import sw_match, h2b, b2h, is_hex, auto_int, bertlv_parse_one from pySim.construct import filter_dict from pySim.exceptions import * from pySim.jsonpath import js_path_find, js_path_modify @@ -914,7 +914,7 @@ class TransRecEF(TransparentEF): return b''.join(chunks) -class BerTlvEF(TransparentEF): +class BerTlvEF(CardEF): """BER-TLV EF (Entry File) in the smart card filesystem. A BER-TLV EF is a binary file with a BER (Basic Encoding Rules) TLV structure @@ -922,6 +922,61 @@ class BerTlvEF(TransparentEF): around TransparentEF as a place-holder, so we can already define EFs of BER-TLV type without fully supporting them.""" + @with_default_category('BER-TLV EF Commands') + class ShellCommands(CommandSet): + """Shell commands specific for BER-TLV EFs.""" + def __init__(self): + super().__init__() + + retrieve_data_parser = argparse.ArgumentParser() + retrieve_data_parser.add_argument('tag', type=auto_int, help='BER-TLV Tag of value to retrieve') + @cmd2.with_argparser(retrieve_data_parser) + def do_retrieve_data(self, opts): + """Retrieve (Read) data from a BER-TLV EF""" + (data, sw) = self._cmd.rs.retrieve_data(opts.tag) + self._cmd.poutput(data) + + def do_retrieve_tags(self, opts): + """List tags available in a given BER-TLV EF""" + tags = self._cmd.rs.retrieve_tags() + self._cmd.poutput(tags) + + set_data_parser = argparse.ArgumentParser() + set_data_parser.add_argument('tag', type=auto_int, help='BER-TLV Tag of value to set') + set_data_parser.add_argument('data', help='Data bytes (hex format) to write') + @cmd2.with_argparser(set_data_parser) + def do_set_data(self, opts): + """Set (Write) data for a given tag in a BER-TLV EF""" + (data, sw) = self._cmd.rs.set_data(opts.tag, opts.data) + if data: + self._cmd.poutput(data) + + del_data_parser = argparse.ArgumentParser() + del_data_parser.add_argument('tag', type=auto_int, help='BER-TLV Tag of value to set') + @cmd2.with_argparser(del_data_parser) + def do_delete_data(self, opts): + """Delete data for a given tag in a BER-TLV EF""" + (data, sw) = self._cmd.rs.set_data(opts.tag, None) + if data: + self._cmd.poutput(data) + + + 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 : Description 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._construct = None + self.size = size + self.shell_commands = [self.ShellCommands()] + class RuntimeState(object): """Represent the runtime state of a session with a card.""" @@ -1172,6 +1227,43 @@ class RuntimeState(object): data_hex = self.selected_file.encode_record_hex(data) return self.update_record(rec_nr, data_hex) + def retrieve_data(self, tag:int=0): + """Read a DO/TLV as binary data. + + Args: + tag : Tag of TLV/DO to read + Returns: + hex string of full BER-TLV DO including Tag and Length + """ + if not isinstance(self.selected_file, BerTlvEF): + raise TypeError("Only works with BER-TLV EF") + # returns a string of hex nibbles + return self.card._scc.retrieve_data(self.selected_file.fid, tag) + + def retrieve_tags(self): + """Retrieve tags available on BER-TLV EF. + + Returns: + list of integer tags contained in EF + """ + if not isinstance(self.selected_file, BerTlvEF): + raise TypeError("Only works with BER-TLV EF") + data, sw = self.card._scc.retrieve_data(self.selected_file.fid, 0x5c) + tag, length, value = bertlv_parse_one(h2b(data)) + return list(value) + + def set_data(self, tag:int, data_hex:str): + """Update a TLV/DO with given binary data + + Args: + tag : Tag of TLV/DO to be written + data_hex : Hex string binary data to be written (value portion) + """ + if not isinstance(self.selected_file, BerTlvEF): + raise TypeError("Only works with BER-TLV EF") + return self.card._scc.set_data(self.selected_file.fid, tag, data_hex, conserve=self.conserve_write) + + class FileData(object): diff --git a/pySim/ts_102_221.py b/pySim/ts_102_221.py index 2c335a69..bb49fe5e 100644 --- a/pySim/ts_102_221.py +++ b/pySim/ts_102_221.py @@ -113,10 +113,14 @@ def interpret_file_descriptor(in_hex): 1: 'transparent', 2: 'linear_fixed', 6: 'cyclic', + 0x39: 'ber_tlv', } fdb = in_bin[0] ftype = (fdb >> 3) & 7 - fstruct = fdb & 7 + if fdb & 0xbf == 0x39: + fstruct = 0x39 + else: + fstruct = fdb & 7 out['shareable'] = True if fdb & 0x40 else False out['file_type'] = ft_dict[ftype] if ftype in ft_dict else ftype out['structure'] = fs_dict[fstruct] if fstruct in fs_dict else fstruct diff --git a/pySim/utils.py b/pySim/utils.py index 3e673865..a3cd1b54 100644 --- a/pySim/utils.py +++ b/pySim/utils.py @@ -89,6 +89,85 @@ def lpad(s:str, l:int, c='f') -> str: def half_round_up(n:int) -> int: return (n + 1)//2 +######################################################################### +# poor man's BER-TLV decoder. To be a more sophisticated OO library later +######################################################################### + +def bertlv_parse_tag(binary:bytes) -> Tuple[dict, bytes]: + """Parse a single Tag value according to ITU-T X.690 8.1.2 + Args: + binary : binary input data of BER-TLV length field + Returns: + Tuple of ({class:int, constructed:bool, tag:int}, remainder:bytes) + """ + cls = binary[0] >> 6 + constructed = True if binary[0] & 0x20 else False + tag = binary[0] & 0x1f + if tag <= 30: + return ({'class':cls, 'constructed':constructed, 'tag': tag}, binary[1:]) + else: # multi-byte tag + tag = 0 + i = 1 + last = False + while not last: + last = False if binary[i] & 0x80 else True + tag <<= 7 + tag |= binary[i] & 0x7f + i += 1 + return ({'class':cls, 'constructed':constructed, 'tag':tag}, binary[i:]) + +def bertlv_parse_len(binary:bytes) -> Tuple[int, bytes]: + """Parse a single Length value according to ITU-T X.690 8.1.3; + only the definite form is supported here. + Args: + binary : binary input data of BER-TLV length field + Returns: + Tuple of (length, remainder) + """ + if binary[0] < 0x80: + return (binary[0], binary[1:]) + else: + num_len_oct = binary[0] & 0x7f + length = 0 + for i in range(1, 1+num_len_oct): + length <<= 8 + length |= binary[i] + return (length, binary[num_len_oct:]) + +def bertlv_encode_len(length:int) -> bytes: + """Encode a single Length value according to ITU-T X.690 8.1.3; + only the definite form is supported here. + Args: + length : length value to be encoded + Returns: + binary output data of BER-TLV length field + """ + if length < 0x80: + return length.to_bytes(1, 'big') + elif length <= 0xff: + return b'\x81' + length.to_bytes(1, 'big') + elif length <= 0xffff: + return b'\x82' + length.to_bytes(2, 'big') + elif length <= 0xffffff: + return b'\x83' + length.to_bytes(3, 'big') + elif length <= 0xffffffff: + return b'\x84' + length.to_bytes(4, 'big') + else: + raise ValueError("Length > 32bits not supported") + +def bertlv_parse_one(binary:bytes) -> (dict, int, bytes): + """Parse a single TLV IE at the start of the given binary data. + Args: + binary : binary input data of BER-TLV length field + Returns: + Tuple of (tag:dict, len:int, remainder:bytes) + """ + (tagdict, remainder) = bertlv_parse_tag(binary) + (length, remainder) = bertlv_parse_len(remainder) + return (tagdict, length, remainder) + + + # IMSI encoded format: # For IMSI 0123456789ABCDE: # @@ -894,6 +973,10 @@ def tabulate_str_list(str_list, width:int = 79, hspace:int = 2, lspace:int = 1, table.append(format_str_row % tuple(str_list_row)) return '\n'.join(table) +def auto_int(x): + """Helper function for argparse to accept hexadecimal integers.""" + return int(x, 0) + class JsonEncoder(json.JSONEncoder): """Extend the standard library JSONEncoder with support for more types.""" def default(self, o):