diff --git a/pySim-shell.py b/pySim-shell.py index e58151b3..12f35fd9 100755 --- a/pySim-shell.py +++ b/pySim-shell.py @@ -49,6 +49,7 @@ from pySim.profile import CardProfile from pySim.ts_51_011 import CardProfileSIM, DF_TELECOM, DF_GSM from pySim.ts_102_221 import CardProfileUICC from pySim.ts_102_221 import CardProfileUICCSIM +from pySim.ts_102_222 import Ts102222Commands from pySim.ts_31_102 import CardApplicationUSIM from pySim.ts_31_103 import CardApplicationISIM from pySim.ara_m import CardApplicationARAM @@ -174,6 +175,7 @@ class PysimApp(cmd2.Cmd): 'conserve_write', False, self.conserve_write) self._onchange_apdu_trace('apdu_trace', False, self.apdu_trace) self.register_command_set(Iso7816Commands()) + self.register_command_set(Ts102222Commands()) self.register_command_set(PySimCommands()) self.iccid, sw = self.card.read_iccid() rs.select('MF', self) diff --git a/pySim/commands.py b/pySim/commands.py index 5e09eeeb..df233939 100644 --- a/pySim/commands.py +++ b/pySim/commands.py @@ -448,6 +448,26 @@ class SimCardCommands(object): """ return self._tp.send_apdu_checksw(self.cla_byte + '44000002' + fid) + def create_file(self, payload: Hexstr): + """Execute CREEATE FILE command as per TS 102 222 Section 6.3""" + return self._tp.send_apdu_checksw(self.cla_byte + 'e00000%02x%s' % (len(payload)//2, payload)) + + def delete_file(self, fid): + """Execute DELETE FILE command as per TS 102 222 Section 6.4""" + return self._tp.send_apdu_checksw(self.cla_byte + 'e4000002' + fid) + + def terminate_df(self, fid): + """Execute TERMINATE DF command as per TS 102 222 Section 6.7""" + return self._tp.send_apdu_checksw(self.cla_byte + 'e6000002' + fid) + + def terminate_ef(self, fid): + """Execute TERMINATE EF command as per TS 102 222 Section 6.8""" + return self._tp.send_apdu_checksw(self.cla_byte + 'e8000002' + fid) + + def terminate_card_usage(self): + """Execute TERMINATE CARD USAGE command as per TS 102 222 Section 6.9""" + return self._tp.send_apdu_checksw(self.cla_byte + 'fe000000') + def manage_channel(self, mode='open', lchan_nr=0): """Execute MANAGE CHANNEL command as per TS 102 221 Section 11.1.17. diff --git a/pySim/filesystem.py b/pySim/filesystem.py index a354dfbe..8303a4b3 100644 --- a/pySim/filesystem.py +++ b/pySim/filesystem.py @@ -1475,6 +1475,10 @@ class RuntimeState(object): (data, sw) = self.card._scc.status() return self.selected_file.decode_select_response(data) + def get_file_for_selectable(self, name: str): + sels = self.selected_file.get_selectables() + return sels[name] + def activate_file(self, name: str): """Request ACTIVATE FILE of specified file.""" sels = self.selected_file.get_selectables() diff --git a/pySim/ts_102_221.py b/pySim/ts_102_221.py index 022a4a77..ac2a60e4 100644 --- a/pySim/ts_102_221.py +++ b/pySim/ts_102_221.py @@ -107,11 +107,11 @@ class FileDescriptor(BER_TLV_IE, tag=0x82): # ETSI TS 102 221 11.1.1.4.4 class FileIdentifier(BER_TLV_IE, tag=0x83): - _construct = GreedyBytes + _construct = HexAdapter(GreedyBytes) # ETSI TS 102 221 11.1.1.4.5 class DfName(BER_TLV_IE, tag=0x84): - _construct = GreedyBytes + _construct = HexAdapter(GreedyBytes) # ETSI TS 102 221 11.1.1.4.6.1 class UiccCharacteristics(BER_TLV_IE, tag=0x80): @@ -157,12 +157,18 @@ class SpecificUiccEnvironmentConditions(BER_TLV_IE, tag=0x88): class Platform2PlatformCatSecuredApdu(BER_TLV_IE, tag=0x89): _construct = GreedyBytes +# sysmoISIM-SJA2 specific +class ToolkitAccessConditions(BER_TLV_IE, tag=0xD2): + _construct = FlagsEnum(Byte, rfm_create=1, rfm_delete_terminate=2, other_applet_create=4, + other_applet_delete_terminate=8) + # ETSI TS 102 221 11.1.1.4.6.0 class ProprietaryInformation(BER_TLV_IE, tag=0xA5, nested=[UiccCharacteristics, ApplicationPowerConsumption, MinApplicationClockFrequency, AvailableMemory, FileDetails, ReservedFileSize, MaximumFileSize, - SupportedFilesystemCommands, SpecificUiccEnvironmentConditions]): + SupportedFilesystemCommands, SpecificUiccEnvironmentConditions, + ToolkitAccessConditions]): pass # ETSI TS 102 221 11.1.1.4.7.1 @@ -176,11 +182,11 @@ class SecurityAttribExpanded(BER_TLV_IE, tag=0xab): # ETSI TS 102 221 11.1.1.4.7.3 class SecurityAttribReferenced(BER_TLV_IE, tag=0x8b): # TODO: longer format with SEID - _construct = Struct('ef_arr_file_id'/Bytes(2), 'ef_arr_record_nr'/Int8ub) + _construct = Struct('ef_arr_file_id'/HexAdapter(Bytes(2)), 'ef_arr_record_nr'/Int8ub) # ETSI TS 102 221 11.1.1.4.8 class ShortFileIdentifier(BER_TLV_IE, tag=0x88): - _construct = Byte + _construct = HexAdapter(COptional(Bytes(1))) # ETSI TS 102 221 11.1.1.4.9 class LifeCycleStatusInteger(BER_TLV_IE, tag=0x8A): @@ -202,6 +208,23 @@ class LifeCycleStatusInteger(BER_TLV_IE, tag=0x8A): ret = lcsi self.decoded = ret return self.decoded + def _to_bytes(self): + if self.decoded == 'no_information': + return b'\x00' + elif self.decoded == 'creation': + return b'\x01' + elif self.decoded == 'initialization': + return b'\x03' + elif self.decoded == 'operational_activated': + return b'\x05' + elif self.decoded == 'operational_deactivated': + return b'\x04' + elif self.decoded == 'termination': + return b'\x0c' + elif isinstance(self.decoded, int): + return self.decoded.to_bytes(1, 'big') + else: + raise ValueError # ETSI TS 102 221 11.1.1.4.9 class PS_DO(BER_TLV_IE, tag=0x90): diff --git a/pySim/ts_102_222.py b/pySim/ts_102_222.py new file mode 100644 index 00000000..2c1d600e --- /dev/null +++ b/pySim/ts_102_222.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 + +# Interactive shell for working with SIM / UICC / USIM / ISIM cards +# +# (C) 2022 by 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 +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from typing import List + +import cmd2 +from cmd2 import style, fg, bg +from cmd2 import CommandSet, with_default_category, with_argparser +import argparse + +from pySim.ts_31_102 import EF_UST_map, EF_USIM_ADF_map +from pySim.ts_31_103 import EF_IST_map, EF_ISIM_ADF_map + +from pySim.exceptions import * +from pySim.utils import h2b, swap_nibbles, b2h, JsonEncoder + +from pySim.ts_102_221 import * + +@with_default_category('TS 102 222 Administrative Commands') +class Ts102222Commands(CommandSet): + """Administrative commands for telecommunication applications.""" + + def __init__(self): + super().__init__() + + delfile_parser = argparse.ArgumentParser() + delfile_parser.add_argument('--force-delete', action='store_true', + help='I really want to permanently delete the file. I know pySim cannot re-create it yet!') + delfile_parser.add_argument('NAME', type=str, help='File name or FID to delete') + + @cmd2.with_argparser(delfile_parser) + def do_delete_file(self, opts): + """Delete the specified file. DANGEROUS! See TS 102 222 Section 6.4. + This will permanently delete the specified file from the card. + pySim has no support to re-create files yet, and even if it did, your card may not allow it!""" + if not opts.force_delete: + self._cmd.perror("Refusing to permanently delete the file, please read the help text.") + return + f = self._cmd.rs.get_file_for_selectable(opts.NAME) + (data, sw) = self._cmd.card._scc.delete_file(f.fid) + + def complete_delete_file(self, text, line, begidx, endidx) -> List[str]: + """Command Line tab completion for DELETE FILE""" + index_dict = {1: self._cmd.rs.selected_file.get_selectable_names()} + return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict) + + termdf_parser = argparse.ArgumentParser() + termdf_parser.add_argument('--force', action='store_true', + help='I really want to terminate the file. I know I can not recover from it!') + termdf_parser.add_argument('NAME', type=str, help='File name or FID') + + @cmd2.with_argparser(termdf_parser) + def do_terminate_df(self, opts): + """Terminate the specified DF. DANGEROUS! See TS 102 222 6.7. + This is a permanent, one-way operation on the card. There is no undo, you can not recover + a terminated DF. The only permitted command for a terminated DF is the DLETE FILE command.""" + if not opts.force: + self._cmd.perror("Refusing to terminate the file, please read the help text.") + return + f = self._cmd.rs.get_file_for_selectable(opts.NAME) + (data, sw) = self._cmd.card._scc.terminate_df(f.fid) + + def complete_terminate_df(self, text, line, begidx, endidx) -> List[str]: + """Command Line tab completion for TERMINATE DF""" + index_dict = {1: self._cmd.rs.selected_file.get_selectable_names()} + return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict) + + @cmd2.with_argparser(termdf_parser) + def do_terminate_ef(self, opts): + """Terminate the specified EF. DANGEROUS! See TS 102 222 6.8. + This is a permanent, one-way operation on the card. There is no undo, you can not recover + a terminated EF. The only permitted command for a terminated EF is the DLETE FILE command.""" + if not opts.force: + self._cmd.perror("Refusing to terminate the file, please read the help text.") + return + f = self._cmd.rs.get_file_for_selectable(opts.NAME) + (data, sw) = self._cmd.card._scc.terminate_ef(f.fid) + + def complete_terminate_ef(self, text, line, begidx, endidx) -> List[str]: + """Command Line tab completion for TERMINATE EF""" + index_dict = {1: self._cmd.rs.selected_file.get_selectable_names()} + return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict) + + tcard_parser = argparse.ArgumentParser() + tcard_parser.add_argument('--force-terminate-card', action='store_true', + help='I really want to permanently terminate the card. It will not be usable afterwards!') + + @cmd2.with_argparser(tcard_parser) + def do_terminate_card_usage(self, opts): + """Terminate the Card. SUPER DANGEROUS! See TS 102 222 Section 6.9. + This will permanently brick the card and can NOT be recovered from!""" + if not opts.force_terminate_card: + self._cmd.perror("Refusing to permanently terminate the card, please read the help text.") + return + (data, sw) = self._cmd.card._scc.terminate_card_usage() + + create_parser = argparse.ArgumentParser() + create_parser.add_argument('FILE_ID', type=str, help='File Identifier as 4-character hex string') + create_parser._action_groups.pop() + create_required = create_parser.add_argument_group('required arguments') + create_optional = create_parser.add_argument_group('optional arguments') + create_required.add_argument('--ef-arr-file-id', required=True, type=str, help='Referenced Security: File Identifier of EF.ARR') + create_required.add_argument('--ef-arr-record-nr', required=True, type=int, help='Referenced Security: Record Number within EF.ARR') + create_required.add_argument('--file-size', required=True, type=int, help='Size of file in octets') + create_required.add_argument('--structure', required=True, type=str, choices=['transparent', 'linear_fixed', 'ber_tlv'], + help='Structure of the to-be-created EF') + create_optional.add_argument('--short-file-id', type=str, help='Short File Identifier as 2-digit hex string') + create_optional.add_argument('--shareable', action='store_true', help='Should the file be shareable?') + create_optional.add_argument('--record-length', type=int, help='Length of each record in octets') + + @cmd2.with_argparser(create_parser) + def do_create_ef(self, opts): + """Create a new EF below the currently selected DF. Requires related privileges.""" + file_descriptor = { + 'file_descriptor_byte': { + 'shareable': opts.shareable, + 'file_type': 'working_ef', + 'structure': opts.structure, + } + } + if opts.structure == 'linear_fixed': + if not opts.record_length: + self._cmd.perror("you must specify the --record-length for linear fixed EF") + return + file_descriptor['record_len'] = opts.record_length + elif opts.structure == 'ber_tlv': + self._cmd.perror("BER-TLV creation not yet fully supported, sorry") + return + ies = [FileDescriptor(decoded=file_descriptor), FileIdentifier(decoded=opts.FILE_ID), + LifeCycleStatusInteger(decoded='operational_activated'), + SecurityAttribReferenced(decoded={'ef_arr_file_id': opts.ef_arr_file_id, + 'ef_arr_record_nr': opts.ef_arr_record_nr }), + FileSize(decoded=opts.file_size), + ShortFileIdentifier(decoded=opts.short_file_id), + ] + fcp = FcpTemplate(children=ies) + (data, sw) = self._cmd.card._scc.create_file(b2h(fcp.to_tlv())) + # the newly-created file is automatically selected but our runtime state knows nothing of it + self._cmd.rs.select_file(self._cmd.rs.selected_file) + + createdf_parser = argparse.ArgumentParser() + createdf_parser.add_argument('FILE_ID', type=str, help='File Identifier as 4-character hex string') + createdf_parser._action_groups.pop() + createdf_required = createdf_parser.add_argument_group('required arguments') + createdf_optional = createdf_parser.add_argument_group('optional arguments') + createdf_sja_optional = createdf_parser.add_argument_group('sysmoISIM-SJA optional arguments') + createdf_required.add_argument('--ef-arr-file-id', required=True, type=str, help='Referenced Security: File Identifier of EF.ARR') + createdf_required.add_argument('--ef-arr-record-nr', required=True, type=int, help='Referenced Security: Record Number within EF.ARR') + createdf_optional.add_argument('--shareable', action='store_true', help='Should the file be shareable?') + createdf_optional.add_argument('--aid', type=str, help='Application ID (creates an ADF, instead of a DF)') + # mandatory by spec, but ignored by several OS, so don't force the user + createdf_optional.add_argument('--total-file-size', type=int, help='Physical memory allocated for DF/ADi in octets') + createdf_sja_optional.add_argument('--permit-rfm-create', action='store_true') + createdf_sja_optional.add_argument('--permit-rfm-delete-terminate', action='store_true') + createdf_sja_optional.add_argument('--permit-other-applet-create', action='store_true') + createdf_sja_optional.add_argument('--permit-other-applet-delete-terminate', action='store_true') + + @cmd2.with_argparser(createdf_parser) + def do_create_df(self, opts): + """Create a new DF below the currently selected DF. Requires related privileges.""" + file_descriptor = { + 'file_descriptor_byte': { + 'shareable': opts.shareable, + 'file_type': 'df', + 'structure': 'no_info_given', + } + } + ies = [] + ies.append(FileDescriptor(decoded=file_descriptor)) + ies.append(FileIdentifier(decoded=opts.FILE_ID)) + if opts.aid: + ies.append(DfName(decoded=opts.aid)) + ies.append(LifeCycleStatusInteger(decoded='operational_activated')) + ies.append(SecurityAttribReferenced(decoded={'ef_arr_file_id': opts.ef_arr_file_id, + 'ef_arr_record_nr': opts.ef_arr_record_nr })) + if opts.total_file_size: + ies.append(TotalFileSize(decoded=opts.total_file_size)) + # TODO: Spec states PIN Status Template DO is mandatory + if opts.permit_rfm_create or opts.permit_rfm_delete_terminate or opts.permit_other_applet_create or opts.permit_other_applet_delete_terminate: + toolkit_ac = { + 'rfm_create': opts.permit_rfm_create, + 'rfm_delete_terminate': opts.permit_rfm_delete_terminate, + 'other_applet_create': opts.permit_other_applet_create, + 'other_applet_delete_terminate': opts.permit_other_applet_delete_terminate, + } + ies.append(ProprietaryInformation(children=[ToolkitAccessConditions(decoded=toolkit_ac)])) + fcp = FcpTemplate(children=ies) + (data, sw) = self._cmd.card._scc.create_file(b2h(fcp.to_tlv())) + # the newly-created file is automatically selected but our runtime state knows nothing of it + self._cmd.rs.select_file(self._cmd.rs.selected_file)