blob: ae783c64299dcc1aaff0f7b2289278df92bc4d31 [file] [log] [blame]
Harald Welteb2edd142021-01-08 23:29:35 +01001#!/usr/bin/env python3
2
3# Interactive shell for working with SIM / UICC / USIM / ISIM cards
4#
Harald Weltecab26c72022-08-06 16:12:30 +02005# (C) 2021-2022 by Harald Welte <laforge@osmocom.org>
Harald Welteb2edd142021-01-08 23:29:35 +01006#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 2 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20from typing import List
21
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
Vadim Yanitskiy0d9f0882022-07-14 19:08:24 +070027from cmd2 import style, fg
Harald Welteb2edd142021-01-08 23:29:35 +010028from cmd2 import CommandSet, with_default_category, with_argparser
29import argparse
30
31import os
32import sys
Philipp Maier2b11c322021-03-17 12:37:39 +010033from pathlib import Path
Philipp Maier76667642021-09-22 16:53:22 +020034from io import StringIO
Harald Welteb2edd142021-01-08 23:29:35 +010035
Harald Weltecab26c72022-08-06 16:12:30 +020036from pprint import pprint as pp
37
Harald Welteb2edd142021-01-08 23:29:35 +010038from pySim.exceptions import *
39from pySim.commands import SimCardCommands
Harald Weltecab26c72022-08-06 16:12:30 +020040from pySim.transport import init_reader, ApduTracer, argparse_add_reader_args, ProactiveHandler
Philipp Maierbb73e512021-05-05 16:14:00 +020041from pySim.cards import card_detect, SimCard
Harald Welte1e52b0d2022-07-16 11:53:21 +020042from pySim.utils import h2b, swap_nibbles, rpad, b2h, JsonEncoder, bertlv_parse_one, sw_match
Harald Weltecab26c72022-08-06 16:12:30 +020043from pySim.utils import sanitize_pin_adm, tabulate_str_list, boxed_heading_str, Hexstr
Philipp Maier76667642021-09-22 16:53:22 +020044from pySim.card_handler import CardHandler, CardHandlerAuto
Harald Welteb2edd142021-01-08 23:29:35 +010045
Harald Welte1e52b0d2022-07-16 11:53:21 +020046from pySim.filesystem import RuntimeState, CardDF, CardADF, CardModel
Philipp Maiera028c7d2021-11-08 16:12:03 +010047from pySim.profile import CardProfile
Vadim Yanitskiy87dd0202023-04-22 20:45:29 +070048from pySim.cdma_ruim import CardProfileRUIM
Harald Welteb2edd142021-01-08 23:29:35 +010049from pySim.ts_102_221 import CardProfileUICC
Harald Welte3c9b7842021-10-19 21:44:24 +020050from pySim.ts_102_222 import Ts102222Commands
Harald Welte5ce35242021-04-02 20:27:05 +020051from pySim.ts_31_102 import CardApplicationUSIM
52from pySim.ts_31_103 import CardApplicationISIM
Harald Welte95ce6b12021-10-20 18:40:54 +020053from pySim.ara_m import CardApplicationARAM
Harald Welte34eb5042022-02-21 17:19:28 +010054from pySim.global_platform import CardApplicationISD
Harald Welte2a33ad22021-10-20 10:14:18 +020055from pySim.gsm_r import DF_EIRENE
Harald Weltecab26c72022-08-06 16:12:30 +020056from pySim.cat import ProactiveCommand
Harald Welteb2edd142021-01-08 23:29:35 +010057
Harald Welte4c1dca02021-10-14 17:48:25 +020058# we need to import this module so that the SysmocomSJA2 sub-class of
59# CardModel is created, which will add the ATR-based matching and
60# calling of SysmocomSJA2.add_files. See CardModel.apply_matching_models
Harald Weltef44256c2021-10-14 15:53:39 +020061import pySim.sysmocom_sja2
62
Harald Welte4442b3d2021-04-03 09:00:16 +020063from pySim.card_key_provider import CardKeyProviderCsv, card_key_provider_register, card_key_provider_get_field
Philipp Maier2b11c322021-03-17 12:37:39 +010064
Harald Weltec91085e2022-02-10 18:05:45 +010065
Philipp Maierea95c392021-09-16 13:10:19 +020066def init_card(sl):
Harald Weltec91085e2022-02-10 18:05:45 +010067 """
68 Detect card in reader and setup card profile and runtime state. This
69 function must be called at least once on startup. The card and runtime
70 state object (rs) is required for all pySim-shell commands.
71 """
Philipp Maierea95c392021-09-16 13:10:19 +020072
Harald Weltec91085e2022-02-10 18:05:45 +010073 # Wait up to three seconds for a card in reader and try to detect
74 # the card type.
75 print("Waiting for card...")
76 try:
77 sl.wait_for_card(3)
78 except NoCardError:
79 print("No card detected!")
80 return None, None
81 except:
82 print("Card not readable!")
83 return None, None
Philipp Maierea95c392021-09-16 13:10:19 +020084
Philipp Maier24031252022-06-14 16:16:42 +020085 generic_card = False
Harald Weltec91085e2022-02-10 18:05:45 +010086 card = card_detect("auto", scc)
87 if card is None:
88 print("Warning: Could not detect card type - assuming a generic card type...")
89 card = SimCard(scc)
Philipp Maier24031252022-06-14 16:16:42 +020090 generic_card = True
Philipp Maiera028c7d2021-11-08 16:12:03 +010091
Harald Weltec91085e2022-02-10 18:05:45 +010092 profile = CardProfile.pick(scc)
93 if profile is None:
94 print("Unsupported card type!")
Philipp Maier7226c092022-06-01 17:58:38 +020095 return None, card
Philipp Maierea95c392021-09-16 13:10:19 +020096
Philipp Maier24031252022-06-14 16:16:42 +020097 # ETSI TS 102 221, Table 9.3 specifies a default for the PIN key
98 # references, however card manufactures may still decide to pick an
99 # arbitrary key reference. In case we run on a generic card class that is
100 # detected as an UICC, we will pick the key reference that is officially
101 # specified.
102 if generic_card and isinstance(profile, CardProfileUICC):
103 card._adm_chv_num = 0x0A
104
Harald Weltec91085e2022-02-10 18:05:45 +0100105 print("Info: Card is of type: %s" % str(profile))
Philipp Maiera028c7d2021-11-08 16:12:03 +0100106
Harald Weltec91085e2022-02-10 18:05:45 +0100107 # FIXME: This shouln't be here, the profile should add the applications,
108 # however, we cannot simply put his into ts_102_221.py since we would
109 # have to e.g. import CardApplicationUSIM from ts_31_102.py, which already
110 # imports from ts_102_221.py. This means we will end up with a circular
111 # import, which needs to be resolved first.
112 if isinstance(profile, CardProfileUICC):
113 profile.add_application(CardApplicationUSIM())
114 profile.add_application(CardApplicationISIM())
115 profile.add_application(CardApplicationARAM())
Harald Welte34eb5042022-02-21 17:19:28 +0100116 profile.add_application(CardApplicationISD())
Philipp Maiera028c7d2021-11-08 16:12:03 +0100117
Harald Weltec91085e2022-02-10 18:05:45 +0100118 # Create runtime state with card profile
119 rs = RuntimeState(card, profile)
Philipp Maierea95c392021-09-16 13:10:19 +0200120
Harald Weltec91085e2022-02-10 18:05:45 +0100121 # FIXME: This is an GSM-R related file, it needs to be added throughout,
122 # the profile. At the moment we add it for all cards, this won't hurt,
123 # but regular SIM and UICC will not have it and fail to select it.
124 rs.mf.add_file(DF_EIRENE())
Philipp Maierea95c392021-09-16 13:10:19 +0200125
Harald Weltec91085e2022-02-10 18:05:45 +0100126 CardModel.apply_matching_models(scc, rs)
Harald Weltef44256c2021-10-14 15:53:39 +0200127
Harald Weltec91085e2022-02-10 18:05:45 +0100128 # inform the transport that we can do context-specific SW interpretation
129 sl.set_sw_interpreter(rs)
Philipp Maierea95c392021-09-16 13:10:19 +0200130
Harald Weltec91085e2022-02-10 18:05:45 +0100131 return rs, card
132
Philipp Maier2b11c322021-03-17 12:37:39 +0100133
Harald Welteb2edd142021-01-08 23:29:35 +0100134class PysimApp(cmd2.Cmd):
Harald Weltec91085e2022-02-10 18:05:45 +0100135 CUSTOM_CATEGORY = 'pySim Commands'
Philipp Maier76667642021-09-22 16:53:22 +0200136
Harald Weltec91085e2022-02-10 18:05:45 +0100137 def __init__(self, card, rs, sl, ch, script=None):
Harald Weltec85d4062023-04-27 17:10:17 +0200138 if version.parse(cmd2.__version__) < version.parse("2.0.0"):
139 kwargs = {'use_ipython': True}
140 else:
141 kwargs = {'include_ipy': True}
142
Harald Weltec91085e2022-02-10 18:05:45 +0100143 super().__init__(persistent_history_file='~/.pysim_shell_history', allow_cli_args=False,
Harald Weltec85d4062023-04-27 17:10:17 +0200144 auto_load_commands=False, startup_script=script, **kwargs)
Harald Weltec91085e2022-02-10 18:05:45 +0100145 self.intro = style('Welcome to pySim-shell!', fg=fg.red)
146 self.default_category = 'pySim-shell built-in commands'
147 self.card = None
148 self.rs = None
Harald Weltea6c0f882022-07-17 14:23:17 +0200149 self.lchan = None
150 self.py_locals = {'card': self.card, 'rs': self.rs, 'lchan': self.lchan}
Harald Weltec91085e2022-02-10 18:05:45 +0100151 self.sl = sl
152 self.ch = ch
Harald Welte1748b932021-04-06 21:12:25 +0200153
Harald Weltec91085e2022-02-10 18:05:45 +0100154 self.numeric_path = False
Harald Weltec91085e2022-02-10 18:05:45 +0100155 self.conserve_write = True
Harald Weltec91085e2022-02-10 18:05:45 +0100156 self.json_pretty_print = True
Harald Weltec91085e2022-02-10 18:05:45 +0100157 self.apdu_trace = False
Philipp Maier5d698e52021-09-16 13:18:01 +0200158
Harald Welte93aac3a2023-04-27 15:18:13 +0200159 if version.parse(cmd2.__version__) < version.parse("2.0.0"):
160 self.add_settable(cmd2.Settable('numeric_path', bool, 'Print File IDs instead of names',
161 onchange_cb=self._onchange_numeric_path))
162 self.add_settable(cmd2.Settable('conserve_write', bool, 'Read and compare before write',
163 onchange_cb=self._onchange_conserve_write))
164 self.add_settable(cmd2.Settable('json_pretty_print', bool, 'Pretty-Print JSON output'))
165 self.add_settable(cmd2.Settable('apdu_trace', bool, 'Trace and display APDUs exchanged with card',
166 onchange_cb=self._onchange_apdu_trace))
167 else:
168 self.add_settable(cmd2.Settable('numeric_path', bool, 'Print File IDs instead of names', self, \
169 onchange_cb=self._onchange_numeric_path)) # pylint: disable=too-many-function-args
170 self.add_settable(cmd2.Settable('conserve_write', bool, 'Read and compare before write', self, \
171 onchange_cb=self._onchange_conserve_write)) # pylint: disable=too-many-function-args
172 self.add_settable(cmd2.Settable('json_pretty_print', bool, 'Pretty-Print JSON output', self)) # pylint: disable=too-many-function-args
173 self.add_settable(cmd2.Settable('apdu_trace', bool, 'Trace and display APDUs exchanged with card', self, \
174 onchange_cb=self._onchange_apdu_trace)) # pylint: disable=too-many-function-args
Harald Weltec91085e2022-02-10 18:05:45 +0100175 self.equip(card, rs)
Philipp Maier5d698e52021-09-16 13:18:01 +0200176
Harald Weltec91085e2022-02-10 18:05:45 +0100177 def equip(self, card, rs):
178 """
179 Equip pySim-shell with the supplied card and runtime state, add (or remove) all required settables and
180 and commands to enable card operations.
181 """
Philipp Maier76667642021-09-22 16:53:22 +0200182
Harald Weltec91085e2022-02-10 18:05:45 +0100183 rc = False
Philipp Maier5d698e52021-09-16 13:18:01 +0200184
Harald Weltec91085e2022-02-10 18:05:45 +0100185 # Unequip everything from pySim-shell that would not work in unequipped state
186 if self.rs:
Harald Weltea6c0f882022-07-17 14:23:17 +0200187 lchan = self.rs.lchan[0]
188 lchan.unregister_cmds(self)
Harald Weltec91085e2022-02-10 18:05:45 +0100189 for cmds in [Iso7816Commands, PySimCommands]:
190 cmd_set = self.find_commandsets(cmds)
191 if cmd_set:
192 self.unregister_command_set(cmd_set[0])
Philipp Maier5d698e52021-09-16 13:18:01 +0200193
Harald Weltec91085e2022-02-10 18:05:45 +0100194 self.card = card
195 self.rs = rs
Philipp Maier5d698e52021-09-16 13:18:01 +0200196
Harald Weltec91085e2022-02-10 18:05:45 +0100197 # When a card object and a runtime state is present, (re)equip pySim-shell with everything that is
198 # needed to operate on cards.
199 if self.card and self.rs:
Harald Weltea6c0f882022-07-17 14:23:17 +0200200 self.lchan = self.rs.lchan[0]
Harald Weltec91085e2022-02-10 18:05:45 +0100201 self._onchange_conserve_write(
202 'conserve_write', False, self.conserve_write)
203 self._onchange_apdu_trace('apdu_trace', False, self.apdu_trace)
204 self.register_command_set(Iso7816Commands())
Harald Welte3c9b7842021-10-19 21:44:24 +0200205 self.register_command_set(Ts102222Commands())
Harald Weltec91085e2022-02-10 18:05:45 +0100206 self.register_command_set(PySimCommands())
207 self.iccid, sw = self.card.read_iccid()
Harald Weltea6c0f882022-07-17 14:23:17 +0200208 self.lchan.select('MF', self)
Harald Weltec91085e2022-02-10 18:05:45 +0100209 rc = True
210 else:
211 self.poutput("pySim-shell not equipped!")
Philipp Maier5d698e52021-09-16 13:18:01 +0200212
Harald Weltec91085e2022-02-10 18:05:45 +0100213 self.update_prompt()
214 return rc
Harald Welteb2edd142021-01-08 23:29:35 +0100215
Harald Weltec91085e2022-02-10 18:05:45 +0100216 def poutput_json(self, data, force_no_pretty=False):
217 """like cmd2.poutput() but for a JSON serializable dict."""
218 if force_no_pretty or self.json_pretty_print == False:
219 output = json.dumps(data, cls=JsonEncoder)
220 else:
221 output = json.dumps(data, cls=JsonEncoder, indent=4)
222 self.poutput(output)
Harald Welteb2edd142021-01-08 23:29:35 +0100223
Harald Weltec91085e2022-02-10 18:05:45 +0100224 def _onchange_numeric_path(self, param_name, old, new):
225 self.update_prompt()
Philipp Maier38c74f62021-03-17 17:19:52 +0100226
Harald Weltec91085e2022-02-10 18:05:45 +0100227 def _onchange_conserve_write(self, param_name, old, new):
228 if self.rs:
229 self.rs.conserve_write = new
Harald Welte7829d8a2021-04-10 11:28:53 +0200230
Harald Weltec91085e2022-02-10 18:05:45 +0100231 def _onchange_apdu_trace(self, param_name, old, new):
232 if self.card:
233 if new == True:
234 self.card._scc._tp.apdu_tracer = self.Cmd2ApduTracer(self)
235 else:
236 self.card._scc._tp.apdu_tracer = None
Harald Welte7829d8a2021-04-10 11:28:53 +0200237
Harald Weltec91085e2022-02-10 18:05:45 +0100238 class Cmd2ApduTracer(ApduTracer):
239 def __init__(self, cmd2_app):
240 self.cmd2 = app
Harald Welte7829d8a2021-04-10 11:28:53 +0200241
Harald Weltec91085e2022-02-10 18:05:45 +0100242 def trace_response(self, cmd, sw, resp):
243 self.cmd2.poutput("-> %s %s" % (cmd[:10], cmd[10:]))
244 self.cmd2.poutput("<- %s: %s" % (sw, resp))
Harald Welteb2edd142021-01-08 23:29:35 +0100245
Harald Weltec91085e2022-02-10 18:05:45 +0100246 def update_prompt(self):
Harald Weltea6c0f882022-07-17 14:23:17 +0200247 if self.lchan:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200248 path_str = self.lchan.selected_file.fully_qualified_path_str(not self.numeric_path)
249 self.prompt = 'pySIM-shell (%s)> ' % (path_str)
Harald Weltec91085e2022-02-10 18:05:45 +0100250 else:
Philipp Maier7226c092022-06-01 17:58:38 +0200251 if self.card:
252 self.prompt = 'pySIM-shell (no card profile)> '
253 else:
254 self.prompt = 'pySIM-shell (no card)> '
Harald Welteb2edd142021-01-08 23:29:35 +0100255
Harald Weltec91085e2022-02-10 18:05:45 +0100256 @cmd2.with_category(CUSTOM_CATEGORY)
257 def do_intro(self, _):
258 """Display the intro banner"""
259 self.poutput(self.intro)
Philipp Maier9764de22021-11-03 10:44:39 +0100260
Harald Weltec91085e2022-02-10 18:05:45 +0100261 def do_eof(self, _: argparse.Namespace) -> bool:
262 self.poutput("")
263 return self.do_quit('')
Philipp Maier5d698e52021-09-16 13:18:01 +0200264
Harald Weltec91085e2022-02-10 18:05:45 +0100265 @cmd2.with_category(CUSTOM_CATEGORY)
266 def do_equip(self, opts):
267 """Equip pySim-shell with card"""
268 rs, card = init_card(sl)
269 self.equip(card, rs)
Philipp Maier76667642021-09-22 16:53:22 +0200270
Philipp Maier7226c092022-06-01 17:58:38 +0200271 apdu_cmd_parser = argparse.ArgumentParser()
272 apdu_cmd_parser.add_argument('APDU', type=str, help='APDU as hex string')
Philipp Maiere7d1b672022-06-01 18:05:34 +0200273 apdu_cmd_parser.add_argument('--expect-sw', help='expect a specified status word', type=str, default=None)
Philipp Maier7226c092022-06-01 17:58:38 +0200274
275 @cmd2.with_argparser(apdu_cmd_parser)
276 def do_apdu(self, opts):
277 """Send a raw APDU to the card, and print SW + Response.
278 DANGEROUS: pySim-shell will not know any card state changes, and
279 not continue to work as expected if you e.g. select a different
280 file."""
281 data, sw = self.card._scc._tp.send_apdu(opts.APDU)
282 if data:
283 self.poutput("SW: %s, RESP: %s" % (sw, data))
284 else:
285 self.poutput("SW: %s" % sw)
Philipp Maiere7d1b672022-06-01 18:05:34 +0200286 if opts.expect_sw:
287 if not sw_match(sw, opts.expect_sw):
288 raise SwMatchError(sw, opts.expect_sw)
Philipp Maier7226c092022-06-01 17:58:38 +0200289
Harald Weltec91085e2022-02-10 18:05:45 +0100290 class InterceptStderr(list):
291 def __init__(self):
292 self._stderr_backup = sys.stderr
Philipp Maier76667642021-09-22 16:53:22 +0200293
Harald Weltec91085e2022-02-10 18:05:45 +0100294 def __enter__(self):
295 self._stringio_stderr = StringIO()
296 sys.stderr = self._stringio_stderr
297 return self
Philipp Maier76667642021-09-22 16:53:22 +0200298
Harald Weltec91085e2022-02-10 18:05:45 +0100299 def __exit__(self, *args):
300 self.stderr = self._stringio_stderr.getvalue().strip()
301 del self._stringio_stderr
302 sys.stderr = self._stderr_backup
Philipp Maier76667642021-09-22 16:53:22 +0200303
Harald Weltec91085e2022-02-10 18:05:45 +0100304 def _show_failure_sign(self):
305 self.poutput(style(" +-------------+", fg=fg.bright_red))
306 self.poutput(style(" + ## ## +", fg=fg.bright_red))
307 self.poutput(style(" + ## ## +", fg=fg.bright_red))
308 self.poutput(style(" + ### +", fg=fg.bright_red))
309 self.poutput(style(" + ## ## +", fg=fg.bright_red))
310 self.poutput(style(" + ## ## +", fg=fg.bright_red))
311 self.poutput(style(" +-------------+", fg=fg.bright_red))
312 self.poutput("")
Philipp Maier76667642021-09-22 16:53:22 +0200313
Harald Weltec91085e2022-02-10 18:05:45 +0100314 def _show_success_sign(self):
315 self.poutput(style(" +-------------+", fg=fg.bright_green))
316 self.poutput(style(" + ## +", fg=fg.bright_green))
317 self.poutput(style(" + ## +", fg=fg.bright_green))
318 self.poutput(style(" + # ## +", fg=fg.bright_green))
319 self.poutput(style(" + ## # +", fg=fg.bright_green))
320 self.poutput(style(" + ## +", fg=fg.bright_green))
321 self.poutput(style(" +-------------+", fg=fg.bright_green))
322 self.poutput("")
Philipp Maier76667642021-09-22 16:53:22 +0200323
Harald Weltec91085e2022-02-10 18:05:45 +0100324 def _process_card(self, first, script_path):
Philipp Maier76667642021-09-22 16:53:22 +0200325
Harald Weltec91085e2022-02-10 18:05:45 +0100326 # Early phase of card initialzation (this part may fail with an exception)
327 try:
328 rs, card = init_card(self.sl)
329 rc = self.equip(card, rs)
330 except:
331 self.poutput("")
332 self.poutput("Card initialization failed with an exception:")
333 self.poutput("---------------------8<---------------------")
334 traceback.print_exc()
335 self.poutput("---------------------8<---------------------")
336 self.poutput("")
337 return -1
Philipp Maier76667642021-09-22 16:53:22 +0200338
Harald Weltec91085e2022-02-10 18:05:45 +0100339 # Actual card processing step. This part should never fail with an exception since the cmd2
340 # do_run_script method will catch any exception that might occur during script execution.
341 if rc:
342 self.poutput("")
343 self.poutput("Transcript stdout:")
344 self.poutput("---------------------8<---------------------")
345 with self.InterceptStderr() as logged:
346 self.do_run_script(script_path)
347 self.poutput("---------------------8<---------------------")
Philipp Maier76667642021-09-22 16:53:22 +0200348
Harald Weltec91085e2022-02-10 18:05:45 +0100349 self.poutput("")
350 self.poutput("Transcript stderr:")
351 if logged.stderr:
352 self.poutput("---------------------8<---------------------")
353 self.poutput(logged.stderr)
354 self.poutput("---------------------8<---------------------")
355 else:
356 self.poutput("(none)")
Philipp Maier76667642021-09-22 16:53:22 +0200357
Harald Weltec91085e2022-02-10 18:05:45 +0100358 # Check for exceptions
359 self.poutput("")
360 if "EXCEPTION of type" not in logged.stderr:
361 return 0
Philipp Maier76667642021-09-22 16:53:22 +0200362
Harald Weltec91085e2022-02-10 18:05:45 +0100363 return -1
Philipp Maier76667642021-09-22 16:53:22 +0200364
Harald Weltec91085e2022-02-10 18:05:45 +0100365 bulk_script_parser = argparse.ArgumentParser()
366 bulk_script_parser.add_argument(
367 'script_path', help="path to the script file")
368 bulk_script_parser.add_argument('--halt_on_error', help='stop card handling if an exeption occurs',
369 action='store_true')
370 bulk_script_parser.add_argument('--tries', type=int, default=2,
371 help='how many tries before trying the next card')
372 bulk_script_parser.add_argument('--on_stop_action', type=str, default=None,
373 help='commandline to execute when card handling has stopped')
374 bulk_script_parser.add_argument('--pre_card_action', type=str, default=None,
375 help='commandline to execute before actually talking to the card')
Philipp Maier76667642021-09-22 16:53:22 +0200376
Harald Weltec91085e2022-02-10 18:05:45 +0100377 @cmd2.with_argparser(bulk_script_parser)
378 @cmd2.with_category(CUSTOM_CATEGORY)
379 def do_bulk_script(self, opts):
380 """Run script on multiple cards (bulk provisioning)"""
Philipp Maier76667642021-09-22 16:53:22 +0200381
Harald Weltec91085e2022-02-10 18:05:45 +0100382 # Make sure that the script file exists and that it is readable.
383 if not os.access(opts.script_path, os.R_OK):
384 self.poutput("Invalid script file!")
385 return
Philipp Maier76667642021-09-22 16:53:22 +0200386
Harald Weltec91085e2022-02-10 18:05:45 +0100387 success_count = 0
388 fail_count = 0
Philipp Maier76667642021-09-22 16:53:22 +0200389
Harald Weltec91085e2022-02-10 18:05:45 +0100390 first = True
391 while 1:
392 # TODO: Count consecutive failures, if more than N consecutive failures occur, then stop.
393 # The ratinale is: There may be a problem with the device, we do want to prevent that
394 # all remaining cards are fired to the error bin. This is only relevant for situations
395 # with large stacks, probably we do not need this feature right now.
Philipp Maier76667642021-09-22 16:53:22 +0200396
Harald Weltec91085e2022-02-10 18:05:45 +0100397 try:
398 # In case of failure, try multiple times.
399 for i in range(opts.tries):
400 # fetch card into reader bay
401 ch.get(first)
Philipp Maier76667642021-09-22 16:53:22 +0200402
Harald Weltec91085e2022-02-10 18:05:45 +0100403 # if necessary execute an action before we start processing the card
404 if(opts.pre_card_action):
405 os.system(opts.pre_card_action)
Philipp Maier76667642021-09-22 16:53:22 +0200406
Harald Weltec91085e2022-02-10 18:05:45 +0100407 # process the card
408 rc = self._process_card(first, opts.script_path)
409 if rc == 0:
410 success_count = success_count + 1
411 self._show_success_sign()
412 self.poutput("Statistics: success :%i, failure: %i" % (
413 success_count, fail_count))
414 break
415 else:
416 fail_count = fail_count + 1
417 self._show_failure_sign()
418 self.poutput("Statistics: success :%i, failure: %i" % (
419 success_count, fail_count))
Philipp Maier76667642021-09-22 16:53:22 +0200420
Harald Weltec91085e2022-02-10 18:05:45 +0100421 # Depending on success or failure, the card goes either in the "error" bin or in the
422 # "done" bin.
423 if rc < 0:
424 ch.error()
425 else:
426 ch.done()
Philipp Maier76667642021-09-22 16:53:22 +0200427
Harald Weltec91085e2022-02-10 18:05:45 +0100428 # In most cases it is possible to proceed with the next card, but the
429 # user may decide to halt immediately when an error occurs
430 if opts.halt_on_error and rc < 0:
431 return
Philipp Maier76667642021-09-22 16:53:22 +0200432
Harald Weltec91085e2022-02-10 18:05:45 +0100433 except (KeyboardInterrupt):
434 self.poutput("")
435 self.poutput("Terminated by user!")
436 return
437 except (SystemExit):
438 # When all cards are processed the card handler device will throw a SystemExit
439 # exception. Also Errors that are not recoverable (cards stuck etc.) will end up here.
440 # The user has the option to execute some action to make aware that the card handler
441 # needs service.
442 if(opts.on_stop_action):
443 os.system(opts.on_stop_action)
444 return
445 except:
446 self.poutput("")
447 self.poutput("Card handling failed with an exception:")
448 self.poutput("---------------------8<---------------------")
449 traceback.print_exc()
450 self.poutput("---------------------8<---------------------")
451 self.poutput("")
452 fail_count = fail_count + 1
453 self._show_failure_sign()
454 self.poutput("Statistics: success :%i, failure: %i" %
455 (success_count, fail_count))
Philipp Maierb52feed2021-09-22 16:43:13 +0200456
Harald Weltec91085e2022-02-10 18:05:45 +0100457 first = False
458
459 echo_parser = argparse.ArgumentParser()
460 echo_parser.add_argument('string', help="string to echo on the shell")
461
462 @cmd2.with_argparser(echo_parser)
463 @cmd2.with_category(CUSTOM_CATEGORY)
464 def do_echo(self, opts):
465 """Echo (print) a string on the console"""
466 self.poutput(opts.string)
467
Harald Weltefc315482022-07-23 12:49:14 +0200468 @cmd2.with_category(CUSTOM_CATEGORY)
469 def do_version(self, opts):
470 """Print the pySim software version."""
471 import pkg_resources
472 self.poutput(pkg_resources.get_distribution('pySim'))
Harald Welteb2edd142021-01-08 23:29:35 +0100473
Harald Welte31d2cf02021-04-03 10:47:29 +0200474@with_default_category('pySim Commands')
475class PySimCommands(CommandSet):
Harald Weltec91085e2022-02-10 18:05:45 +0100476 def __init__(self):
477 super().__init__()
Harald Welteb2edd142021-01-08 23:29:35 +0100478
Harald Weltec91085e2022-02-10 18:05:45 +0100479 dir_parser = argparse.ArgumentParser()
480 dir_parser.add_argument(
481 '--fids', help='Show file identifiers', action='store_true')
482 dir_parser.add_argument(
483 '--names', help='Show file names', action='store_true')
484 dir_parser.add_argument(
485 '--apps', help='Show applications', action='store_true')
486 dir_parser.add_argument(
487 '--all', help='Show all selectable identifiers and names', action='store_true')
Philipp Maier5d3e2592021-02-22 17:22:16 +0100488
Harald Weltec91085e2022-02-10 18:05:45 +0100489 @cmd2.with_argparser(dir_parser)
490 def do_dir(self, opts):
491 """Show a listing of files available in currently selected DF or MF"""
492 if opts.all:
493 flags = []
494 elif opts.fids or opts.names or opts.apps:
495 flags = ['PARENT', 'SELF']
496 if opts.fids:
497 flags += ['FIDS', 'AIDS']
498 if opts.names:
499 flags += ['FNAMES', 'ANAMES']
500 if opts.apps:
501 flags += ['ANAMES', 'AIDS']
502 else:
503 flags = ['PARENT', 'SELF', 'FNAMES', 'ANAMES']
504 selectables = list(
Harald Weltea6c0f882022-07-17 14:23:17 +0200505 self._cmd.lchan.selected_file.get_selectable_names(flags=flags))
Harald Weltec91085e2022-02-10 18:05:45 +0100506 directory_str = tabulate_str_list(
507 selectables, width=79, hspace=2, lspace=1, align_left=True)
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200508 path = self._cmd.lchan.selected_file.fully_qualified_path_str(True)
509 self._cmd.poutput(path)
510 path = self._cmd.lchan.selected_file.fully_qualified_path_str(False)
511 self._cmd.poutput(path)
Harald Weltec91085e2022-02-10 18:05:45 +0100512 self._cmd.poutput(directory_str)
513 self._cmd.poutput("%d files" % len(selectables))
Harald Welteb2edd142021-01-08 23:29:35 +0100514
Philipp Maier7b138b02022-05-31 13:42:56 +0200515 def walk(self, indent=0, action_ef=None, action_df=None, context=None, **kwargs):
Harald Weltec91085e2022-02-10 18:05:45 +0100516 """Recursively walk through the file system, starting at the currently selected DF"""
Philipp Maier7b138b02022-05-31 13:42:56 +0200517
Harald Weltea6c0f882022-07-17 14:23:17 +0200518 if isinstance(self._cmd.lchan.selected_file, CardDF):
Philipp Maier7b138b02022-05-31 13:42:56 +0200519 if action_df:
520 action_df(context, opts)
521
Harald Weltea6c0f882022-07-17 14:23:17 +0200522 files = self._cmd.lchan.selected_file.get_selectables(
Harald Weltec91085e2022-02-10 18:05:45 +0100523 flags=['FNAMES', 'ANAMES'])
524 for f in files:
Philipp Maier7b138b02022-05-31 13:42:56 +0200525 # special case: When no action is performed, just output a directory
526 if not action_ef and not action_df:
Harald Weltec91085e2022-02-10 18:05:45 +0100527 output_str = " " * indent + str(f) + (" " * 250)
528 output_str = output_str[0:25]
529 if isinstance(files[f], CardADF):
530 output_str += " " + str(files[f].aid)
531 else:
532 output_str += " " + str(files[f].fid)
533 output_str += " " + str(files[f].desc)
534 self._cmd.poutput(output_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200535
Harald Weltec91085e2022-02-10 18:05:45 +0100536 if isinstance(files[f], CardDF):
537 skip_df = False
538 try:
Harald Weltea6c0f882022-07-17 14:23:17 +0200539 fcp_dec = self._cmd.lchan.select(f, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100540 except Exception as e:
541 skip_df = True
Harald Weltea6c0f882022-07-17 14:23:17 +0200542 df = self._cmd.lchan.selected_file
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200543 df_path = df.fully_qualified_path_str(True)
544 df_skip_reason_str = df_path + \
Harald Weltec91085e2022-02-10 18:05:45 +0100545 "/" + str(f) + ", " + str(e)
546 if context:
547 context['DF_SKIP'] += 1
548 context['DF_SKIP_REASON'].append(df_skip_reason_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200549
Harald Weltec91085e2022-02-10 18:05:45 +0100550 # If the DF was skipped, we never have entered the directory
551 # below, so we must not move up.
552 if skip_df == False:
Philipp Maier7b138b02022-05-31 13:42:56 +0200553 self.walk(indent + 1, action_ef, action_df, context, **kwargs)
Harald Weltea6c0f882022-07-17 14:23:17 +0200554 fcp_dec = self._cmd.lchan.select("..", self._cmd)
Philipp Maierf408a402021-04-09 21:16:12 +0200555
Philipp Maier7b138b02022-05-31 13:42:56 +0200556 elif action_ef:
Harald Weltea6c0f882022-07-17 14:23:17 +0200557 df_before_action = self._cmd.lchan.selected_file
Philipp Maier7b138b02022-05-31 13:42:56 +0200558 action_ef(f, context, **kwargs)
Harald Weltec91085e2022-02-10 18:05:45 +0100559 # When walking through the file system tree the action must not
560 # always restore the currently selected file to the file that
561 # was selected before executing the action() callback.
Harald Weltea6c0f882022-07-17 14:23:17 +0200562 if df_before_action != self._cmd.lchan.selected_file:
Harald Weltec91085e2022-02-10 18:05:45 +0100563 raise RuntimeError("inconsistent walk, %s is currently selected but expecting %s to be selected"
Harald Weltea6c0f882022-07-17 14:23:17 +0200564 % (str(self._cmd.lchan.selected_file), str(df_before_action)))
Philipp Maierff9dae22021-02-25 17:03:21 +0100565
Harald Weltec91085e2022-02-10 18:05:45 +0100566 def do_tree(self, opts):
567 """Display a filesystem-tree with all selectable files"""
568 self.walk()
Philipp Maierff9dae22021-02-25 17:03:21 +0100569
Philipp Maier7b138b02022-05-31 13:42:56 +0200570 def export_ef(self, filename, context, as_json):
571 """ Select and export a single elementary file (EF) """
Harald Weltec91085e2022-02-10 18:05:45 +0100572 context['COUNT'] += 1
Harald Weltea6c0f882022-07-17 14:23:17 +0200573 df = self._cmd.lchan.selected_file
Philipp Maierac34dcc2021-04-01 17:19:05 +0200574
Philipp Maierea81f752022-05-19 10:13:30 +0200575 # The currently selected file (not the file we are going to export)
576 # must always be an ADF or DF. From this starting point we select
577 # the EF we want to export. To maintain consistency we will then
578 # select the current DF again (see comment below).
Harald Weltec91085e2022-02-10 18:05:45 +0100579 if not isinstance(df, CardDF):
580 raise RuntimeError(
581 "currently selected file %s is not a DF or ADF" % str(df))
Philipp Maierac34dcc2021-04-01 17:19:05 +0200582
Harald Weltec91085e2022-02-10 18:05:45 +0100583 df_path_list = df.fully_qualified_path(True)
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200584 df_path = df.fully_qualified_path_str(True)
585 df_path_fid = df.fully_qualified_path_str(False)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100586
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200587 file_str = df_path + "/" + str(filename)
Harald Weltec91085e2022-02-10 18:05:45 +0100588 self._cmd.poutput(boxed_heading_str(file_str))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100589
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200590 self._cmd.poutput("# directory: %s (%s)" % (df_path, df_path_fid))
Harald Weltec91085e2022-02-10 18:05:45 +0100591 try:
Harald Weltea6c0f882022-07-17 14:23:17 +0200592 fcp_dec = self._cmd.lchan.select(filename, self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100593 self._cmd.poutput("# file: %s (%s)" % (
Harald Weltea6c0f882022-07-17 14:23:17 +0200594 self._cmd.lchan.selected_file.name, self._cmd.lchan.selected_file.fid))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100595
Harald Weltea6c0f882022-07-17 14:23:17 +0200596 structure = self._cmd.lchan.selected_file_structure()
Harald Weltec91085e2022-02-10 18:05:45 +0100597 self._cmd.poutput("# structure: %s" % str(structure))
Harald Weltea6c0f882022-07-17 14:23:17 +0200598 self._cmd.poutput("# RAW FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp_hex))
599 self._cmd.poutput("# Decoded FCP Template: %s" % str(self._cmd.lchan.selected_file_fcp))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100600
Harald Weltec91085e2022-02-10 18:05:45 +0100601 for f in df_path_list:
602 self._cmd.poutput("select " + str(f))
Harald Weltea6c0f882022-07-17 14:23:17 +0200603 self._cmd.poutput("select " + self._cmd.lchan.selected_file.name)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100604
Harald Weltec91085e2022-02-10 18:05:45 +0100605 if structure == 'transparent':
Harald Welte08b11ab2022-02-10 18:56:41 +0100606 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200607 result = self._cmd.lchan.read_binary_dec()
Harald Welte08b11ab2022-02-10 18:56:41 +0100608 self._cmd.poutput("update_binary_decoded '%s'" % json.dumps(result[0], cls=JsonEncoder))
609 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200610 result = self._cmd.lchan.read_binary()
Harald Welte08b11ab2022-02-10 18:56:41 +0100611 self._cmd.poutput("update_binary " + str(result[0]))
Harald Weltec91085e2022-02-10 18:05:45 +0100612 elif structure == 'cyclic' or structure == 'linear_fixed':
613 # Use number of records specified in select response
Harald Weltea6c0f882022-07-17 14:23:17 +0200614 num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
Harald Welte747a9782022-02-13 17:52:28 +0100615 if num_of_rec:
Harald Weltec91085e2022-02-10 18:05:45 +0100616 for r in range(1, num_of_rec + 1):
Harald Welte08b11ab2022-02-10 18:56:41 +0100617 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200618 result = self._cmd.lchan.read_record_dec(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100619 self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
620 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200621 result = self._cmd.lchan.read_record(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100622 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
623
Harald Weltec91085e2022-02-10 18:05:45 +0100624 # When the select response does not return the number of records, read until we hit the
625 # first record that cannot be read.
626 else:
627 r = 1
628 while True:
629 try:
Harald Welte08b11ab2022-02-10 18:56:41 +0100630 if as_json:
Harald Weltea6c0f882022-07-17 14:23:17 +0200631 result = self._cmd.lchan.read_record_dec(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100632 self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
633 else:
Harald Weltea6c0f882022-07-17 14:23:17 +0200634 result = self._cmd.lchan.read_record(r)
Harald Welte08b11ab2022-02-10 18:56:41 +0100635 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
Harald Weltec91085e2022-02-10 18:05:45 +0100636 except SwMatchError as e:
637 # We are past the last valid record - stop
638 if e.sw_actual == "9402":
639 break
640 # Some other problem occurred
641 else:
642 raise e
Harald Weltec91085e2022-02-10 18:05:45 +0100643 r = r + 1
644 elif structure == 'ber_tlv':
Harald Weltea6c0f882022-07-17 14:23:17 +0200645 tags = self._cmd.lchan.retrieve_tags()
Harald Weltec91085e2022-02-10 18:05:45 +0100646 for t in tags:
Harald Weltea6c0f882022-07-17 14:23:17 +0200647 result = self._cmd.lchan.retrieve_data(t)
Harald Weltec91085e2022-02-10 18:05:45 +0100648 (tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
649 self._cmd.poutput("set_data 0x%02x %s" % (t, b2h(val)))
650 else:
651 raise RuntimeError(
652 'Unsupported structure "%s" of file "%s"' % (structure, filename))
653 except Exception as e:
Harald Welteb2e4b4a2022-07-19 23:48:45 +0200654 bad_file_str = df_path + "/" + str(filename) + ", " + str(e)
Harald Weltec91085e2022-02-10 18:05:45 +0100655 self._cmd.poutput("# bad file: %s" % bad_file_str)
656 context['ERR'] += 1
657 context['BAD'].append(bad_file_str)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100658
Harald Weltec91085e2022-02-10 18:05:45 +0100659 # When reading the file is done, make sure the parent file is
660 # selected again. This will be the usual case, however we need
661 # to check before since we must not select the same DF twice
Harald Weltea6c0f882022-07-17 14:23:17 +0200662 if df != self._cmd.lchan.selected_file:
663 self._cmd.lchan.select(df.fid or df.aid, self._cmd)
Philipp Maierac34dcc2021-04-01 17:19:05 +0200664
Harald Weltec91085e2022-02-10 18:05:45 +0100665 self._cmd.poutput("#")
Philipp Maier24f7bd32021-02-25 17:06:18 +0100666
Harald Weltec91085e2022-02-10 18:05:45 +0100667 export_parser = argparse.ArgumentParser()
668 export_parser.add_argument(
669 '--filename', type=str, default=None, help='only export specific file')
Harald Welte08b11ab2022-02-10 18:56:41 +0100670 export_parser.add_argument(
671 '--json', action='store_true', help='export as JSON (less reliable)')
Philipp Maier24f7bd32021-02-25 17:06:18 +0100672
Harald Weltec91085e2022-02-10 18:05:45 +0100673 @cmd2.with_argparser(export_parser)
674 def do_export(self, opts):
675 """Export files to script that can be imported back later"""
676 context = {'ERR': 0, 'COUNT': 0, 'BAD': [],
677 'DF_SKIP': 0, 'DF_SKIP_REASON': []}
Philipp Maier9a4091d2022-05-19 10:20:30 +0200678 kwargs_export = {'as_json': opts.json}
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200679 exception_str_add = ""
680
Harald Weltec91085e2022-02-10 18:05:45 +0100681 if opts.filename:
Philipp Maier7b138b02022-05-31 13:42:56 +0200682 self.export_ef(opts.filename, context, **kwargs_export)
Harald Weltec91085e2022-02-10 18:05:45 +0100683 else:
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200684 try:
685 self.walk(0, self.export_ef, None, context, **kwargs_export)
686 except Exception as e:
687 print("# Stopping early here due to exception: " + str(e))
688 print("#")
689 exception_str_add = ", also had to stop early due to exception:" + str(e)
Philipp Maier80ce71f2021-04-19 21:24:23 +0200690
Harald Weltec91085e2022-02-10 18:05:45 +0100691 self._cmd.poutput(boxed_heading_str("Export summary"))
Philipp Maier80ce71f2021-04-19 21:24:23 +0200692
Harald Weltec91085e2022-02-10 18:05:45 +0100693 self._cmd.poutput("# total files visited: %u" % context['COUNT'])
694 self._cmd.poutput("# bad files: %u" % context['ERR'])
695 for b in context['BAD']:
696 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200697
Harald Weltec91085e2022-02-10 18:05:45 +0100698 self._cmd.poutput("# skipped dedicated files(s): %u" %
699 context['DF_SKIP'])
700 for b in context['DF_SKIP_REASON']:
701 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200702
Harald Weltec91085e2022-02-10 18:05:45 +0100703 if context['ERR'] and context['DF_SKIP']:
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200704 raise RuntimeError("unable to export %i elementary file(s) and %i dedicated file(s)%s" % (
705 context['ERR'], context['DF_SKIP'], exception_str_add))
Harald Weltec91085e2022-02-10 18:05:45 +0100706 elif context['ERR']:
707 raise RuntimeError(
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200708 "unable to export %i elementary file(s)%s" % (context['ERR'], exception_str_add))
Harald Weltec91085e2022-02-10 18:05:45 +0100709 elif context['DF_SKIP']:
710 raise RuntimeError(
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200711 "unable to export %i dedicated files(s)%s" % (context['ERR'], exception_str_add))
Harald Welteb2edd142021-01-08 23:29:35 +0100712
Harald Weltec91085e2022-02-10 18:05:45 +0100713 def do_reset(self, opts):
714 """Reset the Card."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200715 atr = self._cmd.lchan.reset(self._cmd)
Harald Weltec91085e2022-02-10 18:05:45 +0100716 self._cmd.poutput('Card ATR: %s' % atr)
717 self._cmd.update_prompt()
Harald Weltedaf2b392021-05-03 23:17:29 +0200718
Harald Weltec91085e2022-02-10 18:05:45 +0100719 def do_desc(self, opts):
720 """Display human readable file description for the currently selected file"""
Harald Weltea6c0f882022-07-17 14:23:17 +0200721 desc = self._cmd.lchan.selected_file.desc
Harald Weltec91085e2022-02-10 18:05:45 +0100722 if desc:
723 self._cmd.poutput(desc)
724 else:
725 self._cmd.poutput("no description available")
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200726
Harald Weltec91085e2022-02-10 18:05:45 +0100727 def do_verify_adm(self, arg):
728 """VERIFY the ADM1 PIN"""
729 if arg:
730 # use specified ADM-PIN
731 pin_adm = sanitize_pin_adm(arg)
732 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:
745 self._cmd.card.verify_adm(h2b(pin_adm))
746 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)
753 self._cmd.poutput(" ATR: %s" % b2h(self._cmd.card._scc.get_atr()))
754 self._cmd.poutput(" ICCID: %s" % self._cmd.iccid)
755 self._cmd.poutput(" Class-Byte: %s" % self._cmd.card._scc.cla_byte)
756 self._cmd.poutput(" Select-Ctrl: %s" % self._cmd.card._scc.sel_ctrl)
757 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(
806 'pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
Harald Welte31d2cf02021-04-03 10:47:29 +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)
814 (data, sw) = self._cmd.card._scc.verify_chv(opts.pin_nr, h2b(pin))
815 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(
821 'puk_code', type=str, help='PUK code digits \"PUK1\" or \"PUK2\" to get PUK code from external data source')
822 unblock_chv_parser.add_argument(
823 'new_pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
Harald Welte31d2cf02021-04-03 10:47:29 +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)
830 (data, sw) = self._cmd.card._scc.unblock_chv(
831 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(
838 'pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
839 change_chv_parser.add_argument(
840 'new_pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
Harald Welte31d2cf02021-04-03 10:47:29 +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)
847 (data, sw) = self._cmd.card._scc.change_chv(
848 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(
855 'pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
Harald Welte31d2cf02021-04-03 10:47:29 +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)
861 (data, sw) = self._cmd.card._scc.disable_chv(opts.pin_nr, h2b(pin))
862 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(
868 'pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
Harald Welte31d2cf02021-04-03 10:47:29 +0200869
Harald Weltec91085e2022-02-10 18:05:45 +0100870 @cmd2.with_argparser(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)
874 (data, sw) = self._cmd.card._scc.enable_chv(opts.pin_nr, h2b(pin))
875 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 Weltec91085e2022-02-10 18:05:45 +0100879 (data, sw) = self._cmd.card._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."""
901 (data, sw) = self._cmd.card._scc.manage_channel(
902 mode='open', lchan_nr=opts.chan_nr)
Harald Welte703f9332021-04-10 18:39:32 +0200903
Harald Weltec91085e2022-02-10 18:05:45 +0100904 close_chan_parser = argparse.ArgumentParser()
905 close_chan_parser.add_argument(
906 'chan_nr', type=int, default=0, help='Channel Number')
Harald Welte703f9332021-04-10 18:39:32 +0200907
Harald Weltec91085e2022-02-10 18:05:45 +0100908 @cmd2.with_argparser(close_chan_parser)
909 def do_close_channel(self, opts):
910 """Close a logical channel."""
911 (data, sw) = self._cmd.card._scc.manage_channel(
912 mode='close', lchan_nr=opts.chan_nr)
Harald Welte703f9332021-04-10 18:39:32 +0200913
Harald Weltec91085e2022-02-10 18:05:45 +0100914 def do_status(self, opts):
915 """Perform the STATUS command."""
Harald Weltea6c0f882022-07-17 14:23:17 +0200916 fcp_dec = self._cmd.lchan.status()
Harald Weltec91085e2022-02-10 18:05:45 +0100917 self._cmd.poutput_json(fcp_dec)
Harald Welte34b05d32021-05-25 22:03:13 +0200918
Harald Weltec91085e2022-02-10 18:05:45 +0100919 suspend_uicc_parser = argparse.ArgumentParser()
920 suspend_uicc_parser.add_argument('--min-duration-secs', type=int, default=60,
921 help='Proposed minimum duration of suspension')
922 suspend_uicc_parser.add_argument('--max-duration-secs', type=int, default=24*60*60,
923 help='Proposed maximum duration of suspension')
Harald Welteec950532021-10-20 13:09:00 +0200924
Harald Weltec91085e2022-02-10 18:05:45 +0100925 # not ISO7816-4 but TS 102 221
926 @cmd2.with_argparser(suspend_uicc_parser)
927 def do_suspend_uicc(self, opts):
928 """Perform the SUSPEND UICC command. Only supported on some UICC."""
929 (duration, token, sw) = self._cmd.card._scc.suspend_uicc(min_len_secs=opts.min_duration_secs,
930 max_len_secs=opts.max_duration_secs)
931 self._cmd.poutput(
932 'Negotiated Duration: %u secs, Token: %s, SW: %s' % (duration, token, sw))
Harald Welteec950532021-10-20 13:09:00 +0200933
Harald Weltecab26c72022-08-06 16:12:30 +0200934class Proact(ProactiveHandler):
935 def receive_fetch(self, pcmd: ProactiveCommand):
936 # print its parsed representation
937 print(pcmd.decoded)
938 # TODO: implement the basics, such as SMS Sending, ...
939
940
Harald Welte703f9332021-04-10 18:39:32 +0200941
Harald Weltef2e761c2021-04-11 11:56:44 +0200942option_parser = argparse.ArgumentParser(prog='pySim-shell', description='interactive SIM card shell',
943 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
Harald Welte28c24312021-04-11 12:19:36 +0200944argparse_add_reader_args(option_parser)
Harald Weltec8ff0262021-04-11 12:06:13 +0200945
946global_group = option_parser.add_argument_group('General Options')
947global_group.add_argument('--script', metavar='PATH', default=None,
Harald Welte28c24312021-04-11 12:19:36 +0200948 help='script with pySim-shell commands to be executed automatically at start-up')
Harald Weltec91085e2022-02-10 18:05:45 +0100949global_group.add_argument('--csv', metavar='FILE',
950 default=None, help='Read card data from CSV file')
Philipp Maier76667642021-09-22 16:53:22 +0200951global_group.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
Harald Weltec91085e2022-02-10 18:05:45 +0100952 help="Use automatic card handling machine")
Harald Weltec8ff0262021-04-11 12:06:13 +0200953
954adm_group = global_group.add_mutually_exclusive_group()
955adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', default=None,
956 help='ADM PIN used for provisioning (overwrites default)')
957adm_group.add_argument('-A', '--pin-adm-hex', metavar='PIN_ADM1_HEX', dest='pin_adm_hex', default=None,
958 help='ADM PIN used for provisioning, as hex string (16 characters long)')
Harald Welteb2edd142021-01-08 23:29:35 +0100959
960
961if __name__ == '__main__':
962
Harald Weltec91085e2022-02-10 18:05:45 +0100963 # Parse options
964 opts = option_parser.parse_args()
Harald Welteb2edd142021-01-08 23:29:35 +0100965
Harald Weltec91085e2022-02-10 18:05:45 +0100966 # If a script file is specified, be sure that it actually exists
967 if opts.script:
968 if not os.access(opts.script, os.R_OK):
969 print("Invalid script file!")
970 sys.exit(2)
Philipp Maier13e258d2021-04-08 17:48:49 +0200971
Harald Weltec91085e2022-02-10 18:05:45 +0100972 # Register csv-file as card data provider, either from specified CSV
973 # or from CSV file in home directory
974 csv_default = str(Path.home()) + "/.osmocom/pysim/card_data.csv"
975 if opts.csv:
976 card_key_provider_register(CardKeyProviderCsv(opts.csv))
977 if os.path.isfile(csv_default):
978 card_key_provider_register(CardKeyProviderCsv(csv_default))
Philipp Maier2b11c322021-03-17 12:37:39 +0100979
Harald Weltec91085e2022-02-10 18:05:45 +0100980 # Init card reader driver
Harald Weltecab26c72022-08-06 16:12:30 +0200981 sl = init_reader(opts, proactive_handler = Proact())
Harald Weltec91085e2022-02-10 18:05:45 +0100982 if sl is None:
983 exit(1)
Philipp Maierea95c392021-09-16 13:10:19 +0200984
Harald Weltec91085e2022-02-10 18:05:45 +0100985 # Create command layer
986 scc = SimCardCommands(transport=sl)
Philipp Maierea95c392021-09-16 13:10:19 +0200987
Harald Weltec91085e2022-02-10 18:05:45 +0100988 # Create a card handler (for bulk provisioning)
989 if opts.card_handler_config:
990 ch = CardHandlerAuto(None, opts.card_handler_config)
991 else:
992 ch = CardHandler(sl)
Philipp Maier76667642021-09-22 16:53:22 +0200993
Harald Weltec91085e2022-02-10 18:05:45 +0100994 # Detect and initialize the card in the reader. This may fail when there
995 # is no card in the reader or the card is unresponsive. PysimApp is
996 # able to tolerate and recover from that.
997 try:
998 rs, card = init_card(sl)
999 app = PysimApp(card, rs, sl, ch, opts.script)
1000 except:
1001 print("Card initialization failed with an exception:")
1002 print("---------------------8<---------------------")
1003 traceback.print_exc()
1004 print("---------------------8<---------------------")
1005 print("(you may still try to recover from this manually by using the 'equip' command.)")
1006 print(
1007 " it should also be noted that some readers may behave strangely when no card")
1008 print(" is inserted.)")
1009 print("")
Philipp Maier7226c092022-06-01 17:58:38 +02001010 app = PysimApp(card, None, sl, ch, opts.script)
Philipp Maierea95c392021-09-16 13:10:19 +02001011
Harald Weltec91085e2022-02-10 18:05:45 +01001012 # If the user supplies an ADM PIN at via commandline args authenticate
1013 # immediately so that the user does not have to use the shell commands
1014 pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
1015 if pin_adm:
1016 if not card:
1017 print("Card error, cannot do ADM verification with supplied ADM pin now.")
1018 try:
1019 card.verify_adm(h2b(pin_adm))
1020 except Exception as e:
1021 print(e)
Philipp Maier228c98e2021-03-10 20:14:06 +01001022
Harald Weltec91085e2022-02-10 18:05:45 +01001023 app.cmdloop()