blob: 70eaee233f4d989b948efa533b2917cf055248e7 [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).
Harald Welteeecef542024-01-31 19:32:25 +0100248 data, sw = self.card._scc.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 Welte66b33702024-01-16 18:59:43 +0100257 @cmd2.with_category(CUSTOM_CATEGORY)
258 def do_reset(self, opts):
259 """Reset the Card."""
260 atr = self.card.reset()
261 self.poutput('Card ATR: %s' % i2h(atr))
262 self.update_prompt()
263
Harald Weltec91085e2022-02-10 18:05:45 +0100264 class InterceptStderr(list):
265 def __init__(self):
266 self._stderr_backup = sys.stderr
Philipp Maier76667642021-09-22 16:53:22 +0200267
Harald Weltec91085e2022-02-10 18:05:45 +0100268 def __enter__(self):
269 self._stringio_stderr = StringIO()
270 sys.stderr = self._stringio_stderr
271 return self
Philipp Maier76667642021-09-22 16:53:22 +0200272
Harald Weltec91085e2022-02-10 18:05:45 +0100273 def __exit__(self, *args):
274 self.stderr = self._stringio_stderr.getvalue().strip()
275 del self._stringio_stderr
276 sys.stderr = self._stderr_backup
Philipp Maier76667642021-09-22 16:53:22 +0200277
Harald Weltec91085e2022-02-10 18:05:45 +0100278 def _show_failure_sign(self):
Harald Welte961b8032023-04-27 17:30:22 +0200279 self.poutput(style(" +-------------+", fg=LIGHT_RED))
280 self.poutput(style(" + ## ## +", fg=LIGHT_RED))
281 self.poutput(style(" + ## ## +", fg=LIGHT_RED))
282 self.poutput(style(" + ### +", fg=LIGHT_RED))
283 self.poutput(style(" + ## ## +", fg=LIGHT_RED))
284 self.poutput(style(" + ## ## +", fg=LIGHT_RED))
285 self.poutput(style(" +-------------+", fg=LIGHT_RED))
Harald Weltec91085e2022-02-10 18:05:45 +0100286 self.poutput("")
Philipp Maier76667642021-09-22 16:53:22 +0200287
Harald Weltec91085e2022-02-10 18:05:45 +0100288 def _show_success_sign(self):
Harald Welte961b8032023-04-27 17:30:22 +0200289 self.poutput(style(" +-------------+", fg=LIGHT_GREEN))
290 self.poutput(style(" + ## +", fg=LIGHT_GREEN))
291 self.poutput(style(" + ## +", fg=LIGHT_GREEN))
292 self.poutput(style(" + # ## +", fg=LIGHT_GREEN))
293 self.poutput(style(" + ## # +", fg=LIGHT_GREEN))
294 self.poutput(style(" + ## +", fg=LIGHT_GREEN))
295 self.poutput(style(" +-------------+", fg=LIGHT_GREEN))
Harald Weltec91085e2022-02-10 18:05:45 +0100296 self.poutput("")
Philipp Maier76667642021-09-22 16:53:22 +0200297
Harald Weltec91085e2022-02-10 18:05:45 +0100298 def _process_card(self, first, script_path):
Philipp Maier76667642021-09-22 16:53:22 +0200299
Harald Weltec91085e2022-02-10 18:05:45 +0100300 # Early phase of card initialzation (this part may fail with an exception)
301 try:
302 rs, card = init_card(self.sl)
303 rc = self.equip(card, rs)
304 except:
305 self.poutput("")
Philipp Maier6bfa8a82023-10-09 13:32:49 +0200306 self.poutput("Card initialization (%s) failed with an exception:" % str(self.sl))
Harald Weltec91085e2022-02-10 18:05:45 +0100307 self.poutput("---------------------8<---------------------")
308 traceback.print_exc()
309 self.poutput("---------------------8<---------------------")
310 self.poutput("")
311 return -1
Philipp Maier76667642021-09-22 16:53:22 +0200312
Harald Weltec91085e2022-02-10 18:05:45 +0100313 # Actual card processing step. This part should never fail with an exception since the cmd2
314 # do_run_script method will catch any exception that might occur during script execution.
315 if rc:
316 self.poutput("")
317 self.poutput("Transcript stdout:")
318 self.poutput("---------------------8<---------------------")
319 with self.InterceptStderr() as logged:
320 self.do_run_script(script_path)
321 self.poutput("---------------------8<---------------------")
Philipp Maier76667642021-09-22 16:53:22 +0200322
Harald Weltec91085e2022-02-10 18:05:45 +0100323 self.poutput("")
324 self.poutput("Transcript stderr:")
325 if logged.stderr:
326 self.poutput("---------------------8<---------------------")
327 self.poutput(logged.stderr)
328 self.poutput("---------------------8<---------------------")
329 else:
330 self.poutput("(none)")
Philipp Maier76667642021-09-22 16:53:22 +0200331
Harald Weltec91085e2022-02-10 18:05:45 +0100332 # Check for exceptions
333 self.poutput("")
334 if "EXCEPTION of type" not in logged.stderr:
335 return 0
Philipp Maier76667642021-09-22 16:53:22 +0200336
Harald Weltec91085e2022-02-10 18:05:45 +0100337 return -1
Philipp Maier76667642021-09-22 16:53:22 +0200338
Harald Weltec91085e2022-02-10 18:05:45 +0100339 bulk_script_parser = argparse.ArgumentParser()
340 bulk_script_parser.add_argument(
341 'script_path', help="path to the script file")
342 bulk_script_parser.add_argument('--halt_on_error', help='stop card handling if an exeption occurs',
343 action='store_true')
344 bulk_script_parser.add_argument('--tries', type=int, default=2,
345 help='how many tries before trying the next card')
346 bulk_script_parser.add_argument('--on_stop_action', type=str, default=None,
347 help='commandline to execute when card handling has stopped')
348 bulk_script_parser.add_argument('--pre_card_action', type=str, default=None,
349 help='commandline to execute before actually talking to the card')
Philipp Maier76667642021-09-22 16:53:22 +0200350
Harald Weltec91085e2022-02-10 18:05:45 +0100351 @cmd2.with_argparser(bulk_script_parser)
352 @cmd2.with_category(CUSTOM_CATEGORY)
353 def do_bulk_script(self, opts):
354 """Run script on multiple cards (bulk provisioning)"""
Philipp Maier76667642021-09-22 16:53:22 +0200355
Harald Weltec91085e2022-02-10 18:05:45 +0100356 # Make sure that the script file exists and that it is readable.
357 if not os.access(opts.script_path, os.R_OK):
358 self.poutput("Invalid script file!")
359 return
Philipp Maier76667642021-09-22 16:53:22 +0200360
Harald Weltec91085e2022-02-10 18:05:45 +0100361 success_count = 0
362 fail_count = 0
Philipp Maier76667642021-09-22 16:53:22 +0200363
Harald Weltec91085e2022-02-10 18:05:45 +0100364 first = True
365 while 1:
366 # TODO: Count consecutive failures, if more than N consecutive failures occur, then stop.
367 # The ratinale is: There may be a problem with the device, we do want to prevent that
368 # all remaining cards are fired to the error bin. This is only relevant for situations
369 # with large stacks, probably we do not need this feature right now.
Philipp Maier76667642021-09-22 16:53:22 +0200370
Harald Weltec91085e2022-02-10 18:05:45 +0100371 try:
372 # In case of failure, try multiple times.
373 for i in range(opts.tries):
374 # fetch card into reader bay
Philipp Maier91c971b2023-10-09 10:48:46 +0200375 self.ch.get(first)
Philipp Maier76667642021-09-22 16:53:22 +0200376
Harald Weltec91085e2022-02-10 18:05:45 +0100377 # if necessary execute an action before we start processing the card
378 if(opts.pre_card_action):
379 os.system(opts.pre_card_action)
Philipp Maier76667642021-09-22 16:53:22 +0200380
Harald Weltec91085e2022-02-10 18:05:45 +0100381 # process the card
382 rc = self._process_card(first, opts.script_path)
383 if rc == 0:
384 success_count = success_count + 1
385 self._show_success_sign()
386 self.poutput("Statistics: success :%i, failure: %i" % (
387 success_count, fail_count))
388 break
389 else:
390 fail_count = fail_count + 1
391 self._show_failure_sign()
392 self.poutput("Statistics: success :%i, failure: %i" % (
393 success_count, fail_count))
Philipp Maier76667642021-09-22 16:53:22 +0200394
Harald Weltec91085e2022-02-10 18:05:45 +0100395 # Depending on success or failure, the card goes either in the "error" bin or in the
396 # "done" bin.
397 if rc < 0:
Philipp Maier91c971b2023-10-09 10:48:46 +0200398 self.ch.error()
Harald Weltec91085e2022-02-10 18:05:45 +0100399 else:
Philipp Maier91c971b2023-10-09 10:48:46 +0200400 self.ch.done()
Philipp Maier76667642021-09-22 16:53:22 +0200401
Harald Weltec91085e2022-02-10 18:05:45 +0100402 # In most cases it is possible to proceed with the next card, but the
403 # user may decide to halt immediately when an error occurs
404 if opts.halt_on_error and rc < 0:
405 return
Philipp Maier76667642021-09-22 16:53:22 +0200406
Harald Weltec91085e2022-02-10 18:05:45 +0100407 except (KeyboardInterrupt):
408 self.poutput("")
409 self.poutput("Terminated by user!")
410 return
411 except (SystemExit):
412 # When all cards are processed the card handler device will throw a SystemExit
413 # exception. Also Errors that are not recoverable (cards stuck etc.) will end up here.
414 # The user has the option to execute some action to make aware that the card handler
415 # needs service.
416 if(opts.on_stop_action):
417 os.system(opts.on_stop_action)
418 return
419 except:
420 self.poutput("")
Philipp Maier6bfa8a82023-10-09 13:32:49 +0200421 self.poutput("Card handling (%s) failed with an exception:" % str(self.sl))
Harald Weltec91085e2022-02-10 18:05:45 +0100422 self.poutput("---------------------8<---------------------")
423 traceback.print_exc()
424 self.poutput("---------------------8<---------------------")
425 self.poutput("")
426 fail_count = fail_count + 1
427 self._show_failure_sign()
428 self.poutput("Statistics: success :%i, failure: %i" %
429 (success_count, fail_count))
Philipp Maierb52feed2021-09-22 16:43:13 +0200430
Harald Weltec91085e2022-02-10 18:05:45 +0100431 first = False
432
433 echo_parser = argparse.ArgumentParser()
Harald Welte977c5922023-11-01 23:42:55 +0100434 echo_parser.add_argument('string', help="string to echo on the shell", nargs='+')
Harald Weltec91085e2022-02-10 18:05:45 +0100435
436 @cmd2.with_argparser(echo_parser)
437 @cmd2.with_category(CUSTOM_CATEGORY)
438 def do_echo(self, opts):
439 """Echo (print) a string on the console"""
Harald Welte977c5922023-11-01 23:42:55 +0100440 self.poutput(' '.join(opts.string))
Harald Weltec91085e2022-02-10 18:05:45 +0100441
Harald Weltefc315482022-07-23 12:49:14 +0200442 @cmd2.with_category(CUSTOM_CATEGORY)
443 def do_version(self, opts):
444 """Print the pySim software version."""
445 import pkg_resources
446 self.poutput(pkg_resources.get_distribution('pySim'))
Harald Welteb2edd142021-01-08 23:29:35 +0100447
Harald Welte31d2cf02021-04-03 10:47:29 +0200448@with_default_category('pySim Commands')
449class PySimCommands(CommandSet):
Harald Weltec91085e2022-02-10 18:05:45 +0100450 def __init__(self):
451 super().__init__()
Harald Welteb2edd142021-01-08 23:29:35 +0100452
Harald Weltec91085e2022-02-10 18:05:45 +0100453 dir_parser = argparse.ArgumentParser()
454 dir_parser.add_argument(
455 '--fids', help='Show file identifiers', action='store_true')
456 dir_parser.add_argument(
457 '--names', help='Show file names', action='store_true')
458 dir_parser.add_argument(
459 '--apps', help='Show applications', action='store_true')
460 dir_parser.add_argument(
461 '--all', help='Show all selectable identifiers and names', action='store_true')
Philipp Maier5d3e2592021-02-22 17:22:16 +0100462
Harald Weltec91085e2022-02-10 18:05:45 +0100463 @cmd2.with_argparser(dir_parser)
464 def do_dir(self, opts):
465 """Show a listing of files available in currently selected DF or MF"""
466 if opts.all:
467 flags = []
468 elif opts.fids or opts.names or opts.apps:
469 flags = ['PARENT', 'SELF']
470 if opts.fids:
471 flags += ['FIDS', 'AIDS']
472 if opts.names:
473 flags += ['FNAMES', 'ANAMES']
474 if opts.apps:
475 flags += ['ANAMES', 'AIDS']
476 else:
477 flags = ['PARENT', 'SELF', 'FNAMES', 'ANAMES']
478 selectables = list(
Harald Weltea6c0f882022-07-17 14:23:17 +0200479 self._cmd.lchan.selected_file.get_selectable_names(flags=flags))
Harald Weltec91085e2022-02-10 18:05:45 +0100480 directory_str = tabulate_str_list(
481 selectables, width=79, hspace=2, lspace=1, align_left=True)
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200482 path = self._cmd.lchan.selected_file.fully_qualified_path_str(True)
483 self._cmd.poutput(path)
484 path = self._cmd.lchan.selected_file.fully_qualified_path_str(False)
485 self._cmd.poutput(path)
Harald Weltec91085e2022-02-10 18:05:45 +0100486 self._cmd.poutput(directory_str)
487 self._cmd.poutput("%d files" % len(selectables))
Harald Welteb2edd142021-01-08 23:29:35 +0100488
Philipp Maier7b138b02022-05-31 13:42:56 +0200489 def walk(self, indent=0, action_ef=None, action_df=None, context=None, **kwargs):
Harald Weltec91085e2022-02-10 18:05:45 +0100490 """Recursively walk through the file system, starting at the currently selected DF"""
Philipp Maier7b138b02022-05-31 13:42:56 +0200491
Harald Weltea6c0f882022-07-17 14:23:17 +0200492 if isinstance(self._cmd.lchan.selected_file, CardDF):
Philipp Maier7b138b02022-05-31 13:42:56 +0200493 if action_df:
Philipp Maier91c971b2023-10-09 10:48:46 +0200494 action_df(context, **kwargs)
Philipp Maier7b138b02022-05-31 13:42:56 +0200495
Harald Weltea6c0f882022-07-17 14:23:17 +0200496 files = self._cmd.lchan.selected_file.get_selectables(
Harald Weltec91085e2022-02-10 18:05:45 +0100497 flags=['FNAMES', 'ANAMES'])
498 for f in files:
Philipp Maier7b138b02022-05-31 13:42:56 +0200499 # special case: When no action is performed, just output a directory
500 if not action_ef and not action_df:
Harald Weltec91085e2022-02-10 18:05:45 +0100501 output_str = " " * indent + str(f) + (" " * 250)
502 output_str = output_str[0:25]
503 if isinstance(files[f], CardADF):
504 output_str += " " + str(files[f].aid)
505 else:
506 output_str += " " + str(files[f].fid)
507 output_str += " " + str(files[f].desc)
508 self._cmd.poutput(output_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200509
Harald Weltec91085e2022-02-10 18:05:45 +0100510 if isinstance(files[f], CardDF):
511 skip_df = False
512 try:
Harald Weltea6c0f882022-07-17 14:23:17 +0200513 fcp_dec = self._cmd.lchan.select(f, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100514 except Exception as e:
515 skip_df = True
Harald Weltea6c0f882022-07-17 14:23:17 +0200516 df = self._cmd.lchan.selected_file
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200517 df_path = df.fully_qualified_path_str(True)
518 df_skip_reason_str = df_path + \
Harald Weltec91085e2022-02-10 18:05:45 +0100519 "/" + str(f) + ", " + str(e)
520 if context:
521 context['DF_SKIP'] += 1
522 context['DF_SKIP_REASON'].append(df_skip_reason_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200523
Harald Weltec91085e2022-02-10 18:05:45 +0100524 # If the DF was skipped, we never have entered the directory
525 # below, so we must not move up.
526 if skip_df == False:
Philipp Maier7b138b02022-05-31 13:42:56 +0200527 self.walk(indent + 1, action_ef, action_df, context, **kwargs)
Philipp Maier54827372023-10-24 16:18:30 +0200528
529 parent = self._cmd.lchan.selected_file.parent
530 df = self._cmd.lchan.selected_file
531 adf = self._cmd.lchan.selected_adf
532 if isinstance(parent, CardMF) and (adf and adf.has_fs == False):
533 # Not every application that may be present on a GlobalPlatform card will support the SELECT
534 # command as we know it from ETSI TS 102 221, section 11.1.1. In fact the only subset of
535 # SELECT we may rely on is the OPEN SELECT command as specified in GlobalPlatform Card
536 # Specification, section 11.9. Unfortunately the OPEN SELECT command only supports the
537 # "select by name" method, which means we can only select an application and not a file.
538 # The consequence of this is that we may get trapped in an application that does not have
539 # ISIM/USIM like file system support and the only way to leave that application is to select
540 # an ISIM/USIM application in order to get the file system access back.
541 #
542 # To automate this escape-route while traversing the file system we will check whether
543 # the parent file is the MF. When this is the case and the selected ADF has no file system
544 # support, we will select an arbitrary ADF that has file system support first and from there
545 # we will then select the MF.
546 for selectable in parent.get_selectables().items():
547 if isinstance(selectable[1], CardADF) and selectable[1].has_fs == True:
548 self._cmd.lchan.select(selectable[1].name, self._cmd)
549 break
550 self._cmd.lchan.select(df.get_mf().name, self._cmd)
551 else:
552 # Normal DF/ADF selection
553 fcp_dec = self._cmd.lchan.select("..", self._cmd)
Philipp Maierf408a402021-04-09 21:16:12 +0200554
Philipp Maier7b138b02022-05-31 13:42:56 +0200555 elif action_ef:
Harald Weltea6c0f882022-07-17 14:23:17 +0200556 df_before_action = self._cmd.lchan.selected_file
Philipp Maier7b138b02022-05-31 13:42:56 +0200557 action_ef(f, context, **kwargs)
Harald Weltec91085e2022-02-10 18:05:45 +0100558 # When walking through the file system tree the action must not
559 # always restore the currently selected file to the file that
560 # was selected before executing the action() callback.
Harald Weltea6c0f882022-07-17 14:23:17 +0200561 if df_before_action != self._cmd.lchan.selected_file:
Harald Weltec91085e2022-02-10 18:05:45 +0100562 raise RuntimeError("inconsistent walk, %s is currently selected but expecting %s to be selected"
Harald Weltea6c0f882022-07-17 14:23:17 +0200563 % (str(self._cmd.lchan.selected_file), str(df_before_action)))
Philipp Maierff9dae22021-02-25 17:03:21 +0100564
Harald Weltec91085e2022-02-10 18:05:45 +0100565 def do_tree(self, opts):
566 """Display a filesystem-tree with all selectable files"""
567 self.walk()
Philipp Maierff9dae22021-02-25 17:03:21 +0100568
Philipp Maier7b138b02022-05-31 13:42:56 +0200569 def export_ef(self, filename, context, as_json):
570 """ Select and export a single elementary file (EF) """
Harald Weltec91085e2022-02-10 18:05:45 +0100571 context['COUNT'] += 1
Harald Weltea6c0f882022-07-17 14:23:17 +0200572 df = self._cmd.lchan.selected_file
Philipp Maierac34dcc2021-04-01 17:19:05 +0200573
Philipp Maierea81f752022-05-19 10:13:30 +0200574 # The currently selected file (not the file we are going to export)
575 # must always be an ADF or DF. From this starting point we select
576 # the EF we want to export. To maintain consistency we will then
577 # select the current DF again (see comment below).
Harald Weltec91085e2022-02-10 18:05:45 +0100578 if not isinstance(df, CardDF):
579 raise RuntimeError(
580 "currently selected file %s is not a DF or ADF" % str(df))
Philipp Maierac34dcc2021-04-01 17:19:05 +0200581
Harald Weltec91085e2022-02-10 18:05:45 +0100582 df_path_list = df.fully_qualified_path(True)
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200583 df_path = df.fully_qualified_path_str(True)
584 df_path_fid = df.fully_qualified_path_str(False)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100585
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200586 file_str = df_path + "/" + str(filename)
Harald Weltec91085e2022-02-10 18:05:45 +0100587 self._cmd.poutput(boxed_heading_str(file_str))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100588
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200589 self._cmd.poutput("# directory: %s (%s)" % (df_path, df_path_fid))
Harald Weltec91085e2022-02-10 18:05:45 +0100590 try:
Harald Weltea6c0f882022-07-17 14:23:17 +0200591 fcp_dec = self._cmd.lchan.select(filename, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100592 self._cmd.poutput("# file: %s (%s)" % (
Harald Weltea6c0f882022-07-17 14:23:17 +0200593 self._cmd.lchan.selected_file.name, self._cmd.lchan.selected_file.fid))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100594
Harald Weltea6c0f882022-07-17 14:23:17 +0200595 structure = self._cmd.lchan.selected_file_structure()
Harald Weltec91085e2022-02-10 18:05:45 +0100596 self._cmd.poutput("# structure: %s" % str(structure))
Harald Weltea6c0f882022-07-17 14:23:17 +0200597 self._cmd.poutput("# RAW FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp_hex))
598 self._cmd.poutput("# Decoded FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100599
Harald Weltec91085e2022-02-10 18:05:45 +0100600 for f in df_path_list:
601 self._cmd.poutput("select " + str(f))
Harald Weltea6c0f882022-07-17 14:23:17 +0200602 self._cmd.poutput("select " + self._cmd.lchan.selected_file.name)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100603
Harald Weltec91085e2022-02-10 18:05:45 +0100604 if structure == 'transparent':
Harald Welte08b11ab2022-02-10 18:56:41 +0100605 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200606 result = self._cmd.lchan.read_binary_dec()
Harald Welte08b11ab2022-02-10 18:56:41 +0100607 self._cmd.poutput("update_binary_decoded '%s'" % json.dumps(result[0], cls=JsonEncoder))
608 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200609 result = self._cmd.lchan.read_binary()
Harald Welte08b11ab2022-02-10 18:56:41 +0100610 self._cmd.poutput("update_binary " + str(result[0]))
Harald Weltec91085e2022-02-10 18:05:45 +0100611 elif structure == 'cyclic' or structure == 'linear_fixed':
612 # Use number of records specified in select response
Harald Weltea6c0f882022-07-17 14:23:17 +0200613 num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
Harald Welte747a9782022-02-13 17:52:28 +0100614 if num_of_rec:
Harald Weltec91085e2022-02-10 18:05:45 +0100615 for r in range(1, num_of_rec + 1):
Harald Welte08b11ab2022-02-10 18:56:41 +0100616 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200617 result = self._cmd.lchan.read_record_dec(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100618 self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
619 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200620 result = self._cmd.lchan.read_record(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100621 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
622
Harald Weltec91085e2022-02-10 18:05:45 +0100623 # When the select response does not return the number of records, read until we hit the
624 # first record that cannot be read.
625 else:
626 r = 1
627 while True:
628 try:
Harald Welte08b11ab2022-02-10 18:56:41 +0100629 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200630 result = self._cmd.lchan.read_record_dec(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100631 self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
632 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200633 result = self._cmd.lchan.read_record(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100634 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
Harald Weltec91085e2022-02-10 18:05:45 +0100635 except SwMatchError as e:
636 # We are past the last valid record - stop
637 if e.sw_actual == "9402":
638 break
639 # Some other problem occurred
640 else:
641 raise e
Harald Weltec91085e2022-02-10 18:05:45 +0100642 r = r + 1
643 elif structure == 'ber_tlv':
Harald Weltea6c0f882022-07-17 14:23:17 +0200644 tags = self._cmd.lchan.retrieve_tags()
Harald Weltec91085e2022-02-10 18:05:45 +0100645 for t in tags:
Harald Weltea6c0f882022-07-17 14:23:17 +0200646 result = self._cmd.lchan.retrieve_data(t)
Harald Weltec91085e2022-02-10 18:05:45 +0100647 (tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
648 self._cmd.poutput("set_data 0x%02x %s" % (t, b2h(val)))
649 else:
650 raise RuntimeError(
651 'Unsupported structure "%s" of file "%s"' % (structure, filename))
652 except Exception as e:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200653 bad_file_str = df_path + "/" + str(filename) + ", " + str(e)
Harald Weltec91085e2022-02-10 18:05:45 +0100654 self._cmd.poutput("# bad file: %s" % bad_file_str)
655 context['ERR'] += 1
656 context['BAD'].append(bad_file_str)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100657
Harald Weltec91085e2022-02-10 18:05:45 +0100658 # When reading the file is done, make sure the parent file is
659 # selected again. This will be the usual case, however we need
660 # to check before since we must not select the same DF twice
Harald Weltea6c0f882022-07-17 14:23:17 +0200661 if df != self._cmd.lchan.selected_file:
662 self._cmd.lchan.select(df.fid or df.aid, self._cmd)
Philipp Maierac34dcc2021-04-01 17:19:05 +0200663
Harald Weltec91085e2022-02-10 18:05:45 +0100664 self._cmd.poutput("#")
Philipp Maier24f7bd32021-02-25 17:06:18 +0100665
Harald Weltec91085e2022-02-10 18:05:45 +0100666 export_parser = argparse.ArgumentParser()
667 export_parser.add_argument(
668 '--filename', type=str, default=None, help='only export specific file')
Harald Welte08b11ab2022-02-10 18:56:41 +0100669 export_parser.add_argument(
670 '--json', action='store_true', help='export as JSON (less reliable)')
Philipp Maier24f7bd32021-02-25 17:06:18 +0100671
Harald Weltec91085e2022-02-10 18:05:45 +0100672 @cmd2.with_argparser(export_parser)
673 def do_export(self, opts):
674 """Export files to script that can be imported back later"""
675 context = {'ERR': 0, 'COUNT': 0, 'BAD': [],
676 'DF_SKIP': 0, 'DF_SKIP_REASON': []}
Philipp Maier9a4091d2022-05-19 10:20:30 +0200677 kwargs_export = {'as_json': opts.json}
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200678 exception_str_add = ""
679
Harald Weltec91085e2022-02-10 18:05:45 +0100680 if opts.filename:
Philipp Maier7b138b02022-05-31 13:42:56 +0200681 self.export_ef(opts.filename, context, **kwargs_export)
Harald Weltec91085e2022-02-10 18:05:45 +0100682 else:
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200683 try:
684 self.walk(0, self.export_ef, None, context, **kwargs_export)
685 except Exception as e:
686 print("# Stopping early here due to exception: " + str(e))
687 print("#")
688 exception_str_add = ", also had to stop early due to exception:" + str(e)
Philipp Maier80ce71f2021-04-19 21:24:23 +0200689
Harald Weltec91085e2022-02-10 18:05:45 +0100690 self._cmd.poutput(boxed_heading_str("Export summary"))
Philipp Maier80ce71f2021-04-19 21:24:23 +0200691
Harald Weltec91085e2022-02-10 18:05:45 +0100692 self._cmd.poutput("# total files visited: %u" % context['COUNT'])
693 self._cmd.poutput("# bad files: %u" % context['ERR'])
694 for b in context['BAD']:
695 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200696
Harald Weltec91085e2022-02-10 18:05:45 +0100697 self._cmd.poutput("# skipped dedicated files(s): %u" %
698 context['DF_SKIP'])
699 for b in context['DF_SKIP_REASON']:
700 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200701
Harald Weltec91085e2022-02-10 18:05:45 +0100702 if context['ERR'] and context['DF_SKIP']:
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200703 raise RuntimeError("unable to export %i elementary file(s) and %i dedicated file(s)%s" % (
704 context['ERR'], context['DF_SKIP'], exception_str_add))
Harald Weltec91085e2022-02-10 18:05:45 +0100705 elif context['ERR']:
706 raise RuntimeError(
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200707 "unable to export %i elementary file(s)%s" % (context['ERR'], exception_str_add))
Harald Weltec91085e2022-02-10 18:05:45 +0100708 elif context['DF_SKIP']:
709 raise RuntimeError(
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200710 "unable to export %i dedicated files(s)%s" % (context['ERR'], exception_str_add))
Harald Welteb2edd142021-01-08 23:29:35 +0100711
Harald Weltec91085e2022-02-10 18:05:45 +0100712 def do_desc(self, opts):
713 """Display human readable file description for the currently selected file"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200714 desc = self._cmd.lchan.selected_file.desc
Harald Weltec91085e2022-02-10 18:05:45 +0100715 if desc:
716 self._cmd.poutput(desc)
717 else:
718 self._cmd.poutput("no description available")
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200719
Harald Welte469db932023-11-01 23:17:06 +0100720 verify_adm_parser = argparse.ArgumentParser()
Harald Weltef9ea63e2023-11-01 23:35:31 +0100721 verify_adm_parser.add_argument('ADM1', nargs='?', type=is_hexstr_or_decimal,
Harald Welte469db932023-11-01 23:17:06 +0100722 help='ADM1 pin value. If none given, CSV file will be queried')
723
724 @cmd2.with_argparser(verify_adm_parser)
725 def do_verify_adm(self, opts):
726 """Verify the ADM (Administrator) PIN specified as argument. This is typically needed in order
727to get write/update permissions to most of the files on SIM cards.
728
729Currently only ADM1 is supported."""
730 if opts.ADM1:
Harald Weltec91085e2022-02-10 18:05:45 +0100731 # use specified ADM-PIN
Harald Welte469db932023-11-01 23:17:06 +0100732 pin_adm = sanitize_pin_adm(opts.ADM1)
Harald Weltec91085e2022-02-10 18:05:45 +0100733 else:
734 # try to find an ADM-PIN if none is specified
735 result = card_key_provider_get_field(
736 'ADM1', key='ICCID', value=self._cmd.iccid)
737 pin_adm = sanitize_pin_adm(result)
738 if pin_adm:
739 self._cmd.poutput(
740 "found ADM-PIN '%s' for ICCID '%s'" % (result, self._cmd.iccid))
741 else:
742 raise ValueError(
743 "cannot find ADM-PIN for ICCID '%s'" % (self._cmd.iccid))
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200744
Harald Weltec91085e2022-02-10 18:05:45 +0100745 if pin_adm:
Harald Welte46255122023-10-21 23:40:42 +0200746 self._cmd.lchan.scc.verify_chv(self._cmd.card._adm_chv_num, h2b(pin_adm))
Harald Weltec91085e2022-02-10 18:05:45 +0100747 else:
748 raise ValueError("error: cannot authenticate, no adm-pin!")
749
Philipp Maier7b9e2442023-03-22 15:19:54 +0100750 def do_cardinfo(self, opts):
751 """Display information about the currently inserted card"""
752 self._cmd.poutput("Card info:")
753 self._cmd.poutput(" Name: %s" % self._cmd.card.name)
Harald Welte46255122023-10-21 23:40:42 +0200754 self._cmd.poutput(" ATR: %s" % b2h(self._cmd.lchan.scc.get_atr()))
Philipp Maier7b9e2442023-03-22 15:19:54 +0100755 self._cmd.poutput(" ICCID: %s" % self._cmd.iccid)
Harald Welte46255122023-10-21 23:40:42 +0200756 self._cmd.poutput(" Class-Byte: %s" % self._cmd.lchan.scc.cla_byte)
757 self._cmd.poutput(" Select-Ctrl: %s" % self._cmd.lchan.scc.sel_ctrl)
Philipp Maier7b9e2442023-03-22 15:19:54 +0100758 self._cmd.poutput(" AIDs:")
759 for a in self._cmd.rs.mf.applications:
760 self._cmd.poutput(" %s" % a)
Harald Welteb2edd142021-01-08 23:29:35 +0100761
Harald Welte31d2cf02021-04-03 10:47:29 +0200762@with_default_category('ISO7816 Commands')
763class Iso7816Commands(CommandSet):
Harald Weltec91085e2022-02-10 18:05:45 +0100764 def __init__(self):
765 super().__init__()
Harald Welte31d2cf02021-04-03 10:47:29 +0200766
Harald Weltec91085e2022-02-10 18:05:45 +0100767 def do_select(self, opts):
768 """SELECT a File (ADF/DF/EF)"""
769 if len(opts.arg_list) == 0:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200770 path = self._cmd.lchan.selected_file.fully_qualified_path_str(True)
771 path_fid = self._cmd.lchan.selected_file.fully_qualified_path_str(False)
772 self._cmd.poutput("currently selected file: %s (%s)" % (path, path_fid))
Harald Weltec91085e2022-02-10 18:05:45 +0100773 return
Harald Welte31d2cf02021-04-03 10:47:29 +0200774
Harald Weltec91085e2022-02-10 18:05:45 +0100775 path = opts.arg_list[0]
Harald Weltea6c0f882022-07-17 14:23:17 +0200776 fcp_dec = self._cmd.lchan.select(path, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100777 self._cmd.update_prompt()
778 self._cmd.poutput_json(fcp_dec)
Harald Welte31d2cf02021-04-03 10:47:29 +0200779
Harald Weltec91085e2022-02-10 18:05:45 +0100780 def complete_select(self, text, line, begidx, endidx) -> List[str]:
781 """Command Line tab completion for SELECT"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200782 index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
Harald Weltec91085e2022-02-10 18:05:45 +0100783 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200784
Harald Weltec91085e2022-02-10 18:05:45 +0100785 def get_code(self, code):
786 """Use code either directly or try to get it from external data source"""
787 auto = ('PIN1', 'PIN2', 'PUK1', 'PUK2')
Harald Welte31d2cf02021-04-03 10:47:29 +0200788
Harald Weltec91085e2022-02-10 18:05:45 +0100789 if str(code).upper() not in auto:
790 return sanitize_pin_adm(code)
Harald Welte31d2cf02021-04-03 10:47:29 +0200791
Harald Weltec91085e2022-02-10 18:05:45 +0100792 result = card_key_provider_get_field(
793 str(code), key='ICCID', value=self._cmd.iccid)
794 result = sanitize_pin_adm(result)
795 if result:
796 self._cmd.poutput("found %s '%s' for ICCID '%s'" %
797 (code.upper(), result, self._cmd.iccid))
798 else:
799 self._cmd.poutput("cannot find %s for ICCID '%s'" %
800 (code.upper(), self._cmd.iccid))
801 return result
Harald Welte31d2cf02021-04-03 10:47:29 +0200802
Harald Weltec91085e2022-02-10 18:05:45 +0100803 verify_chv_parser = argparse.ArgumentParser()
804 verify_chv_parser.add_argument(
805 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
806 verify_chv_parser.add_argument(
Harald Welte1c849f82023-11-01 23:48:28 +0100807 '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 +0200808
Harald Weltec91085e2022-02-10 18:05:45 +0100809 @cmd2.with_argparser(verify_chv_parser)
810 def do_verify_chv(self, opts):
Harald Welte12af7932022-02-15 16:39:08 +0100811 """Verify (authenticate) using specified CHV (PIN) code, which is how the specifications
812 call it if you authenticate yourself using the specified PIN. There usually is at least PIN1 and
813 PIN2."""
Harald Weltec91085e2022-02-10 18:05:45 +0100814 pin = self.get_code(opts.pin_code)
Harald Welte46255122023-10-21 23:40:42 +0200815 (data, sw) = self._cmd.lchan.scc.verify_chv(opts.pin_nr, h2b(pin))
Harald Weltec91085e2022-02-10 18:05:45 +0100816 self._cmd.poutput("CHV verification successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200817
Harald Weltec91085e2022-02-10 18:05:45 +0100818 unblock_chv_parser = argparse.ArgumentParser()
819 unblock_chv_parser.add_argument(
820 '--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
821 unblock_chv_parser.add_argument(
Harald Welte1c849f82023-11-01 23:48:28 +0100822 '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 +0100823 unblock_chv_parser.add_argument(
Harald Welte1c849f82023-11-01 23:48:28 +0100824 '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 +0200825
Harald Weltec91085e2022-02-10 18:05:45 +0100826 @cmd2.with_argparser(unblock_chv_parser)
827 def do_unblock_chv(self, opts):
828 """Unblock PIN code using specified PUK code"""
829 new_pin = self.get_code(opts.new_pin_code)
830 puk = self.get_code(opts.puk_code)
Harald Welte46255122023-10-21 23:40:42 +0200831 (data, sw) = self._cmd.lchan.scc.unblock_chv(
Harald Weltec91085e2022-02-10 18:05:45 +0100832 opts.pin_nr, h2b(puk), h2b(new_pin))
833 self._cmd.poutput("CHV unblock successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200834
Harald Weltec91085e2022-02-10 18:05:45 +0100835 change_chv_parser = argparse.ArgumentParser()
836 change_chv_parser.add_argument(
837 '--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
838 change_chv_parser.add_argument(
Harald Welte1c849f82023-11-01 23:48:28 +0100839 '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 +0100840 change_chv_parser.add_argument(
Harald Welte1c849f82023-11-01 23:48:28 +0100841 '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 +0200842
Harald Weltec91085e2022-02-10 18:05:45 +0100843 @cmd2.with_argparser(change_chv_parser)
844 def do_change_chv(self, opts):
845 """Change PIN code to a new PIN code"""
846 new_pin = self.get_code(opts.new_pin_code)
847 pin = self.get_code(opts.pin_code)
Harald Welte46255122023-10-21 23:40:42 +0200848 (data, sw) = self._cmd.lchan.scc.change_chv(
Harald Weltec91085e2022-02-10 18:05:45 +0100849 opts.pin_nr, h2b(pin), h2b(new_pin))
850 self._cmd.poutput("CHV change successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200851
Harald Weltec91085e2022-02-10 18:05:45 +0100852 disable_chv_parser = argparse.ArgumentParser()
853 disable_chv_parser.add_argument(
854 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
855 disable_chv_parser.add_argument(
Harald Welte1c849f82023-11-01 23:48:28 +0100856 '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 +0200857
Harald Weltec91085e2022-02-10 18:05:45 +0100858 @cmd2.with_argparser(disable_chv_parser)
859 def do_disable_chv(self, opts):
860 """Disable PIN code using specified PIN code"""
861 pin = self.get_code(opts.pin_code)
Harald Welte46255122023-10-21 23:40:42 +0200862 (data, sw) = self._cmd.lchan.scc.disable_chv(opts.pin_nr, h2b(pin))
Harald Weltec91085e2022-02-10 18:05:45 +0100863 self._cmd.poutput("CHV disable successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200864
Harald Weltec91085e2022-02-10 18:05:45 +0100865 enable_chv_parser = argparse.ArgumentParser()
866 enable_chv_parser.add_argument(
867 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
868 enable_chv_parser.add_argument(
Harald Welte1c849f82023-11-01 23:48:28 +0100869 '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 +0200870
Harald Weltec91085e2022-02-10 18:05:45 +0100871 @cmd2.with_argparser(enable_chv_parser)
872 def do_enable_chv(self, opts):
873 """Enable PIN code using specified PIN code"""
874 pin = self.get_code(opts.pin_code)
Harald Welte46255122023-10-21 23:40:42 +0200875 (data, sw) = self._cmd.lchan.scc.enable_chv(opts.pin_nr, h2b(pin))
Harald Weltec91085e2022-02-10 18:05:45 +0100876 self._cmd.poutput("CHV enable successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200877
Harald Weltec91085e2022-02-10 18:05:45 +0100878 def do_deactivate_file(self, opts):
Harald Welte799c3542022-02-15 15:56:28 +0100879 """Deactivate the currently selected EF"""
Harald Welte46255122023-10-21 23:40:42 +0200880 (data, sw) = self._cmd.lchan.scc.deactivate_file()
Harald Weltea4631612021-04-10 18:17:55 +0200881
Harald Welte799c3542022-02-15 15:56:28 +0100882 activate_file_parser = argparse.ArgumentParser()
883 activate_file_parser.add_argument('NAME', type=str, help='File name or FID of file to activate')
884 @cmd2.with_argparser(activate_file_parser)
Harald Weltec91085e2022-02-10 18:05:45 +0100885 def do_activate_file(self, opts):
Harald Weltecd18ed02024-01-15 09:21:03 +0100886 """Activate the specified EF by sending an ACTIVATE FILE apdu command (used to be called REHABILITATE
887in TS 11.11 for classic SIM).
888
889This command is used to (re-)activate a file that is currently in deactivated (sometimes also called
890"invalidated") state. You need to call this from the DF above the to-be-activated EF and specify the name or
891FID of the file to activate.
892
893Note that for *deactivation* the to-be-deactivated EF must be selected, but for *activation*, the DF
894above the to-be-activated EF must be selected!"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200895 (data, sw) = self._cmd.lchan.activate_file(opts.NAME)
Harald Welte485692b2021-05-25 22:21:44 +0200896
Harald Weltec91085e2022-02-10 18:05:45 +0100897 def complete_activate_file(self, text, line, begidx, endidx) -> List[str]:
898 """Command Line tab completion for ACTIVATE FILE"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200899 index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
Harald Weltec91085e2022-02-10 18:05:45 +0100900 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200901
Harald Weltec91085e2022-02-10 18:05:45 +0100902 open_chan_parser = argparse.ArgumentParser()
903 open_chan_parser.add_argument(
904 'chan_nr', type=int, default=0, help='Channel Number')
Harald Welte703f9332021-04-10 18:39:32 +0200905
Harald Weltec91085e2022-02-10 18:05:45 +0100906 @cmd2.with_argparser(open_chan_parser)
907 def do_open_channel(self, opts):
908 """Open a logical channel."""
Harald Welte46255122023-10-21 23:40:42 +0200909 (data, sw) = self._cmd.lchan.scc.manage_channel(
Harald Weltec91085e2022-02-10 18:05:45 +0100910 mode='open', lchan_nr=opts.chan_nr)
Harald Weltebdf59572023-10-21 20:06:19 +0200911 # this is executed only in successful case, as unsuccessful raises exception
912 self._cmd.lchan.add_lchan(opts.chan_nr)
Harald Welte703f9332021-04-10 18:39:32 +0200913
Harald Weltec91085e2022-02-10 18:05:45 +0100914 close_chan_parser = argparse.ArgumentParser()
915 close_chan_parser.add_argument(
916 'chan_nr', type=int, default=0, help='Channel Number')
Harald Welte703f9332021-04-10 18:39:32 +0200917
Harald Weltec91085e2022-02-10 18:05:45 +0100918 @cmd2.with_argparser(close_chan_parser)
919 def do_close_channel(self, opts):
920 """Close a logical channel."""
Harald Welte46255122023-10-21 23:40:42 +0200921 (data, sw) = self._cmd.lchan.scc.manage_channel(
Harald Weltec91085e2022-02-10 18:05:45 +0100922 mode='close', lchan_nr=opts.chan_nr)
Harald Weltebdf59572023-10-21 20:06:19 +0200923 # this is executed only in successful case, as unsuccessful raises exception
924 self._cmd.rs.del_lchan(opts.chan_nr)
Harald Welte703f9332021-04-10 18:39:32 +0200925
Harald Welte20650992023-10-21 23:47:02 +0200926 switch_chan_parser = argparse.ArgumentParser()
927 switch_chan_parser.add_argument(
928 'chan_nr', type=int, default=0, help='Channel Number')
929
930 @cmd2.with_argparser(switch_chan_parser)
931 def do_switch_channel(self, opts):
932 """Switch currently active logical channel."""
933 self._cmd.lchan._select_pre(self._cmd)
934 self._cmd.lchan = self._cmd.rs.lchan[opts.chan_nr]
935 self._cmd.lchan._select_post(self._cmd)
936 self._cmd.update_prompt()
937
Harald Weltec91085e2022-02-10 18:05:45 +0100938 def do_status(self, opts):
939 """Perform the STATUS command."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200940 fcp_dec = self._cmd.lchan.status()
Harald Weltec91085e2022-02-10 18:05:45 +0100941 self._cmd.poutput_json(fcp_dec)
Harald Welte34b05d32021-05-25 22:03:13 +0200942
Harald Welteec950532021-10-20 13:09:00 +0200943
Harald Weltecab26c72022-08-06 16:12:30 +0200944class Proact(ProactiveHandler):
945 def receive_fetch(self, pcmd: ProactiveCommand):
946 # print its parsed representation
947 print(pcmd.decoded)
948 # TODO: implement the basics, such as SMS Sending, ...
949
950
Harald Welte703f9332021-04-10 18:39:32 +0200951
Harald Weltef422eb12023-06-09 11:15:09 +0200952option_parser = argparse.ArgumentParser(description='interactive SIM card shell',
Harald Weltef2e761c2021-04-11 11:56:44 +0200953 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
Harald Welte28c24312021-04-11 12:19:36 +0200954argparse_add_reader_args(option_parser)
Harald Weltec8ff0262021-04-11 12:06:13 +0200955
956global_group = option_parser.add_argument_group('General Options')
957global_group.add_argument('--script', metavar='PATH', default=None,
Harald Welte28c24312021-04-11 12:19:36 +0200958 help='script with pySim-shell commands to be executed automatically at start-up')
Harald Weltec91085e2022-02-10 18:05:45 +0100959global_group.add_argument('--csv', metavar='FILE',
960 default=None, help='Read card data from CSV file')
Philipp Maier76667642021-09-22 16:53:22 +0200961global_group.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
Harald Weltec91085e2022-02-10 18:05:45 +0100962 help="Use automatic card handling machine")
Harald Weltec8ff0262021-04-11 12:06:13 +0200963
964adm_group = global_group.add_mutually_exclusive_group()
965adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', default=None,
966 help='ADM PIN used for provisioning (overwrites default)')
967adm_group.add_argument('-A', '--pin-adm-hex', metavar='PIN_ADM1_HEX', dest='pin_adm_hex', default=None,
968 help='ADM PIN used for provisioning, as hex string (16 characters long)')
Harald Welteb2edd142021-01-08 23:29:35 +0100969
Harald Welte38306df2023-07-11 21:17:55 +0200970option_parser.add_argument("command", nargs='?',
971 help="A pySim-shell command that would optionally be executed at startup")
972option_parser.add_argument('command_args', nargs=argparse.REMAINDER,
973 help="Optional Arguments for command")
974
Harald Welteb2edd142021-01-08 23:29:35 +0100975
976if __name__ == '__main__':
977
Harald Weltec91085e2022-02-10 18:05:45 +0100978 # Parse options
979 opts = option_parser.parse_args()
Harald Welteb2edd142021-01-08 23:29:35 +0100980
Harald Weltec91085e2022-02-10 18:05:45 +0100981 # If a script file is specified, be sure that it actually exists
982 if opts.script:
983 if not os.access(opts.script, os.R_OK):
984 print("Invalid script file!")
985 sys.exit(2)
Philipp Maier13e258d2021-04-08 17:48:49 +0200986
Harald Weltec91085e2022-02-10 18:05:45 +0100987 # Register csv-file as card data provider, either from specified CSV
988 # or from CSV file in home directory
989 csv_default = str(Path.home()) + "/.osmocom/pysim/card_data.csv"
990 if opts.csv:
991 card_key_provider_register(CardKeyProviderCsv(opts.csv))
992 if os.path.isfile(csv_default):
993 card_key_provider_register(CardKeyProviderCsv(csv_default))
Philipp Maier2b11c322021-03-17 12:37:39 +0100994
Harald Weltec91085e2022-02-10 18:05:45 +0100995 # Init card reader driver
Harald Weltecab26c72022-08-06 16:12:30 +0200996 sl = init_reader(opts, proactive_handler = Proact())
Philipp Maierea95c392021-09-16 13:10:19 +0200997
Harald Weltec91085e2022-02-10 18:05:45 +0100998 # Create a card handler (for bulk provisioning)
999 if opts.card_handler_config:
1000 ch = CardHandlerAuto(None, opts.card_handler_config)
1001 else:
1002 ch = CardHandler(sl)
Philipp Maier76667642021-09-22 16:53:22 +02001003
Harald Weltec91085e2022-02-10 18:05:45 +01001004 # Detect and initialize the card in the reader. This may fail when there
1005 # is no card in the reader or the card is unresponsive. PysimApp is
1006 # able to tolerate and recover from that.
1007 try:
1008 rs, card = init_card(sl)
1009 app = PysimApp(card, rs, sl, ch, opts.script)
1010 except:
Philipp Maier6bfa8a82023-10-09 13:32:49 +02001011 print("Card initialization (%s) failed with an exception:" % str(sl))
Harald Weltec91085e2022-02-10 18:05:45 +01001012 print("---------------------8<---------------------")
1013 traceback.print_exc()
1014 print("---------------------8<---------------------")
1015 print("(you may still try to recover from this manually by using the 'equip' command.)")
1016 print(
1017 " it should also be noted that some readers may behave strangely when no card")
1018 print(" is inserted.)")
1019 print("")
Philipp Maiereb3b0dd2023-11-23 11:46:39 +01001020 if opts.script:
1021 print("will not execute startup script due to card initialization errors!")
1022 app = PysimApp(None, None, sl, ch)
Philipp Maierea95c392021-09-16 13:10:19 +02001023
Harald Weltec91085e2022-02-10 18:05:45 +01001024 # If the user supplies an ADM PIN at via commandline args authenticate
1025 # immediately so that the user does not have to use the shell commands
1026 pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
1027 if pin_adm:
1028 if not card:
1029 print("Card error, cannot do ADM verification with supplied ADM pin now.")
1030 try:
Philipp Maier4840d4d2023-09-06 14:54:14 +02001031 card._scc.verify_chv(card._adm_chv_num, h2b(pin_adm))
Harald Weltec91085e2022-02-10 18:05:45 +01001032 except Exception as e:
1033 print(e)
Philipp Maier228c98e2021-03-10 20:14:06 +01001034
Harald Welte38306df2023-07-11 21:17:55 +02001035 if opts.command:
1036 app.onecmd_plus_hooks('{} {}'.format(opts.command, ' '.join(opts.command_args)))
1037 else:
1038 app.cmdloop()