blob: 79b4d8b766511be36c7aa691f574a8448e993b6c [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
Philipp Maier91c971b2023-10-09 10:48:46 +020091 # Create command layer
92 scc = SimCardCommands(transport=sl)
93
Harald Weltec91085e2022-02-10 18:05:45 +010094 # Wait up to three seconds for a card in reader and try to detect
95 # the card type.
96 print("Waiting for card...")
97 try:
98 sl.wait_for_card(3)
99 except NoCardError:
100 print("No card detected!")
101 return None, None
102 except:
103 print("Card not readable!")
104 return None, None
Philipp Maierea95c392021-09-16 13:10:19 +0200105
Philipp Maier24031252022-06-14 16:16:42 +0200106 generic_card = False
Harald Weltef8d2e2b2023-07-09 17:58:38 +0200107 card = card_detect(scc)
Harald Weltec91085e2022-02-10 18:05:45 +0100108 if card is None:
109 print("Warning: Could not detect card type - assuming a generic card type...")
Harald Weltef8d2e2b2023-07-09 17:58:38 +0200110 card = SimCardBase(scc)
Philipp Maier24031252022-06-14 16:16:42 +0200111 generic_card = True
Philipp Maiera028c7d2021-11-08 16:12:03 +0100112
Harald Weltec91085e2022-02-10 18:05:45 +0100113 profile = CardProfile.pick(scc)
114 if profile is None:
115 print("Unsupported card type!")
Philipp Maier7226c092022-06-01 17:58:38 +0200116 return None, card
Philipp Maierea95c392021-09-16 13:10:19 +0200117
Philipp Maier24031252022-06-14 16:16:42 +0200118 # ETSI TS 102 221, Table 9.3 specifies a default for the PIN key
119 # references, however card manufactures may still decide to pick an
120 # arbitrary key reference. In case we run on a generic card class that is
121 # detected as an UICC, we will pick the key reference that is officially
122 # specified.
123 if generic_card and isinstance(profile, CardProfileUICC):
124 card._adm_chv_num = 0x0A
125
Harald Weltec91085e2022-02-10 18:05:45 +0100126 print("Info: Card is of type: %s" % str(profile))
Philipp Maiera028c7d2021-11-08 16:12:03 +0100127
Harald Welte6ad9a242023-07-11 18:55:29 +0200128 # FIXME: this shouldn't really be here but somewhere else/more generic.
129 # We cannot do it within pySim/profile.py as that would create circular
130 # dependencies between the individual profiles and profile.py.
Harald Weltec91085e2022-02-10 18:05:45 +0100131 if isinstance(profile, CardProfileUICC):
Harald Welte6ad9a242023-07-11 18:55:29 +0200132 for app_cls in CardApplication.__subclasses__():
133 constr_sig = inspect.signature(app_cls.__init__)
134 # skip any intermediary sub-classes such as CardApplicationSD
135 if len(constr_sig.parameters) != 1:
136 continue
137 profile.add_application(app_cls())
Harald Weltef4a01472023-07-04 21:14:23 +0200138 # We have chosen SimCard() above, but we now know it actually is an UICC
139 # so it's safe to assume it supports USIM application (which we're adding above).
140 # IF we don't do this, we will have a SimCard but try USIM specific commands like
141 # the update_ust method (see https://osmocom.org/issues/6055)
142 if generic_card:
Harald Weltef8d2e2b2023-07-09 17:58:38 +0200143 card = UiccCardBase(scc)
Philipp Maiera028c7d2021-11-08 16:12:03 +0100144
Harald Weltec91085e2022-02-10 18:05:45 +0100145 # Create runtime state with card profile
146 rs = RuntimeState(card, profile)
Philipp Maierea95c392021-09-16 13:10:19 +0200147
Harald Weltec91085e2022-02-10 18:05:45 +0100148 CardModel.apply_matching_models(scc, rs)
Harald Weltef44256c2021-10-14 15:53:39 +0200149
Harald Weltec91085e2022-02-10 18:05:45 +0100150 # inform the transport that we can do context-specific SW interpretation
151 sl.set_sw_interpreter(rs)
Philipp Maierea95c392021-09-16 13:10:19 +0200152
Harald Weltec91085e2022-02-10 18:05:45 +0100153 return rs, card
154
Philipp Maier2b11c322021-03-17 12:37:39 +0100155
Harald Weltee1268722023-06-24 07:57:08 +0200156class Cmd2Compat(cmd2.Cmd):
157 """Backwards-compatibility wrapper around cmd2.Cmd to support older and newer
158 releases. See https://github.com/python-cmd2/cmd2/blob/master/CHANGELOG.md"""
159 def run_editor(self, file_path: Optional[str] = None) -> None:
160 if version.parse(cmd2.__version__) < version.parse("2.0.0"):
Harald Welte0ec01502023-06-24 10:00:12 +0200161 return self._run_editor(file_path) # pylint: disable=no-member
Harald Weltee1268722023-06-24 07:57:08 +0200162 else:
Harald Welte0ec01502023-06-24 10:00:12 +0200163 return super().run_editor(file_path) # pylint: disable=no-member
164
165class Settable2Compat(cmd2.Settable):
166 """Backwards-compatibility wrapper around cmd2.Settable to support older and newer
167 releases. See https://github.com/python-cmd2/cmd2/blob/master/CHANGELOG.md"""
168 def __init__(self, name, val_type, description, settable_object, **kwargs):
169 if version.parse(cmd2.__version__) < version.parse("2.0.0"):
170 super().__init__(name, val_type, description, **kwargs) # pylint: disable=no-value-for-parameter
171 else:
172 super().__init__(name, val_type, description, settable_object, **kwargs) # pylint: disable=too-many-function-args
Harald Weltee1268722023-06-24 07:57:08 +0200173
174class PysimApp(Cmd2Compat):
Harald Weltec91085e2022-02-10 18:05:45 +0100175 CUSTOM_CATEGORY = 'pySim Commands'
Philipp Maier76667642021-09-22 16:53:22 +0200176
Harald Weltec91085e2022-02-10 18:05:45 +0100177 def __init__(self, card, rs, sl, ch, script=None):
Harald Weltec85d4062023-04-27 17:10:17 +0200178 if version.parse(cmd2.__version__) < version.parse("2.0.0"):
179 kwargs = {'use_ipython': True}
180 else:
181 kwargs = {'include_ipy': True}
182
Oliver Smithe47ea5f2023-05-23 12:54:15 +0200183 # pylint: disable=unexpected-keyword-arg
Harald Weltec91085e2022-02-10 18:05:45 +0100184 super().__init__(persistent_history_file='~/.pysim_shell_history', allow_cli_args=False,
Harald Weltec85d4062023-04-27 17:10:17 +0200185 auto_load_commands=False, startup_script=script, **kwargs)
Harald Welte961b8032023-04-27 17:30:22 +0200186 self.intro = style('Welcome to pySim-shell!', fg=RED)
Harald Weltec91085e2022-02-10 18:05:45 +0100187 self.default_category = 'pySim-shell built-in commands'
188 self.card = None
189 self.rs = None
Harald Weltea6c0f882022-07-17 14:23:17 +0200190 self.lchan = None
191 self.py_locals = {'card': self.card, 'rs': self.rs, 'lchan': self.lchan}
Harald Weltec91085e2022-02-10 18:05:45 +0100192 self.sl = sl
193 self.ch = ch
Harald Welte1748b932021-04-06 21:12:25 +0200194
Harald Weltec91085e2022-02-10 18:05:45 +0100195 self.numeric_path = False
Harald Weltec91085e2022-02-10 18:05:45 +0100196 self.conserve_write = True
Harald Weltec91085e2022-02-10 18:05:45 +0100197 self.json_pretty_print = True
Harald Weltec91085e2022-02-10 18:05:45 +0100198 self.apdu_trace = False
Philipp Maier5d698e52021-09-16 13:18:01 +0200199
Harald Welte0ec01502023-06-24 10:00:12 +0200200 self.add_settable(Settable2Compat('numeric_path', bool, 'Print File IDs instead of names', self,
201 onchange_cb=self._onchange_numeric_path))
202 self.add_settable(Settable2Compat('conserve_write', bool, 'Read and compare before write', self,
203 onchange_cb=self._onchange_conserve_write))
204 self.add_settable(Settable2Compat('json_pretty_print', bool, 'Pretty-Print JSON output', self))
205 self.add_settable(Settable2Compat('apdu_trace', bool, 'Trace and display APDUs exchanged with card', self,
206 onchange_cb=self._onchange_apdu_trace))
Harald Weltec91085e2022-02-10 18:05:45 +0100207 self.equip(card, rs)
Philipp Maier5d698e52021-09-16 13:18:01 +0200208
Harald Weltec91085e2022-02-10 18:05:45 +0100209 def equip(self, card, rs):
210 """
211 Equip pySim-shell with the supplied card and runtime state, add (or remove) all required settables and
212 and commands to enable card operations.
213 """
Philipp Maier76667642021-09-22 16:53:22 +0200214
Harald Weltec91085e2022-02-10 18:05:45 +0100215 rc = False
Philipp Maier5d698e52021-09-16 13:18:01 +0200216
Harald Weltec91085e2022-02-10 18:05:45 +0100217 # Unequip everything from pySim-shell that would not work in unequipped state
218 if self.rs:
Harald Weltea6c0f882022-07-17 14:23:17 +0200219 lchan = self.rs.lchan[0]
220 lchan.unregister_cmds(self)
iw0f818acd2023-06-19 10:27:04 +0200221 if self.rs.profile:
222 for cmd_set in self.rs.profile.shell_cmdsets:
223 self.unregister_command_set(cmd_set)
224
Harald Welte892526f2023-06-06 17:09:50 +0200225 for cmds in [Iso7816Commands, Ts102222Commands, PySimCommands]:
Harald Weltec91085e2022-02-10 18:05:45 +0100226 cmd_set = self.find_commandsets(cmds)
227 if cmd_set:
228 self.unregister_command_set(cmd_set[0])
Philipp Maier5d698e52021-09-16 13:18:01 +0200229
Harald Weltec91085e2022-02-10 18:05:45 +0100230 self.card = card
231 self.rs = rs
Philipp Maier5d698e52021-09-16 13:18:01 +0200232
Harald Weltec91085e2022-02-10 18:05:45 +0100233 # When a card object and a runtime state is present, (re)equip pySim-shell with everything that is
234 # needed to operate on cards.
235 if self.card and self.rs:
Harald Weltea6c0f882022-07-17 14:23:17 +0200236 self.lchan = self.rs.lchan[0]
Harald Weltec91085e2022-02-10 18:05:45 +0100237 self._onchange_conserve_write(
238 'conserve_write', False, self.conserve_write)
239 self._onchange_apdu_trace('apdu_trace', False, self.apdu_trace)
Harald Welte659781c2023-06-06 17:00:51 +0200240 if self.rs.profile:
241 for cmd_set in self.rs.profile.shell_cmdsets:
242 self.register_command_set(cmd_set)
Harald Weltec91085e2022-02-10 18:05:45 +0100243 self.register_command_set(Iso7816Commands())
Harald Welte3c9b7842021-10-19 21:44:24 +0200244 self.register_command_set(Ts102222Commands())
Harald Weltec91085e2022-02-10 18:05:45 +0100245 self.register_command_set(PySimCommands())
Philipp Maiera42ee6f2023-08-16 10:44:57 +0200246
247 self.lchan.select('MF/EF.ICCID', self)
248 self.iccid = dec_iccid(self.lchan.read_binary()[0])
249
Harald Weltea6c0f882022-07-17 14:23:17 +0200250 self.lchan.select('MF', self)
Harald Weltec91085e2022-02-10 18:05:45 +0100251 rc = True
252 else:
253 self.poutput("pySim-shell not equipped!")
Philipp Maier5d698e52021-09-16 13:18:01 +0200254
Harald Weltec91085e2022-02-10 18:05:45 +0100255 self.update_prompt()
256 return rc
Harald Welteb2edd142021-01-08 23:29:35 +0100257
Harald Weltec91085e2022-02-10 18:05:45 +0100258 def poutput_json(self, data, force_no_pretty=False):
259 """like cmd2.poutput() but for a JSON serializable dict."""
260 if force_no_pretty or self.json_pretty_print == False:
261 output = json.dumps(data, cls=JsonEncoder)
262 else:
263 output = json.dumps(data, cls=JsonEncoder, indent=4)
264 self.poutput(output)
Harald Welteb2edd142021-01-08 23:29:35 +0100265
Harald Weltec91085e2022-02-10 18:05:45 +0100266 def _onchange_numeric_path(self, param_name, old, new):
267 self.update_prompt()
Philipp Maier38c74f62021-03-17 17:19:52 +0100268
Harald Weltec91085e2022-02-10 18:05:45 +0100269 def _onchange_conserve_write(self, param_name, old, new):
270 if self.rs:
271 self.rs.conserve_write = new
Harald Welte7829d8a2021-04-10 11:28:53 +0200272
Harald Weltec91085e2022-02-10 18:05:45 +0100273 def _onchange_apdu_trace(self, param_name, old, new):
274 if self.card:
275 if new == True:
276 self.card._scc._tp.apdu_tracer = self.Cmd2ApduTracer(self)
277 else:
278 self.card._scc._tp.apdu_tracer = None
Harald Welte7829d8a2021-04-10 11:28:53 +0200279
Harald Weltec91085e2022-02-10 18:05:45 +0100280 class Cmd2ApduTracer(ApduTracer):
281 def __init__(self, cmd2_app):
Philipp Maier91c971b2023-10-09 10:48:46 +0200282 self.cmd2 = cmd2_app
Harald Welte7829d8a2021-04-10 11:28:53 +0200283
Harald Weltec91085e2022-02-10 18:05:45 +0100284 def trace_response(self, cmd, sw, resp):
285 self.cmd2.poutput("-> %s %s" % (cmd[:10], cmd[10:]))
286 self.cmd2.poutput("<- %s: %s" % (sw, resp))
Harald Welteb2edd142021-01-08 23:29:35 +0100287
Harald Weltec91085e2022-02-10 18:05:45 +0100288 def update_prompt(self):
Harald Weltea6c0f882022-07-17 14:23:17 +0200289 if self.lchan:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200290 path_str = self.lchan.selected_file.fully_qualified_path_str(not self.numeric_path)
291 self.prompt = 'pySIM-shell (%s)> ' % (path_str)
Harald Weltec91085e2022-02-10 18:05:45 +0100292 else:
Philipp Maier7226c092022-06-01 17:58:38 +0200293 if self.card:
294 self.prompt = 'pySIM-shell (no card profile)> '
295 else:
296 self.prompt = 'pySIM-shell (no card)> '
Harald Welteb2edd142021-01-08 23:29:35 +0100297
Harald Weltec91085e2022-02-10 18:05:45 +0100298 @cmd2.with_category(CUSTOM_CATEGORY)
299 def do_intro(self, _):
300 """Display the intro banner"""
301 self.poutput(self.intro)
Philipp Maier9764de22021-11-03 10:44:39 +0100302
Harald Weltec91085e2022-02-10 18:05:45 +0100303 def do_eof(self, _: argparse.Namespace) -> bool:
304 self.poutput("")
305 return self.do_quit('')
Philipp Maier5d698e52021-09-16 13:18:01 +0200306
Harald Weltec91085e2022-02-10 18:05:45 +0100307 @cmd2.with_category(CUSTOM_CATEGORY)
308 def do_equip(self, opts):
309 """Equip pySim-shell with card"""
Philipp Maiere6cba762023-08-11 11:23:17 +0200310 if self.rs and self.rs.profile:
Harald Welte659781c2023-06-06 17:00:51 +0200311 for cmd_set in self.rs.profile.shell_cmdsets:
312 self.unregister_command_set(cmd_set)
Philipp Maier91c971b2023-10-09 10:48:46 +0200313 rs, card = init_card(self.sl)
Harald Weltec91085e2022-02-10 18:05:45 +0100314 self.equip(card, rs)
Philipp Maier76667642021-09-22 16:53:22 +0200315
Philipp Maier7226c092022-06-01 17:58:38 +0200316 apdu_cmd_parser = argparse.ArgumentParser()
317 apdu_cmd_parser.add_argument('APDU', type=str, help='APDU as hex string')
Philipp Maiere7d1b672022-06-01 18:05:34 +0200318 apdu_cmd_parser.add_argument('--expect-sw', help='expect a specified status word', type=str, default=None)
Philipp Maier7226c092022-06-01 17:58:38 +0200319
320 @cmd2.with_argparser(apdu_cmd_parser)
321 def do_apdu(self, opts):
322 """Send a raw APDU to the card, and print SW + Response.
323 DANGEROUS: pySim-shell will not know any card state changes, and
324 not continue to work as expected if you e.g. select a different
325 file."""
326 data, sw = self.card._scc._tp.send_apdu(opts.APDU)
327 if data:
328 self.poutput("SW: %s, RESP: %s" % (sw, data))
329 else:
330 self.poutput("SW: %s" % sw)
Philipp Maiere7d1b672022-06-01 18:05:34 +0200331 if opts.expect_sw:
332 if not sw_match(sw, opts.expect_sw):
333 raise SwMatchError(sw, opts.expect_sw)
Philipp Maier7226c092022-06-01 17:58:38 +0200334
Harald Weltec91085e2022-02-10 18:05:45 +0100335 class InterceptStderr(list):
336 def __init__(self):
337 self._stderr_backup = sys.stderr
Philipp Maier76667642021-09-22 16:53:22 +0200338
Harald Weltec91085e2022-02-10 18:05:45 +0100339 def __enter__(self):
340 self._stringio_stderr = StringIO()
341 sys.stderr = self._stringio_stderr
342 return self
Philipp Maier76667642021-09-22 16:53:22 +0200343
Harald Weltec91085e2022-02-10 18:05:45 +0100344 def __exit__(self, *args):
345 self.stderr = self._stringio_stderr.getvalue().strip()
346 del self._stringio_stderr
347 sys.stderr = self._stderr_backup
Philipp Maier76667642021-09-22 16:53:22 +0200348
Harald Weltec91085e2022-02-10 18:05:45 +0100349 def _show_failure_sign(self):
Harald Welte961b8032023-04-27 17:30:22 +0200350 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))
354 self.poutput(style(" + ## ## +", fg=LIGHT_RED))
355 self.poutput(style(" + ## ## +", fg=LIGHT_RED))
356 self.poutput(style(" +-------------+", fg=LIGHT_RED))
Harald Weltec91085e2022-02-10 18:05:45 +0100357 self.poutput("")
Philipp Maier76667642021-09-22 16:53:22 +0200358
Harald Weltec91085e2022-02-10 18:05:45 +0100359 def _show_success_sign(self):
Harald Welte961b8032023-04-27 17:30:22 +0200360 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))
364 self.poutput(style(" + ## # +", fg=LIGHT_GREEN))
365 self.poutput(style(" + ## +", fg=LIGHT_GREEN))
366 self.poutput(style(" +-------------+", fg=LIGHT_GREEN))
Harald Weltec91085e2022-02-10 18:05:45 +0100367 self.poutput("")
Philipp Maier76667642021-09-22 16:53:22 +0200368
Harald Weltec91085e2022-02-10 18:05:45 +0100369 def _process_card(self, first, script_path):
Philipp Maier76667642021-09-22 16:53:22 +0200370
Harald Weltec91085e2022-02-10 18:05:45 +0100371 # Early phase of card initialzation (this part may fail with an exception)
372 try:
373 rs, card = init_card(self.sl)
374 rc = self.equip(card, rs)
375 except:
376 self.poutput("")
377 self.poutput("Card initialization failed with an exception:")
378 self.poutput("---------------------8<---------------------")
379 traceback.print_exc()
380 self.poutput("---------------------8<---------------------")
381 self.poutput("")
382 return -1
Philipp Maier76667642021-09-22 16:53:22 +0200383
Harald Weltec91085e2022-02-10 18:05:45 +0100384 # Actual card processing step. This part should never fail with an exception since the cmd2
385 # do_run_script method will catch any exception that might occur during script execution.
386 if rc:
387 self.poutput("")
388 self.poutput("Transcript stdout:")
389 self.poutput("---------------------8<---------------------")
390 with self.InterceptStderr() as logged:
391 self.do_run_script(script_path)
392 self.poutput("---------------------8<---------------------")
Philipp Maier76667642021-09-22 16:53:22 +0200393
Harald Weltec91085e2022-02-10 18:05:45 +0100394 self.poutput("")
395 self.poutput("Transcript stderr:")
396 if logged.stderr:
397 self.poutput("---------------------8<---------------------")
398 self.poutput(logged.stderr)
399 self.poutput("---------------------8<---------------------")
400 else:
401 self.poutput("(none)")
Philipp Maier76667642021-09-22 16:53:22 +0200402
Harald Weltec91085e2022-02-10 18:05:45 +0100403 # Check for exceptions
404 self.poutput("")
405 if "EXCEPTION of type" not in logged.stderr:
406 return 0
Philipp Maier76667642021-09-22 16:53:22 +0200407
Harald Weltec91085e2022-02-10 18:05:45 +0100408 return -1
Philipp Maier76667642021-09-22 16:53:22 +0200409
Harald Weltec91085e2022-02-10 18:05:45 +0100410 bulk_script_parser = argparse.ArgumentParser()
411 bulk_script_parser.add_argument(
412 'script_path', help="path to the script file")
413 bulk_script_parser.add_argument('--halt_on_error', help='stop card handling if an exeption occurs',
414 action='store_true')
415 bulk_script_parser.add_argument('--tries', type=int, default=2,
416 help='how many tries before trying the next card')
417 bulk_script_parser.add_argument('--on_stop_action', type=str, default=None,
418 help='commandline to execute when card handling has stopped')
419 bulk_script_parser.add_argument('--pre_card_action', type=str, default=None,
420 help='commandline to execute before actually talking to the card')
Philipp Maier76667642021-09-22 16:53:22 +0200421
Harald Weltec91085e2022-02-10 18:05:45 +0100422 @cmd2.with_argparser(bulk_script_parser)
423 @cmd2.with_category(CUSTOM_CATEGORY)
424 def do_bulk_script(self, opts):
425 """Run script on multiple cards (bulk provisioning)"""
Philipp Maier76667642021-09-22 16:53:22 +0200426
Harald Weltec91085e2022-02-10 18:05:45 +0100427 # Make sure that the script file exists and that it is readable.
428 if not os.access(opts.script_path, os.R_OK):
429 self.poutput("Invalid script file!")
430 return
Philipp Maier76667642021-09-22 16:53:22 +0200431
Harald Weltec91085e2022-02-10 18:05:45 +0100432 success_count = 0
433 fail_count = 0
Philipp Maier76667642021-09-22 16:53:22 +0200434
Harald Weltec91085e2022-02-10 18:05:45 +0100435 first = True
436 while 1:
437 # TODO: Count consecutive failures, if more than N consecutive failures occur, then stop.
438 # The ratinale is: There may be a problem with the device, we do want to prevent that
439 # all remaining cards are fired to the error bin. This is only relevant for situations
440 # with large stacks, probably we do not need this feature right now.
Philipp Maier76667642021-09-22 16:53:22 +0200441
Harald Weltec91085e2022-02-10 18:05:45 +0100442 try:
443 # In case of failure, try multiple times.
444 for i in range(opts.tries):
445 # fetch card into reader bay
Philipp Maier91c971b2023-10-09 10:48:46 +0200446 self.ch.get(first)
Philipp Maier76667642021-09-22 16:53:22 +0200447
Harald Weltec91085e2022-02-10 18:05:45 +0100448 # if necessary execute an action before we start processing the card
449 if(opts.pre_card_action):
450 os.system(opts.pre_card_action)
Philipp Maier76667642021-09-22 16:53:22 +0200451
Harald Weltec91085e2022-02-10 18:05:45 +0100452 # process the card
453 rc = self._process_card(first, opts.script_path)
454 if rc == 0:
455 success_count = success_count + 1
456 self._show_success_sign()
457 self.poutput("Statistics: success :%i, failure: %i" % (
458 success_count, fail_count))
459 break
460 else:
461 fail_count = fail_count + 1
462 self._show_failure_sign()
463 self.poutput("Statistics: success :%i, failure: %i" % (
464 success_count, fail_count))
Philipp Maier76667642021-09-22 16:53:22 +0200465
Harald Weltec91085e2022-02-10 18:05:45 +0100466 # Depending on success or failure, the card goes either in the "error" bin or in the
467 # "done" bin.
468 if rc < 0:
Philipp Maier91c971b2023-10-09 10:48:46 +0200469 self.ch.error()
Harald Weltec91085e2022-02-10 18:05:45 +0100470 else:
Philipp Maier91c971b2023-10-09 10:48:46 +0200471 self.ch.done()
Philipp Maier76667642021-09-22 16:53:22 +0200472
Harald Weltec91085e2022-02-10 18:05:45 +0100473 # In most cases it is possible to proceed with the next card, but the
474 # user may decide to halt immediately when an error occurs
475 if opts.halt_on_error and rc < 0:
476 return
Philipp Maier76667642021-09-22 16:53:22 +0200477
Harald Weltec91085e2022-02-10 18:05:45 +0100478 except (KeyboardInterrupt):
479 self.poutput("")
480 self.poutput("Terminated by user!")
481 return
482 except (SystemExit):
483 # When all cards are processed the card handler device will throw a SystemExit
484 # exception. Also Errors that are not recoverable (cards stuck etc.) will end up here.
485 # The user has the option to execute some action to make aware that the card handler
486 # needs service.
487 if(opts.on_stop_action):
488 os.system(opts.on_stop_action)
489 return
490 except:
491 self.poutput("")
492 self.poutput("Card handling failed with an exception:")
493 self.poutput("---------------------8<---------------------")
494 traceback.print_exc()
495 self.poutput("---------------------8<---------------------")
496 self.poutput("")
497 fail_count = fail_count + 1
498 self._show_failure_sign()
499 self.poutput("Statistics: success :%i, failure: %i" %
500 (success_count, fail_count))
Philipp Maierb52feed2021-09-22 16:43:13 +0200501
Harald Weltec91085e2022-02-10 18:05:45 +0100502 first = False
503
504 echo_parser = argparse.ArgumentParser()
505 echo_parser.add_argument('string', help="string to echo on the shell")
506
507 @cmd2.with_argparser(echo_parser)
508 @cmd2.with_category(CUSTOM_CATEGORY)
509 def do_echo(self, opts):
510 """Echo (print) a string on the console"""
511 self.poutput(opts.string)
512
Harald Weltefc315482022-07-23 12:49:14 +0200513 @cmd2.with_category(CUSTOM_CATEGORY)
514 def do_version(self, opts):
515 """Print the pySim software version."""
516 import pkg_resources
517 self.poutput(pkg_resources.get_distribution('pySim'))
Harald Welteb2edd142021-01-08 23:29:35 +0100518
Harald Welte31d2cf02021-04-03 10:47:29 +0200519@with_default_category('pySim Commands')
520class PySimCommands(CommandSet):
Harald Weltec91085e2022-02-10 18:05:45 +0100521 def __init__(self):
522 super().__init__()
Harald Welteb2edd142021-01-08 23:29:35 +0100523
Harald Weltec91085e2022-02-10 18:05:45 +0100524 dir_parser = argparse.ArgumentParser()
525 dir_parser.add_argument(
526 '--fids', help='Show file identifiers', action='store_true')
527 dir_parser.add_argument(
528 '--names', help='Show file names', action='store_true')
529 dir_parser.add_argument(
530 '--apps', help='Show applications', action='store_true')
531 dir_parser.add_argument(
532 '--all', help='Show all selectable identifiers and names', action='store_true')
Philipp Maier5d3e2592021-02-22 17:22:16 +0100533
Harald Weltec91085e2022-02-10 18:05:45 +0100534 @cmd2.with_argparser(dir_parser)
535 def do_dir(self, opts):
536 """Show a listing of files available in currently selected DF or MF"""
537 if opts.all:
538 flags = []
539 elif opts.fids or opts.names or opts.apps:
540 flags = ['PARENT', 'SELF']
541 if opts.fids:
542 flags += ['FIDS', 'AIDS']
543 if opts.names:
544 flags += ['FNAMES', 'ANAMES']
545 if opts.apps:
546 flags += ['ANAMES', 'AIDS']
547 else:
548 flags = ['PARENT', 'SELF', 'FNAMES', 'ANAMES']
549 selectables = list(
Harald Weltea6c0f882022-07-17 14:23:17 +0200550 self._cmd.lchan.selected_file.get_selectable_names(flags=flags))
Harald Weltec91085e2022-02-10 18:05:45 +0100551 directory_str = tabulate_str_list(
552 selectables, width=79, hspace=2, lspace=1, align_left=True)
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200553 path = self._cmd.lchan.selected_file.fully_qualified_path_str(True)
554 self._cmd.poutput(path)
555 path = self._cmd.lchan.selected_file.fully_qualified_path_str(False)
556 self._cmd.poutput(path)
Harald Weltec91085e2022-02-10 18:05:45 +0100557 self._cmd.poutput(directory_str)
558 self._cmd.poutput("%d files" % len(selectables))
Harald Welteb2edd142021-01-08 23:29:35 +0100559
Philipp Maier7b138b02022-05-31 13:42:56 +0200560 def walk(self, indent=0, action_ef=None, action_df=None, context=None, **kwargs):
Harald Weltec91085e2022-02-10 18:05:45 +0100561 """Recursively walk through the file system, starting at the currently selected DF"""
Philipp Maier7b138b02022-05-31 13:42:56 +0200562
Harald Weltea6c0f882022-07-17 14:23:17 +0200563 if isinstance(self._cmd.lchan.selected_file, CardDF):
Philipp Maier7b138b02022-05-31 13:42:56 +0200564 if action_df:
Philipp Maier91c971b2023-10-09 10:48:46 +0200565 action_df(context, **kwargs)
Philipp Maier7b138b02022-05-31 13:42:56 +0200566
Harald Weltea6c0f882022-07-17 14:23:17 +0200567 files = self._cmd.lchan.selected_file.get_selectables(
Harald Weltec91085e2022-02-10 18:05:45 +0100568 flags=['FNAMES', 'ANAMES'])
569 for f in files:
Philipp Maier7b138b02022-05-31 13:42:56 +0200570 # special case: When no action is performed, just output a directory
571 if not action_ef and not action_df:
Harald Weltec91085e2022-02-10 18:05:45 +0100572 output_str = " " * indent + str(f) + (" " * 250)
573 output_str = output_str[0:25]
574 if isinstance(files[f], CardADF):
575 output_str += " " + str(files[f].aid)
576 else:
577 output_str += " " + str(files[f].fid)
578 output_str += " " + str(files[f].desc)
579 self._cmd.poutput(output_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200580
Harald Weltec91085e2022-02-10 18:05:45 +0100581 if isinstance(files[f], CardDF):
582 skip_df = False
583 try:
Harald Weltea6c0f882022-07-17 14:23:17 +0200584 fcp_dec = self._cmd.lchan.select(f, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100585 except Exception as e:
586 skip_df = True
Harald Weltea6c0f882022-07-17 14:23:17 +0200587 df = self._cmd.lchan.selected_file
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200588 df_path = df.fully_qualified_path_str(True)
589 df_skip_reason_str = df_path + \
Harald Weltec91085e2022-02-10 18:05:45 +0100590 "/" + str(f) + ", " + str(e)
591 if context:
592 context['DF_SKIP'] += 1
593 context['DF_SKIP_REASON'].append(df_skip_reason_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200594
Harald Weltec91085e2022-02-10 18:05:45 +0100595 # If the DF was skipped, we never have entered the directory
596 # below, so we must not move up.
597 if skip_df == False:
Philipp Maier7b138b02022-05-31 13:42:56 +0200598 self.walk(indent + 1, action_ef, action_df, context, **kwargs)
Harald Weltea6c0f882022-07-17 14:23:17 +0200599 fcp_dec = self._cmd.lchan.select("..", self._cmd)
Philipp Maierf408a402021-04-09 21:16:12 +0200600
Philipp Maier7b138b02022-05-31 13:42:56 +0200601 elif action_ef:
Harald Weltea6c0f882022-07-17 14:23:17 +0200602 df_before_action = self._cmd.lchan.selected_file
Philipp Maier7b138b02022-05-31 13:42:56 +0200603 action_ef(f, context, **kwargs)
Harald Weltec91085e2022-02-10 18:05:45 +0100604 # When walking through the file system tree the action must not
605 # always restore the currently selected file to the file that
606 # was selected before executing the action() callback.
Harald Weltea6c0f882022-07-17 14:23:17 +0200607 if df_before_action != self._cmd.lchan.selected_file:
Harald Weltec91085e2022-02-10 18:05:45 +0100608 raise RuntimeError("inconsistent walk, %s is currently selected but expecting %s to be selected"
Harald Weltea6c0f882022-07-17 14:23:17 +0200609 % (str(self._cmd.lchan.selected_file), str(df_before_action)))
Philipp Maierff9dae22021-02-25 17:03:21 +0100610
Harald Weltec91085e2022-02-10 18:05:45 +0100611 def do_tree(self, opts):
612 """Display a filesystem-tree with all selectable files"""
613 self.walk()
Philipp Maierff9dae22021-02-25 17:03:21 +0100614
Philipp Maier7b138b02022-05-31 13:42:56 +0200615 def export_ef(self, filename, context, as_json):
616 """ Select and export a single elementary file (EF) """
Harald Weltec91085e2022-02-10 18:05:45 +0100617 context['COUNT'] += 1
Harald Weltea6c0f882022-07-17 14:23:17 +0200618 df = self._cmd.lchan.selected_file
Philipp Maierac34dcc2021-04-01 17:19:05 +0200619
Philipp Maierea81f752022-05-19 10:13:30 +0200620 # The currently selected file (not the file we are going to export)
621 # must always be an ADF or DF. From this starting point we select
622 # the EF we want to export. To maintain consistency we will then
623 # select the current DF again (see comment below).
Harald Weltec91085e2022-02-10 18:05:45 +0100624 if not isinstance(df, CardDF):
625 raise RuntimeError(
626 "currently selected file %s is not a DF or ADF" % str(df))
Philipp Maierac34dcc2021-04-01 17:19:05 +0200627
Harald Weltec91085e2022-02-10 18:05:45 +0100628 df_path_list = df.fully_qualified_path(True)
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200629 df_path = df.fully_qualified_path_str(True)
630 df_path_fid = df.fully_qualified_path_str(False)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100631
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200632 file_str = df_path + "/" + str(filename)
Harald Weltec91085e2022-02-10 18:05:45 +0100633 self._cmd.poutput(boxed_heading_str(file_str))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100634
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200635 self._cmd.poutput("# directory: %s (%s)" % (df_path, df_path_fid))
Harald Weltec91085e2022-02-10 18:05:45 +0100636 try:
Harald Weltea6c0f882022-07-17 14:23:17 +0200637 fcp_dec = self._cmd.lchan.select(filename, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100638 self._cmd.poutput("# file: %s (%s)" % (
Harald Weltea6c0f882022-07-17 14:23:17 +0200639 self._cmd.lchan.selected_file.name, self._cmd.lchan.selected_file.fid))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100640
Harald Weltea6c0f882022-07-17 14:23:17 +0200641 structure = self._cmd.lchan.selected_file_structure()
Harald Weltec91085e2022-02-10 18:05:45 +0100642 self._cmd.poutput("# structure: %s" % str(structure))
Harald Weltea6c0f882022-07-17 14:23:17 +0200643 self._cmd.poutput("# RAW FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp_hex))
644 self._cmd.poutput("# Decoded FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100645
Harald Weltec91085e2022-02-10 18:05:45 +0100646 for f in df_path_list:
647 self._cmd.poutput("select " + str(f))
Harald Weltea6c0f882022-07-17 14:23:17 +0200648 self._cmd.poutput("select " + self._cmd.lchan.selected_file.name)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100649
Harald Weltec91085e2022-02-10 18:05:45 +0100650 if structure == 'transparent':
Harald Welte08b11ab2022-02-10 18:56:41 +0100651 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200652 result = self._cmd.lchan.read_binary_dec()
Harald Welte08b11ab2022-02-10 18:56:41 +0100653 self._cmd.poutput("update_binary_decoded '%s'" % json.dumps(result[0], cls=JsonEncoder))
654 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200655 result = self._cmd.lchan.read_binary()
Harald Welte08b11ab2022-02-10 18:56:41 +0100656 self._cmd.poutput("update_binary " + str(result[0]))
Harald Weltec91085e2022-02-10 18:05:45 +0100657 elif structure == 'cyclic' or structure == 'linear_fixed':
658 # Use number of records specified in select response
Harald Weltea6c0f882022-07-17 14:23:17 +0200659 num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
Harald Welte747a9782022-02-13 17:52:28 +0100660 if num_of_rec:
Harald Weltec91085e2022-02-10 18:05:45 +0100661 for r in range(1, num_of_rec + 1):
Harald Welte08b11ab2022-02-10 18:56:41 +0100662 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200663 result = self._cmd.lchan.read_record_dec(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100664 self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
665 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200666 result = self._cmd.lchan.read_record(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100667 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
668
Harald Weltec91085e2022-02-10 18:05:45 +0100669 # When the select response does not return the number of records, read until we hit the
670 # first record that cannot be read.
671 else:
672 r = 1
673 while True:
674 try:
Harald Welte08b11ab2022-02-10 18:56:41 +0100675 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200676 result = self._cmd.lchan.read_record_dec(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100677 self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
678 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200679 result = self._cmd.lchan.read_record(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100680 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
Harald Weltec91085e2022-02-10 18:05:45 +0100681 except SwMatchError as e:
682 # We are past the last valid record - stop
683 if e.sw_actual == "9402":
684 break
685 # Some other problem occurred
686 else:
687 raise e
Harald Weltec91085e2022-02-10 18:05:45 +0100688 r = r + 1
689 elif structure == 'ber_tlv':
Harald Weltea6c0f882022-07-17 14:23:17 +0200690 tags = self._cmd.lchan.retrieve_tags()
Harald Weltec91085e2022-02-10 18:05:45 +0100691 for t in tags:
Harald Weltea6c0f882022-07-17 14:23:17 +0200692 result = self._cmd.lchan.retrieve_data(t)
Harald Weltec91085e2022-02-10 18:05:45 +0100693 (tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
694 self._cmd.poutput("set_data 0x%02x %s" % (t, b2h(val)))
695 else:
696 raise RuntimeError(
697 'Unsupported structure "%s" of file "%s"' % (structure, filename))
698 except Exception as e:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200699 bad_file_str = df_path + "/" + str(filename) + ", " + str(e)
Harald Weltec91085e2022-02-10 18:05:45 +0100700 self._cmd.poutput("# bad file: %s" % bad_file_str)
701 context['ERR'] += 1
702 context['BAD'].append(bad_file_str)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100703
Harald Weltec91085e2022-02-10 18:05:45 +0100704 # When reading the file is done, make sure the parent file is
705 # selected again. This will be the usual case, however we need
706 # to check before since we must not select the same DF twice
Harald Weltea6c0f882022-07-17 14:23:17 +0200707 if df != self._cmd.lchan.selected_file:
708 self._cmd.lchan.select(df.fid or df.aid, self._cmd)
Philipp Maierac34dcc2021-04-01 17:19:05 +0200709
Harald Weltec91085e2022-02-10 18:05:45 +0100710 self._cmd.poutput("#")
Philipp Maier24f7bd32021-02-25 17:06:18 +0100711
Harald Weltec91085e2022-02-10 18:05:45 +0100712 export_parser = argparse.ArgumentParser()
713 export_parser.add_argument(
714 '--filename', type=str, default=None, help='only export specific file')
Harald Welte08b11ab2022-02-10 18:56:41 +0100715 export_parser.add_argument(
716 '--json', action='store_true', help='export as JSON (less reliable)')
Philipp Maier24f7bd32021-02-25 17:06:18 +0100717
Harald Weltec91085e2022-02-10 18:05:45 +0100718 @cmd2.with_argparser(export_parser)
719 def do_export(self, opts):
720 """Export files to script that can be imported back later"""
721 context = {'ERR': 0, 'COUNT': 0, 'BAD': [],
722 'DF_SKIP': 0, 'DF_SKIP_REASON': []}
Philipp Maier9a4091d2022-05-19 10:20:30 +0200723 kwargs_export = {'as_json': opts.json}
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200724 exception_str_add = ""
725
Harald Weltec91085e2022-02-10 18:05:45 +0100726 if opts.filename:
Philipp Maier7b138b02022-05-31 13:42:56 +0200727 self.export_ef(opts.filename, context, **kwargs_export)
Harald Weltec91085e2022-02-10 18:05:45 +0100728 else:
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200729 try:
730 self.walk(0, self.export_ef, None, context, **kwargs_export)
731 except Exception as e:
732 print("# Stopping early here due to exception: " + str(e))
733 print("#")
734 exception_str_add = ", also had to stop early due to exception:" + str(e)
Philipp Maier80ce71f2021-04-19 21:24:23 +0200735
Harald Weltec91085e2022-02-10 18:05:45 +0100736 self._cmd.poutput(boxed_heading_str("Export summary"))
Philipp Maier80ce71f2021-04-19 21:24:23 +0200737
Harald Weltec91085e2022-02-10 18:05:45 +0100738 self._cmd.poutput("# total files visited: %u" % context['COUNT'])
739 self._cmd.poutput("# bad files: %u" % context['ERR'])
740 for b in context['BAD']:
741 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200742
Harald Weltec91085e2022-02-10 18:05:45 +0100743 self._cmd.poutput("# skipped dedicated files(s): %u" %
744 context['DF_SKIP'])
745 for b in context['DF_SKIP_REASON']:
746 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200747
Harald Weltec91085e2022-02-10 18:05:45 +0100748 if context['ERR'] and context['DF_SKIP']:
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200749 raise RuntimeError("unable to export %i elementary file(s) and %i dedicated file(s)%s" % (
750 context['ERR'], context['DF_SKIP'], exception_str_add))
Harald Weltec91085e2022-02-10 18:05:45 +0100751 elif context['ERR']:
752 raise RuntimeError(
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200753 "unable to export %i elementary file(s)%s" % (context['ERR'], exception_str_add))
Harald Weltec91085e2022-02-10 18:05:45 +0100754 elif context['DF_SKIP']:
755 raise RuntimeError(
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200756 "unable to export %i dedicated files(s)%s" % (context['ERR'], exception_str_add))
Harald Welteb2edd142021-01-08 23:29:35 +0100757
Harald Weltec91085e2022-02-10 18:05:45 +0100758 def do_reset(self, opts):
759 """Reset the Card."""
Philipp Maiere345e112023-06-09 15:19:56 +0200760 atr = self._cmd.card.reset()
761 self._cmd.poutput('Card ATR: %s' % i2h(atr))
Harald Weltec91085e2022-02-10 18:05:45 +0100762 self._cmd.update_prompt()
Harald Weltedaf2b392021-05-03 23:17:29 +0200763
Harald Weltec91085e2022-02-10 18:05:45 +0100764 def do_desc(self, opts):
765 """Display human readable file description for the currently selected file"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200766 desc = self._cmd.lchan.selected_file.desc
Harald Weltec91085e2022-02-10 18:05:45 +0100767 if desc:
768 self._cmd.poutput(desc)
769 else:
770 self._cmd.poutput("no description available")
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200771
Harald Weltec91085e2022-02-10 18:05:45 +0100772 def do_verify_adm(self, arg):
773 """VERIFY the ADM1 PIN"""
774 if arg:
775 # use specified ADM-PIN
776 pin_adm = sanitize_pin_adm(arg)
777 else:
778 # try to find an ADM-PIN if none is specified
779 result = card_key_provider_get_field(
780 'ADM1', key='ICCID', value=self._cmd.iccid)
781 pin_adm = sanitize_pin_adm(result)
782 if pin_adm:
783 self._cmd.poutput(
784 "found ADM-PIN '%s' for ICCID '%s'" % (result, self._cmd.iccid))
785 else:
786 raise ValueError(
787 "cannot find ADM-PIN for ICCID '%s'" % (self._cmd.iccid))
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200788
Harald Weltec91085e2022-02-10 18:05:45 +0100789 if pin_adm:
Philipp Maiercfb665b2023-07-20 17:30:24 +0200790 self._cmd.card._scc.verify_chv(self._cmd.card._adm_chv_num, h2b(pin_adm))
Harald Weltec91085e2022-02-10 18:05:45 +0100791 else:
792 raise ValueError("error: cannot authenticate, no adm-pin!")
793
Philipp Maier7b9e2442023-03-22 15:19:54 +0100794 def do_cardinfo(self, opts):
795 """Display information about the currently inserted card"""
796 self._cmd.poutput("Card info:")
797 self._cmd.poutput(" Name: %s" % self._cmd.card.name)
798 self._cmd.poutput(" ATR: %s" % b2h(self._cmd.card._scc.get_atr()))
799 self._cmd.poutput(" ICCID: %s" % self._cmd.iccid)
800 self._cmd.poutput(" Class-Byte: %s" % self._cmd.card._scc.cla_byte)
801 self._cmd.poutput(" Select-Ctrl: %s" % self._cmd.card._scc.sel_ctrl)
802 self._cmd.poutput(" AIDs:")
803 for a in self._cmd.rs.mf.applications:
804 self._cmd.poutput(" %s" % a)
Harald Welteb2edd142021-01-08 23:29:35 +0100805
Harald Welte31d2cf02021-04-03 10:47:29 +0200806@with_default_category('ISO7816 Commands')
807class Iso7816Commands(CommandSet):
Harald Weltec91085e2022-02-10 18:05:45 +0100808 def __init__(self):
809 super().__init__()
Harald Welte31d2cf02021-04-03 10:47:29 +0200810
Harald Weltec91085e2022-02-10 18:05:45 +0100811 def do_select(self, opts):
812 """SELECT a File (ADF/DF/EF)"""
813 if len(opts.arg_list) == 0:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200814 path = self._cmd.lchan.selected_file.fully_qualified_path_str(True)
815 path_fid = self._cmd.lchan.selected_file.fully_qualified_path_str(False)
816 self._cmd.poutput("currently selected file: %s (%s)" % (path, path_fid))
Harald Weltec91085e2022-02-10 18:05:45 +0100817 return
Harald Welte31d2cf02021-04-03 10:47:29 +0200818
Harald Weltec91085e2022-02-10 18:05:45 +0100819 path = opts.arg_list[0]
Harald Weltea6c0f882022-07-17 14:23:17 +0200820 fcp_dec = self._cmd.lchan.select(path, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100821 self._cmd.update_prompt()
822 self._cmd.poutput_json(fcp_dec)
Harald Welte31d2cf02021-04-03 10:47:29 +0200823
Harald Weltec91085e2022-02-10 18:05:45 +0100824 def complete_select(self, text, line, begidx, endidx) -> List[str]:
825 """Command Line tab completion for SELECT"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200826 index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
Harald Weltec91085e2022-02-10 18:05:45 +0100827 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200828
Harald Weltec91085e2022-02-10 18:05:45 +0100829 def get_code(self, code):
830 """Use code either directly or try to get it from external data source"""
831 auto = ('PIN1', 'PIN2', 'PUK1', 'PUK2')
Harald Welte31d2cf02021-04-03 10:47:29 +0200832
Harald Weltec91085e2022-02-10 18:05:45 +0100833 if str(code).upper() not in auto:
834 return sanitize_pin_adm(code)
Harald Welte31d2cf02021-04-03 10:47:29 +0200835
Harald Weltec91085e2022-02-10 18:05:45 +0100836 result = card_key_provider_get_field(
837 str(code), key='ICCID', value=self._cmd.iccid)
838 result = sanitize_pin_adm(result)
839 if result:
840 self._cmd.poutput("found %s '%s' for ICCID '%s'" %
841 (code.upper(), result, self._cmd.iccid))
842 else:
843 self._cmd.poutput("cannot find %s for ICCID '%s'" %
844 (code.upper(), self._cmd.iccid))
845 return result
Harald Welte31d2cf02021-04-03 10:47:29 +0200846
Harald Weltec91085e2022-02-10 18:05:45 +0100847 verify_chv_parser = argparse.ArgumentParser()
848 verify_chv_parser.add_argument(
849 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
850 verify_chv_parser.add_argument(
851 '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 +0200852
Harald Weltec91085e2022-02-10 18:05:45 +0100853 @cmd2.with_argparser(verify_chv_parser)
854 def do_verify_chv(self, opts):
Harald Welte12af7932022-02-15 16:39:08 +0100855 """Verify (authenticate) using specified CHV (PIN) code, which is how the specifications
856 call it if you authenticate yourself using the specified PIN. There usually is at least PIN1 and
857 PIN2."""
Harald Weltec91085e2022-02-10 18:05:45 +0100858 pin = self.get_code(opts.pin_code)
859 (data, sw) = self._cmd.card._scc.verify_chv(opts.pin_nr, h2b(pin))
860 self._cmd.poutput("CHV verification successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200861
Harald Weltec91085e2022-02-10 18:05:45 +0100862 unblock_chv_parser = argparse.ArgumentParser()
863 unblock_chv_parser.add_argument(
864 '--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
865 unblock_chv_parser.add_argument(
866 'puk_code', type=str, help='PUK code digits \"PUK1\" or \"PUK2\" to get PUK code from external data source')
867 unblock_chv_parser.add_argument(
868 '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 +0200869
Harald Weltec91085e2022-02-10 18:05:45 +0100870 @cmd2.with_argparser(unblock_chv_parser)
871 def do_unblock_chv(self, opts):
872 """Unblock PIN code using specified PUK code"""
873 new_pin = self.get_code(opts.new_pin_code)
874 puk = self.get_code(opts.puk_code)
875 (data, sw) = self._cmd.card._scc.unblock_chv(
876 opts.pin_nr, h2b(puk), h2b(new_pin))
877 self._cmd.poutput("CHV unblock successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200878
Harald Weltec91085e2022-02-10 18:05:45 +0100879 change_chv_parser = argparse.ArgumentParser()
880 change_chv_parser.add_argument(
881 '--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
882 change_chv_parser.add_argument(
883 'pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
884 change_chv_parser.add_argument(
885 '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 +0200886
Harald Weltec91085e2022-02-10 18:05:45 +0100887 @cmd2.with_argparser(change_chv_parser)
888 def do_change_chv(self, opts):
889 """Change PIN code to a new PIN code"""
890 new_pin = self.get_code(opts.new_pin_code)
891 pin = self.get_code(opts.pin_code)
892 (data, sw) = self._cmd.card._scc.change_chv(
893 opts.pin_nr, h2b(pin), h2b(new_pin))
894 self._cmd.poutput("CHV change successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200895
Harald Weltec91085e2022-02-10 18:05:45 +0100896 disable_chv_parser = argparse.ArgumentParser()
897 disable_chv_parser.add_argument(
898 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
899 disable_chv_parser.add_argument(
900 '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 +0200901
Harald Weltec91085e2022-02-10 18:05:45 +0100902 @cmd2.with_argparser(disable_chv_parser)
903 def do_disable_chv(self, opts):
904 """Disable PIN code using specified PIN code"""
905 pin = self.get_code(opts.pin_code)
906 (data, sw) = self._cmd.card._scc.disable_chv(opts.pin_nr, h2b(pin))
907 self._cmd.poutput("CHV disable successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200908
Harald Weltec91085e2022-02-10 18:05:45 +0100909 enable_chv_parser = argparse.ArgumentParser()
910 enable_chv_parser.add_argument(
911 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
912 enable_chv_parser.add_argument(
913 '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 +0200914
Harald Weltec91085e2022-02-10 18:05:45 +0100915 @cmd2.with_argparser(enable_chv_parser)
916 def do_enable_chv(self, opts):
917 """Enable PIN code using specified PIN code"""
918 pin = self.get_code(opts.pin_code)
919 (data, sw) = self._cmd.card._scc.enable_chv(opts.pin_nr, h2b(pin))
920 self._cmd.poutput("CHV enable successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200921
Harald Weltec91085e2022-02-10 18:05:45 +0100922 def do_deactivate_file(self, opts):
Harald Welte799c3542022-02-15 15:56:28 +0100923 """Deactivate the currently selected EF"""
Harald Weltec91085e2022-02-10 18:05:45 +0100924 (data, sw) = self._cmd.card._scc.deactivate_file()
Harald Weltea4631612021-04-10 18:17:55 +0200925
Harald Welte799c3542022-02-15 15:56:28 +0100926 activate_file_parser = argparse.ArgumentParser()
927 activate_file_parser.add_argument('NAME', type=str, help='File name or FID of file to activate')
928 @cmd2.with_argparser(activate_file_parser)
Harald Weltec91085e2022-02-10 18:05:45 +0100929 def do_activate_file(self, opts):
Harald Welte12af7932022-02-15 16:39:08 +0100930 """Activate the specified EF. This used to be called REHABILITATE in TS 11.11 for classic
931 SIM. You need to specify the name or FID of the file to activate."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200932 (data, sw) = self._cmd.lchan.activate_file(opts.NAME)
Harald Welte485692b2021-05-25 22:21:44 +0200933
Harald Weltec91085e2022-02-10 18:05:45 +0100934 def complete_activate_file(self, text, line, begidx, endidx) -> List[str]:
935 """Command Line tab completion for ACTIVATE FILE"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200936 index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
Harald Weltec91085e2022-02-10 18:05:45 +0100937 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200938
Harald Weltec91085e2022-02-10 18:05:45 +0100939 open_chan_parser = argparse.ArgumentParser()
940 open_chan_parser.add_argument(
941 'chan_nr', type=int, default=0, help='Channel Number')
Harald Welte703f9332021-04-10 18:39:32 +0200942
Harald Weltec91085e2022-02-10 18:05:45 +0100943 @cmd2.with_argparser(open_chan_parser)
944 def do_open_channel(self, opts):
945 """Open a logical channel."""
946 (data, sw) = self._cmd.card._scc.manage_channel(
947 mode='open', lchan_nr=opts.chan_nr)
Harald Welte703f9332021-04-10 18:39:32 +0200948
Harald Weltec91085e2022-02-10 18:05:45 +0100949 close_chan_parser = argparse.ArgumentParser()
950 close_chan_parser.add_argument(
951 'chan_nr', type=int, default=0, help='Channel Number')
Harald Welte703f9332021-04-10 18:39:32 +0200952
Harald Weltec91085e2022-02-10 18:05:45 +0100953 @cmd2.with_argparser(close_chan_parser)
954 def do_close_channel(self, opts):
955 """Close a logical channel."""
956 (data, sw) = self._cmd.card._scc.manage_channel(
957 mode='close', lchan_nr=opts.chan_nr)
Harald Welte703f9332021-04-10 18:39:32 +0200958
Harald Weltec91085e2022-02-10 18:05:45 +0100959 def do_status(self, opts):
960 """Perform the STATUS command."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200961 fcp_dec = self._cmd.lchan.status()
Harald Weltec91085e2022-02-10 18:05:45 +0100962 self._cmd.poutput_json(fcp_dec)
Harald Welte34b05d32021-05-25 22:03:13 +0200963
Harald Welteec950532021-10-20 13:09:00 +0200964
Harald Weltecab26c72022-08-06 16:12:30 +0200965class Proact(ProactiveHandler):
966 def receive_fetch(self, pcmd: ProactiveCommand):
967 # print its parsed representation
968 print(pcmd.decoded)
969 # TODO: implement the basics, such as SMS Sending, ...
970
971
Harald Welte703f9332021-04-10 18:39:32 +0200972
Harald Weltef422eb12023-06-09 11:15:09 +0200973option_parser = argparse.ArgumentParser(description='interactive SIM card shell',
Harald Weltef2e761c2021-04-11 11:56:44 +0200974 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
Harald Welte28c24312021-04-11 12:19:36 +0200975argparse_add_reader_args(option_parser)
Harald Weltec8ff0262021-04-11 12:06:13 +0200976
977global_group = option_parser.add_argument_group('General Options')
978global_group.add_argument('--script', metavar='PATH', default=None,
Harald Welte28c24312021-04-11 12:19:36 +0200979 help='script with pySim-shell commands to be executed automatically at start-up')
Harald Weltec91085e2022-02-10 18:05:45 +0100980global_group.add_argument('--csv', metavar='FILE',
981 default=None, help='Read card data from CSV file')
Philipp Maier76667642021-09-22 16:53:22 +0200982global_group.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
Harald Weltec91085e2022-02-10 18:05:45 +0100983 help="Use automatic card handling machine")
Harald Weltec8ff0262021-04-11 12:06:13 +0200984
985adm_group = global_group.add_mutually_exclusive_group()
986adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', default=None,
987 help='ADM PIN used for provisioning (overwrites default)')
988adm_group.add_argument('-A', '--pin-adm-hex', metavar='PIN_ADM1_HEX', dest='pin_adm_hex', default=None,
989 help='ADM PIN used for provisioning, as hex string (16 characters long)')
Harald Welteb2edd142021-01-08 23:29:35 +0100990
Harald Welte38306df2023-07-11 21:17:55 +0200991option_parser.add_argument("command", nargs='?',
992 help="A pySim-shell command that would optionally be executed at startup")
993option_parser.add_argument('command_args', nargs=argparse.REMAINDER,
994 help="Optional Arguments for command")
995
Harald Welteb2edd142021-01-08 23:29:35 +0100996
997if __name__ == '__main__':
998
Harald Weltec91085e2022-02-10 18:05:45 +0100999 # Parse options
1000 opts = option_parser.parse_args()
Harald Welteb2edd142021-01-08 23:29:35 +01001001
Harald Weltec91085e2022-02-10 18:05:45 +01001002 # If a script file is specified, be sure that it actually exists
1003 if opts.script:
1004 if not os.access(opts.script, os.R_OK):
1005 print("Invalid script file!")
1006 sys.exit(2)
Philipp Maier13e258d2021-04-08 17:48:49 +02001007
Harald Weltec91085e2022-02-10 18:05:45 +01001008 # Register csv-file as card data provider, either from specified CSV
1009 # or from CSV file in home directory
1010 csv_default = str(Path.home()) + "/.osmocom/pysim/card_data.csv"
1011 if opts.csv:
1012 card_key_provider_register(CardKeyProviderCsv(opts.csv))
1013 if os.path.isfile(csv_default):
1014 card_key_provider_register(CardKeyProviderCsv(csv_default))
Philipp Maier2b11c322021-03-17 12:37:39 +01001015
Harald Weltec91085e2022-02-10 18:05:45 +01001016 # Init card reader driver
Harald Weltecab26c72022-08-06 16:12:30 +02001017 sl = init_reader(opts, proactive_handler = Proact())
Harald Weltec91085e2022-02-10 18:05:45 +01001018 if sl is None:
1019 exit(1)
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 Maier8e03f2f2023-10-09 13:27:59 +02001043 app = PysimApp(None, 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:
Philipp Maier4840d4d2023-09-06 14:54:14 +02001052 card._scc.verify_chv(card._adm_chv_num, h2b(pin_adm))
Harald Weltec91085e2022-02-10 18:05:45 +01001053 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()