blob: af7dbca33151ddfe5014034f30b841dc3d8c1e93 [file] [log] [blame]
Harald Welteb2edd142021-01-08 23:29:35 +01001#!/usr/bin/env python3
2
3# Interactive shell for working with SIM / UICC / USIM / ISIM cards
4#
Harald Weltecab26c72022-08-06 16:12:30 +02005# (C) 2021-2022 by Harald Welte <laforge@osmocom.org>
Harald Welteb2edd142021-01-08 23:29:35 +01006#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 2 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
Harald Weltee1268722023-06-24 07:57:08 +020020from typing import List, Optional
Harald Welteb2edd142021-01-08 23:29:35 +010021
22import json
Philipp Maier5d698e52021-09-16 13:18:01 +020023import traceback
Harald Welteb2edd142021-01-08 23:29:35 +010024
25import cmd2
Harald Welte93aac3a2023-04-27 15:18:13 +020026from packaging import version
Harald Welte961b8032023-04-27 17:30:22 +020027from cmd2 import style
28# cmd2 >= 2.3.0 has deprecated the bg/fg in favor of Bg/Fg :(
29if version.parse(cmd2.__version__) < version.parse("2.3.0"):
Oliver Smithe47ea5f2023-05-23 12:54:15 +020030 from cmd2 import fg, bg # pylint: disable=no-name-in-module
Harald Welte961b8032023-04-27 17:30:22 +020031 RED = fg.red
32 LIGHT_RED = fg.bright_red
33 LIGHT_GREEN = fg.bright_green
34else:
35 from cmd2 import Fg, Bg # pylint: disable=no-name-in-module
36 RED = Fg.RED
37 LIGHT_RED = Fg.LIGHT_RED
38 LIGHT_GREEN = Fg.LIGHT_GREEN
Harald Welteb2edd142021-01-08 23:29:35 +010039from cmd2 import CommandSet, with_default_category, with_argparser
40import argparse
41
42import os
43import sys
Harald Welte6ad9a242023-07-11 18:55:29 +020044import inspect
Philipp Maier2b11c322021-03-17 12:37:39 +010045from pathlib import Path
Philipp Maier76667642021-09-22 16:53:22 +020046from io import StringIO
Harald Welteb2edd142021-01-08 23:29:35 +010047
Harald Weltecab26c72022-08-06 16:12:30 +020048from pprint import pprint as pp
49
Harald Welteb2edd142021-01-08 23:29:35 +010050from pySim.exceptions import *
51from pySim.commands import SimCardCommands
Harald Weltecab26c72022-08-06 16:12:30 +020052from pySim.transport import init_reader, ApduTracer, argparse_add_reader_args, ProactiveHandler
Harald Weltef8d2e2b2023-07-09 17:58:38 +020053from pySim.cards import card_detect, SimCardBase, UiccCardBase
Philipp Maiere345e112023-06-09 15:19:56 +020054from pySim.utils import h2b, b2h, i2h, swap_nibbles, rpad, JsonEncoder, bertlv_parse_one, sw_match
Philipp Maiera42ee6f2023-08-16 10:44:57 +020055from pySim.utils import sanitize_pin_adm, tabulate_str_list, boxed_heading_str, Hexstr, dec_iccid
Philipp Maier76667642021-09-22 16:53:22 +020056from pySim.card_handler import CardHandler, CardHandlerAuto
Harald Welteb2edd142021-01-08 23:29:35 +010057
Harald Welte531894d2023-07-11 19:11:11 +020058from pySim.filesystem import CardDF, CardADF, CardModel, CardApplication
59from pySim.runtime import RuntimeState
Philipp Maiera028c7d2021-11-08 16:12:03 +010060from pySim.profile import CardProfile
Vadim Yanitskiy87dd0202023-04-22 20:45:29 +070061from pySim.cdma_ruim import CardProfileRUIM
Harald Welteb2edd142021-01-08 23:29:35 +010062from pySim.ts_102_221 import CardProfileUICC
Harald Welte3c9b7842021-10-19 21:44:24 +020063from pySim.ts_102_222 import Ts102222Commands
Harald Welte2a33ad22021-10-20 10:14:18 +020064from pySim.gsm_r import DF_EIRENE
Harald Weltecab26c72022-08-06 16:12:30 +020065from pySim.cat import ProactiveCommand
Harald Welteb2edd142021-01-08 23:29:35 +010066
Harald Welte4c1dca02021-10-14 17:48:25 +020067# we need to import this module so that the SysmocomSJA2 sub-class of
68# CardModel is created, which will add the ATR-based matching and
69# calling of SysmocomSJA2.add_files. See CardModel.apply_matching_models
Harald Weltef44256c2021-10-14 15:53:39 +020070import pySim.sysmocom_sja2
71
Harald Welte6ad9a242023-07-11 18:55:29 +020072# we need to import these modules so that the various sub-classes of
73# CardProfile are created, which will be used in init_card() to iterate
74# over all known CardProfile sub-classes.
75import pySim.ts_31_102
76import pySim.ts_31_103
77import pySim.ts_31_104
78import pySim.ara_m
79import pySim.global_platform
80
Harald Welte4442b3d2021-04-03 09:00:16 +020081from pySim.card_key_provider import CardKeyProviderCsv, card_key_provider_register, card_key_provider_get_field
Philipp Maier2b11c322021-03-17 12:37:39 +010082
Harald Weltec91085e2022-02-10 18:05:45 +010083
Philipp Maierea95c392021-09-16 13:10:19 +020084def init_card(sl):
Harald Weltec91085e2022-02-10 18:05:45 +010085 """
86 Detect card in reader and setup card profile and runtime state. This
87 function must be called at least once on startup. The card and runtime
88 state object (rs) is required for all pySim-shell commands.
89 """
Philipp Maierea95c392021-09-16 13:10:19 +020090
Harald Weltec91085e2022-02-10 18:05:45 +010091 # Wait up to three seconds for a card in reader and try to detect
92 # the card type.
93 print("Waiting for card...")
94 try:
95 sl.wait_for_card(3)
96 except NoCardError:
97 print("No card detected!")
98 return None, None
99 except:
100 print("Card not readable!")
101 return None, None
Philipp Maierea95c392021-09-16 13:10:19 +0200102
Philipp Maier24031252022-06-14 16:16:42 +0200103 generic_card = False
Harald Weltef8d2e2b2023-07-09 17:58:38 +0200104 card = card_detect(scc)
Harald Weltec91085e2022-02-10 18:05:45 +0100105 if card is None:
106 print("Warning: Could not detect card type - assuming a generic card type...")
Harald Weltef8d2e2b2023-07-09 17:58:38 +0200107 card = SimCardBase(scc)
Philipp Maier24031252022-06-14 16:16:42 +0200108 generic_card = True
Philipp Maiera028c7d2021-11-08 16:12:03 +0100109
Harald Weltec91085e2022-02-10 18:05:45 +0100110 profile = CardProfile.pick(scc)
111 if profile is None:
112 print("Unsupported card type!")
Philipp Maier7226c092022-06-01 17:58:38 +0200113 return None, card
Philipp Maierea95c392021-09-16 13:10:19 +0200114
Philipp Maier24031252022-06-14 16:16:42 +0200115 # ETSI TS 102 221, Table 9.3 specifies a default for the PIN key
116 # references, however card manufactures may still decide to pick an
117 # arbitrary key reference. In case we run on a generic card class that is
118 # detected as an UICC, we will pick the key reference that is officially
119 # specified.
120 if generic_card and isinstance(profile, CardProfileUICC):
121 card._adm_chv_num = 0x0A
122
Harald Weltec91085e2022-02-10 18:05:45 +0100123 print("Info: Card is of type: %s" % str(profile))
Philipp Maiera028c7d2021-11-08 16:12:03 +0100124
Harald Welte6ad9a242023-07-11 18:55:29 +0200125 # FIXME: this shouldn't really be here but somewhere else/more generic.
126 # We cannot do it within pySim/profile.py as that would create circular
127 # dependencies between the individual profiles and profile.py.
Harald Weltec91085e2022-02-10 18:05:45 +0100128 if isinstance(profile, CardProfileUICC):
Harald Welte6ad9a242023-07-11 18:55:29 +0200129 for app_cls in CardApplication.__subclasses__():
130 constr_sig = inspect.signature(app_cls.__init__)
131 # skip any intermediary sub-classes such as CardApplicationSD
132 if len(constr_sig.parameters) != 1:
133 continue
134 profile.add_application(app_cls())
Harald Weltef4a01472023-07-04 21:14:23 +0200135 # We have chosen SimCard() above, but we now know it actually is an UICC
136 # so it's safe to assume it supports USIM application (which we're adding above).
137 # IF we don't do this, we will have a SimCard but try USIM specific commands like
138 # the update_ust method (see https://osmocom.org/issues/6055)
139 if generic_card:
Harald Weltef8d2e2b2023-07-09 17:58:38 +0200140 card = UiccCardBase(scc)
Philipp Maiera028c7d2021-11-08 16:12:03 +0100141
Harald Weltec91085e2022-02-10 18:05:45 +0100142 # Create runtime state with card profile
143 rs = RuntimeState(card, profile)
Philipp Maierea95c392021-09-16 13:10:19 +0200144
Harald Weltec91085e2022-02-10 18:05:45 +0100145 CardModel.apply_matching_models(scc, rs)
Harald Weltef44256c2021-10-14 15:53:39 +0200146
Harald Weltec91085e2022-02-10 18:05:45 +0100147 # inform the transport that we can do context-specific SW interpretation
148 sl.set_sw_interpreter(rs)
Philipp Maierea95c392021-09-16 13:10:19 +0200149
Harald Weltec91085e2022-02-10 18:05:45 +0100150 return rs, card
151
Philipp Maier2b11c322021-03-17 12:37:39 +0100152
Harald Weltee1268722023-06-24 07:57:08 +0200153class Cmd2Compat(cmd2.Cmd):
154 """Backwards-compatibility wrapper around cmd2.Cmd to support older and newer
155 releases. See https://github.com/python-cmd2/cmd2/blob/master/CHANGELOG.md"""
156 def run_editor(self, file_path: Optional[str] = None) -> None:
157 if version.parse(cmd2.__version__) < version.parse("2.0.0"):
Harald Welte0ec01502023-06-24 10:00:12 +0200158 return self._run_editor(file_path) # pylint: disable=no-member
Harald Weltee1268722023-06-24 07:57:08 +0200159 else:
Harald Welte0ec01502023-06-24 10:00:12 +0200160 return super().run_editor(file_path) # pylint: disable=no-member
161
162class Settable2Compat(cmd2.Settable):
163 """Backwards-compatibility wrapper around cmd2.Settable to support older and newer
164 releases. See https://github.com/python-cmd2/cmd2/blob/master/CHANGELOG.md"""
165 def __init__(self, name, val_type, description, settable_object, **kwargs):
166 if version.parse(cmd2.__version__) < version.parse("2.0.0"):
167 super().__init__(name, val_type, description, **kwargs) # pylint: disable=no-value-for-parameter
168 else:
169 super().__init__(name, val_type, description, settable_object, **kwargs) # pylint: disable=too-many-function-args
Harald Weltee1268722023-06-24 07:57:08 +0200170
171class PysimApp(Cmd2Compat):
Harald Weltec91085e2022-02-10 18:05:45 +0100172 CUSTOM_CATEGORY = 'pySim Commands'
Philipp Maier76667642021-09-22 16:53:22 +0200173
Harald Weltec91085e2022-02-10 18:05:45 +0100174 def __init__(self, card, rs, sl, ch, script=None):
Harald Weltec85d4062023-04-27 17:10:17 +0200175 if version.parse(cmd2.__version__) < version.parse("2.0.0"):
176 kwargs = {'use_ipython': True}
177 else:
178 kwargs = {'include_ipy': True}
179
Oliver Smithe47ea5f2023-05-23 12:54:15 +0200180 # pylint: disable=unexpected-keyword-arg
Harald Weltec91085e2022-02-10 18:05:45 +0100181 super().__init__(persistent_history_file='~/.pysim_shell_history', allow_cli_args=False,
Harald Weltec85d4062023-04-27 17:10:17 +0200182 auto_load_commands=False, startup_script=script, **kwargs)
Harald Welte961b8032023-04-27 17:30:22 +0200183 self.intro = style('Welcome to pySim-shell!', fg=RED)
Harald Weltec91085e2022-02-10 18:05:45 +0100184 self.default_category = 'pySim-shell built-in commands'
185 self.card = None
186 self.rs = None
Harald Weltea6c0f882022-07-17 14:23:17 +0200187 self.lchan = None
188 self.py_locals = {'card': self.card, 'rs': self.rs, 'lchan': self.lchan}
Harald Weltec91085e2022-02-10 18:05:45 +0100189 self.sl = sl
190 self.ch = ch
Harald Welte1748b932021-04-06 21:12:25 +0200191
Harald Weltec91085e2022-02-10 18:05:45 +0100192 self.numeric_path = False
Harald Weltec91085e2022-02-10 18:05:45 +0100193 self.conserve_write = True
Harald Weltec91085e2022-02-10 18:05:45 +0100194 self.json_pretty_print = True
Harald Weltec91085e2022-02-10 18:05:45 +0100195 self.apdu_trace = False
Philipp Maier5d698e52021-09-16 13:18:01 +0200196
Harald Welte0ec01502023-06-24 10:00:12 +0200197 self.add_settable(Settable2Compat('numeric_path', bool, 'Print File IDs instead of names', self,
198 onchange_cb=self._onchange_numeric_path))
199 self.add_settable(Settable2Compat('conserve_write', bool, 'Read and compare before write', self,
200 onchange_cb=self._onchange_conserve_write))
201 self.add_settable(Settable2Compat('json_pretty_print', bool, 'Pretty-Print JSON output', self))
202 self.add_settable(Settable2Compat('apdu_trace', bool, 'Trace and display APDUs exchanged with card', self,
203 onchange_cb=self._onchange_apdu_trace))
Harald Weltec91085e2022-02-10 18:05:45 +0100204 self.equip(card, rs)
Philipp Maier5d698e52021-09-16 13:18:01 +0200205
Harald Weltec91085e2022-02-10 18:05:45 +0100206 def equip(self, card, rs):
207 """
208 Equip pySim-shell with the supplied card and runtime state, add (or remove) all required settables and
209 and commands to enable card operations.
210 """
Philipp Maier76667642021-09-22 16:53:22 +0200211
Harald Weltec91085e2022-02-10 18:05:45 +0100212 rc = False
Philipp Maier5d698e52021-09-16 13:18:01 +0200213
Harald Weltec91085e2022-02-10 18:05:45 +0100214 # Unequip everything from pySim-shell that would not work in unequipped state
215 if self.rs:
Harald Weltea6c0f882022-07-17 14:23:17 +0200216 lchan = self.rs.lchan[0]
217 lchan.unregister_cmds(self)
iw0f818acd2023-06-19 10:27:04 +0200218 if self.rs.profile:
219 for cmd_set in self.rs.profile.shell_cmdsets:
220 self.unregister_command_set(cmd_set)
221
Harald Welte892526f2023-06-06 17:09:50 +0200222 for cmds in [Iso7816Commands, Ts102222Commands, PySimCommands]:
Harald Weltec91085e2022-02-10 18:05:45 +0100223 cmd_set = self.find_commandsets(cmds)
224 if cmd_set:
225 self.unregister_command_set(cmd_set[0])
Philipp Maier5d698e52021-09-16 13:18:01 +0200226
Harald Weltec91085e2022-02-10 18:05:45 +0100227 self.card = card
228 self.rs = rs
Philipp Maier5d698e52021-09-16 13:18:01 +0200229
Harald Weltec91085e2022-02-10 18:05:45 +0100230 # When a card object and a runtime state is present, (re)equip pySim-shell with everything that is
231 # needed to operate on cards.
232 if self.card and self.rs:
Harald Weltea6c0f882022-07-17 14:23:17 +0200233 self.lchan = self.rs.lchan[0]
Harald Weltec91085e2022-02-10 18:05:45 +0100234 self._onchange_conserve_write(
235 'conserve_write', False, self.conserve_write)
236 self._onchange_apdu_trace('apdu_trace', False, self.apdu_trace)
Harald Welte659781c2023-06-06 17:00:51 +0200237 if self.rs.profile:
238 for cmd_set in self.rs.profile.shell_cmdsets:
239 self.register_command_set(cmd_set)
Harald Weltec91085e2022-02-10 18:05:45 +0100240 self.register_command_set(Iso7816Commands())
Harald Welte3c9b7842021-10-19 21:44:24 +0200241 self.register_command_set(Ts102222Commands())
Harald Weltec91085e2022-02-10 18:05:45 +0100242 self.register_command_set(PySimCommands())
Philipp Maiera42ee6f2023-08-16 10:44:57 +0200243
244 self.lchan.select('MF/EF.ICCID', self)
245 self.iccid = dec_iccid(self.lchan.read_binary()[0])
246
Harald Weltea6c0f882022-07-17 14:23:17 +0200247 self.lchan.select('MF', self)
Harald Weltec91085e2022-02-10 18:05:45 +0100248 rc = True
249 else:
250 self.poutput("pySim-shell not equipped!")
Philipp Maier5d698e52021-09-16 13:18:01 +0200251
Harald Weltec91085e2022-02-10 18:05:45 +0100252 self.update_prompt()
253 return rc
Harald Welteb2edd142021-01-08 23:29:35 +0100254
Harald Weltec91085e2022-02-10 18:05:45 +0100255 def poutput_json(self, data, force_no_pretty=False):
256 """like cmd2.poutput() but for a JSON serializable dict."""
257 if force_no_pretty or self.json_pretty_print == False:
258 output = json.dumps(data, cls=JsonEncoder)
259 else:
260 output = json.dumps(data, cls=JsonEncoder, indent=4)
261 self.poutput(output)
Harald Welteb2edd142021-01-08 23:29:35 +0100262
Harald Weltec91085e2022-02-10 18:05:45 +0100263 def _onchange_numeric_path(self, param_name, old, new):
264 self.update_prompt()
Philipp Maier38c74f62021-03-17 17:19:52 +0100265
Harald Weltec91085e2022-02-10 18:05:45 +0100266 def _onchange_conserve_write(self, param_name, old, new):
267 if self.rs:
268 self.rs.conserve_write = new
Harald Welte7829d8a2021-04-10 11:28:53 +0200269
Harald Weltec91085e2022-02-10 18:05:45 +0100270 def _onchange_apdu_trace(self, param_name, old, new):
271 if self.card:
272 if new == True:
273 self.card._scc._tp.apdu_tracer = self.Cmd2ApduTracer(self)
274 else:
275 self.card._scc._tp.apdu_tracer = None
Harald Welte7829d8a2021-04-10 11:28:53 +0200276
Harald Weltec91085e2022-02-10 18:05:45 +0100277 class Cmd2ApduTracer(ApduTracer):
278 def __init__(self, cmd2_app):
279 self.cmd2 = app
Harald Welte7829d8a2021-04-10 11:28:53 +0200280
Harald Weltec91085e2022-02-10 18:05:45 +0100281 def trace_response(self, cmd, sw, resp):
282 self.cmd2.poutput("-> %s %s" % (cmd[:10], cmd[10:]))
283 self.cmd2.poutput("<- %s: %s" % (sw, resp))
Harald Welteb2edd142021-01-08 23:29:35 +0100284
Harald Weltec91085e2022-02-10 18:05:45 +0100285 def update_prompt(self):
Harald Weltea6c0f882022-07-17 14:23:17 +0200286 if self.lchan:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200287 path_str = self.lchan.selected_file.fully_qualified_path_str(not self.numeric_path)
288 self.prompt = 'pySIM-shell (%s)> ' % (path_str)
Harald Weltec91085e2022-02-10 18:05:45 +0100289 else:
Philipp Maier7226c092022-06-01 17:58:38 +0200290 if self.card:
291 self.prompt = 'pySIM-shell (no card profile)> '
292 else:
293 self.prompt = 'pySIM-shell (no card)> '
Harald Welteb2edd142021-01-08 23:29:35 +0100294
Harald Weltec91085e2022-02-10 18:05:45 +0100295 @cmd2.with_category(CUSTOM_CATEGORY)
296 def do_intro(self, _):
297 """Display the intro banner"""
298 self.poutput(self.intro)
Philipp Maier9764de22021-11-03 10:44:39 +0100299
Harald Weltec91085e2022-02-10 18:05:45 +0100300 def do_eof(self, _: argparse.Namespace) -> bool:
301 self.poutput("")
302 return self.do_quit('')
Philipp Maier5d698e52021-09-16 13:18:01 +0200303
Harald Weltec91085e2022-02-10 18:05:45 +0100304 @cmd2.with_category(CUSTOM_CATEGORY)
305 def do_equip(self, opts):
306 """Equip pySim-shell with card"""
Philipp Maiere6cba762023-08-11 11:23:17 +0200307 if self.rs and self.rs.profile:
Harald Welte659781c2023-06-06 17:00:51 +0200308 for cmd_set in self.rs.profile.shell_cmdsets:
309 self.unregister_command_set(cmd_set)
Harald Weltec91085e2022-02-10 18:05:45 +0100310 rs, card = init_card(sl)
311 self.equip(card, rs)
Philipp Maier76667642021-09-22 16:53:22 +0200312
Philipp Maier7226c092022-06-01 17:58:38 +0200313 apdu_cmd_parser = argparse.ArgumentParser()
314 apdu_cmd_parser.add_argument('APDU', type=str, help='APDU as hex string')
Philipp Maiere7d1b672022-06-01 18:05:34 +0200315 apdu_cmd_parser.add_argument('--expect-sw', help='expect a specified status word', type=str, default=None)
Philipp Maier7226c092022-06-01 17:58:38 +0200316
317 @cmd2.with_argparser(apdu_cmd_parser)
318 def do_apdu(self, opts):
319 """Send a raw APDU to the card, and print SW + Response.
320 DANGEROUS: pySim-shell will not know any card state changes, and
321 not continue to work as expected if you e.g. select a different
322 file."""
323 data, sw = self.card._scc._tp.send_apdu(opts.APDU)
324 if data:
325 self.poutput("SW: %s, RESP: %s" % (sw, data))
326 else:
327 self.poutput("SW: %s" % sw)
Philipp Maiere7d1b672022-06-01 18:05:34 +0200328 if opts.expect_sw:
329 if not sw_match(sw, opts.expect_sw):
330 raise SwMatchError(sw, opts.expect_sw)
Philipp Maier7226c092022-06-01 17:58:38 +0200331
Harald Weltec91085e2022-02-10 18:05:45 +0100332 class InterceptStderr(list):
333 def __init__(self):
334 self._stderr_backup = sys.stderr
Philipp Maier76667642021-09-22 16:53:22 +0200335
Harald Weltec91085e2022-02-10 18:05:45 +0100336 def __enter__(self):
337 self._stringio_stderr = StringIO()
338 sys.stderr = self._stringio_stderr
339 return self
Philipp Maier76667642021-09-22 16:53:22 +0200340
Harald Weltec91085e2022-02-10 18:05:45 +0100341 def __exit__(self, *args):
342 self.stderr = self._stringio_stderr.getvalue().strip()
343 del self._stringio_stderr
344 sys.stderr = self._stderr_backup
Philipp Maier76667642021-09-22 16:53:22 +0200345
Harald Weltec91085e2022-02-10 18:05:45 +0100346 def _show_failure_sign(self):
Harald Welte961b8032023-04-27 17:30:22 +0200347 self.poutput(style(" +-------------+", fg=LIGHT_RED))
348 self.poutput(style(" + ## ## +", fg=LIGHT_RED))
349 self.poutput(style(" + ## ## +", fg=LIGHT_RED))
350 self.poutput(style(" + ### +", fg=LIGHT_RED))
351 self.poutput(style(" + ## ## +", fg=LIGHT_RED))
352 self.poutput(style(" + ## ## +", fg=LIGHT_RED))
353 self.poutput(style(" +-------------+", fg=LIGHT_RED))
Harald Weltec91085e2022-02-10 18:05:45 +0100354 self.poutput("")
Philipp Maier76667642021-09-22 16:53:22 +0200355
Harald Weltec91085e2022-02-10 18:05:45 +0100356 def _show_success_sign(self):
Harald Welte961b8032023-04-27 17:30:22 +0200357 self.poutput(style(" +-------------+", fg=LIGHT_GREEN))
358 self.poutput(style(" + ## +", fg=LIGHT_GREEN))
359 self.poutput(style(" + ## +", fg=LIGHT_GREEN))
360 self.poutput(style(" + # ## +", fg=LIGHT_GREEN))
361 self.poutput(style(" + ## # +", fg=LIGHT_GREEN))
362 self.poutput(style(" + ## +", fg=LIGHT_GREEN))
363 self.poutput(style(" +-------------+", fg=LIGHT_GREEN))
Harald Weltec91085e2022-02-10 18:05:45 +0100364 self.poutput("")
Philipp Maier76667642021-09-22 16:53:22 +0200365
Harald Weltec91085e2022-02-10 18:05:45 +0100366 def _process_card(self, first, script_path):
Philipp Maier76667642021-09-22 16:53:22 +0200367
Harald Weltec91085e2022-02-10 18:05:45 +0100368 # Early phase of card initialzation (this part may fail with an exception)
369 try:
370 rs, card = init_card(self.sl)
371 rc = self.equip(card, rs)
372 except:
373 self.poutput("")
374 self.poutput("Card initialization failed with an exception:")
375 self.poutput("---------------------8<---------------------")
376 traceback.print_exc()
377 self.poutput("---------------------8<---------------------")
378 self.poutput("")
379 return -1
Philipp Maier76667642021-09-22 16:53:22 +0200380
Harald Weltec91085e2022-02-10 18:05:45 +0100381 # Actual card processing step. This part should never fail with an exception since the cmd2
382 # do_run_script method will catch any exception that might occur during script execution.
383 if rc:
384 self.poutput("")
385 self.poutput("Transcript stdout:")
386 self.poutput("---------------------8<---------------------")
387 with self.InterceptStderr() as logged:
388 self.do_run_script(script_path)
389 self.poutput("---------------------8<---------------------")
Philipp Maier76667642021-09-22 16:53:22 +0200390
Harald Weltec91085e2022-02-10 18:05:45 +0100391 self.poutput("")
392 self.poutput("Transcript stderr:")
393 if logged.stderr:
394 self.poutput("---------------------8<---------------------")
395 self.poutput(logged.stderr)
396 self.poutput("---------------------8<---------------------")
397 else:
398 self.poutput("(none)")
Philipp Maier76667642021-09-22 16:53:22 +0200399
Harald Weltec91085e2022-02-10 18:05:45 +0100400 # Check for exceptions
401 self.poutput("")
402 if "EXCEPTION of type" not in logged.stderr:
403 return 0
Philipp Maier76667642021-09-22 16:53:22 +0200404
Harald Weltec91085e2022-02-10 18:05:45 +0100405 return -1
Philipp Maier76667642021-09-22 16:53:22 +0200406
Harald Weltec91085e2022-02-10 18:05:45 +0100407 bulk_script_parser = argparse.ArgumentParser()
408 bulk_script_parser.add_argument(
409 'script_path', help="path to the script file")
410 bulk_script_parser.add_argument('--halt_on_error', help='stop card handling if an exeption occurs',
411 action='store_true')
412 bulk_script_parser.add_argument('--tries', type=int, default=2,
413 help='how many tries before trying the next card')
414 bulk_script_parser.add_argument('--on_stop_action', type=str, default=None,
415 help='commandline to execute when card handling has stopped')
416 bulk_script_parser.add_argument('--pre_card_action', type=str, default=None,
417 help='commandline to execute before actually talking to the card')
Philipp Maier76667642021-09-22 16:53:22 +0200418
Harald Weltec91085e2022-02-10 18:05:45 +0100419 @cmd2.with_argparser(bulk_script_parser)
420 @cmd2.with_category(CUSTOM_CATEGORY)
421 def do_bulk_script(self, opts):
422 """Run script on multiple cards (bulk provisioning)"""
Philipp Maier76667642021-09-22 16:53:22 +0200423
Harald Weltec91085e2022-02-10 18:05:45 +0100424 # Make sure that the script file exists and that it is readable.
425 if not os.access(opts.script_path, os.R_OK):
426 self.poutput("Invalid script file!")
427 return
Philipp Maier76667642021-09-22 16:53:22 +0200428
Harald Weltec91085e2022-02-10 18:05:45 +0100429 success_count = 0
430 fail_count = 0
Philipp Maier76667642021-09-22 16:53:22 +0200431
Harald Weltec91085e2022-02-10 18:05:45 +0100432 first = True
433 while 1:
434 # TODO: Count consecutive failures, if more than N consecutive failures occur, then stop.
435 # The ratinale is: There may be a problem with the device, we do want to prevent that
436 # all remaining cards are fired to the error bin. This is only relevant for situations
437 # with large stacks, probably we do not need this feature right now.
Philipp Maier76667642021-09-22 16:53:22 +0200438
Harald Weltec91085e2022-02-10 18:05:45 +0100439 try:
440 # In case of failure, try multiple times.
441 for i in range(opts.tries):
442 # fetch card into reader bay
443 ch.get(first)
Philipp Maier76667642021-09-22 16:53:22 +0200444
Harald Weltec91085e2022-02-10 18:05:45 +0100445 # if necessary execute an action before we start processing the card
446 if(opts.pre_card_action):
447 os.system(opts.pre_card_action)
Philipp Maier76667642021-09-22 16:53:22 +0200448
Harald Weltec91085e2022-02-10 18:05:45 +0100449 # process the card
450 rc = self._process_card(first, opts.script_path)
451 if rc == 0:
452 success_count = success_count + 1
453 self._show_success_sign()
454 self.poutput("Statistics: success :%i, failure: %i" % (
455 success_count, fail_count))
456 break
457 else:
458 fail_count = fail_count + 1
459 self._show_failure_sign()
460 self.poutput("Statistics: success :%i, failure: %i" % (
461 success_count, fail_count))
Philipp Maier76667642021-09-22 16:53:22 +0200462
Harald Weltec91085e2022-02-10 18:05:45 +0100463 # Depending on success or failure, the card goes either in the "error" bin or in the
464 # "done" bin.
465 if rc < 0:
466 ch.error()
467 else:
468 ch.done()
Philipp Maier76667642021-09-22 16:53:22 +0200469
Harald Weltec91085e2022-02-10 18:05:45 +0100470 # In most cases it is possible to proceed with the next card, but the
471 # user may decide to halt immediately when an error occurs
472 if opts.halt_on_error and rc < 0:
473 return
Philipp Maier76667642021-09-22 16:53:22 +0200474
Harald Weltec91085e2022-02-10 18:05:45 +0100475 except (KeyboardInterrupt):
476 self.poutput("")
477 self.poutput("Terminated by user!")
478 return
479 except (SystemExit):
480 # When all cards are processed the card handler device will throw a SystemExit
481 # exception. Also Errors that are not recoverable (cards stuck etc.) will end up here.
482 # The user has the option to execute some action to make aware that the card handler
483 # needs service.
484 if(opts.on_stop_action):
485 os.system(opts.on_stop_action)
486 return
487 except:
488 self.poutput("")
489 self.poutput("Card handling failed with an exception:")
490 self.poutput("---------------------8<---------------------")
491 traceback.print_exc()
492 self.poutput("---------------------8<---------------------")
493 self.poutput("")
494 fail_count = fail_count + 1
495 self._show_failure_sign()
496 self.poutput("Statistics: success :%i, failure: %i" %
497 (success_count, fail_count))
Philipp Maierb52feed2021-09-22 16:43:13 +0200498
Harald Weltec91085e2022-02-10 18:05:45 +0100499 first = False
500
501 echo_parser = argparse.ArgumentParser()
502 echo_parser.add_argument('string', help="string to echo on the shell")
503
504 @cmd2.with_argparser(echo_parser)
505 @cmd2.with_category(CUSTOM_CATEGORY)
506 def do_echo(self, opts):
507 """Echo (print) a string on the console"""
508 self.poutput(opts.string)
509
Harald Weltefc315482022-07-23 12:49:14 +0200510 @cmd2.with_category(CUSTOM_CATEGORY)
511 def do_version(self, opts):
512 """Print the pySim software version."""
513 import pkg_resources
514 self.poutput(pkg_resources.get_distribution('pySim'))
Harald Welteb2edd142021-01-08 23:29:35 +0100515
Harald Welte31d2cf02021-04-03 10:47:29 +0200516@with_default_category('pySim Commands')
517class PySimCommands(CommandSet):
Harald Weltec91085e2022-02-10 18:05:45 +0100518 def __init__(self):
519 super().__init__()
Harald Welteb2edd142021-01-08 23:29:35 +0100520
Harald Weltec91085e2022-02-10 18:05:45 +0100521 dir_parser = argparse.ArgumentParser()
522 dir_parser.add_argument(
523 '--fids', help='Show file identifiers', action='store_true')
524 dir_parser.add_argument(
525 '--names', help='Show file names', action='store_true')
526 dir_parser.add_argument(
527 '--apps', help='Show applications', action='store_true')
528 dir_parser.add_argument(
529 '--all', help='Show all selectable identifiers and names', action='store_true')
Philipp Maier5d3e2592021-02-22 17:22:16 +0100530
Harald Weltec91085e2022-02-10 18:05:45 +0100531 @cmd2.with_argparser(dir_parser)
532 def do_dir(self, opts):
533 """Show a listing of files available in currently selected DF or MF"""
534 if opts.all:
535 flags = []
536 elif opts.fids or opts.names or opts.apps:
537 flags = ['PARENT', 'SELF']
538 if opts.fids:
539 flags += ['FIDS', 'AIDS']
540 if opts.names:
541 flags += ['FNAMES', 'ANAMES']
542 if opts.apps:
543 flags += ['ANAMES', 'AIDS']
544 else:
545 flags = ['PARENT', 'SELF', 'FNAMES', 'ANAMES']
546 selectables = list(
Harald Weltea6c0f882022-07-17 14:23:17 +0200547 self._cmd.lchan.selected_file.get_selectable_names(flags=flags))
Harald Weltec91085e2022-02-10 18:05:45 +0100548 directory_str = tabulate_str_list(
549 selectables, width=79, hspace=2, lspace=1, align_left=True)
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200550 path = self._cmd.lchan.selected_file.fully_qualified_path_str(True)
551 self._cmd.poutput(path)
552 path = self._cmd.lchan.selected_file.fully_qualified_path_str(False)
553 self._cmd.poutput(path)
Harald Weltec91085e2022-02-10 18:05:45 +0100554 self._cmd.poutput(directory_str)
555 self._cmd.poutput("%d files" % len(selectables))
Harald Welteb2edd142021-01-08 23:29:35 +0100556
Philipp Maier7b138b02022-05-31 13:42:56 +0200557 def walk(self, indent=0, action_ef=None, action_df=None, context=None, **kwargs):
Harald Weltec91085e2022-02-10 18:05:45 +0100558 """Recursively walk through the file system, starting at the currently selected DF"""
Philipp Maier7b138b02022-05-31 13:42:56 +0200559
Harald Weltea6c0f882022-07-17 14:23:17 +0200560 if isinstance(self._cmd.lchan.selected_file, CardDF):
Philipp Maier7b138b02022-05-31 13:42:56 +0200561 if action_df:
562 action_df(context, opts)
563
Harald Weltea6c0f882022-07-17 14:23:17 +0200564 files = self._cmd.lchan.selected_file.get_selectables(
Harald Weltec91085e2022-02-10 18:05:45 +0100565 flags=['FNAMES', 'ANAMES'])
566 for f in files:
Philipp Maier7b138b02022-05-31 13:42:56 +0200567 # special case: When no action is performed, just output a directory
568 if not action_ef and not action_df:
Harald Weltec91085e2022-02-10 18:05:45 +0100569 output_str = " " * indent + str(f) + (" " * 250)
570 output_str = output_str[0:25]
571 if isinstance(files[f], CardADF):
572 output_str += " " + str(files[f].aid)
573 else:
574 output_str += " " + str(files[f].fid)
575 output_str += " " + str(files[f].desc)
576 self._cmd.poutput(output_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200577
Harald Weltec91085e2022-02-10 18:05:45 +0100578 if isinstance(files[f], CardDF):
579 skip_df = False
580 try:
Harald Weltea6c0f882022-07-17 14:23:17 +0200581 fcp_dec = self._cmd.lchan.select(f, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100582 except Exception as e:
583 skip_df = True
Harald Weltea6c0f882022-07-17 14:23:17 +0200584 df = self._cmd.lchan.selected_file
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200585 df_path = df.fully_qualified_path_str(True)
586 df_skip_reason_str = df_path + \
Harald Weltec91085e2022-02-10 18:05:45 +0100587 "/" + str(f) + ", " + str(e)
588 if context:
589 context['DF_SKIP'] += 1
590 context['DF_SKIP_REASON'].append(df_skip_reason_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200591
Harald Weltec91085e2022-02-10 18:05:45 +0100592 # If the DF was skipped, we never have entered the directory
593 # below, so we must not move up.
594 if skip_df == False:
Philipp Maier7b138b02022-05-31 13:42:56 +0200595 self.walk(indent + 1, action_ef, action_df, context, **kwargs)
Harald Weltea6c0f882022-07-17 14:23:17 +0200596 fcp_dec = self._cmd.lchan.select("..", self._cmd)
Philipp Maierf408a402021-04-09 21:16:12 +0200597
Philipp Maier7b138b02022-05-31 13:42:56 +0200598 elif action_ef:
Harald Weltea6c0f882022-07-17 14:23:17 +0200599 df_before_action = self._cmd.lchan.selected_file
Philipp Maier7b138b02022-05-31 13:42:56 +0200600 action_ef(f, context, **kwargs)
Harald Weltec91085e2022-02-10 18:05:45 +0100601 # When walking through the file system tree the action must not
602 # always restore the currently selected file to the file that
603 # was selected before executing the action() callback.
Harald Weltea6c0f882022-07-17 14:23:17 +0200604 if df_before_action != self._cmd.lchan.selected_file:
Harald Weltec91085e2022-02-10 18:05:45 +0100605 raise RuntimeError("inconsistent walk, %s is currently selected but expecting %s to be selected"
Harald Weltea6c0f882022-07-17 14:23:17 +0200606 % (str(self._cmd.lchan.selected_file), str(df_before_action)))
Philipp Maierff9dae22021-02-25 17:03:21 +0100607
Harald Weltec91085e2022-02-10 18:05:45 +0100608 def do_tree(self, opts):
609 """Display a filesystem-tree with all selectable files"""
610 self.walk()
Philipp Maierff9dae22021-02-25 17:03:21 +0100611
Philipp Maier7b138b02022-05-31 13:42:56 +0200612 def export_ef(self, filename, context, as_json):
613 """ Select and export a single elementary file (EF) """
Harald Weltec91085e2022-02-10 18:05:45 +0100614 context['COUNT'] += 1
Harald Weltea6c0f882022-07-17 14:23:17 +0200615 df = self._cmd.lchan.selected_file
Philipp Maierac34dcc2021-04-01 17:19:05 +0200616
Philipp Maierea81f752022-05-19 10:13:30 +0200617 # The currently selected file (not the file we are going to export)
618 # must always be an ADF or DF. From this starting point we select
619 # the EF we want to export. To maintain consistency we will then
620 # select the current DF again (see comment below).
Harald Weltec91085e2022-02-10 18:05:45 +0100621 if not isinstance(df, CardDF):
622 raise RuntimeError(
623 "currently selected file %s is not a DF or ADF" % str(df))
Philipp Maierac34dcc2021-04-01 17:19:05 +0200624
Harald Weltec91085e2022-02-10 18:05:45 +0100625 df_path_list = df.fully_qualified_path(True)
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200626 df_path = df.fully_qualified_path_str(True)
627 df_path_fid = df.fully_qualified_path_str(False)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100628
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200629 file_str = df_path + "/" + str(filename)
Harald Weltec91085e2022-02-10 18:05:45 +0100630 self._cmd.poutput(boxed_heading_str(file_str))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100631
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200632 self._cmd.poutput("# directory: %s (%s)" % (df_path, df_path_fid))
Harald Weltec91085e2022-02-10 18:05:45 +0100633 try:
Harald Weltea6c0f882022-07-17 14:23:17 +0200634 fcp_dec = self._cmd.lchan.select(filename, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100635 self._cmd.poutput("# file: %s (%s)" % (
Harald Weltea6c0f882022-07-17 14:23:17 +0200636 self._cmd.lchan.selected_file.name, self._cmd.lchan.selected_file.fid))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100637
Harald Weltea6c0f882022-07-17 14:23:17 +0200638 structure = self._cmd.lchan.selected_file_structure()
Harald Weltec91085e2022-02-10 18:05:45 +0100639 self._cmd.poutput("# structure: %s" % str(structure))
Harald Weltea6c0f882022-07-17 14:23:17 +0200640 self._cmd.poutput("# RAW FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp_hex))
641 self._cmd.poutput("# Decoded FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100642
Harald Weltec91085e2022-02-10 18:05:45 +0100643 for f in df_path_list:
644 self._cmd.poutput("select " + str(f))
Harald Weltea6c0f882022-07-17 14:23:17 +0200645 self._cmd.poutput("select " + self._cmd.lchan.selected_file.name)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100646
Harald Weltec91085e2022-02-10 18:05:45 +0100647 if structure == 'transparent':
Harald Welte08b11ab2022-02-10 18:56:41 +0100648 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200649 result = self._cmd.lchan.read_binary_dec()
Harald Welte08b11ab2022-02-10 18:56:41 +0100650 self._cmd.poutput("update_binary_decoded '%s'" % json.dumps(result[0], cls=JsonEncoder))
651 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200652 result = self._cmd.lchan.read_binary()
Harald Welte08b11ab2022-02-10 18:56:41 +0100653 self._cmd.poutput("update_binary " + str(result[0]))
Harald Weltec91085e2022-02-10 18:05:45 +0100654 elif structure == 'cyclic' or structure == 'linear_fixed':
655 # Use number of records specified in select response
Harald Weltea6c0f882022-07-17 14:23:17 +0200656 num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
Harald Welte747a9782022-02-13 17:52:28 +0100657 if num_of_rec:
Harald Weltec91085e2022-02-10 18:05:45 +0100658 for r in range(1, num_of_rec + 1):
Harald Welte08b11ab2022-02-10 18:56:41 +0100659 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200660 result = self._cmd.lchan.read_record_dec(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100661 self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
662 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200663 result = self._cmd.lchan.read_record(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100664 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
665
Harald Weltec91085e2022-02-10 18:05:45 +0100666 # When the select response does not return the number of records, read until we hit the
667 # first record that cannot be read.
668 else:
669 r = 1
670 while True:
671 try:
Harald Welte08b11ab2022-02-10 18:56:41 +0100672 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200673 result = self._cmd.lchan.read_record_dec(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100674 self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
675 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200676 result = self._cmd.lchan.read_record(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100677 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
Harald Weltec91085e2022-02-10 18:05:45 +0100678 except SwMatchError as e:
679 # We are past the last valid record - stop
680 if e.sw_actual == "9402":
681 break
682 # Some other problem occurred
683 else:
684 raise e
Harald Weltec91085e2022-02-10 18:05:45 +0100685 r = r + 1
686 elif structure == 'ber_tlv':
Harald Weltea6c0f882022-07-17 14:23:17 +0200687 tags = self._cmd.lchan.retrieve_tags()
Harald Weltec91085e2022-02-10 18:05:45 +0100688 for t in tags:
Harald Weltea6c0f882022-07-17 14:23:17 +0200689 result = self._cmd.lchan.retrieve_data(t)
Harald Weltec91085e2022-02-10 18:05:45 +0100690 (tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
691 self._cmd.poutput("set_data 0x%02x %s" % (t, b2h(val)))
692 else:
693 raise RuntimeError(
694 'Unsupported structure "%s" of file "%s"' % (structure, filename))
695 except Exception as e:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200696 bad_file_str = df_path + "/" + str(filename) + ", " + str(e)
Harald Weltec91085e2022-02-10 18:05:45 +0100697 self._cmd.poutput("# bad file: %s" % bad_file_str)
698 context['ERR'] += 1
699 context['BAD'].append(bad_file_str)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100700
Harald Weltec91085e2022-02-10 18:05:45 +0100701 # When reading the file is done, make sure the parent file is
702 # selected again. This will be the usual case, however we need
703 # to check before since we must not select the same DF twice
Harald Weltea6c0f882022-07-17 14:23:17 +0200704 if df != self._cmd.lchan.selected_file:
705 self._cmd.lchan.select(df.fid or df.aid, self._cmd)
Philipp Maierac34dcc2021-04-01 17:19:05 +0200706
Harald Weltec91085e2022-02-10 18:05:45 +0100707 self._cmd.poutput("#")
Philipp Maier24f7bd32021-02-25 17:06:18 +0100708
Harald Weltec91085e2022-02-10 18:05:45 +0100709 export_parser = argparse.ArgumentParser()
710 export_parser.add_argument(
711 '--filename', type=str, default=None, help='only export specific file')
Harald Welte08b11ab2022-02-10 18:56:41 +0100712 export_parser.add_argument(
713 '--json', action='store_true', help='export as JSON (less reliable)')
Philipp Maier24f7bd32021-02-25 17:06:18 +0100714
Harald Weltec91085e2022-02-10 18:05:45 +0100715 @cmd2.with_argparser(export_parser)
716 def do_export(self, opts):
717 """Export files to script that can be imported back later"""
718 context = {'ERR': 0, 'COUNT': 0, 'BAD': [],
719 'DF_SKIP': 0, 'DF_SKIP_REASON': []}
Philipp Maier9a4091d2022-05-19 10:20:30 +0200720 kwargs_export = {'as_json': opts.json}
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200721 exception_str_add = ""
722
Harald Weltec91085e2022-02-10 18:05:45 +0100723 if opts.filename:
Philipp Maier7b138b02022-05-31 13:42:56 +0200724 self.export_ef(opts.filename, context, **kwargs_export)
Harald Weltec91085e2022-02-10 18:05:45 +0100725 else:
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200726 try:
727 self.walk(0, self.export_ef, None, context, **kwargs_export)
728 except Exception as e:
729 print("# Stopping early here due to exception: " + str(e))
730 print("#")
731 exception_str_add = ", also had to stop early due to exception:" + str(e)
Philipp Maier80ce71f2021-04-19 21:24:23 +0200732
Harald Weltec91085e2022-02-10 18:05:45 +0100733 self._cmd.poutput(boxed_heading_str("Export summary"))
Philipp Maier80ce71f2021-04-19 21:24:23 +0200734
Harald Weltec91085e2022-02-10 18:05:45 +0100735 self._cmd.poutput("# total files visited: %u" % context['COUNT'])
736 self._cmd.poutput("# bad files: %u" % context['ERR'])
737 for b in context['BAD']:
738 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200739
Harald Weltec91085e2022-02-10 18:05:45 +0100740 self._cmd.poutput("# skipped dedicated files(s): %u" %
741 context['DF_SKIP'])
742 for b in context['DF_SKIP_REASON']:
743 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200744
Harald Weltec91085e2022-02-10 18:05:45 +0100745 if context['ERR'] and context['DF_SKIP']:
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200746 raise RuntimeError("unable to export %i elementary file(s) and %i dedicated file(s)%s" % (
747 context['ERR'], context['DF_SKIP'], exception_str_add))
Harald Weltec91085e2022-02-10 18:05:45 +0100748 elif context['ERR']:
749 raise RuntimeError(
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200750 "unable to export %i elementary file(s)%s" % (context['ERR'], exception_str_add))
Harald Weltec91085e2022-02-10 18:05:45 +0100751 elif context['DF_SKIP']:
752 raise RuntimeError(
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200753 "unable to export %i dedicated files(s)%s" % (context['ERR'], exception_str_add))
Harald Welteb2edd142021-01-08 23:29:35 +0100754
Harald Weltec91085e2022-02-10 18:05:45 +0100755 def do_reset(self, opts):
756 """Reset the Card."""
Philipp Maiere345e112023-06-09 15:19:56 +0200757 atr = self._cmd.card.reset()
758 self._cmd.poutput('Card ATR: %s' % i2h(atr))
Harald Weltec91085e2022-02-10 18:05:45 +0100759 self._cmd.update_prompt()
Harald Weltedaf2b392021-05-03 23:17:29 +0200760
Harald Weltec91085e2022-02-10 18:05:45 +0100761 def do_desc(self, opts):
762 """Display human readable file description for the currently selected file"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200763 desc = self._cmd.lchan.selected_file.desc
Harald Weltec91085e2022-02-10 18:05:45 +0100764 if desc:
765 self._cmd.poutput(desc)
766 else:
767 self._cmd.poutput("no description available")
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200768
Harald Weltec91085e2022-02-10 18:05:45 +0100769 def do_verify_adm(self, arg):
770 """VERIFY the ADM1 PIN"""
771 if arg:
772 # use specified ADM-PIN
773 pin_adm = sanitize_pin_adm(arg)
774 else:
775 # try to find an ADM-PIN if none is specified
776 result = card_key_provider_get_field(
777 'ADM1', key='ICCID', value=self._cmd.iccid)
778 pin_adm = sanitize_pin_adm(result)
779 if pin_adm:
780 self._cmd.poutput(
781 "found ADM-PIN '%s' for ICCID '%s'" % (result, self._cmd.iccid))
782 else:
783 raise ValueError(
784 "cannot find ADM-PIN for ICCID '%s'" % (self._cmd.iccid))
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200785
Harald Weltec91085e2022-02-10 18:05:45 +0100786 if pin_adm:
Philipp Maiercfb665b2023-07-20 17:30:24 +0200787 self._cmd.card._scc.verify_chv(self._cmd.card._adm_chv_num, h2b(pin_adm))
Harald Weltec91085e2022-02-10 18:05:45 +0100788 else:
789 raise ValueError("error: cannot authenticate, no adm-pin!")
790
Philipp Maier7b9e2442023-03-22 15:19:54 +0100791 def do_cardinfo(self, opts):
792 """Display information about the currently inserted card"""
793 self._cmd.poutput("Card info:")
794 self._cmd.poutput(" Name: %s" % self._cmd.card.name)
795 self._cmd.poutput(" ATR: %s" % b2h(self._cmd.card._scc.get_atr()))
796 self._cmd.poutput(" ICCID: %s" % self._cmd.iccid)
797 self._cmd.poutput(" Class-Byte: %s" % self._cmd.card._scc.cla_byte)
798 self._cmd.poutput(" Select-Ctrl: %s" % self._cmd.card._scc.sel_ctrl)
799 self._cmd.poutput(" AIDs:")
800 for a in self._cmd.rs.mf.applications:
801 self._cmd.poutput(" %s" % a)
Harald Welteb2edd142021-01-08 23:29:35 +0100802
Harald Welte31d2cf02021-04-03 10:47:29 +0200803@with_default_category('ISO7816 Commands')
804class Iso7816Commands(CommandSet):
Harald Weltec91085e2022-02-10 18:05:45 +0100805 def __init__(self):
806 super().__init__()
Harald Welte31d2cf02021-04-03 10:47:29 +0200807
Harald Weltec91085e2022-02-10 18:05:45 +0100808 def do_select(self, opts):
809 """SELECT a File (ADF/DF/EF)"""
810 if len(opts.arg_list) == 0:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200811 path = self._cmd.lchan.selected_file.fully_qualified_path_str(True)
812 path_fid = self._cmd.lchan.selected_file.fully_qualified_path_str(False)
813 self._cmd.poutput("currently selected file: %s (%s)" % (path, path_fid))
Harald Weltec91085e2022-02-10 18:05:45 +0100814 return
Harald Welte31d2cf02021-04-03 10:47:29 +0200815
Harald Weltec91085e2022-02-10 18:05:45 +0100816 path = opts.arg_list[0]
Harald Weltea6c0f882022-07-17 14:23:17 +0200817 fcp_dec = self._cmd.lchan.select(path, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100818 self._cmd.update_prompt()
819 self._cmd.poutput_json(fcp_dec)
Harald Welte31d2cf02021-04-03 10:47:29 +0200820
Harald Weltec91085e2022-02-10 18:05:45 +0100821 def complete_select(self, text, line, begidx, endidx) -> List[str]:
822 """Command Line tab completion for SELECT"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200823 index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
Harald Weltec91085e2022-02-10 18:05:45 +0100824 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200825
Harald Weltec91085e2022-02-10 18:05:45 +0100826 def get_code(self, code):
827 """Use code either directly or try to get it from external data source"""
828 auto = ('PIN1', 'PIN2', 'PUK1', 'PUK2')
Harald Welte31d2cf02021-04-03 10:47:29 +0200829
Harald Weltec91085e2022-02-10 18:05:45 +0100830 if str(code).upper() not in auto:
831 return sanitize_pin_adm(code)
Harald Welte31d2cf02021-04-03 10:47:29 +0200832
Harald Weltec91085e2022-02-10 18:05:45 +0100833 result = card_key_provider_get_field(
834 str(code), key='ICCID', value=self._cmd.iccid)
835 result = sanitize_pin_adm(result)
836 if result:
837 self._cmd.poutput("found %s '%s' for ICCID '%s'" %
838 (code.upper(), result, self._cmd.iccid))
839 else:
840 self._cmd.poutput("cannot find %s for ICCID '%s'" %
841 (code.upper(), self._cmd.iccid))
842 return result
Harald Welte31d2cf02021-04-03 10:47:29 +0200843
Harald Weltec91085e2022-02-10 18:05:45 +0100844 verify_chv_parser = argparse.ArgumentParser()
845 verify_chv_parser.add_argument(
846 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
847 verify_chv_parser.add_argument(
848 'pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
Harald Welte31d2cf02021-04-03 10:47:29 +0200849
Harald Weltec91085e2022-02-10 18:05:45 +0100850 @cmd2.with_argparser(verify_chv_parser)
851 def do_verify_chv(self, opts):
Harald Welte12af7932022-02-15 16:39:08 +0100852 """Verify (authenticate) using specified CHV (PIN) code, which is how the specifications
853 call it if you authenticate yourself using the specified PIN. There usually is at least PIN1 and
854 PIN2."""
Harald Weltec91085e2022-02-10 18:05:45 +0100855 pin = self.get_code(opts.pin_code)
856 (data, sw) = self._cmd.card._scc.verify_chv(opts.pin_nr, h2b(pin))
857 self._cmd.poutput("CHV verification successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200858
Harald Weltec91085e2022-02-10 18:05:45 +0100859 unblock_chv_parser = argparse.ArgumentParser()
860 unblock_chv_parser.add_argument(
861 '--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
862 unblock_chv_parser.add_argument(
863 'puk_code', type=str, help='PUK code digits \"PUK1\" or \"PUK2\" to get PUK code from external data source')
864 unblock_chv_parser.add_argument(
865 'new_pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
Harald Welte31d2cf02021-04-03 10:47:29 +0200866
Harald Weltec91085e2022-02-10 18:05:45 +0100867 @cmd2.with_argparser(unblock_chv_parser)
868 def do_unblock_chv(self, opts):
869 """Unblock PIN code using specified PUK code"""
870 new_pin = self.get_code(opts.new_pin_code)
871 puk = self.get_code(opts.puk_code)
872 (data, sw) = self._cmd.card._scc.unblock_chv(
873 opts.pin_nr, h2b(puk), h2b(new_pin))
874 self._cmd.poutput("CHV unblock successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200875
Harald Weltec91085e2022-02-10 18:05:45 +0100876 change_chv_parser = argparse.ArgumentParser()
877 change_chv_parser.add_argument(
878 '--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
879 change_chv_parser.add_argument(
880 'pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
881 change_chv_parser.add_argument(
882 'new_pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
Harald Welte31d2cf02021-04-03 10:47:29 +0200883
Harald Weltec91085e2022-02-10 18:05:45 +0100884 @cmd2.with_argparser(change_chv_parser)
885 def do_change_chv(self, opts):
886 """Change PIN code to a new PIN code"""
887 new_pin = self.get_code(opts.new_pin_code)
888 pin = self.get_code(opts.pin_code)
889 (data, sw) = self._cmd.card._scc.change_chv(
890 opts.pin_nr, h2b(pin), h2b(new_pin))
891 self._cmd.poutput("CHV change successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200892
Harald Weltec91085e2022-02-10 18:05:45 +0100893 disable_chv_parser = argparse.ArgumentParser()
894 disable_chv_parser.add_argument(
895 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
896 disable_chv_parser.add_argument(
897 'pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
Harald Welte31d2cf02021-04-03 10:47:29 +0200898
Harald Weltec91085e2022-02-10 18:05:45 +0100899 @cmd2.with_argparser(disable_chv_parser)
900 def do_disable_chv(self, opts):
901 """Disable PIN code using specified PIN code"""
902 pin = self.get_code(opts.pin_code)
903 (data, sw) = self._cmd.card._scc.disable_chv(opts.pin_nr, h2b(pin))
904 self._cmd.poutput("CHV disable successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200905
Harald Weltec91085e2022-02-10 18:05:45 +0100906 enable_chv_parser = argparse.ArgumentParser()
907 enable_chv_parser.add_argument(
908 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
909 enable_chv_parser.add_argument(
910 'pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
Harald Welte31d2cf02021-04-03 10:47:29 +0200911
Harald Weltec91085e2022-02-10 18:05:45 +0100912 @cmd2.with_argparser(enable_chv_parser)
913 def do_enable_chv(self, opts):
914 """Enable PIN code using specified PIN code"""
915 pin = self.get_code(opts.pin_code)
916 (data, sw) = self._cmd.card._scc.enable_chv(opts.pin_nr, h2b(pin))
917 self._cmd.poutput("CHV enable successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200918
Harald Weltec91085e2022-02-10 18:05:45 +0100919 def do_deactivate_file(self, opts):
Harald Welte799c3542022-02-15 15:56:28 +0100920 """Deactivate the currently selected EF"""
Harald Weltec91085e2022-02-10 18:05:45 +0100921 (data, sw) = self._cmd.card._scc.deactivate_file()
Harald Weltea4631612021-04-10 18:17:55 +0200922
Harald Welte799c3542022-02-15 15:56:28 +0100923 activate_file_parser = argparse.ArgumentParser()
924 activate_file_parser.add_argument('NAME', type=str, help='File name or FID of file to activate')
925 @cmd2.with_argparser(activate_file_parser)
Harald Weltec91085e2022-02-10 18:05:45 +0100926 def do_activate_file(self, opts):
Harald Welte12af7932022-02-15 16:39:08 +0100927 """Activate the specified EF. This used to be called REHABILITATE in TS 11.11 for classic
928 SIM. You need to specify the name or FID of the file to activate."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200929 (data, sw) = self._cmd.lchan.activate_file(opts.NAME)
Harald Welte485692b2021-05-25 22:21:44 +0200930
Harald Weltec91085e2022-02-10 18:05:45 +0100931 def complete_activate_file(self, text, line, begidx, endidx) -> List[str]:
932 """Command Line tab completion for ACTIVATE FILE"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200933 index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
Harald Weltec91085e2022-02-10 18:05:45 +0100934 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200935
Harald Weltec91085e2022-02-10 18:05:45 +0100936 open_chan_parser = argparse.ArgumentParser()
937 open_chan_parser.add_argument(
938 'chan_nr', type=int, default=0, help='Channel Number')
Harald Welte703f9332021-04-10 18:39:32 +0200939
Harald Weltec91085e2022-02-10 18:05:45 +0100940 @cmd2.with_argparser(open_chan_parser)
941 def do_open_channel(self, opts):
942 """Open a logical channel."""
943 (data, sw) = self._cmd.card._scc.manage_channel(
944 mode='open', lchan_nr=opts.chan_nr)
Harald Welte703f9332021-04-10 18:39:32 +0200945
Harald Weltec91085e2022-02-10 18:05:45 +0100946 close_chan_parser = argparse.ArgumentParser()
947 close_chan_parser.add_argument(
948 'chan_nr', type=int, default=0, help='Channel Number')
Harald Welte703f9332021-04-10 18:39:32 +0200949
Harald Weltec91085e2022-02-10 18:05:45 +0100950 @cmd2.with_argparser(close_chan_parser)
951 def do_close_channel(self, opts):
952 """Close a logical channel."""
953 (data, sw) = self._cmd.card._scc.manage_channel(
954 mode='close', lchan_nr=opts.chan_nr)
Harald Welte703f9332021-04-10 18:39:32 +0200955
Harald Weltec91085e2022-02-10 18:05:45 +0100956 def do_status(self, opts):
957 """Perform the STATUS command."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200958 fcp_dec = self._cmd.lchan.status()
Harald Weltec91085e2022-02-10 18:05:45 +0100959 self._cmd.poutput_json(fcp_dec)
Harald Welte34b05d32021-05-25 22:03:13 +0200960
Harald Welteec950532021-10-20 13:09:00 +0200961
Harald Weltecab26c72022-08-06 16:12:30 +0200962class Proact(ProactiveHandler):
963 def receive_fetch(self, pcmd: ProactiveCommand):
964 # print its parsed representation
965 print(pcmd.decoded)
966 # TODO: implement the basics, such as SMS Sending, ...
967
968
Harald Welte703f9332021-04-10 18:39:32 +0200969
Harald Weltef422eb12023-06-09 11:15:09 +0200970option_parser = argparse.ArgumentParser(description='interactive SIM card shell',
Harald Weltef2e761c2021-04-11 11:56:44 +0200971 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
Harald Welte28c24312021-04-11 12:19:36 +0200972argparse_add_reader_args(option_parser)
Harald Weltec8ff0262021-04-11 12:06:13 +0200973
974global_group = option_parser.add_argument_group('General Options')
975global_group.add_argument('--script', metavar='PATH', default=None,
Harald Welte28c24312021-04-11 12:19:36 +0200976 help='script with pySim-shell commands to be executed automatically at start-up')
Harald Weltec91085e2022-02-10 18:05:45 +0100977global_group.add_argument('--csv', metavar='FILE',
978 default=None, help='Read card data from CSV file')
Philipp Maier76667642021-09-22 16:53:22 +0200979global_group.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
Harald Weltec91085e2022-02-10 18:05:45 +0100980 help="Use automatic card handling machine")
Harald Weltec8ff0262021-04-11 12:06:13 +0200981
982adm_group = global_group.add_mutually_exclusive_group()
983adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', default=None,
984 help='ADM PIN used for provisioning (overwrites default)')
985adm_group.add_argument('-A', '--pin-adm-hex', metavar='PIN_ADM1_HEX', dest='pin_adm_hex', default=None,
986 help='ADM PIN used for provisioning, as hex string (16 characters long)')
Harald Welteb2edd142021-01-08 23:29:35 +0100987
Harald Welte38306df2023-07-11 21:17:55 +0200988option_parser.add_argument("command", nargs='?',
989 help="A pySim-shell command that would optionally be executed at startup")
990option_parser.add_argument('command_args', nargs=argparse.REMAINDER,
991 help="Optional Arguments for command")
992
Harald Welteb2edd142021-01-08 23:29:35 +0100993
994if __name__ == '__main__':
995
Harald Weltec91085e2022-02-10 18:05:45 +0100996 # Parse options
997 opts = option_parser.parse_args()
Harald Welteb2edd142021-01-08 23:29:35 +0100998
Harald Weltec91085e2022-02-10 18:05:45 +0100999 # If a script file is specified, be sure that it actually exists
1000 if opts.script:
1001 if not os.access(opts.script, os.R_OK):
1002 print("Invalid script file!")
1003 sys.exit(2)
Philipp Maier13e258d2021-04-08 17:48:49 +02001004
Harald Weltec91085e2022-02-10 18:05:45 +01001005 # Register csv-file as card data provider, either from specified CSV
1006 # or from CSV file in home directory
1007 csv_default = str(Path.home()) + "/.osmocom/pysim/card_data.csv"
1008 if opts.csv:
1009 card_key_provider_register(CardKeyProviderCsv(opts.csv))
1010 if os.path.isfile(csv_default):
1011 card_key_provider_register(CardKeyProviderCsv(csv_default))
Philipp Maier2b11c322021-03-17 12:37:39 +01001012
Harald Weltec91085e2022-02-10 18:05:45 +01001013 # Init card reader driver
Harald Weltecab26c72022-08-06 16:12:30 +02001014 sl = init_reader(opts, proactive_handler = Proact())
Harald Weltec91085e2022-02-10 18:05:45 +01001015 if sl is None:
1016 exit(1)
Philipp Maierea95c392021-09-16 13:10:19 +02001017
Harald Weltec91085e2022-02-10 18:05:45 +01001018 # Create command layer
1019 scc = SimCardCommands(transport=sl)
Philipp Maierea95c392021-09-16 13:10:19 +02001020
Harald Weltec91085e2022-02-10 18:05:45 +01001021 # Create a card handler (for bulk provisioning)
1022 if opts.card_handler_config:
1023 ch = CardHandlerAuto(None, opts.card_handler_config)
1024 else:
1025 ch = CardHandler(sl)
Philipp Maier76667642021-09-22 16:53:22 +02001026
Harald Weltec91085e2022-02-10 18:05:45 +01001027 # Detect and initialize the card in the reader. This may fail when there
1028 # is no card in the reader or the card is unresponsive. PysimApp is
1029 # able to tolerate and recover from that.
1030 try:
1031 rs, card = init_card(sl)
1032 app = PysimApp(card, rs, sl, ch, opts.script)
1033 except:
1034 print("Card initialization failed with an exception:")
1035 print("---------------------8<---------------------")
1036 traceback.print_exc()
1037 print("---------------------8<---------------------")
1038 print("(you may still try to recover from this manually by using the 'equip' command.)")
1039 print(
1040 " it should also be noted that some readers may behave strangely when no card")
1041 print(" is inserted.)")
1042 print("")
Philipp Maier7226c092022-06-01 17:58:38 +02001043 app = PysimApp(card, None, sl, ch, opts.script)
Philipp Maierea95c392021-09-16 13:10:19 +02001044
Harald Weltec91085e2022-02-10 18:05:45 +01001045 # If the user supplies an ADM PIN at via commandline args authenticate
1046 # immediately so that the user does not have to use the shell commands
1047 pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
1048 if pin_adm:
1049 if not card:
1050 print("Card error, cannot do ADM verification with supplied ADM pin now.")
1051 try:
1052 card.verify_adm(h2b(pin_adm))
1053 except Exception as e:
1054 print(e)
Philipp Maier228c98e2021-03-10 20:14:06 +01001055
Harald Welte38306df2023-07-11 21:17:55 +02001056 if opts.command:
1057 app.onecmd_plus_hooks('{} {}'.format(opts.command, ' '.join(opts.command_args)))
1058 else:
1059 app.cmdloop()