blob: 213f057dd4737ccddad1cf3ee9a1f2ccea43b3d7 [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
Harald Weltecab26c72022-08-06 16:12:30 +020055from pySim.utils import sanitize_pin_adm, tabulate_str_list, boxed_heading_str, Hexstr
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())
243 self.iccid, sw = self.card.read_iccid()
Harald Weltea6c0f882022-07-17 14:23:17 +0200244 self.lchan.select('MF', self)
Harald Weltec91085e2022-02-10 18:05:45 +0100245 rc = True
246 else:
247 self.poutput("pySim-shell not equipped!")
Philipp Maier5d698e52021-09-16 13:18:01 +0200248
Harald Weltec91085e2022-02-10 18:05:45 +0100249 self.update_prompt()
250 return rc
Harald Welteb2edd142021-01-08 23:29:35 +0100251
Harald Weltec91085e2022-02-10 18:05:45 +0100252 def poutput_json(self, data, force_no_pretty=False):
253 """like cmd2.poutput() but for a JSON serializable dict."""
254 if force_no_pretty or self.json_pretty_print == False:
255 output = json.dumps(data, cls=JsonEncoder)
256 else:
257 output = json.dumps(data, cls=JsonEncoder, indent=4)
258 self.poutput(output)
Harald Welteb2edd142021-01-08 23:29:35 +0100259
Harald Weltec91085e2022-02-10 18:05:45 +0100260 def _onchange_numeric_path(self, param_name, old, new):
261 self.update_prompt()
Philipp Maier38c74f62021-03-17 17:19:52 +0100262
Harald Weltec91085e2022-02-10 18:05:45 +0100263 def _onchange_conserve_write(self, param_name, old, new):
264 if self.rs:
265 self.rs.conserve_write = new
Harald Welte7829d8a2021-04-10 11:28:53 +0200266
Harald Weltec91085e2022-02-10 18:05:45 +0100267 def _onchange_apdu_trace(self, param_name, old, new):
268 if self.card:
269 if new == True:
270 self.card._scc._tp.apdu_tracer = self.Cmd2ApduTracer(self)
271 else:
272 self.card._scc._tp.apdu_tracer = None
Harald Welte7829d8a2021-04-10 11:28:53 +0200273
Harald Weltec91085e2022-02-10 18:05:45 +0100274 class Cmd2ApduTracer(ApduTracer):
275 def __init__(self, cmd2_app):
276 self.cmd2 = app
Harald Welte7829d8a2021-04-10 11:28:53 +0200277
Harald Weltec91085e2022-02-10 18:05:45 +0100278 def trace_response(self, cmd, sw, resp):
279 self.cmd2.poutput("-> %s %s" % (cmd[:10], cmd[10:]))
280 self.cmd2.poutput("<- %s: %s" % (sw, resp))
Harald Welteb2edd142021-01-08 23:29:35 +0100281
Harald Weltec91085e2022-02-10 18:05:45 +0100282 def update_prompt(self):
Harald Weltea6c0f882022-07-17 14:23:17 +0200283 if self.lchan:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200284 path_str = self.lchan.selected_file.fully_qualified_path_str(not self.numeric_path)
285 self.prompt = 'pySIM-shell (%s)> ' % (path_str)
Harald Weltec91085e2022-02-10 18:05:45 +0100286 else:
Philipp Maier7226c092022-06-01 17:58:38 +0200287 if self.card:
288 self.prompt = 'pySIM-shell (no card profile)> '
289 else:
290 self.prompt = 'pySIM-shell (no card)> '
Harald Welteb2edd142021-01-08 23:29:35 +0100291
Harald Weltec91085e2022-02-10 18:05:45 +0100292 @cmd2.with_category(CUSTOM_CATEGORY)
293 def do_intro(self, _):
294 """Display the intro banner"""
295 self.poutput(self.intro)
Philipp Maier9764de22021-11-03 10:44:39 +0100296
Harald Weltec91085e2022-02-10 18:05:45 +0100297 def do_eof(self, _: argparse.Namespace) -> bool:
298 self.poutput("")
299 return self.do_quit('')
Philipp Maier5d698e52021-09-16 13:18:01 +0200300
Harald Weltec91085e2022-02-10 18:05:45 +0100301 @cmd2.with_category(CUSTOM_CATEGORY)
302 def do_equip(self, opts):
303 """Equip pySim-shell with card"""
Harald Welte659781c2023-06-06 17:00:51 +0200304 if self.rs.profile:
305 for cmd_set in self.rs.profile.shell_cmdsets:
306 self.unregister_command_set(cmd_set)
Harald Weltec91085e2022-02-10 18:05:45 +0100307 rs, card = init_card(sl)
308 self.equip(card, rs)
Philipp Maier76667642021-09-22 16:53:22 +0200309
Philipp Maier7226c092022-06-01 17:58:38 +0200310 apdu_cmd_parser = argparse.ArgumentParser()
311 apdu_cmd_parser.add_argument('APDU', type=str, help='APDU as hex string')
Philipp Maiere7d1b672022-06-01 18:05:34 +0200312 apdu_cmd_parser.add_argument('--expect-sw', help='expect a specified status word', type=str, default=None)
Philipp Maier7226c092022-06-01 17:58:38 +0200313
314 @cmd2.with_argparser(apdu_cmd_parser)
315 def do_apdu(self, opts):
316 """Send a raw APDU to the card, and print SW + Response.
317 DANGEROUS: pySim-shell will not know any card state changes, and
318 not continue to work as expected if you e.g. select a different
319 file."""
320 data, sw = self.card._scc._tp.send_apdu(opts.APDU)
321 if data:
322 self.poutput("SW: %s, RESP: %s" % (sw, data))
323 else:
324 self.poutput("SW: %s" % sw)
Philipp Maiere7d1b672022-06-01 18:05:34 +0200325 if opts.expect_sw:
326 if not sw_match(sw, opts.expect_sw):
327 raise SwMatchError(sw, opts.expect_sw)
Philipp Maier7226c092022-06-01 17:58:38 +0200328
Harald Weltec91085e2022-02-10 18:05:45 +0100329 class InterceptStderr(list):
330 def __init__(self):
331 self._stderr_backup = sys.stderr
Philipp Maier76667642021-09-22 16:53:22 +0200332
Harald Weltec91085e2022-02-10 18:05:45 +0100333 def __enter__(self):
334 self._stringio_stderr = StringIO()
335 sys.stderr = self._stringio_stderr
336 return self
Philipp Maier76667642021-09-22 16:53:22 +0200337
Harald Weltec91085e2022-02-10 18:05:45 +0100338 def __exit__(self, *args):
339 self.stderr = self._stringio_stderr.getvalue().strip()
340 del self._stringio_stderr
341 sys.stderr = self._stderr_backup
Philipp Maier76667642021-09-22 16:53:22 +0200342
Harald Weltec91085e2022-02-10 18:05:45 +0100343 def _show_failure_sign(self):
Harald Welte961b8032023-04-27 17:30:22 +0200344 self.poutput(style(" +-------------+", fg=LIGHT_RED))
345 self.poutput(style(" + ## ## +", fg=LIGHT_RED))
346 self.poutput(style(" + ## ## +", fg=LIGHT_RED))
347 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))
Harald Weltec91085e2022-02-10 18:05:45 +0100351 self.poutput("")
Philipp Maier76667642021-09-22 16:53:22 +0200352
Harald Weltec91085e2022-02-10 18:05:45 +0100353 def _show_success_sign(self):
Harald Welte961b8032023-04-27 17:30:22 +0200354 self.poutput(style(" +-------------+", fg=LIGHT_GREEN))
355 self.poutput(style(" + ## +", fg=LIGHT_GREEN))
356 self.poutput(style(" + ## +", fg=LIGHT_GREEN))
357 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))
Harald Weltec91085e2022-02-10 18:05:45 +0100361 self.poutput("")
Philipp Maier76667642021-09-22 16:53:22 +0200362
Harald Weltec91085e2022-02-10 18:05:45 +0100363 def _process_card(self, first, script_path):
Philipp Maier76667642021-09-22 16:53:22 +0200364
Harald Weltec91085e2022-02-10 18:05:45 +0100365 # Early phase of card initialzation (this part may fail with an exception)
366 try:
367 rs, card = init_card(self.sl)
368 rc = self.equip(card, rs)
369 except:
370 self.poutput("")
371 self.poutput("Card initialization failed with an exception:")
372 self.poutput("---------------------8<---------------------")
373 traceback.print_exc()
374 self.poutput("---------------------8<---------------------")
375 self.poutput("")
376 return -1
Philipp Maier76667642021-09-22 16:53:22 +0200377
Harald Weltec91085e2022-02-10 18:05:45 +0100378 # Actual card processing step. This part should never fail with an exception since the cmd2
379 # do_run_script method will catch any exception that might occur during script execution.
380 if rc:
381 self.poutput("")
382 self.poutput("Transcript stdout:")
383 self.poutput("---------------------8<---------------------")
384 with self.InterceptStderr() as logged:
385 self.do_run_script(script_path)
386 self.poutput("---------------------8<---------------------")
Philipp Maier76667642021-09-22 16:53:22 +0200387
Harald Weltec91085e2022-02-10 18:05:45 +0100388 self.poutput("")
389 self.poutput("Transcript stderr:")
390 if logged.stderr:
391 self.poutput("---------------------8<---------------------")
392 self.poutput(logged.stderr)
393 self.poutput("---------------------8<---------------------")
394 else:
395 self.poutput("(none)")
Philipp Maier76667642021-09-22 16:53:22 +0200396
Harald Weltec91085e2022-02-10 18:05:45 +0100397 # Check for exceptions
398 self.poutput("")
399 if "EXCEPTION of type" not in logged.stderr:
400 return 0
Philipp Maier76667642021-09-22 16:53:22 +0200401
Harald Weltec91085e2022-02-10 18:05:45 +0100402 return -1
Philipp Maier76667642021-09-22 16:53:22 +0200403
Harald Weltec91085e2022-02-10 18:05:45 +0100404 bulk_script_parser = argparse.ArgumentParser()
405 bulk_script_parser.add_argument(
406 'script_path', help="path to the script file")
407 bulk_script_parser.add_argument('--halt_on_error', help='stop card handling if an exeption occurs',
408 action='store_true')
409 bulk_script_parser.add_argument('--tries', type=int, default=2,
410 help='how many tries before trying the next card')
411 bulk_script_parser.add_argument('--on_stop_action', type=str, default=None,
412 help='commandline to execute when card handling has stopped')
413 bulk_script_parser.add_argument('--pre_card_action', type=str, default=None,
414 help='commandline to execute before actually talking to the card')
Philipp Maier76667642021-09-22 16:53:22 +0200415
Harald Weltec91085e2022-02-10 18:05:45 +0100416 @cmd2.with_argparser(bulk_script_parser)
417 @cmd2.with_category(CUSTOM_CATEGORY)
418 def do_bulk_script(self, opts):
419 """Run script on multiple cards (bulk provisioning)"""
Philipp Maier76667642021-09-22 16:53:22 +0200420
Harald Weltec91085e2022-02-10 18:05:45 +0100421 # Make sure that the script file exists and that it is readable.
422 if not os.access(opts.script_path, os.R_OK):
423 self.poutput("Invalid script file!")
424 return
Philipp Maier76667642021-09-22 16:53:22 +0200425
Harald Weltec91085e2022-02-10 18:05:45 +0100426 success_count = 0
427 fail_count = 0
Philipp Maier76667642021-09-22 16:53:22 +0200428
Harald Weltec91085e2022-02-10 18:05:45 +0100429 first = True
430 while 1:
431 # TODO: Count consecutive failures, if more than N consecutive failures occur, then stop.
432 # The ratinale is: There may be a problem with the device, we do want to prevent that
433 # all remaining cards are fired to the error bin. This is only relevant for situations
434 # with large stacks, probably we do not need this feature right now.
Philipp Maier76667642021-09-22 16:53:22 +0200435
Harald Weltec91085e2022-02-10 18:05:45 +0100436 try:
437 # In case of failure, try multiple times.
438 for i in range(opts.tries):
439 # fetch card into reader bay
440 ch.get(first)
Philipp Maier76667642021-09-22 16:53:22 +0200441
Harald Weltec91085e2022-02-10 18:05:45 +0100442 # if necessary execute an action before we start processing the card
443 if(opts.pre_card_action):
444 os.system(opts.pre_card_action)
Philipp Maier76667642021-09-22 16:53:22 +0200445
Harald Weltec91085e2022-02-10 18:05:45 +0100446 # process the card
447 rc = self._process_card(first, opts.script_path)
448 if rc == 0:
449 success_count = success_count + 1
450 self._show_success_sign()
451 self.poutput("Statistics: success :%i, failure: %i" % (
452 success_count, fail_count))
453 break
454 else:
455 fail_count = fail_count + 1
456 self._show_failure_sign()
457 self.poutput("Statistics: success :%i, failure: %i" % (
458 success_count, fail_count))
Philipp Maier76667642021-09-22 16:53:22 +0200459
Harald Weltec91085e2022-02-10 18:05:45 +0100460 # Depending on success or failure, the card goes either in the "error" bin or in the
461 # "done" bin.
462 if rc < 0:
463 ch.error()
464 else:
465 ch.done()
Philipp Maier76667642021-09-22 16:53:22 +0200466
Harald Weltec91085e2022-02-10 18:05:45 +0100467 # In most cases it is possible to proceed with the next card, but the
468 # user may decide to halt immediately when an error occurs
469 if opts.halt_on_error and rc < 0:
470 return
Philipp Maier76667642021-09-22 16:53:22 +0200471
Harald Weltec91085e2022-02-10 18:05:45 +0100472 except (KeyboardInterrupt):
473 self.poutput("")
474 self.poutput("Terminated by user!")
475 return
476 except (SystemExit):
477 # When all cards are processed the card handler device will throw a SystemExit
478 # exception. Also Errors that are not recoverable (cards stuck etc.) will end up here.
479 # The user has the option to execute some action to make aware that the card handler
480 # needs service.
481 if(opts.on_stop_action):
482 os.system(opts.on_stop_action)
483 return
484 except:
485 self.poutput("")
486 self.poutput("Card handling failed with an exception:")
487 self.poutput("---------------------8<---------------------")
488 traceback.print_exc()
489 self.poutput("---------------------8<---------------------")
490 self.poutput("")
491 fail_count = fail_count + 1
492 self._show_failure_sign()
493 self.poutput("Statistics: success :%i, failure: %i" %
494 (success_count, fail_count))
Philipp Maierb52feed2021-09-22 16:43:13 +0200495
Harald Weltec91085e2022-02-10 18:05:45 +0100496 first = False
497
498 echo_parser = argparse.ArgumentParser()
499 echo_parser.add_argument('string', help="string to echo on the shell")
500
501 @cmd2.with_argparser(echo_parser)
502 @cmd2.with_category(CUSTOM_CATEGORY)
503 def do_echo(self, opts):
504 """Echo (print) a string on the console"""
505 self.poutput(opts.string)
506
Harald Weltefc315482022-07-23 12:49:14 +0200507 @cmd2.with_category(CUSTOM_CATEGORY)
508 def do_version(self, opts):
509 """Print the pySim software version."""
510 import pkg_resources
511 self.poutput(pkg_resources.get_distribution('pySim'))
Harald Welteb2edd142021-01-08 23:29:35 +0100512
Harald Welte31d2cf02021-04-03 10:47:29 +0200513@with_default_category('pySim Commands')
514class PySimCommands(CommandSet):
Harald Weltec91085e2022-02-10 18:05:45 +0100515 def __init__(self):
516 super().__init__()
Harald Welteb2edd142021-01-08 23:29:35 +0100517
Harald Weltec91085e2022-02-10 18:05:45 +0100518 dir_parser = argparse.ArgumentParser()
519 dir_parser.add_argument(
520 '--fids', help='Show file identifiers', action='store_true')
521 dir_parser.add_argument(
522 '--names', help='Show file names', action='store_true')
523 dir_parser.add_argument(
524 '--apps', help='Show applications', action='store_true')
525 dir_parser.add_argument(
526 '--all', help='Show all selectable identifiers and names', action='store_true')
Philipp Maier5d3e2592021-02-22 17:22:16 +0100527
Harald Weltec91085e2022-02-10 18:05:45 +0100528 @cmd2.with_argparser(dir_parser)
529 def do_dir(self, opts):
530 """Show a listing of files available in currently selected DF or MF"""
531 if opts.all:
532 flags = []
533 elif opts.fids or opts.names or opts.apps:
534 flags = ['PARENT', 'SELF']
535 if opts.fids:
536 flags += ['FIDS', 'AIDS']
537 if opts.names:
538 flags += ['FNAMES', 'ANAMES']
539 if opts.apps:
540 flags += ['ANAMES', 'AIDS']
541 else:
542 flags = ['PARENT', 'SELF', 'FNAMES', 'ANAMES']
543 selectables = list(
Harald Weltea6c0f882022-07-17 14:23:17 +0200544 self._cmd.lchan.selected_file.get_selectable_names(flags=flags))
Harald Weltec91085e2022-02-10 18:05:45 +0100545 directory_str = tabulate_str_list(
546 selectables, width=79, hspace=2, lspace=1, align_left=True)
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200547 path = self._cmd.lchan.selected_file.fully_qualified_path_str(True)
548 self._cmd.poutput(path)
549 path = self._cmd.lchan.selected_file.fully_qualified_path_str(False)
550 self._cmd.poutput(path)
Harald Weltec91085e2022-02-10 18:05:45 +0100551 self._cmd.poutput(directory_str)
552 self._cmd.poutput("%d files" % len(selectables))
Harald Welteb2edd142021-01-08 23:29:35 +0100553
Philipp Maier7b138b02022-05-31 13:42:56 +0200554 def walk(self, indent=0, action_ef=None, action_df=None, context=None, **kwargs):
Harald Weltec91085e2022-02-10 18:05:45 +0100555 """Recursively walk through the file system, starting at the currently selected DF"""
Philipp Maier7b138b02022-05-31 13:42:56 +0200556
Harald Weltea6c0f882022-07-17 14:23:17 +0200557 if isinstance(self._cmd.lchan.selected_file, CardDF):
Philipp Maier7b138b02022-05-31 13:42:56 +0200558 if action_df:
559 action_df(context, opts)
560
Harald Weltea6c0f882022-07-17 14:23:17 +0200561 files = self._cmd.lchan.selected_file.get_selectables(
Harald Weltec91085e2022-02-10 18:05:45 +0100562 flags=['FNAMES', 'ANAMES'])
563 for f in files:
Philipp Maier7b138b02022-05-31 13:42:56 +0200564 # special case: When no action is performed, just output a directory
565 if not action_ef and not action_df:
Harald Weltec91085e2022-02-10 18:05:45 +0100566 output_str = " " * indent + str(f) + (" " * 250)
567 output_str = output_str[0:25]
568 if isinstance(files[f], CardADF):
569 output_str += " " + str(files[f].aid)
570 else:
571 output_str += " " + str(files[f].fid)
572 output_str += " " + str(files[f].desc)
573 self._cmd.poutput(output_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200574
Harald Weltec91085e2022-02-10 18:05:45 +0100575 if isinstance(files[f], CardDF):
576 skip_df = False
577 try:
Harald Weltea6c0f882022-07-17 14:23:17 +0200578 fcp_dec = self._cmd.lchan.select(f, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100579 except Exception as e:
580 skip_df = True
Harald Weltea6c0f882022-07-17 14:23:17 +0200581 df = self._cmd.lchan.selected_file
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200582 df_path = df.fully_qualified_path_str(True)
583 df_skip_reason_str = df_path + \
Harald Weltec91085e2022-02-10 18:05:45 +0100584 "/" + str(f) + ", " + str(e)
585 if context:
586 context['DF_SKIP'] += 1
587 context['DF_SKIP_REASON'].append(df_skip_reason_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200588
Harald Weltec91085e2022-02-10 18:05:45 +0100589 # If the DF was skipped, we never have entered the directory
590 # below, so we must not move up.
591 if skip_df == False:
Philipp Maier7b138b02022-05-31 13:42:56 +0200592 self.walk(indent + 1, action_ef, action_df, context, **kwargs)
Harald Weltea6c0f882022-07-17 14:23:17 +0200593 fcp_dec = self._cmd.lchan.select("..", self._cmd)
Philipp Maierf408a402021-04-09 21:16:12 +0200594
Philipp Maier7b138b02022-05-31 13:42:56 +0200595 elif action_ef:
Harald Weltea6c0f882022-07-17 14:23:17 +0200596 df_before_action = self._cmd.lchan.selected_file
Philipp Maier7b138b02022-05-31 13:42:56 +0200597 action_ef(f, context, **kwargs)
Harald Weltec91085e2022-02-10 18:05:45 +0100598 # When walking through the file system tree the action must not
599 # always restore the currently selected file to the file that
600 # was selected before executing the action() callback.
Harald Weltea6c0f882022-07-17 14:23:17 +0200601 if df_before_action != self._cmd.lchan.selected_file:
Harald Weltec91085e2022-02-10 18:05:45 +0100602 raise RuntimeError("inconsistent walk, %s is currently selected but expecting %s to be selected"
Harald Weltea6c0f882022-07-17 14:23:17 +0200603 % (str(self._cmd.lchan.selected_file), str(df_before_action)))
Philipp Maierff9dae22021-02-25 17:03:21 +0100604
Harald Weltec91085e2022-02-10 18:05:45 +0100605 def do_tree(self, opts):
606 """Display a filesystem-tree with all selectable files"""
607 self.walk()
Philipp Maierff9dae22021-02-25 17:03:21 +0100608
Philipp Maier7b138b02022-05-31 13:42:56 +0200609 def export_ef(self, filename, context, as_json):
610 """ Select and export a single elementary file (EF) """
Harald Weltec91085e2022-02-10 18:05:45 +0100611 context['COUNT'] += 1
Harald Weltea6c0f882022-07-17 14:23:17 +0200612 df = self._cmd.lchan.selected_file
Philipp Maierac34dcc2021-04-01 17:19:05 +0200613
Philipp Maierea81f752022-05-19 10:13:30 +0200614 # The currently selected file (not the file we are going to export)
615 # must always be an ADF or DF. From this starting point we select
616 # the EF we want to export. To maintain consistency we will then
617 # select the current DF again (see comment below).
Harald Weltec91085e2022-02-10 18:05:45 +0100618 if not isinstance(df, CardDF):
619 raise RuntimeError(
620 "currently selected file %s is not a DF or ADF" % str(df))
Philipp Maierac34dcc2021-04-01 17:19:05 +0200621
Harald Weltec91085e2022-02-10 18:05:45 +0100622 df_path_list = df.fully_qualified_path(True)
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200623 df_path = df.fully_qualified_path_str(True)
624 df_path_fid = df.fully_qualified_path_str(False)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100625
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200626 file_str = df_path + "/" + str(filename)
Harald Weltec91085e2022-02-10 18:05:45 +0100627 self._cmd.poutput(boxed_heading_str(file_str))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100628
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200629 self._cmd.poutput("# directory: %s (%s)" % (df_path, df_path_fid))
Harald Weltec91085e2022-02-10 18:05:45 +0100630 try:
Harald Weltea6c0f882022-07-17 14:23:17 +0200631 fcp_dec = self._cmd.lchan.select(filename, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100632 self._cmd.poutput("# file: %s (%s)" % (
Harald Weltea6c0f882022-07-17 14:23:17 +0200633 self._cmd.lchan.selected_file.name, self._cmd.lchan.selected_file.fid))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100634
Harald Weltea6c0f882022-07-17 14:23:17 +0200635 structure = self._cmd.lchan.selected_file_structure()
Harald Weltec91085e2022-02-10 18:05:45 +0100636 self._cmd.poutput("# structure: %s" % str(structure))
Harald Weltea6c0f882022-07-17 14:23:17 +0200637 self._cmd.poutput("# RAW FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp_hex))
638 self._cmd.poutput("# Decoded FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100639
Harald Weltec91085e2022-02-10 18:05:45 +0100640 for f in df_path_list:
641 self._cmd.poutput("select " + str(f))
Harald Weltea6c0f882022-07-17 14:23:17 +0200642 self._cmd.poutput("select " + self._cmd.lchan.selected_file.name)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100643
Harald Weltec91085e2022-02-10 18:05:45 +0100644 if structure == 'transparent':
Harald Welte08b11ab2022-02-10 18:56:41 +0100645 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200646 result = self._cmd.lchan.read_binary_dec()
Harald Welte08b11ab2022-02-10 18:56:41 +0100647 self._cmd.poutput("update_binary_decoded '%s'" % json.dumps(result[0], cls=JsonEncoder))
648 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200649 result = self._cmd.lchan.read_binary()
Harald Welte08b11ab2022-02-10 18:56:41 +0100650 self._cmd.poutput("update_binary " + str(result[0]))
Harald Weltec91085e2022-02-10 18:05:45 +0100651 elif structure == 'cyclic' or structure == 'linear_fixed':
652 # Use number of records specified in select response
Harald Weltea6c0f882022-07-17 14:23:17 +0200653 num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
Harald Welte747a9782022-02-13 17:52:28 +0100654 if num_of_rec:
Harald Weltec91085e2022-02-10 18:05:45 +0100655 for r in range(1, num_of_rec + 1):
Harald Welte08b11ab2022-02-10 18:56:41 +0100656 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200657 result = self._cmd.lchan.read_record_dec(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100658 self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
659 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200660 result = self._cmd.lchan.read_record(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100661 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
662
Harald Weltec91085e2022-02-10 18:05:45 +0100663 # When the select response does not return the number of records, read until we hit the
664 # first record that cannot be read.
665 else:
666 r = 1
667 while True:
668 try:
Harald Welte08b11ab2022-02-10 18:56:41 +0100669 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200670 result = self._cmd.lchan.read_record_dec(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100671 self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
672 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200673 result = self._cmd.lchan.read_record(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100674 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
Harald Weltec91085e2022-02-10 18:05:45 +0100675 except SwMatchError as e:
676 # We are past the last valid record - stop
677 if e.sw_actual == "9402":
678 break
679 # Some other problem occurred
680 else:
681 raise e
Harald Weltec91085e2022-02-10 18:05:45 +0100682 r = r + 1
683 elif structure == 'ber_tlv':
Harald Weltea6c0f882022-07-17 14:23:17 +0200684 tags = self._cmd.lchan.retrieve_tags()
Harald Weltec91085e2022-02-10 18:05:45 +0100685 for t in tags:
Harald Weltea6c0f882022-07-17 14:23:17 +0200686 result = self._cmd.lchan.retrieve_data(t)
Harald Weltec91085e2022-02-10 18:05:45 +0100687 (tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
688 self._cmd.poutput("set_data 0x%02x %s" % (t, b2h(val)))
689 else:
690 raise RuntimeError(
691 'Unsupported structure "%s" of file "%s"' % (structure, filename))
692 except Exception as e:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200693 bad_file_str = df_path + "/" + str(filename) + ", " + str(e)
Harald Weltec91085e2022-02-10 18:05:45 +0100694 self._cmd.poutput("# bad file: %s" % bad_file_str)
695 context['ERR'] += 1
696 context['BAD'].append(bad_file_str)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100697
Harald Weltec91085e2022-02-10 18:05:45 +0100698 # When reading the file is done, make sure the parent file is
699 # selected again. This will be the usual case, however we need
700 # to check before since we must not select the same DF twice
Harald Weltea6c0f882022-07-17 14:23:17 +0200701 if df != self._cmd.lchan.selected_file:
702 self._cmd.lchan.select(df.fid or df.aid, self._cmd)
Philipp Maierac34dcc2021-04-01 17:19:05 +0200703
Harald Weltec91085e2022-02-10 18:05:45 +0100704 self._cmd.poutput("#")
Philipp Maier24f7bd32021-02-25 17:06:18 +0100705
Harald Weltec91085e2022-02-10 18:05:45 +0100706 export_parser = argparse.ArgumentParser()
707 export_parser.add_argument(
708 '--filename', type=str, default=None, help='only export specific file')
Harald Welte08b11ab2022-02-10 18:56:41 +0100709 export_parser.add_argument(
710 '--json', action='store_true', help='export as JSON (less reliable)')
Philipp Maier24f7bd32021-02-25 17:06:18 +0100711
Harald Weltec91085e2022-02-10 18:05:45 +0100712 @cmd2.with_argparser(export_parser)
713 def do_export(self, opts):
714 """Export files to script that can be imported back later"""
715 context = {'ERR': 0, 'COUNT': 0, 'BAD': [],
716 'DF_SKIP': 0, 'DF_SKIP_REASON': []}
Philipp Maier9a4091d2022-05-19 10:20:30 +0200717 kwargs_export = {'as_json': opts.json}
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200718 exception_str_add = ""
719
Harald Weltec91085e2022-02-10 18:05:45 +0100720 if opts.filename:
Philipp Maier7b138b02022-05-31 13:42:56 +0200721 self.export_ef(opts.filename, context, **kwargs_export)
Harald Weltec91085e2022-02-10 18:05:45 +0100722 else:
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200723 try:
724 self.walk(0, self.export_ef, None, context, **kwargs_export)
725 except Exception as e:
726 print("# Stopping early here due to exception: " + str(e))
727 print("#")
728 exception_str_add = ", also had to stop early due to exception:" + str(e)
Philipp Maier80ce71f2021-04-19 21:24:23 +0200729
Harald Weltec91085e2022-02-10 18:05:45 +0100730 self._cmd.poutput(boxed_heading_str("Export summary"))
Philipp Maier80ce71f2021-04-19 21:24:23 +0200731
Harald Weltec91085e2022-02-10 18:05:45 +0100732 self._cmd.poutput("# total files visited: %u" % context['COUNT'])
733 self._cmd.poutput("# bad files: %u" % context['ERR'])
734 for b in context['BAD']:
735 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200736
Harald Weltec91085e2022-02-10 18:05:45 +0100737 self._cmd.poutput("# skipped dedicated files(s): %u" %
738 context['DF_SKIP'])
739 for b in context['DF_SKIP_REASON']:
740 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200741
Harald Weltec91085e2022-02-10 18:05:45 +0100742 if context['ERR'] and context['DF_SKIP']:
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200743 raise RuntimeError("unable to export %i elementary file(s) and %i dedicated file(s)%s" % (
744 context['ERR'], context['DF_SKIP'], exception_str_add))
Harald Weltec91085e2022-02-10 18:05:45 +0100745 elif context['ERR']:
746 raise RuntimeError(
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200747 "unable to export %i elementary file(s)%s" % (context['ERR'], exception_str_add))
Harald Weltec91085e2022-02-10 18:05:45 +0100748 elif context['DF_SKIP']:
749 raise RuntimeError(
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200750 "unable to export %i dedicated files(s)%s" % (context['ERR'], exception_str_add))
Harald Welteb2edd142021-01-08 23:29:35 +0100751
Harald Weltec91085e2022-02-10 18:05:45 +0100752 def do_reset(self, opts):
753 """Reset the Card."""
Philipp Maiere345e112023-06-09 15:19:56 +0200754 atr = self._cmd.card.reset()
755 self._cmd.poutput('Card ATR: %s' % i2h(atr))
Harald Weltec91085e2022-02-10 18:05:45 +0100756 self._cmd.update_prompt()
Harald Weltedaf2b392021-05-03 23:17:29 +0200757
Harald Weltec91085e2022-02-10 18:05:45 +0100758 def do_desc(self, opts):
759 """Display human readable file description for the currently selected file"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200760 desc = self._cmd.lchan.selected_file.desc
Harald Weltec91085e2022-02-10 18:05:45 +0100761 if desc:
762 self._cmd.poutput(desc)
763 else:
764 self._cmd.poutput("no description available")
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200765
Harald Weltec91085e2022-02-10 18:05:45 +0100766 def do_verify_adm(self, arg):
767 """VERIFY the ADM1 PIN"""
768 if arg:
769 # use specified ADM-PIN
770 pin_adm = sanitize_pin_adm(arg)
771 else:
772 # try to find an ADM-PIN if none is specified
773 result = card_key_provider_get_field(
774 'ADM1', key='ICCID', value=self._cmd.iccid)
775 pin_adm = sanitize_pin_adm(result)
776 if pin_adm:
777 self._cmd.poutput(
778 "found ADM-PIN '%s' for ICCID '%s'" % (result, self._cmd.iccid))
779 else:
780 raise ValueError(
781 "cannot find ADM-PIN for ICCID '%s'" % (self._cmd.iccid))
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200782
Harald Weltec91085e2022-02-10 18:05:45 +0100783 if pin_adm:
784 self._cmd.card.verify_adm(h2b(pin_adm))
785 else:
786 raise ValueError("error: cannot authenticate, no adm-pin!")
787
Philipp Maier7b9e2442023-03-22 15:19:54 +0100788 def do_cardinfo(self, opts):
789 """Display information about the currently inserted card"""
790 self._cmd.poutput("Card info:")
791 self._cmd.poutput(" Name: %s" % self._cmd.card.name)
792 self._cmd.poutput(" ATR: %s" % b2h(self._cmd.card._scc.get_atr()))
793 self._cmd.poutput(" ICCID: %s" % self._cmd.iccid)
794 self._cmd.poutput(" Class-Byte: %s" % self._cmd.card._scc.cla_byte)
795 self._cmd.poutput(" Select-Ctrl: %s" % self._cmd.card._scc.sel_ctrl)
796 self._cmd.poutput(" AIDs:")
797 for a in self._cmd.rs.mf.applications:
798 self._cmd.poutput(" %s" % a)
Harald Welteb2edd142021-01-08 23:29:35 +0100799
Harald Welte31d2cf02021-04-03 10:47:29 +0200800@with_default_category('ISO7816 Commands')
801class Iso7816Commands(CommandSet):
Harald Weltec91085e2022-02-10 18:05:45 +0100802 def __init__(self):
803 super().__init__()
Harald Welte31d2cf02021-04-03 10:47:29 +0200804
Harald Weltec91085e2022-02-10 18:05:45 +0100805 def do_select(self, opts):
806 """SELECT a File (ADF/DF/EF)"""
807 if len(opts.arg_list) == 0:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200808 path = self._cmd.lchan.selected_file.fully_qualified_path_str(True)
809 path_fid = self._cmd.lchan.selected_file.fully_qualified_path_str(False)
810 self._cmd.poutput("currently selected file: %s (%s)" % (path, path_fid))
Harald Weltec91085e2022-02-10 18:05:45 +0100811 return
Harald Welte31d2cf02021-04-03 10:47:29 +0200812
Harald Weltec91085e2022-02-10 18:05:45 +0100813 path = opts.arg_list[0]
Harald Weltea6c0f882022-07-17 14:23:17 +0200814 fcp_dec = self._cmd.lchan.select(path, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100815 self._cmd.update_prompt()
816 self._cmd.poutput_json(fcp_dec)
Harald Welte31d2cf02021-04-03 10:47:29 +0200817
Harald Weltec91085e2022-02-10 18:05:45 +0100818 def complete_select(self, text, line, begidx, endidx) -> List[str]:
819 """Command Line tab completion for SELECT"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200820 index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
Harald Weltec91085e2022-02-10 18:05:45 +0100821 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200822
Harald Weltec91085e2022-02-10 18:05:45 +0100823 def get_code(self, code):
824 """Use code either directly or try to get it from external data source"""
825 auto = ('PIN1', 'PIN2', 'PUK1', 'PUK2')
Harald Welte31d2cf02021-04-03 10:47:29 +0200826
Harald Weltec91085e2022-02-10 18:05:45 +0100827 if str(code).upper() not in auto:
828 return sanitize_pin_adm(code)
Harald Welte31d2cf02021-04-03 10:47:29 +0200829
Harald Weltec91085e2022-02-10 18:05:45 +0100830 result = card_key_provider_get_field(
831 str(code), key='ICCID', value=self._cmd.iccid)
832 result = sanitize_pin_adm(result)
833 if result:
834 self._cmd.poutput("found %s '%s' for ICCID '%s'" %
835 (code.upper(), result, self._cmd.iccid))
836 else:
837 self._cmd.poutput("cannot find %s for ICCID '%s'" %
838 (code.upper(), self._cmd.iccid))
839 return result
Harald Welte31d2cf02021-04-03 10:47:29 +0200840
Harald Weltec91085e2022-02-10 18:05:45 +0100841 verify_chv_parser = argparse.ArgumentParser()
842 verify_chv_parser.add_argument(
843 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
844 verify_chv_parser.add_argument(
845 '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 +0200846
Harald Weltec91085e2022-02-10 18:05:45 +0100847 @cmd2.with_argparser(verify_chv_parser)
848 def do_verify_chv(self, opts):
Harald Welte12af7932022-02-15 16:39:08 +0100849 """Verify (authenticate) using specified CHV (PIN) code, which is how the specifications
850 call it if you authenticate yourself using the specified PIN. There usually is at least PIN1 and
851 PIN2."""
Harald Weltec91085e2022-02-10 18:05:45 +0100852 pin = self.get_code(opts.pin_code)
853 (data, sw) = self._cmd.card._scc.verify_chv(opts.pin_nr, h2b(pin))
854 self._cmd.poutput("CHV verification successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200855
Harald Weltec91085e2022-02-10 18:05:45 +0100856 unblock_chv_parser = argparse.ArgumentParser()
857 unblock_chv_parser.add_argument(
858 '--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
859 unblock_chv_parser.add_argument(
860 'puk_code', type=str, help='PUK code digits \"PUK1\" or \"PUK2\" to get PUK code from external data source')
861 unblock_chv_parser.add_argument(
862 '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 +0200863
Harald Weltec91085e2022-02-10 18:05:45 +0100864 @cmd2.with_argparser(unblock_chv_parser)
865 def do_unblock_chv(self, opts):
866 """Unblock PIN code using specified PUK code"""
867 new_pin = self.get_code(opts.new_pin_code)
868 puk = self.get_code(opts.puk_code)
869 (data, sw) = self._cmd.card._scc.unblock_chv(
870 opts.pin_nr, h2b(puk), h2b(new_pin))
871 self._cmd.poutput("CHV unblock successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200872
Harald Weltec91085e2022-02-10 18:05:45 +0100873 change_chv_parser = argparse.ArgumentParser()
874 change_chv_parser.add_argument(
875 '--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
876 change_chv_parser.add_argument(
877 'pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
878 change_chv_parser.add_argument(
879 '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 +0200880
Harald Weltec91085e2022-02-10 18:05:45 +0100881 @cmd2.with_argparser(change_chv_parser)
882 def do_change_chv(self, opts):
883 """Change PIN code to a new PIN code"""
884 new_pin = self.get_code(opts.new_pin_code)
885 pin = self.get_code(opts.pin_code)
886 (data, sw) = self._cmd.card._scc.change_chv(
887 opts.pin_nr, h2b(pin), h2b(new_pin))
888 self._cmd.poutput("CHV change successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200889
Harald Weltec91085e2022-02-10 18:05:45 +0100890 disable_chv_parser = argparse.ArgumentParser()
891 disable_chv_parser.add_argument(
892 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
893 disable_chv_parser.add_argument(
894 '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 +0200895
Harald Weltec91085e2022-02-10 18:05:45 +0100896 @cmd2.with_argparser(disable_chv_parser)
897 def do_disable_chv(self, opts):
898 """Disable PIN code using specified PIN code"""
899 pin = self.get_code(opts.pin_code)
900 (data, sw) = self._cmd.card._scc.disable_chv(opts.pin_nr, h2b(pin))
901 self._cmd.poutput("CHV disable successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200902
Harald Weltec91085e2022-02-10 18:05:45 +0100903 enable_chv_parser = argparse.ArgumentParser()
904 enable_chv_parser.add_argument(
905 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
906 enable_chv_parser.add_argument(
907 '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 +0200908
Harald Weltec91085e2022-02-10 18:05:45 +0100909 @cmd2.with_argparser(enable_chv_parser)
910 def do_enable_chv(self, opts):
911 """Enable PIN code using specified PIN code"""
912 pin = self.get_code(opts.pin_code)
913 (data, sw) = self._cmd.card._scc.enable_chv(opts.pin_nr, h2b(pin))
914 self._cmd.poutput("CHV enable successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200915
Harald Weltec91085e2022-02-10 18:05:45 +0100916 def do_deactivate_file(self, opts):
Harald Welte799c3542022-02-15 15:56:28 +0100917 """Deactivate the currently selected EF"""
Harald Weltec91085e2022-02-10 18:05:45 +0100918 (data, sw) = self._cmd.card._scc.deactivate_file()
Harald Weltea4631612021-04-10 18:17:55 +0200919
Harald Welte799c3542022-02-15 15:56:28 +0100920 activate_file_parser = argparse.ArgumentParser()
921 activate_file_parser.add_argument('NAME', type=str, help='File name or FID of file to activate')
922 @cmd2.with_argparser(activate_file_parser)
Harald Weltec91085e2022-02-10 18:05:45 +0100923 def do_activate_file(self, opts):
Harald Welte12af7932022-02-15 16:39:08 +0100924 """Activate the specified EF. This used to be called REHABILITATE in TS 11.11 for classic
925 SIM. You need to specify the name or FID of the file to activate."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200926 (data, sw) = self._cmd.lchan.activate_file(opts.NAME)
Harald Welte485692b2021-05-25 22:21:44 +0200927
Harald Weltec91085e2022-02-10 18:05:45 +0100928 def complete_activate_file(self, text, line, begidx, endidx) -> List[str]:
929 """Command Line tab completion for ACTIVATE FILE"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200930 index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
Harald Weltec91085e2022-02-10 18:05:45 +0100931 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200932
Harald Weltec91085e2022-02-10 18:05:45 +0100933 open_chan_parser = argparse.ArgumentParser()
934 open_chan_parser.add_argument(
935 'chan_nr', type=int, default=0, help='Channel Number')
Harald Welte703f9332021-04-10 18:39:32 +0200936
Harald Weltec91085e2022-02-10 18:05:45 +0100937 @cmd2.with_argparser(open_chan_parser)
938 def do_open_channel(self, opts):
939 """Open a logical channel."""
940 (data, sw) = self._cmd.card._scc.manage_channel(
941 mode='open', lchan_nr=opts.chan_nr)
Harald Welte703f9332021-04-10 18:39:32 +0200942
Harald Weltec91085e2022-02-10 18:05:45 +0100943 close_chan_parser = argparse.ArgumentParser()
944 close_chan_parser.add_argument(
945 'chan_nr', type=int, default=0, help='Channel Number')
Harald Welte703f9332021-04-10 18:39:32 +0200946
Harald Weltec91085e2022-02-10 18:05:45 +0100947 @cmd2.with_argparser(close_chan_parser)
948 def do_close_channel(self, opts):
949 """Close a logical channel."""
950 (data, sw) = self._cmd.card._scc.manage_channel(
951 mode='close', lchan_nr=opts.chan_nr)
Harald Welte703f9332021-04-10 18:39:32 +0200952
Harald Weltec91085e2022-02-10 18:05:45 +0100953 def do_status(self, opts):
954 """Perform the STATUS command."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200955 fcp_dec = self._cmd.lchan.status()
Harald Weltec91085e2022-02-10 18:05:45 +0100956 self._cmd.poutput_json(fcp_dec)
Harald Welte34b05d32021-05-25 22:03:13 +0200957
Harald Welteec950532021-10-20 13:09:00 +0200958
Harald Weltecab26c72022-08-06 16:12:30 +0200959class Proact(ProactiveHandler):
960 def receive_fetch(self, pcmd: ProactiveCommand):
961 # print its parsed representation
962 print(pcmd.decoded)
963 # TODO: implement the basics, such as SMS Sending, ...
964
965
Harald Welte703f9332021-04-10 18:39:32 +0200966
Harald Weltef422eb12023-06-09 11:15:09 +0200967option_parser = argparse.ArgumentParser(description='interactive SIM card shell',
Harald Weltef2e761c2021-04-11 11:56:44 +0200968 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
Harald Welte28c24312021-04-11 12:19:36 +0200969argparse_add_reader_args(option_parser)
Harald Weltec8ff0262021-04-11 12:06:13 +0200970
971global_group = option_parser.add_argument_group('General Options')
972global_group.add_argument('--script', metavar='PATH', default=None,
Harald Welte28c24312021-04-11 12:19:36 +0200973 help='script with pySim-shell commands to be executed automatically at start-up')
Harald Weltec91085e2022-02-10 18:05:45 +0100974global_group.add_argument('--csv', metavar='FILE',
975 default=None, help='Read card data from CSV file')
Philipp Maier76667642021-09-22 16:53:22 +0200976global_group.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
Harald Weltec91085e2022-02-10 18:05:45 +0100977 help="Use automatic card handling machine")
Harald Weltec8ff0262021-04-11 12:06:13 +0200978
979adm_group = global_group.add_mutually_exclusive_group()
980adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', default=None,
981 help='ADM PIN used for provisioning (overwrites default)')
982adm_group.add_argument('-A', '--pin-adm-hex', metavar='PIN_ADM1_HEX', dest='pin_adm_hex', default=None,
983 help='ADM PIN used for provisioning, as hex string (16 characters long)')
Harald Welteb2edd142021-01-08 23:29:35 +0100984
985
986if __name__ == '__main__':
987
Harald Weltec91085e2022-02-10 18:05:45 +0100988 # Parse options
989 opts = option_parser.parse_args()
Harald Welteb2edd142021-01-08 23:29:35 +0100990
Harald Weltec91085e2022-02-10 18:05:45 +0100991 # If a script file is specified, be sure that it actually exists
992 if opts.script:
993 if not os.access(opts.script, os.R_OK):
994 print("Invalid script file!")
995 sys.exit(2)
Philipp Maier13e258d2021-04-08 17:48:49 +0200996
Harald Weltec91085e2022-02-10 18:05:45 +0100997 # Register csv-file as card data provider, either from specified CSV
998 # or from CSV file in home directory
999 csv_default = str(Path.home()) + "/.osmocom/pysim/card_data.csv"
1000 if opts.csv:
1001 card_key_provider_register(CardKeyProviderCsv(opts.csv))
1002 if os.path.isfile(csv_default):
1003 card_key_provider_register(CardKeyProviderCsv(csv_default))
Philipp Maier2b11c322021-03-17 12:37:39 +01001004
Harald Weltec91085e2022-02-10 18:05:45 +01001005 # Init card reader driver
Harald Weltecab26c72022-08-06 16:12:30 +02001006 sl = init_reader(opts, proactive_handler = Proact())
Harald Weltec91085e2022-02-10 18:05:45 +01001007 if sl is None:
1008 exit(1)
Philipp Maierea95c392021-09-16 13:10:19 +02001009
Harald Weltec91085e2022-02-10 18:05:45 +01001010 # Create command layer
1011 scc = SimCardCommands(transport=sl)
Philipp Maierea95c392021-09-16 13:10:19 +02001012
Harald Weltec91085e2022-02-10 18:05:45 +01001013 # Create a card handler (for bulk provisioning)
1014 if opts.card_handler_config:
1015 ch = CardHandlerAuto(None, opts.card_handler_config)
1016 else:
1017 ch = CardHandler(sl)
Philipp Maier76667642021-09-22 16:53:22 +02001018
Harald Weltec91085e2022-02-10 18:05:45 +01001019 # Detect and initialize the card in the reader. This may fail when there
1020 # is no card in the reader or the card is unresponsive. PysimApp is
1021 # able to tolerate and recover from that.
1022 try:
1023 rs, card = init_card(sl)
1024 app = PysimApp(card, rs, sl, ch, opts.script)
1025 except:
1026 print("Card initialization failed with an exception:")
1027 print("---------------------8<---------------------")
1028 traceback.print_exc()
1029 print("---------------------8<---------------------")
1030 print("(you may still try to recover from this manually by using the 'equip' command.)")
1031 print(
1032 " it should also be noted that some readers may behave strangely when no card")
1033 print(" is inserted.)")
1034 print("")
Philipp Maier7226c092022-06-01 17:58:38 +02001035 app = PysimApp(card, None, sl, ch, opts.script)
Philipp Maierea95c392021-09-16 13:10:19 +02001036
Harald Weltec91085e2022-02-10 18:05:45 +01001037 # If the user supplies an ADM PIN at via commandline args authenticate
1038 # immediately so that the user does not have to use the shell commands
1039 pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
1040 if pin_adm:
1041 if not card:
1042 print("Card error, cannot do ADM verification with supplied ADM pin now.")
1043 try:
1044 card.verify_adm(h2b(pin_adm))
1045 except Exception as e:
1046 print(e)
Philipp Maier228c98e2021-03-10 20:14:06 +01001047
Harald Weltec91085e2022-02-10 18:05:45 +01001048 app.cmdloop()