blob: 6ff484b69cbb20a3ae91be3767b035b1741e22c6 [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 Welte8fab4632023-11-03 01:03:28 +01005# (C) 2021-2023 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 *
Harald Weltecab26c72022-08-06 16:12:30 +020051from pySim.transport import init_reader, ApduTracer, argparse_add_reader_args, ProactiveHandler
Philipp Maiere345e112023-06-09 15:19:56 +020052from pySim.utils import h2b, b2h, i2h, swap_nibbles, rpad, JsonEncoder, bertlv_parse_one, sw_match
Philipp Maiera42ee6f2023-08-16 10:44:57 +020053from pySim.utils import sanitize_pin_adm, tabulate_str_list, boxed_heading_str, Hexstr, dec_iccid
Harald Welte1c849f82023-11-01 23:48:28 +010054from pySim.utils import is_hexstr_or_decimal, is_hexstr, is_decimal
Philipp Maier76667642021-09-22 16:53:22 +020055from pySim.card_handler import CardHandler, CardHandlerAuto
Harald Welteb2edd142021-01-08 23:29:35 +010056
Philipp Maier54827372023-10-24 16:18:30 +020057from pySim.filesystem import CardMF, CardDF, CardADF
Harald Welte3c9b7842021-10-19 21:44:24 +020058from pySim.ts_102_222 import Ts102222Commands
Harald Welte2a33ad22021-10-20 10:14:18 +020059from pySim.gsm_r import DF_EIRENE
Harald Weltecab26c72022-08-06 16:12:30 +020060from pySim.cat import ProactiveCommand
Harald Welteb2edd142021-01-08 23:29:35 +010061
Harald Welte4442b3d2021-04-03 09:00:16 +020062from pySim.card_key_provider import CardKeyProviderCsv, card_key_provider_register, card_key_provider_get_field
Philipp Maier2b11c322021-03-17 12:37:39 +010063
Harald Welte8fab4632023-11-03 01:03:28 +010064from pySim.app import init_card
Harald Weltec91085e2022-02-10 18:05:45 +010065
Philipp Maier2b11c322021-03-17 12:37:39 +010066
Harald Weltee1268722023-06-24 07:57:08 +020067class Cmd2Compat(cmd2.Cmd):
68 """Backwards-compatibility wrapper around cmd2.Cmd to support older and newer
69 releases. See https://github.com/python-cmd2/cmd2/blob/master/CHANGELOG.md"""
70 def run_editor(self, file_path: Optional[str] = None) -> None:
71 if version.parse(cmd2.__version__) < version.parse("2.0.0"):
Harald Welte0ec01502023-06-24 10:00:12 +020072 return self._run_editor(file_path) # pylint: disable=no-member
Harald Weltee1268722023-06-24 07:57:08 +020073 else:
Harald Welte0ec01502023-06-24 10:00:12 +020074 return super().run_editor(file_path) # pylint: disable=no-member
75
76class Settable2Compat(cmd2.Settable):
77 """Backwards-compatibility wrapper around cmd2.Settable to support older and newer
78 releases. See https://github.com/python-cmd2/cmd2/blob/master/CHANGELOG.md"""
79 def __init__(self, name, val_type, description, settable_object, **kwargs):
80 if version.parse(cmd2.__version__) < version.parse("2.0.0"):
81 super().__init__(name, val_type, description, **kwargs) # pylint: disable=no-value-for-parameter
82 else:
83 super().__init__(name, val_type, description, settable_object, **kwargs) # pylint: disable=too-many-function-args
Harald Weltee1268722023-06-24 07:57:08 +020084
85class PysimApp(Cmd2Compat):
Harald Weltec91085e2022-02-10 18:05:45 +010086 CUSTOM_CATEGORY = 'pySim Commands'
Harald Welte0ba3fd92023-11-01 19:18:24 +010087 BANNER = """Welcome to pySim-shell!
88(C) 2021-2023 by Harald Welte, sysmocom - s.f.m.c. GmbH and contributors
89Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/shell.html """
Philipp Maier76667642021-09-22 16:53:22 +020090
Harald Weltec91085e2022-02-10 18:05:45 +010091 def __init__(self, card, rs, sl, ch, script=None):
Harald Weltec85d4062023-04-27 17:10:17 +020092 if version.parse(cmd2.__version__) < version.parse("2.0.0"):
93 kwargs = {'use_ipython': True}
94 else:
95 kwargs = {'include_ipy': True}
96
Oliver Smithe47ea5f2023-05-23 12:54:15 +020097 # pylint: disable=unexpected-keyword-arg
Harald Weltec91085e2022-02-10 18:05:45 +010098 super().__init__(persistent_history_file='~/.pysim_shell_history', allow_cli_args=False,
Harald Weltec85d4062023-04-27 17:10:17 +020099 auto_load_commands=False, startup_script=script, **kwargs)
Harald Welte0ba3fd92023-11-01 19:18:24 +0100100 self.intro = style(self.BANNER, fg=RED)
Harald Weltec91085e2022-02-10 18:05:45 +0100101 self.default_category = 'pySim-shell built-in commands'
102 self.card = None
103 self.rs = None
Harald Weltea6c0f882022-07-17 14:23:17 +0200104 self.lchan = None
105 self.py_locals = {'card': self.card, 'rs': self.rs, 'lchan': self.lchan}
Harald Weltec91085e2022-02-10 18:05:45 +0100106 self.sl = sl
107 self.ch = ch
Harald Welte1748b932021-04-06 21:12:25 +0200108
Harald Weltec91085e2022-02-10 18:05:45 +0100109 self.numeric_path = False
Harald Weltec91085e2022-02-10 18:05:45 +0100110 self.conserve_write = True
Harald Weltec91085e2022-02-10 18:05:45 +0100111 self.json_pretty_print = True
Harald Weltec91085e2022-02-10 18:05:45 +0100112 self.apdu_trace = False
Philipp Maier5d698e52021-09-16 13:18:01 +0200113
Harald Welte0ec01502023-06-24 10:00:12 +0200114 self.add_settable(Settable2Compat('numeric_path', bool, 'Print File IDs instead of names', self,
115 onchange_cb=self._onchange_numeric_path))
116 self.add_settable(Settable2Compat('conserve_write', bool, 'Read and compare before write', self,
117 onchange_cb=self._onchange_conserve_write))
118 self.add_settable(Settable2Compat('json_pretty_print', bool, 'Pretty-Print JSON output', self))
119 self.add_settable(Settable2Compat('apdu_trace', bool, 'Trace and display APDUs exchanged with card', self,
120 onchange_cb=self._onchange_apdu_trace))
Harald Weltec91085e2022-02-10 18:05:45 +0100121 self.equip(card, rs)
Philipp Maier5d698e52021-09-16 13:18:01 +0200122
Harald Weltec91085e2022-02-10 18:05:45 +0100123 def equip(self, card, rs):
124 """
125 Equip pySim-shell with the supplied card and runtime state, add (or remove) all required settables and
126 and commands to enable card operations.
127 """
Philipp Maier76667642021-09-22 16:53:22 +0200128
Harald Weltec91085e2022-02-10 18:05:45 +0100129 rc = False
Philipp Maier5d698e52021-09-16 13:18:01 +0200130
Harald Weltec91085e2022-02-10 18:05:45 +0100131 # Unequip everything from pySim-shell that would not work in unequipped state
132 if self.rs:
Harald Weltea6c0f882022-07-17 14:23:17 +0200133 lchan = self.rs.lchan[0]
134 lchan.unregister_cmds(self)
iw0f818acd2023-06-19 10:27:04 +0200135 if self.rs.profile:
136 for cmd_set in self.rs.profile.shell_cmdsets:
137 self.unregister_command_set(cmd_set)
138
Harald Welte892526f2023-06-06 17:09:50 +0200139 for cmds in [Iso7816Commands, Ts102222Commands, PySimCommands]:
Harald Weltec91085e2022-02-10 18:05:45 +0100140 cmd_set = self.find_commandsets(cmds)
141 if cmd_set:
142 self.unregister_command_set(cmd_set[0])
Philipp Maier5d698e52021-09-16 13:18:01 +0200143
Harald Weltec91085e2022-02-10 18:05:45 +0100144 self.card = card
145 self.rs = rs
Philipp Maier5d698e52021-09-16 13:18:01 +0200146
Harald Weltec91085e2022-02-10 18:05:45 +0100147 # When a card object and a runtime state is present, (re)equip pySim-shell with everything that is
148 # needed to operate on cards.
149 if self.card and self.rs:
Harald Weltea6c0f882022-07-17 14:23:17 +0200150 self.lchan = self.rs.lchan[0]
Harald Weltec91085e2022-02-10 18:05:45 +0100151 self._onchange_conserve_write(
152 'conserve_write', False, self.conserve_write)
153 self._onchange_apdu_trace('apdu_trace', False, self.apdu_trace)
Harald Welte659781c2023-06-06 17:00:51 +0200154 if self.rs.profile:
155 for cmd_set in self.rs.profile.shell_cmdsets:
156 self.register_command_set(cmd_set)
Harald Weltec91085e2022-02-10 18:05:45 +0100157 self.register_command_set(Iso7816Commands())
Harald Welte3c9b7842021-10-19 21:44:24 +0200158 self.register_command_set(Ts102222Commands())
Harald Weltec91085e2022-02-10 18:05:45 +0100159 self.register_command_set(PySimCommands())
Philipp Maiera42ee6f2023-08-16 10:44:57 +0200160
Philipp Maier7c0cd0a2023-10-16 15:02:07 +0200161 try:
162 self.lchan.select('MF/EF.ICCID', self)
163 self.iccid = dec_iccid(self.lchan.read_binary()[0])
164 except:
165 self.iccid = None
Philipp Maiera42ee6f2023-08-16 10:44:57 +0200166
Harald Weltea6c0f882022-07-17 14:23:17 +0200167 self.lchan.select('MF', self)
Harald Weltec91085e2022-02-10 18:05:45 +0100168 rc = True
169 else:
170 self.poutput("pySim-shell not equipped!")
Philipp Maier5d698e52021-09-16 13:18:01 +0200171
Harald Weltec91085e2022-02-10 18:05:45 +0100172 self.update_prompt()
173 return rc
Harald Welteb2edd142021-01-08 23:29:35 +0100174
Harald Weltec91085e2022-02-10 18:05:45 +0100175 def poutput_json(self, data, force_no_pretty=False):
176 """like cmd2.poutput() but for a JSON serializable dict."""
177 if force_no_pretty or self.json_pretty_print == False:
178 output = json.dumps(data, cls=JsonEncoder)
179 else:
180 output = json.dumps(data, cls=JsonEncoder, indent=4)
181 self.poutput(output)
Harald Welteb2edd142021-01-08 23:29:35 +0100182
Harald Weltec91085e2022-02-10 18:05:45 +0100183 def _onchange_numeric_path(self, param_name, old, new):
184 self.update_prompt()
Philipp Maier38c74f62021-03-17 17:19:52 +0100185
Harald Weltec91085e2022-02-10 18:05:45 +0100186 def _onchange_conserve_write(self, param_name, old, new):
187 if self.rs:
188 self.rs.conserve_write = new
Harald Welte7829d8a2021-04-10 11:28:53 +0200189
Harald Weltec91085e2022-02-10 18:05:45 +0100190 def _onchange_apdu_trace(self, param_name, old, new):
191 if self.card:
192 if new == True:
193 self.card._scc._tp.apdu_tracer = self.Cmd2ApduTracer(self)
194 else:
195 self.card._scc._tp.apdu_tracer = None
Harald Welte7829d8a2021-04-10 11:28:53 +0200196
Harald Weltec91085e2022-02-10 18:05:45 +0100197 class Cmd2ApduTracer(ApduTracer):
198 def __init__(self, cmd2_app):
Philipp Maier91c971b2023-10-09 10:48:46 +0200199 self.cmd2 = cmd2_app
Harald Welte7829d8a2021-04-10 11:28:53 +0200200
Harald Weltec91085e2022-02-10 18:05:45 +0100201 def trace_response(self, cmd, sw, resp):
202 self.cmd2.poutput("-> %s %s" % (cmd[:10], cmd[10:]))
203 self.cmd2.poutput("<- %s: %s" % (sw, resp))
Harald Welteb2edd142021-01-08 23:29:35 +0100204
Harald Weltec91085e2022-02-10 18:05:45 +0100205 def update_prompt(self):
Harald Weltea6c0f882022-07-17 14:23:17 +0200206 if self.lchan:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200207 path_str = self.lchan.selected_file.fully_qualified_path_str(not self.numeric_path)
Harald Welte237ddb52023-10-22 10:36:58 +0200208 self.prompt = 'pySIM-shell (%02u:%s)> ' % (self.lchan.lchan_nr, path_str)
Harald Weltec91085e2022-02-10 18:05:45 +0100209 else:
Philipp Maier7226c092022-06-01 17:58:38 +0200210 if self.card:
211 self.prompt = 'pySIM-shell (no card profile)> '
212 else:
213 self.prompt = 'pySIM-shell (no card)> '
Harald Welteb2edd142021-01-08 23:29:35 +0100214
Harald Weltec91085e2022-02-10 18:05:45 +0100215 @cmd2.with_category(CUSTOM_CATEGORY)
216 def do_intro(self, _):
217 """Display the intro banner"""
218 self.poutput(self.intro)
Philipp Maier9764de22021-11-03 10:44:39 +0100219
Harald Weltec91085e2022-02-10 18:05:45 +0100220 def do_eof(self, _: argparse.Namespace) -> bool:
221 self.poutput("")
222 return self.do_quit('')
Philipp Maier5d698e52021-09-16 13:18:01 +0200223
Harald Weltec91085e2022-02-10 18:05:45 +0100224 @cmd2.with_category(CUSTOM_CATEGORY)
225 def do_equip(self, opts):
226 """Equip pySim-shell with card"""
Philipp Maiere6cba762023-08-11 11:23:17 +0200227 if self.rs and self.rs.profile:
Harald Welte659781c2023-06-06 17:00:51 +0200228 for cmd_set in self.rs.profile.shell_cmdsets:
229 self.unregister_command_set(cmd_set)
Philipp Maier91c971b2023-10-09 10:48:46 +0200230 rs, card = init_card(self.sl)
Harald Weltec91085e2022-02-10 18:05:45 +0100231 self.equip(card, rs)
Philipp Maier76667642021-09-22 16:53:22 +0200232
Philipp Maier7226c092022-06-01 17:58:38 +0200233 apdu_cmd_parser = argparse.ArgumentParser()
Harald Welte4e59d892023-11-01 23:40:07 +0100234 apdu_cmd_parser.add_argument('APDU', type=is_hexstr, help='APDU as hex string')
Philipp Maiere7d1b672022-06-01 18:05:34 +0200235 apdu_cmd_parser.add_argument('--expect-sw', help='expect a specified status word', type=str, default=None)
Philipp Maier7226c092022-06-01 17:58:38 +0200236
237 @cmd2.with_argparser(apdu_cmd_parser)
238 def do_apdu(self, opts):
239 """Send a raw APDU to the card, and print SW + Response.
Philipp Maier1c207a22023-11-29 13:04:09 +0100240 CAUTION: this command bypasses the logical channel handling of pySim-shell and card state changes are not
241 tracked. Dpending on the raw APDU sent, pySim-shell may not continue to work as expected if you e.g. select
242 a different file."""
243
244 # When sending raw APDUs we access the scc object through _scc member of the card object. It should also be
245 # noted that the apdu command plays an exceptional role since it is the only card accessing command that
246 # can be executed without the presence of a runtime state (self.rs) object. However, this also means that
247 # self.lchan is also not present (see method equip).
248 data, sw = self.card._scc._tp.send_apdu(opts.APDU)
Philipp Maier7226c092022-06-01 17:58:38 +0200249 if data:
250 self.poutput("SW: %s, RESP: %s" % (sw, data))
251 else:
252 self.poutput("SW: %s" % sw)
Philipp Maiere7d1b672022-06-01 18:05:34 +0200253 if opts.expect_sw:
254 if not sw_match(sw, opts.expect_sw):
255 raise SwMatchError(sw, opts.expect_sw)
Philipp Maier7226c092022-06-01 17:58:38 +0200256
Harald Weltec91085e2022-02-10 18:05:45 +0100257 class InterceptStderr(list):
258 def __init__(self):
259 self._stderr_backup = sys.stderr
Philipp Maier76667642021-09-22 16:53:22 +0200260
Harald Weltec91085e2022-02-10 18:05:45 +0100261 def __enter__(self):
262 self._stringio_stderr = StringIO()
263 sys.stderr = self._stringio_stderr
264 return self
Philipp Maier76667642021-09-22 16:53:22 +0200265
Harald Weltec91085e2022-02-10 18:05:45 +0100266 def __exit__(self, *args):
267 self.stderr = self._stringio_stderr.getvalue().strip()
268 del self._stringio_stderr
269 sys.stderr = self._stderr_backup
Philipp Maier76667642021-09-22 16:53:22 +0200270
Harald Weltec91085e2022-02-10 18:05:45 +0100271 def _show_failure_sign(self):
Harald Welte961b8032023-04-27 17:30:22 +0200272 self.poutput(style(" +-------------+", fg=LIGHT_RED))
273 self.poutput(style(" + ## ## +", fg=LIGHT_RED))
274 self.poutput(style(" + ## ## +", fg=LIGHT_RED))
275 self.poutput(style(" + ### +", fg=LIGHT_RED))
276 self.poutput(style(" + ## ## +", fg=LIGHT_RED))
277 self.poutput(style(" + ## ## +", fg=LIGHT_RED))
278 self.poutput(style(" +-------------+", fg=LIGHT_RED))
Harald Weltec91085e2022-02-10 18:05:45 +0100279 self.poutput("")
Philipp Maier76667642021-09-22 16:53:22 +0200280
Harald Weltec91085e2022-02-10 18:05:45 +0100281 def _show_success_sign(self):
Harald Welte961b8032023-04-27 17:30:22 +0200282 self.poutput(style(" +-------------+", fg=LIGHT_GREEN))
283 self.poutput(style(" + ## +", fg=LIGHT_GREEN))
284 self.poutput(style(" + ## +", fg=LIGHT_GREEN))
285 self.poutput(style(" + # ## +", fg=LIGHT_GREEN))
286 self.poutput(style(" + ## # +", fg=LIGHT_GREEN))
287 self.poutput(style(" + ## +", fg=LIGHT_GREEN))
288 self.poutput(style(" +-------------+", fg=LIGHT_GREEN))
Harald Weltec91085e2022-02-10 18:05:45 +0100289 self.poutput("")
Philipp Maier76667642021-09-22 16:53:22 +0200290
Harald Weltec91085e2022-02-10 18:05:45 +0100291 def _process_card(self, first, script_path):
Philipp Maier76667642021-09-22 16:53:22 +0200292
Harald Weltec91085e2022-02-10 18:05:45 +0100293 # Early phase of card initialzation (this part may fail with an exception)
294 try:
295 rs, card = init_card(self.sl)
296 rc = self.equip(card, rs)
297 except:
298 self.poutput("")
Philipp Maier6bfa8a82023-10-09 13:32:49 +0200299 self.poutput("Card initialization (%s) failed with an exception:" % str(self.sl))
Harald Weltec91085e2022-02-10 18:05:45 +0100300 self.poutput("---------------------8<---------------------")
301 traceback.print_exc()
302 self.poutput("---------------------8<---------------------")
303 self.poutput("")
304 return -1
Philipp Maier76667642021-09-22 16:53:22 +0200305
Harald Weltec91085e2022-02-10 18:05:45 +0100306 # Actual card processing step. This part should never fail with an exception since the cmd2
307 # do_run_script method will catch any exception that might occur during script execution.
308 if rc:
309 self.poutput("")
310 self.poutput("Transcript stdout:")
311 self.poutput("---------------------8<---------------------")
312 with self.InterceptStderr() as logged:
313 self.do_run_script(script_path)
314 self.poutput("---------------------8<---------------------")
Philipp Maier76667642021-09-22 16:53:22 +0200315
Harald Weltec91085e2022-02-10 18:05:45 +0100316 self.poutput("")
317 self.poutput("Transcript stderr:")
318 if logged.stderr:
319 self.poutput("---------------------8<---------------------")
320 self.poutput(logged.stderr)
321 self.poutput("---------------------8<---------------------")
322 else:
323 self.poutput("(none)")
Philipp Maier76667642021-09-22 16:53:22 +0200324
Harald Weltec91085e2022-02-10 18:05:45 +0100325 # Check for exceptions
326 self.poutput("")
327 if "EXCEPTION of type" not in logged.stderr:
328 return 0
Philipp Maier76667642021-09-22 16:53:22 +0200329
Harald Weltec91085e2022-02-10 18:05:45 +0100330 return -1
Philipp Maier76667642021-09-22 16:53:22 +0200331
Harald Weltec91085e2022-02-10 18:05:45 +0100332 bulk_script_parser = argparse.ArgumentParser()
333 bulk_script_parser.add_argument(
334 'script_path', help="path to the script file")
335 bulk_script_parser.add_argument('--halt_on_error', help='stop card handling if an exeption occurs',
336 action='store_true')
337 bulk_script_parser.add_argument('--tries', type=int, default=2,
338 help='how many tries before trying the next card')
339 bulk_script_parser.add_argument('--on_stop_action', type=str, default=None,
340 help='commandline to execute when card handling has stopped')
341 bulk_script_parser.add_argument('--pre_card_action', type=str, default=None,
342 help='commandline to execute before actually talking to the card')
Philipp Maier76667642021-09-22 16:53:22 +0200343
Harald Weltec91085e2022-02-10 18:05:45 +0100344 @cmd2.with_argparser(bulk_script_parser)
345 @cmd2.with_category(CUSTOM_CATEGORY)
346 def do_bulk_script(self, opts):
347 """Run script on multiple cards (bulk provisioning)"""
Philipp Maier76667642021-09-22 16:53:22 +0200348
Harald Weltec91085e2022-02-10 18:05:45 +0100349 # Make sure that the script file exists and that it is readable.
350 if not os.access(opts.script_path, os.R_OK):
351 self.poutput("Invalid script file!")
352 return
Philipp Maier76667642021-09-22 16:53:22 +0200353
Harald Weltec91085e2022-02-10 18:05:45 +0100354 success_count = 0
355 fail_count = 0
Philipp Maier76667642021-09-22 16:53:22 +0200356
Harald Weltec91085e2022-02-10 18:05:45 +0100357 first = True
358 while 1:
359 # TODO: Count consecutive failures, if more than N consecutive failures occur, then stop.
360 # The ratinale is: There may be a problem with the device, we do want to prevent that
361 # all remaining cards are fired to the error bin. This is only relevant for situations
362 # with large stacks, probably we do not need this feature right now.
Philipp Maier76667642021-09-22 16:53:22 +0200363
Harald Weltec91085e2022-02-10 18:05:45 +0100364 try:
365 # In case of failure, try multiple times.
366 for i in range(opts.tries):
367 # fetch card into reader bay
Philipp Maier91c971b2023-10-09 10:48:46 +0200368 self.ch.get(first)
Philipp Maier76667642021-09-22 16:53:22 +0200369
Harald Weltec91085e2022-02-10 18:05:45 +0100370 # if necessary execute an action before we start processing the card
371 if(opts.pre_card_action):
372 os.system(opts.pre_card_action)
Philipp Maier76667642021-09-22 16:53:22 +0200373
Harald Weltec91085e2022-02-10 18:05:45 +0100374 # process the card
375 rc = self._process_card(first, opts.script_path)
376 if rc == 0:
377 success_count = success_count + 1
378 self._show_success_sign()
379 self.poutput("Statistics: success :%i, failure: %i" % (
380 success_count, fail_count))
381 break
382 else:
383 fail_count = fail_count + 1
384 self._show_failure_sign()
385 self.poutput("Statistics: success :%i, failure: %i" % (
386 success_count, fail_count))
Philipp Maier76667642021-09-22 16:53:22 +0200387
Harald Weltec91085e2022-02-10 18:05:45 +0100388 # Depending on success or failure, the card goes either in the "error" bin or in the
389 # "done" bin.
390 if rc < 0:
Philipp Maier91c971b2023-10-09 10:48:46 +0200391 self.ch.error()
Harald Weltec91085e2022-02-10 18:05:45 +0100392 else:
Philipp Maier91c971b2023-10-09 10:48:46 +0200393 self.ch.done()
Philipp Maier76667642021-09-22 16:53:22 +0200394
Harald Weltec91085e2022-02-10 18:05:45 +0100395 # In most cases it is possible to proceed with the next card, but the
396 # user may decide to halt immediately when an error occurs
397 if opts.halt_on_error and rc < 0:
398 return
Philipp Maier76667642021-09-22 16:53:22 +0200399
Harald Weltec91085e2022-02-10 18:05:45 +0100400 except (KeyboardInterrupt):
401 self.poutput("")
402 self.poutput("Terminated by user!")
403 return
404 except (SystemExit):
405 # When all cards are processed the card handler device will throw a SystemExit
406 # exception. Also Errors that are not recoverable (cards stuck etc.) will end up here.
407 # The user has the option to execute some action to make aware that the card handler
408 # needs service.
409 if(opts.on_stop_action):
410 os.system(opts.on_stop_action)
411 return
412 except:
413 self.poutput("")
Philipp Maier6bfa8a82023-10-09 13:32:49 +0200414 self.poutput("Card handling (%s) failed with an exception:" % str(self.sl))
Harald Weltec91085e2022-02-10 18:05:45 +0100415 self.poutput("---------------------8<---------------------")
416 traceback.print_exc()
417 self.poutput("---------------------8<---------------------")
418 self.poutput("")
419 fail_count = fail_count + 1
420 self._show_failure_sign()
421 self.poutput("Statistics: success :%i, failure: %i" %
422 (success_count, fail_count))
Philipp Maierb52feed2021-09-22 16:43:13 +0200423
Harald Weltec91085e2022-02-10 18:05:45 +0100424 first = False
425
426 echo_parser = argparse.ArgumentParser()
Harald Welte977c5922023-11-01 23:42:55 +0100427 echo_parser.add_argument('string', help="string to echo on the shell", nargs='+')
Harald Weltec91085e2022-02-10 18:05:45 +0100428
429 @cmd2.with_argparser(echo_parser)
430 @cmd2.with_category(CUSTOM_CATEGORY)
431 def do_echo(self, opts):
432 """Echo (print) a string on the console"""
Harald Welte977c5922023-11-01 23:42:55 +0100433 self.poutput(' '.join(opts.string))
Harald Weltec91085e2022-02-10 18:05:45 +0100434
Harald Weltefc315482022-07-23 12:49:14 +0200435 @cmd2.with_category(CUSTOM_CATEGORY)
436 def do_version(self, opts):
437 """Print the pySim software version."""
438 import pkg_resources
439 self.poutput(pkg_resources.get_distribution('pySim'))
Harald Welteb2edd142021-01-08 23:29:35 +0100440
Harald Welte31d2cf02021-04-03 10:47:29 +0200441@with_default_category('pySim Commands')
442class PySimCommands(CommandSet):
Harald Weltec91085e2022-02-10 18:05:45 +0100443 def __init__(self):
444 super().__init__()
Harald Welteb2edd142021-01-08 23:29:35 +0100445
Harald Weltec91085e2022-02-10 18:05:45 +0100446 dir_parser = argparse.ArgumentParser()
447 dir_parser.add_argument(
448 '--fids', help='Show file identifiers', action='store_true')
449 dir_parser.add_argument(
450 '--names', help='Show file names', action='store_true')
451 dir_parser.add_argument(
452 '--apps', help='Show applications', action='store_true')
453 dir_parser.add_argument(
454 '--all', help='Show all selectable identifiers and names', action='store_true')
Philipp Maier5d3e2592021-02-22 17:22:16 +0100455
Harald Weltec91085e2022-02-10 18:05:45 +0100456 @cmd2.with_argparser(dir_parser)
457 def do_dir(self, opts):
458 """Show a listing of files available in currently selected DF or MF"""
459 if opts.all:
460 flags = []
461 elif opts.fids or opts.names or opts.apps:
462 flags = ['PARENT', 'SELF']
463 if opts.fids:
464 flags += ['FIDS', 'AIDS']
465 if opts.names:
466 flags += ['FNAMES', 'ANAMES']
467 if opts.apps:
468 flags += ['ANAMES', 'AIDS']
469 else:
470 flags = ['PARENT', 'SELF', 'FNAMES', 'ANAMES']
471 selectables = list(
Harald Weltea6c0f882022-07-17 14:23:17 +0200472 self._cmd.lchan.selected_file.get_selectable_names(flags=flags))
Harald Weltec91085e2022-02-10 18:05:45 +0100473 directory_str = tabulate_str_list(
474 selectables, width=79, hspace=2, lspace=1, align_left=True)
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200475 path = self._cmd.lchan.selected_file.fully_qualified_path_str(True)
476 self._cmd.poutput(path)
477 path = self._cmd.lchan.selected_file.fully_qualified_path_str(False)
478 self._cmd.poutput(path)
Harald Weltec91085e2022-02-10 18:05:45 +0100479 self._cmd.poutput(directory_str)
480 self._cmd.poutput("%d files" % len(selectables))
Harald Welteb2edd142021-01-08 23:29:35 +0100481
Philipp Maier7b138b02022-05-31 13:42:56 +0200482 def walk(self, indent=0, action_ef=None, action_df=None, context=None, **kwargs):
Harald Weltec91085e2022-02-10 18:05:45 +0100483 """Recursively walk through the file system, starting at the currently selected DF"""
Philipp Maier7b138b02022-05-31 13:42:56 +0200484
Harald Weltea6c0f882022-07-17 14:23:17 +0200485 if isinstance(self._cmd.lchan.selected_file, CardDF):
Philipp Maier7b138b02022-05-31 13:42:56 +0200486 if action_df:
Philipp Maier91c971b2023-10-09 10:48:46 +0200487 action_df(context, **kwargs)
Philipp Maier7b138b02022-05-31 13:42:56 +0200488
Harald Weltea6c0f882022-07-17 14:23:17 +0200489 files = self._cmd.lchan.selected_file.get_selectables(
Harald Weltec91085e2022-02-10 18:05:45 +0100490 flags=['FNAMES', 'ANAMES'])
491 for f in files:
Philipp Maier7b138b02022-05-31 13:42:56 +0200492 # special case: When no action is performed, just output a directory
493 if not action_ef and not action_df:
Harald Weltec91085e2022-02-10 18:05:45 +0100494 output_str = " " * indent + str(f) + (" " * 250)
495 output_str = output_str[0:25]
496 if isinstance(files[f], CardADF):
497 output_str += " " + str(files[f].aid)
498 else:
499 output_str += " " + str(files[f].fid)
500 output_str += " " + str(files[f].desc)
501 self._cmd.poutput(output_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200502
Harald Weltec91085e2022-02-10 18:05:45 +0100503 if isinstance(files[f], CardDF):
504 skip_df = False
505 try:
Harald Weltea6c0f882022-07-17 14:23:17 +0200506 fcp_dec = self._cmd.lchan.select(f, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100507 except Exception as e:
508 skip_df = True
Harald Weltea6c0f882022-07-17 14:23:17 +0200509 df = self._cmd.lchan.selected_file
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200510 df_path = df.fully_qualified_path_str(True)
511 df_skip_reason_str = df_path + \
Harald Weltec91085e2022-02-10 18:05:45 +0100512 "/" + str(f) + ", " + str(e)
513 if context:
514 context['DF_SKIP'] += 1
515 context['DF_SKIP_REASON'].append(df_skip_reason_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200516
Harald Weltec91085e2022-02-10 18:05:45 +0100517 # If the DF was skipped, we never have entered the directory
518 # below, so we must not move up.
519 if skip_df == False:
Philipp Maier7b138b02022-05-31 13:42:56 +0200520 self.walk(indent + 1, action_ef, action_df, context, **kwargs)
Philipp Maier54827372023-10-24 16:18:30 +0200521
522 parent = self._cmd.lchan.selected_file.parent
523 df = self._cmd.lchan.selected_file
524 adf = self._cmd.lchan.selected_adf
525 if isinstance(parent, CardMF) and (adf and adf.has_fs == False):
526 # Not every application that may be present on a GlobalPlatform card will support the SELECT
527 # command as we know it from ETSI TS 102 221, section 11.1.1. In fact the only subset of
528 # SELECT we may rely on is the OPEN SELECT command as specified in GlobalPlatform Card
529 # Specification, section 11.9. Unfortunately the OPEN SELECT command only supports the
530 # "select by name" method, which means we can only select an application and not a file.
531 # The consequence of this is that we may get trapped in an application that does not have
532 # ISIM/USIM like file system support and the only way to leave that application is to select
533 # an ISIM/USIM application in order to get the file system access back.
534 #
535 # To automate this escape-route while traversing the file system we will check whether
536 # the parent file is the MF. When this is the case and the selected ADF has no file system
537 # support, we will select an arbitrary ADF that has file system support first and from there
538 # we will then select the MF.
539 for selectable in parent.get_selectables().items():
540 if isinstance(selectable[1], CardADF) and selectable[1].has_fs == True:
541 self._cmd.lchan.select(selectable[1].name, self._cmd)
542 break
543 self._cmd.lchan.select(df.get_mf().name, self._cmd)
544 else:
545 # Normal DF/ADF selection
546 fcp_dec = self._cmd.lchan.select("..", self._cmd)
Philipp Maierf408a402021-04-09 21:16:12 +0200547
Philipp Maier7b138b02022-05-31 13:42:56 +0200548 elif action_ef:
Harald Weltea6c0f882022-07-17 14:23:17 +0200549 df_before_action = self._cmd.lchan.selected_file
Philipp Maier7b138b02022-05-31 13:42:56 +0200550 action_ef(f, context, **kwargs)
Harald Weltec91085e2022-02-10 18:05:45 +0100551 # When walking through the file system tree the action must not
552 # always restore the currently selected file to the file that
553 # was selected before executing the action() callback.
Harald Weltea6c0f882022-07-17 14:23:17 +0200554 if df_before_action != self._cmd.lchan.selected_file:
Harald Weltec91085e2022-02-10 18:05:45 +0100555 raise RuntimeError("inconsistent walk, %s is currently selected but expecting %s to be selected"
Harald Weltea6c0f882022-07-17 14:23:17 +0200556 % (str(self._cmd.lchan.selected_file), str(df_before_action)))
Philipp Maierff9dae22021-02-25 17:03:21 +0100557
Harald Weltec91085e2022-02-10 18:05:45 +0100558 def do_tree(self, opts):
559 """Display a filesystem-tree with all selectable files"""
560 self.walk()
Philipp Maierff9dae22021-02-25 17:03:21 +0100561
Philipp Maier7b138b02022-05-31 13:42:56 +0200562 def export_ef(self, filename, context, as_json):
563 """ Select and export a single elementary file (EF) """
Harald Weltec91085e2022-02-10 18:05:45 +0100564 context['COUNT'] += 1
Harald Weltea6c0f882022-07-17 14:23:17 +0200565 df = self._cmd.lchan.selected_file
Philipp Maierac34dcc2021-04-01 17:19:05 +0200566
Philipp Maierea81f752022-05-19 10:13:30 +0200567 # The currently selected file (not the file we are going to export)
568 # must always be an ADF or DF. From this starting point we select
569 # the EF we want to export. To maintain consistency we will then
570 # select the current DF again (see comment below).
Harald Weltec91085e2022-02-10 18:05:45 +0100571 if not isinstance(df, CardDF):
572 raise RuntimeError(
573 "currently selected file %s is not a DF or ADF" % str(df))
Philipp Maierac34dcc2021-04-01 17:19:05 +0200574
Harald Weltec91085e2022-02-10 18:05:45 +0100575 df_path_list = df.fully_qualified_path(True)
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200576 df_path = df.fully_qualified_path_str(True)
577 df_path_fid = df.fully_qualified_path_str(False)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100578
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200579 file_str = df_path + "/" + str(filename)
Harald Weltec91085e2022-02-10 18:05:45 +0100580 self._cmd.poutput(boxed_heading_str(file_str))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100581
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200582 self._cmd.poutput("# directory: %s (%s)" % (df_path, df_path_fid))
Harald Weltec91085e2022-02-10 18:05:45 +0100583 try:
Harald Weltea6c0f882022-07-17 14:23:17 +0200584 fcp_dec = self._cmd.lchan.select(filename, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100585 self._cmd.poutput("# file: %s (%s)" % (
Harald Weltea6c0f882022-07-17 14:23:17 +0200586 self._cmd.lchan.selected_file.name, self._cmd.lchan.selected_file.fid))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100587
Harald Weltea6c0f882022-07-17 14:23:17 +0200588 structure = self._cmd.lchan.selected_file_structure()
Harald Weltec91085e2022-02-10 18:05:45 +0100589 self._cmd.poutput("# structure: %s" % str(structure))
Harald Weltea6c0f882022-07-17 14:23:17 +0200590 self._cmd.poutput("# RAW FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp_hex))
591 self._cmd.poutput("# Decoded FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100592
Harald Weltec91085e2022-02-10 18:05:45 +0100593 for f in df_path_list:
594 self._cmd.poutput("select " + str(f))
Harald Weltea6c0f882022-07-17 14:23:17 +0200595 self._cmd.poutput("select " + self._cmd.lchan.selected_file.name)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100596
Harald Weltec91085e2022-02-10 18:05:45 +0100597 if structure == 'transparent':
Harald Welte08b11ab2022-02-10 18:56:41 +0100598 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200599 result = self._cmd.lchan.read_binary_dec()
Harald Welte08b11ab2022-02-10 18:56:41 +0100600 self._cmd.poutput("update_binary_decoded '%s'" % json.dumps(result[0], cls=JsonEncoder))
601 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200602 result = self._cmd.lchan.read_binary()
Harald Welte08b11ab2022-02-10 18:56:41 +0100603 self._cmd.poutput("update_binary " + str(result[0]))
Harald Weltec91085e2022-02-10 18:05:45 +0100604 elif structure == 'cyclic' or structure == 'linear_fixed':
605 # Use number of records specified in select response
Harald Weltea6c0f882022-07-17 14:23:17 +0200606 num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
Harald Welte747a9782022-02-13 17:52:28 +0100607 if num_of_rec:
Harald Weltec91085e2022-02-10 18:05:45 +0100608 for r in range(1, num_of_rec + 1):
Harald Welte08b11ab2022-02-10 18:56:41 +0100609 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200610 result = self._cmd.lchan.read_record_dec(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100611 self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
612 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200613 result = self._cmd.lchan.read_record(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100614 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
615
Harald Weltec91085e2022-02-10 18:05:45 +0100616 # When the select response does not return the number of records, read until we hit the
617 # first record that cannot be read.
618 else:
619 r = 1
620 while True:
621 try:
Harald Welte08b11ab2022-02-10 18:56:41 +0100622 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200623 result = self._cmd.lchan.read_record_dec(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100624 self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
625 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200626 result = self._cmd.lchan.read_record(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100627 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
Harald Weltec91085e2022-02-10 18:05:45 +0100628 except SwMatchError as e:
629 # We are past the last valid record - stop
630 if e.sw_actual == "9402":
631 break
632 # Some other problem occurred
633 else:
634 raise e
Harald Weltec91085e2022-02-10 18:05:45 +0100635 r = r + 1
636 elif structure == 'ber_tlv':
Harald Weltea6c0f882022-07-17 14:23:17 +0200637 tags = self._cmd.lchan.retrieve_tags()
Harald Weltec91085e2022-02-10 18:05:45 +0100638 for t in tags:
Harald Weltea6c0f882022-07-17 14:23:17 +0200639 result = self._cmd.lchan.retrieve_data(t)
Harald Weltec91085e2022-02-10 18:05:45 +0100640 (tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
641 self._cmd.poutput("set_data 0x%02x %s" % (t, b2h(val)))
642 else:
643 raise RuntimeError(
644 'Unsupported structure "%s" of file "%s"' % (structure, filename))
645 except Exception as e:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200646 bad_file_str = df_path + "/" + str(filename) + ", " + str(e)
Harald Weltec91085e2022-02-10 18:05:45 +0100647 self._cmd.poutput("# bad file: %s" % bad_file_str)
648 context['ERR'] += 1
649 context['BAD'].append(bad_file_str)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100650
Harald Weltec91085e2022-02-10 18:05:45 +0100651 # When reading the file is done, make sure the parent file is
652 # selected again. This will be the usual case, however we need
653 # to check before since we must not select the same DF twice
Harald Weltea6c0f882022-07-17 14:23:17 +0200654 if df != self._cmd.lchan.selected_file:
655 self._cmd.lchan.select(df.fid or df.aid, self._cmd)
Philipp Maierac34dcc2021-04-01 17:19:05 +0200656
Harald Weltec91085e2022-02-10 18:05:45 +0100657 self._cmd.poutput("#")
Philipp Maier24f7bd32021-02-25 17:06:18 +0100658
Harald Weltec91085e2022-02-10 18:05:45 +0100659 export_parser = argparse.ArgumentParser()
660 export_parser.add_argument(
661 '--filename', type=str, default=None, help='only export specific file')
Harald Welte08b11ab2022-02-10 18:56:41 +0100662 export_parser.add_argument(
663 '--json', action='store_true', help='export as JSON (less reliable)')
Philipp Maier24f7bd32021-02-25 17:06:18 +0100664
Harald Weltec91085e2022-02-10 18:05:45 +0100665 @cmd2.with_argparser(export_parser)
666 def do_export(self, opts):
667 """Export files to script that can be imported back later"""
668 context = {'ERR': 0, 'COUNT': 0, 'BAD': [],
669 'DF_SKIP': 0, 'DF_SKIP_REASON': []}
Philipp Maier9a4091d2022-05-19 10:20:30 +0200670 kwargs_export = {'as_json': opts.json}
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200671 exception_str_add = ""
672
Harald Weltec91085e2022-02-10 18:05:45 +0100673 if opts.filename:
Philipp Maier7b138b02022-05-31 13:42:56 +0200674 self.export_ef(opts.filename, context, **kwargs_export)
Harald Weltec91085e2022-02-10 18:05:45 +0100675 else:
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200676 try:
677 self.walk(0, self.export_ef, None, context, **kwargs_export)
678 except Exception as e:
679 print("# Stopping early here due to exception: " + str(e))
680 print("#")
681 exception_str_add = ", also had to stop early due to exception:" + str(e)
Philipp Maier80ce71f2021-04-19 21:24:23 +0200682
Harald Weltec91085e2022-02-10 18:05:45 +0100683 self._cmd.poutput(boxed_heading_str("Export summary"))
Philipp Maier80ce71f2021-04-19 21:24:23 +0200684
Harald Weltec91085e2022-02-10 18:05:45 +0100685 self._cmd.poutput("# total files visited: %u" % context['COUNT'])
686 self._cmd.poutput("# bad files: %u" % context['ERR'])
687 for b in context['BAD']:
688 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200689
Harald Weltec91085e2022-02-10 18:05:45 +0100690 self._cmd.poutput("# skipped dedicated files(s): %u" %
691 context['DF_SKIP'])
692 for b in context['DF_SKIP_REASON']:
693 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200694
Harald Weltec91085e2022-02-10 18:05:45 +0100695 if context['ERR'] and context['DF_SKIP']:
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200696 raise RuntimeError("unable to export %i elementary file(s) and %i dedicated file(s)%s" % (
697 context['ERR'], context['DF_SKIP'], exception_str_add))
Harald Weltec91085e2022-02-10 18:05:45 +0100698 elif context['ERR']:
699 raise RuntimeError(
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200700 "unable to export %i elementary file(s)%s" % (context['ERR'], exception_str_add))
Harald Weltec91085e2022-02-10 18:05:45 +0100701 elif context['DF_SKIP']:
702 raise RuntimeError(
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200703 "unable to export %i dedicated files(s)%s" % (context['ERR'], exception_str_add))
Harald Welteb2edd142021-01-08 23:29:35 +0100704
Harald Weltec91085e2022-02-10 18:05:45 +0100705 def do_reset(self, opts):
706 """Reset the Card."""
Philipp Maiere345e112023-06-09 15:19:56 +0200707 atr = self._cmd.card.reset()
708 self._cmd.poutput('Card ATR: %s' % i2h(atr))
Harald Weltec91085e2022-02-10 18:05:45 +0100709 self._cmd.update_prompt()
Harald Weltedaf2b392021-05-03 23:17:29 +0200710
Harald Weltec91085e2022-02-10 18:05:45 +0100711 def do_desc(self, opts):
712 """Display human readable file description for the currently selected file"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200713 desc = self._cmd.lchan.selected_file.desc
Harald Weltec91085e2022-02-10 18:05:45 +0100714 if desc:
715 self._cmd.poutput(desc)
716 else:
717 self._cmd.poutput("no description available")
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200718
Harald Welte469db932023-11-01 23:17:06 +0100719 verify_adm_parser = argparse.ArgumentParser()
Harald Weltef9ea63e2023-11-01 23:35:31 +0100720 verify_adm_parser.add_argument('ADM1', nargs='?', type=is_hexstr_or_decimal,
Harald Welte469db932023-11-01 23:17:06 +0100721 help='ADM1 pin value. If none given, CSV file will be queried')
722
723 @cmd2.with_argparser(verify_adm_parser)
724 def do_verify_adm(self, opts):
725 """Verify the ADM (Administrator) PIN specified as argument. This is typically needed in order
726to get write/update permissions to most of the files on SIM cards.
727
728Currently only ADM1 is supported."""
729 if opts.ADM1:
Harald Weltec91085e2022-02-10 18:05:45 +0100730 # use specified ADM-PIN
Harald Welte469db932023-11-01 23:17:06 +0100731 pin_adm = sanitize_pin_adm(opts.ADM1)
Harald Weltec91085e2022-02-10 18:05:45 +0100732 else:
733 # try to find an ADM-PIN if none is specified
734 result = card_key_provider_get_field(
735 'ADM1', key='ICCID', value=self._cmd.iccid)
736 pin_adm = sanitize_pin_adm(result)
737 if pin_adm:
738 self._cmd.poutput(
739 "found ADM-PIN '%s' for ICCID '%s'" % (result, self._cmd.iccid))
740 else:
741 raise ValueError(
742 "cannot find ADM-PIN for ICCID '%s'" % (self._cmd.iccid))
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200743
Harald Weltec91085e2022-02-10 18:05:45 +0100744 if pin_adm:
Harald Welte46255122023-10-21 23:40:42 +0200745 self._cmd.lchan.scc.verify_chv(self._cmd.card._adm_chv_num, h2b(pin_adm))
Harald Weltec91085e2022-02-10 18:05:45 +0100746 else:
747 raise ValueError("error: cannot authenticate, no adm-pin!")
748
Philipp Maier7b9e2442023-03-22 15:19:54 +0100749 def do_cardinfo(self, opts):
750 """Display information about the currently inserted card"""
751 self._cmd.poutput("Card info:")
752 self._cmd.poutput(" Name: %s" % self._cmd.card.name)
Harald Welte46255122023-10-21 23:40:42 +0200753 self._cmd.poutput(" ATR: %s" % b2h(self._cmd.lchan.scc.get_atr()))
Philipp Maier7b9e2442023-03-22 15:19:54 +0100754 self._cmd.poutput(" ICCID: %s" % self._cmd.iccid)
Harald Welte46255122023-10-21 23:40:42 +0200755 self._cmd.poutput(" Class-Byte: %s" % self._cmd.lchan.scc.cla_byte)
756 self._cmd.poutput(" Select-Ctrl: %s" % self._cmd.lchan.scc.sel_ctrl)
Philipp Maier7b9e2442023-03-22 15:19:54 +0100757 self._cmd.poutput(" AIDs:")
758 for a in self._cmd.rs.mf.applications:
759 self._cmd.poutput(" %s" % a)
Harald Welteb2edd142021-01-08 23:29:35 +0100760
Harald Welte31d2cf02021-04-03 10:47:29 +0200761@with_default_category('ISO7816 Commands')
762class Iso7816Commands(CommandSet):
Harald Weltec91085e2022-02-10 18:05:45 +0100763 def __init__(self):
764 super().__init__()
Harald Welte31d2cf02021-04-03 10:47:29 +0200765
Harald Weltec91085e2022-02-10 18:05:45 +0100766 def do_select(self, opts):
767 """SELECT a File (ADF/DF/EF)"""
768 if len(opts.arg_list) == 0:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200769 path = self._cmd.lchan.selected_file.fully_qualified_path_str(True)
770 path_fid = self._cmd.lchan.selected_file.fully_qualified_path_str(False)
771 self._cmd.poutput("currently selected file: %s (%s)" % (path, path_fid))
Harald Weltec91085e2022-02-10 18:05:45 +0100772 return
Harald Welte31d2cf02021-04-03 10:47:29 +0200773
Harald Weltec91085e2022-02-10 18:05:45 +0100774 path = opts.arg_list[0]
Harald Weltea6c0f882022-07-17 14:23:17 +0200775 fcp_dec = self._cmd.lchan.select(path, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100776 self._cmd.update_prompt()
777 self._cmd.poutput_json(fcp_dec)
Harald Welte31d2cf02021-04-03 10:47:29 +0200778
Harald Weltec91085e2022-02-10 18:05:45 +0100779 def complete_select(self, text, line, begidx, endidx) -> List[str]:
780 """Command Line tab completion for SELECT"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200781 index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
Harald Weltec91085e2022-02-10 18:05:45 +0100782 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200783
Harald Weltec91085e2022-02-10 18:05:45 +0100784 def get_code(self, code):
785 """Use code either directly or try to get it from external data source"""
786 auto = ('PIN1', 'PIN2', 'PUK1', 'PUK2')
Harald Welte31d2cf02021-04-03 10:47:29 +0200787
Harald Weltec91085e2022-02-10 18:05:45 +0100788 if str(code).upper() not in auto:
789 return sanitize_pin_adm(code)
Harald Welte31d2cf02021-04-03 10:47:29 +0200790
Harald Weltec91085e2022-02-10 18:05:45 +0100791 result = card_key_provider_get_field(
792 str(code), key='ICCID', value=self._cmd.iccid)
793 result = sanitize_pin_adm(result)
794 if result:
795 self._cmd.poutput("found %s '%s' for ICCID '%s'" %
796 (code.upper(), result, self._cmd.iccid))
797 else:
798 self._cmd.poutput("cannot find %s for ICCID '%s'" %
799 (code.upper(), self._cmd.iccid))
800 return result
Harald Welte31d2cf02021-04-03 10:47:29 +0200801
Harald Weltec91085e2022-02-10 18:05:45 +0100802 verify_chv_parser = argparse.ArgumentParser()
803 verify_chv_parser.add_argument(
804 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
805 verify_chv_parser.add_argument(
Harald Welte1c849f82023-11-01 23:48:28 +0100806 'pin_code', type=is_decimal, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
Harald Welte31d2cf02021-04-03 10:47:29 +0200807
Harald Weltec91085e2022-02-10 18:05:45 +0100808 @cmd2.with_argparser(verify_chv_parser)
809 def do_verify_chv(self, opts):
Harald Welte12af7932022-02-15 16:39:08 +0100810 """Verify (authenticate) using specified CHV (PIN) code, which is how the specifications
811 call it if you authenticate yourself using the specified PIN. There usually is at least PIN1 and
812 PIN2."""
Harald Weltec91085e2022-02-10 18:05:45 +0100813 pin = self.get_code(opts.pin_code)
Harald Welte46255122023-10-21 23:40:42 +0200814 (data, sw) = self._cmd.lchan.scc.verify_chv(opts.pin_nr, h2b(pin))
Harald Weltec91085e2022-02-10 18:05:45 +0100815 self._cmd.poutput("CHV verification successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200816
Harald Weltec91085e2022-02-10 18:05:45 +0100817 unblock_chv_parser = argparse.ArgumentParser()
818 unblock_chv_parser.add_argument(
819 '--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
820 unblock_chv_parser.add_argument(
Harald Welte1c849f82023-11-01 23:48:28 +0100821 'puk_code', type=is_decimal, help='PUK code digits \"PUK1\" or \"PUK2\" to get PUK code from external data source')
Harald Weltec91085e2022-02-10 18:05:45 +0100822 unblock_chv_parser.add_argument(
Harald Welte1c849f82023-11-01 23:48:28 +0100823 'new_pin_code', type=is_decimal, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
Harald Welte31d2cf02021-04-03 10:47:29 +0200824
Harald Weltec91085e2022-02-10 18:05:45 +0100825 @cmd2.with_argparser(unblock_chv_parser)
826 def do_unblock_chv(self, opts):
827 """Unblock PIN code using specified PUK code"""
828 new_pin = self.get_code(opts.new_pin_code)
829 puk = self.get_code(opts.puk_code)
Harald Welte46255122023-10-21 23:40:42 +0200830 (data, sw) = self._cmd.lchan.scc.unblock_chv(
Harald Weltec91085e2022-02-10 18:05:45 +0100831 opts.pin_nr, h2b(puk), h2b(new_pin))
832 self._cmd.poutput("CHV unblock successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200833
Harald Weltec91085e2022-02-10 18:05:45 +0100834 change_chv_parser = argparse.ArgumentParser()
835 change_chv_parser.add_argument(
836 '--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
837 change_chv_parser.add_argument(
Harald Welte1c849f82023-11-01 23:48:28 +0100838 'pin_code', type=is_decimal, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
Harald Weltec91085e2022-02-10 18:05:45 +0100839 change_chv_parser.add_argument(
Harald Welte1c849f82023-11-01 23:48:28 +0100840 'new_pin_code', type=is_decimal, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
Harald Welte31d2cf02021-04-03 10:47:29 +0200841
Harald Weltec91085e2022-02-10 18:05:45 +0100842 @cmd2.with_argparser(change_chv_parser)
843 def do_change_chv(self, opts):
844 """Change PIN code to a new PIN code"""
845 new_pin = self.get_code(opts.new_pin_code)
846 pin = self.get_code(opts.pin_code)
Harald Welte46255122023-10-21 23:40:42 +0200847 (data, sw) = self._cmd.lchan.scc.change_chv(
Harald Weltec91085e2022-02-10 18:05:45 +0100848 opts.pin_nr, h2b(pin), h2b(new_pin))
849 self._cmd.poutput("CHV change successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200850
Harald Weltec91085e2022-02-10 18:05:45 +0100851 disable_chv_parser = argparse.ArgumentParser()
852 disable_chv_parser.add_argument(
853 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
854 disable_chv_parser.add_argument(
Harald Welte1c849f82023-11-01 23:48:28 +0100855 'pin_code', type=is_decimal, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
Harald Welte31d2cf02021-04-03 10:47:29 +0200856
Harald Weltec91085e2022-02-10 18:05:45 +0100857 @cmd2.with_argparser(disable_chv_parser)
858 def do_disable_chv(self, opts):
859 """Disable PIN code using specified PIN code"""
860 pin = self.get_code(opts.pin_code)
Harald Welte46255122023-10-21 23:40:42 +0200861 (data, sw) = self._cmd.lchan.scc.disable_chv(opts.pin_nr, h2b(pin))
Harald Weltec91085e2022-02-10 18:05:45 +0100862 self._cmd.poutput("CHV disable successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200863
Harald Weltec91085e2022-02-10 18:05:45 +0100864 enable_chv_parser = argparse.ArgumentParser()
865 enable_chv_parser.add_argument(
866 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
867 enable_chv_parser.add_argument(
Harald Welte1c849f82023-11-01 23:48:28 +0100868 'pin_code', type=is_decimal, 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(enable_chv_parser)
871 def do_enable_chv(self, opts):
872 """Enable PIN code using specified PIN code"""
873 pin = self.get_code(opts.pin_code)
Harald Welte46255122023-10-21 23:40:42 +0200874 (data, sw) = self._cmd.lchan.scc.enable_chv(opts.pin_nr, h2b(pin))
Harald Weltec91085e2022-02-10 18:05:45 +0100875 self._cmd.poutput("CHV enable successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200876
Harald Weltec91085e2022-02-10 18:05:45 +0100877 def do_deactivate_file(self, opts):
Harald Welte799c3542022-02-15 15:56:28 +0100878 """Deactivate the currently selected EF"""
Harald Welte46255122023-10-21 23:40:42 +0200879 (data, sw) = self._cmd.lchan.scc.deactivate_file()
Harald Weltea4631612021-04-10 18:17:55 +0200880
Harald Welte799c3542022-02-15 15:56:28 +0100881 activate_file_parser = argparse.ArgumentParser()
882 activate_file_parser.add_argument('NAME', type=str, help='File name or FID of file to activate')
883 @cmd2.with_argparser(activate_file_parser)
Harald Weltec91085e2022-02-10 18:05:45 +0100884 def do_activate_file(self, opts):
Harald Welte12af7932022-02-15 16:39:08 +0100885 """Activate the specified EF. This used to be called REHABILITATE in TS 11.11 for classic
886 SIM. You need to specify the name or FID of the file to activate."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200887 (data, sw) = self._cmd.lchan.activate_file(opts.NAME)
Harald Welte485692b2021-05-25 22:21:44 +0200888
Harald Weltec91085e2022-02-10 18:05:45 +0100889 def complete_activate_file(self, text, line, begidx, endidx) -> List[str]:
890 """Command Line tab completion for ACTIVATE FILE"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200891 index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
Harald Weltec91085e2022-02-10 18:05:45 +0100892 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200893
Harald Weltec91085e2022-02-10 18:05:45 +0100894 open_chan_parser = argparse.ArgumentParser()
895 open_chan_parser.add_argument(
896 'chan_nr', type=int, default=0, help='Channel Number')
Harald Welte703f9332021-04-10 18:39:32 +0200897
Harald Weltec91085e2022-02-10 18:05:45 +0100898 @cmd2.with_argparser(open_chan_parser)
899 def do_open_channel(self, opts):
900 """Open a logical channel."""
Harald Welte46255122023-10-21 23:40:42 +0200901 (data, sw) = self._cmd.lchan.scc.manage_channel(
Harald Weltec91085e2022-02-10 18:05:45 +0100902 mode='open', lchan_nr=opts.chan_nr)
Harald Weltebdf59572023-10-21 20:06:19 +0200903 # this is executed only in successful case, as unsuccessful raises exception
904 self._cmd.lchan.add_lchan(opts.chan_nr)
Harald Welte703f9332021-04-10 18:39:32 +0200905
Harald Weltec91085e2022-02-10 18:05:45 +0100906 close_chan_parser = argparse.ArgumentParser()
907 close_chan_parser.add_argument(
908 'chan_nr', type=int, default=0, help='Channel Number')
Harald Welte703f9332021-04-10 18:39:32 +0200909
Harald Weltec91085e2022-02-10 18:05:45 +0100910 @cmd2.with_argparser(close_chan_parser)
911 def do_close_channel(self, opts):
912 """Close a logical channel."""
Harald Welte46255122023-10-21 23:40:42 +0200913 (data, sw) = self._cmd.lchan.scc.manage_channel(
Harald Weltec91085e2022-02-10 18:05:45 +0100914 mode='close', lchan_nr=opts.chan_nr)
Harald Weltebdf59572023-10-21 20:06:19 +0200915 # this is executed only in successful case, as unsuccessful raises exception
916 self._cmd.rs.del_lchan(opts.chan_nr)
Harald Welte703f9332021-04-10 18:39:32 +0200917
Harald Welte20650992023-10-21 23:47:02 +0200918 switch_chan_parser = argparse.ArgumentParser()
919 switch_chan_parser.add_argument(
920 'chan_nr', type=int, default=0, help='Channel Number')
921
922 @cmd2.with_argparser(switch_chan_parser)
923 def do_switch_channel(self, opts):
924 """Switch currently active logical channel."""
925 self._cmd.lchan._select_pre(self._cmd)
926 self._cmd.lchan = self._cmd.rs.lchan[opts.chan_nr]
927 self._cmd.lchan._select_post(self._cmd)
928 self._cmd.update_prompt()
929
Harald Weltec91085e2022-02-10 18:05:45 +0100930 def do_status(self, opts):
931 """Perform the STATUS command."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200932 fcp_dec = self._cmd.lchan.status()
Harald Weltec91085e2022-02-10 18:05:45 +0100933 self._cmd.poutput_json(fcp_dec)
Harald Welte34b05d32021-05-25 22:03:13 +0200934
Harald Welteec950532021-10-20 13:09:00 +0200935
Harald Weltecab26c72022-08-06 16:12:30 +0200936class Proact(ProactiveHandler):
937 def receive_fetch(self, pcmd: ProactiveCommand):
938 # print its parsed representation
939 print(pcmd.decoded)
940 # TODO: implement the basics, such as SMS Sending, ...
941
942
Harald Welte703f9332021-04-10 18:39:32 +0200943
Harald Weltef422eb12023-06-09 11:15:09 +0200944option_parser = argparse.ArgumentParser(description='interactive SIM card shell',
Harald Weltef2e761c2021-04-11 11:56:44 +0200945 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
Harald Welte28c24312021-04-11 12:19:36 +0200946argparse_add_reader_args(option_parser)
Harald Weltec8ff0262021-04-11 12:06:13 +0200947
948global_group = option_parser.add_argument_group('General Options')
949global_group.add_argument('--script', metavar='PATH', default=None,
Harald Welte28c24312021-04-11 12:19:36 +0200950 help='script with pySim-shell commands to be executed automatically at start-up')
Harald Weltec91085e2022-02-10 18:05:45 +0100951global_group.add_argument('--csv', metavar='FILE',
952 default=None, help='Read card data from CSV file')
Philipp Maier76667642021-09-22 16:53:22 +0200953global_group.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
Harald Weltec91085e2022-02-10 18:05:45 +0100954 help="Use automatic card handling machine")
Harald Weltec8ff0262021-04-11 12:06:13 +0200955
956adm_group = global_group.add_mutually_exclusive_group()
957adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', default=None,
958 help='ADM PIN used for provisioning (overwrites default)')
959adm_group.add_argument('-A', '--pin-adm-hex', metavar='PIN_ADM1_HEX', dest='pin_adm_hex', default=None,
960 help='ADM PIN used for provisioning, as hex string (16 characters long)')
Harald Welteb2edd142021-01-08 23:29:35 +0100961
Harald Welte38306df2023-07-11 21:17:55 +0200962option_parser.add_argument("command", nargs='?',
963 help="A pySim-shell command that would optionally be executed at startup")
964option_parser.add_argument('command_args', nargs=argparse.REMAINDER,
965 help="Optional Arguments for command")
966
Harald Welteb2edd142021-01-08 23:29:35 +0100967
968if __name__ == '__main__':
969
Harald Weltec91085e2022-02-10 18:05:45 +0100970 # Parse options
971 opts = option_parser.parse_args()
Harald Welteb2edd142021-01-08 23:29:35 +0100972
Harald Weltec91085e2022-02-10 18:05:45 +0100973 # If a script file is specified, be sure that it actually exists
974 if opts.script:
975 if not os.access(opts.script, os.R_OK):
976 print("Invalid script file!")
977 sys.exit(2)
Philipp Maier13e258d2021-04-08 17:48:49 +0200978
Harald Weltec91085e2022-02-10 18:05:45 +0100979 # Register csv-file as card data provider, either from specified CSV
980 # or from CSV file in home directory
981 csv_default = str(Path.home()) + "/.osmocom/pysim/card_data.csv"
982 if opts.csv:
983 card_key_provider_register(CardKeyProviderCsv(opts.csv))
984 if os.path.isfile(csv_default):
985 card_key_provider_register(CardKeyProviderCsv(csv_default))
Philipp Maier2b11c322021-03-17 12:37:39 +0100986
Harald Weltec91085e2022-02-10 18:05:45 +0100987 # Init card reader driver
Harald Weltecab26c72022-08-06 16:12:30 +0200988 sl = init_reader(opts, proactive_handler = Proact())
Philipp Maierea95c392021-09-16 13:10:19 +0200989
Harald Weltec91085e2022-02-10 18:05:45 +0100990 # Create a card handler (for bulk provisioning)
991 if opts.card_handler_config:
992 ch = CardHandlerAuto(None, opts.card_handler_config)
993 else:
994 ch = CardHandler(sl)
Philipp Maier76667642021-09-22 16:53:22 +0200995
Harald Weltec91085e2022-02-10 18:05:45 +0100996 # Detect and initialize the card in the reader. This may fail when there
997 # is no card in the reader or the card is unresponsive. PysimApp is
998 # able to tolerate and recover from that.
999 try:
1000 rs, card = init_card(sl)
1001 app = PysimApp(card, rs, sl, ch, opts.script)
1002 except:
Philipp Maier6bfa8a82023-10-09 13:32:49 +02001003 print("Card initialization (%s) failed with an exception:" % str(sl))
Harald Weltec91085e2022-02-10 18:05:45 +01001004 print("---------------------8<---------------------")
1005 traceback.print_exc()
1006 print("---------------------8<---------------------")
1007 print("(you may still try to recover from this manually by using the 'equip' command.)")
1008 print(
1009 " it should also be noted that some readers may behave strangely when no card")
1010 print(" is inserted.)")
1011 print("")
Philipp Maiereb3b0dd2023-11-23 11:46:39 +01001012 if opts.script:
1013 print("will not execute startup script due to card initialization errors!")
1014 app = PysimApp(None, None, sl, ch)
Philipp Maierea95c392021-09-16 13:10:19 +02001015
Harald Weltec91085e2022-02-10 18:05:45 +01001016 # If the user supplies an ADM PIN at via commandline args authenticate
1017 # immediately so that the user does not have to use the shell commands
1018 pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
1019 if pin_adm:
1020 if not card:
1021 print("Card error, cannot do ADM verification with supplied ADM pin now.")
1022 try:
Philipp Maier4840d4d2023-09-06 14:54:14 +02001023 card._scc.verify_chv(card._adm_chv_num, h2b(pin_adm))
Harald Weltec91085e2022-02-10 18:05:45 +01001024 except Exception as e:
1025 print(e)
Philipp Maier228c98e2021-03-10 20:14:06 +01001026
Harald Welte38306df2023-07-11 21:17:55 +02001027 if opts.command:
1028 app.onecmd_plus_hooks('{} {}'.format(opts.command, ' '.join(opts.command_args)))
1029 else:
1030 app.cmdloop()