blob: 33332a226832088613a13086e6ba9cf3319086aa [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#
5# (C) 2021 by Harald Welte <laforge@osmocom.org>
6#
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
Vadim Yanitskiy0d9f0882022-07-14 19:08:24 +070026from cmd2 import style, fg
Harald Welteb2edd142021-01-08 23:29:35 +010027from cmd2 import CommandSet, with_default_category, with_argparser
28import argparse
29
30import os
31import sys
Philipp Maier2b11c322021-03-17 12:37:39 +010032from pathlib import Path
Philipp Maier76667642021-09-22 16:53:22 +020033from io import StringIO
Harald Welteb2edd142021-01-08 23:29:35 +010034
Robert Falkenberg9d16fbc2021-04-12 11:43:22 +020035from pySim.ts_51_011 import EF, DF, EF_SST_map
Harald Welteb2edd142021-01-08 23:29:35 +010036from pySim.ts_31_102 import EF_UST_map, EF_USIM_ADF_map
37from pySim.ts_31_103 import EF_IST_map, EF_ISIM_ADF_map
38
39from pySim.exceptions import *
40from pySim.commands import SimCardCommands
Harald Welte28c24312021-04-11 12:19:36 +020041from pySim.transport import init_reader, ApduTracer, argparse_add_reader_args
Philipp Maierbb73e512021-05-05 16:14:00 +020042from pySim.cards import card_detect, SimCard
Philipp Maiere7d1b672022-06-01 18:05:34 +020043from pySim.utils import h2b, swap_nibbles, rpad, b2h, h2s, JsonEncoder, bertlv_parse_one, sw_match
Philipp Maier80ce71f2021-04-19 21:24:23 +020044from pySim.utils import dec_st, sanitize_pin_adm, tabulate_str_list, is_hex, boxed_heading_str
Philipp Maier76667642021-09-22 16:53:22 +020045from pySim.card_handler import CardHandler, CardHandlerAuto
Harald Welteb2edd142021-01-08 23:29:35 +010046
Harald Weltef44256c2021-10-14 15:53:39 +020047from pySim.filesystem import CardMF, RuntimeState, CardDF, CardADF, CardModel
Philipp Maiera028c7d2021-11-08 16:12:03 +010048from pySim.profile import CardProfile
Harald Welteb2edd142021-01-08 23:29:35 +010049from pySim.ts_51_011 import CardProfileSIM, DF_TELECOM, DF_GSM
50from pySim.ts_102_221 import CardProfileUICC
Philipp Maiera028c7d2021-11-08 16:12:03 +010051from pySim.ts_102_221 import CardProfileUICCSIM
Harald Welte3c9b7842021-10-19 21:44:24 +020052from pySim.ts_102_222 import Ts102222Commands
Harald Welte5ce35242021-04-02 20:27:05 +020053from pySim.ts_31_102 import CardApplicationUSIM
54from pySim.ts_31_103 import CardApplicationISIM
Harald Welte95ce6b12021-10-20 18:40:54 +020055from pySim.ara_m import CardApplicationARAM
Harald Welte34eb5042022-02-21 17:19:28 +010056from pySim.global_platform import CardApplicationISD
Harald Welte2a33ad22021-10-20 10:14:18 +020057from pySim.gsm_r import DF_EIRENE
Harald Welteb2edd142021-01-08 23:29:35 +010058
Harald Welte4c1dca02021-10-14 17:48:25 +020059# we need to import this module so that the SysmocomSJA2 sub-class of
60# CardModel is created, which will add the ATR-based matching and
61# calling of SysmocomSJA2.add_files. See CardModel.apply_matching_models
Harald Weltef44256c2021-10-14 15:53:39 +020062import pySim.sysmocom_sja2
63
Harald Welte4442b3d2021-04-03 09:00:16 +020064from pySim.card_key_provider import CardKeyProviderCsv, card_key_provider_register, card_key_provider_get_field
Philipp Maier2b11c322021-03-17 12:37:39 +010065
Harald Weltec91085e2022-02-10 18:05:45 +010066
Philipp Maierea95c392021-09-16 13:10:19 +020067def init_card(sl):
Harald Weltec91085e2022-02-10 18:05:45 +010068 """
69 Detect card in reader and setup card profile and runtime state. This
70 function must be called at least once on startup. The card and runtime
71 state object (rs) is required for all pySim-shell commands.
72 """
Philipp Maierea95c392021-09-16 13:10:19 +020073
Harald Weltec91085e2022-02-10 18:05:45 +010074 # Wait up to three seconds for a card in reader and try to detect
75 # the card type.
76 print("Waiting for card...")
77 try:
78 sl.wait_for_card(3)
79 except NoCardError:
80 print("No card detected!")
81 return None, None
82 except:
83 print("Card not readable!")
84 return None, None
Philipp Maierea95c392021-09-16 13:10:19 +020085
Philipp Maier24031252022-06-14 16:16:42 +020086 generic_card = False
Harald Weltec91085e2022-02-10 18:05:45 +010087 card = card_detect("auto", scc)
88 if card is None:
89 print("Warning: Could not detect card type - assuming a generic card type...")
90 card = SimCard(scc)
Philipp Maier24031252022-06-14 16:16:42 +020091 generic_card = True
Philipp Maiera028c7d2021-11-08 16:12:03 +010092
Harald Weltec91085e2022-02-10 18:05:45 +010093 profile = CardProfile.pick(scc)
94 if profile is None:
95 print("Unsupported card type!")
Philipp Maier7226c092022-06-01 17:58:38 +020096 return None, card
Philipp Maierea95c392021-09-16 13:10:19 +020097
Philipp Maier24031252022-06-14 16:16:42 +020098 # ETSI TS 102 221, Table 9.3 specifies a default for the PIN key
99 # references, however card manufactures may still decide to pick an
100 # arbitrary key reference. In case we run on a generic card class that is
101 # detected as an UICC, we will pick the key reference that is officially
102 # specified.
103 if generic_card and isinstance(profile, CardProfileUICC):
104 card._adm_chv_num = 0x0A
105
Harald Weltec91085e2022-02-10 18:05:45 +0100106 print("Info: Card is of type: %s" % str(profile))
Philipp Maiera028c7d2021-11-08 16:12:03 +0100107
Harald Weltec91085e2022-02-10 18:05:45 +0100108 # FIXME: This shouln't be here, the profile should add the applications,
109 # however, we cannot simply put his into ts_102_221.py since we would
110 # have to e.g. import CardApplicationUSIM from ts_31_102.py, which already
111 # imports from ts_102_221.py. This means we will end up with a circular
112 # import, which needs to be resolved first.
113 if isinstance(profile, CardProfileUICC):
114 profile.add_application(CardApplicationUSIM())
115 profile.add_application(CardApplicationISIM())
116 profile.add_application(CardApplicationARAM())
Harald Welte34eb5042022-02-21 17:19:28 +0100117 profile.add_application(CardApplicationISD())
Philipp Maiera028c7d2021-11-08 16:12:03 +0100118
Harald Weltec91085e2022-02-10 18:05:45 +0100119 # Create runtime state with card profile
120 rs = RuntimeState(card, profile)
Philipp Maierea95c392021-09-16 13:10:19 +0200121
Harald Weltec91085e2022-02-10 18:05:45 +0100122 # FIXME: This is an GSM-R related file, it needs to be added throughout,
123 # the profile. At the moment we add it for all cards, this won't hurt,
124 # but regular SIM and UICC will not have it and fail to select it.
125 rs.mf.add_file(DF_EIRENE())
Philipp Maierea95c392021-09-16 13:10:19 +0200126
Harald Weltec91085e2022-02-10 18:05:45 +0100127 CardModel.apply_matching_models(scc, rs)
Harald Weltef44256c2021-10-14 15:53:39 +0200128
Harald Weltec91085e2022-02-10 18:05:45 +0100129 # inform the transport that we can do context-specific SW interpretation
130 sl.set_sw_interpreter(rs)
Philipp Maierea95c392021-09-16 13:10:19 +0200131
Harald Weltec91085e2022-02-10 18:05:45 +0100132 return rs, card
133
Philipp Maier2b11c322021-03-17 12:37:39 +0100134
Harald Welteb2edd142021-01-08 23:29:35 +0100135class PysimApp(cmd2.Cmd):
Harald Weltec91085e2022-02-10 18:05:45 +0100136 CUSTOM_CATEGORY = 'pySim Commands'
Philipp Maier76667642021-09-22 16:53:22 +0200137
Harald Weltec91085e2022-02-10 18:05:45 +0100138 def __init__(self, card, rs, sl, ch, script=None):
139 super().__init__(persistent_history_file='~/.pysim_shell_history', allow_cli_args=False,
140 use_ipython=True, auto_load_commands=False, startup_script=script)
141 self.intro = style('Welcome to pySim-shell!', fg=fg.red)
142 self.default_category = 'pySim-shell built-in commands'
143 self.card = None
144 self.rs = None
145 self.py_locals = {'card': self.card, 'rs': self.rs}
146 self.sl = sl
147 self.ch = ch
Harald Welte1748b932021-04-06 21:12:25 +0200148
Harald Weltec91085e2022-02-10 18:05:45 +0100149 self.numeric_path = False
150 self.add_settable(cmd2.Settable('numeric_path', bool, 'Print File IDs instead of names',
151 onchange_cb=self._onchange_numeric_path))
152 self.conserve_write = True
153 self.add_settable(cmd2.Settable('conserve_write', bool, 'Read and compare before write',
154 onchange_cb=self._onchange_conserve_write))
155 self.json_pretty_print = True
156 self.add_settable(cmd2.Settable('json_pretty_print',
157 bool, 'Pretty-Print JSON output'))
158 self.apdu_trace = False
159 self.add_settable(cmd2.Settable('apdu_trace', bool, 'Trace and display APDUs exchanged with card',
160 onchange_cb=self._onchange_apdu_trace))
Philipp Maier5d698e52021-09-16 13:18:01 +0200161
Harald Weltec91085e2022-02-10 18:05:45 +0100162 self.equip(card, rs)
Philipp Maier5d698e52021-09-16 13:18:01 +0200163
Harald Weltec91085e2022-02-10 18:05:45 +0100164 def equip(self, card, rs):
165 """
166 Equip pySim-shell with the supplied card and runtime state, add (or remove) all required settables and
167 and commands to enable card operations.
168 """
Philipp Maier76667642021-09-22 16:53:22 +0200169
Harald Weltec91085e2022-02-10 18:05:45 +0100170 rc = False
Philipp Maier5d698e52021-09-16 13:18:01 +0200171
Harald Weltec91085e2022-02-10 18:05:45 +0100172 # Unequip everything from pySim-shell that would not work in unequipped state
173 if self.rs:
174 self.rs.unregister_cmds(self)
175 for cmds in [Iso7816Commands, PySimCommands]:
176 cmd_set = self.find_commandsets(cmds)
177 if cmd_set:
178 self.unregister_command_set(cmd_set[0])
Philipp Maier5d698e52021-09-16 13:18:01 +0200179
Harald Weltec91085e2022-02-10 18:05:45 +0100180 self.card = card
181 self.rs = rs
Philipp Maier5d698e52021-09-16 13:18:01 +0200182
Harald Weltec91085e2022-02-10 18:05:45 +0100183 # When a card object and a runtime state is present, (re)equip pySim-shell with everything that is
184 # needed to operate on cards.
185 if self.card and self.rs:
186 self._onchange_conserve_write(
187 'conserve_write', False, self.conserve_write)
188 self._onchange_apdu_trace('apdu_trace', False, self.apdu_trace)
189 self.register_command_set(Iso7816Commands())
Harald Welte3c9b7842021-10-19 21:44:24 +0200190 self.register_command_set(Ts102222Commands())
Harald Weltec91085e2022-02-10 18:05:45 +0100191 self.register_command_set(PySimCommands())
192 self.iccid, sw = self.card.read_iccid()
193 rs.select('MF', self)
194 rc = True
195 else:
196 self.poutput("pySim-shell not equipped!")
Philipp Maier5d698e52021-09-16 13:18:01 +0200197
Harald Weltec91085e2022-02-10 18:05:45 +0100198 self.update_prompt()
199 return rc
Harald Welteb2edd142021-01-08 23:29:35 +0100200
Harald Weltec91085e2022-02-10 18:05:45 +0100201 def poutput_json(self, data, force_no_pretty=False):
202 """like cmd2.poutput() but for a JSON serializable dict."""
203 if force_no_pretty or self.json_pretty_print == False:
204 output = json.dumps(data, cls=JsonEncoder)
205 else:
206 output = json.dumps(data, cls=JsonEncoder, indent=4)
207 self.poutput(output)
Harald Welteb2edd142021-01-08 23:29:35 +0100208
Harald Weltec91085e2022-02-10 18:05:45 +0100209 def _onchange_numeric_path(self, param_name, old, new):
210 self.update_prompt()
Philipp Maier38c74f62021-03-17 17:19:52 +0100211
Harald Weltec91085e2022-02-10 18:05:45 +0100212 def _onchange_conserve_write(self, param_name, old, new):
213 if self.rs:
214 self.rs.conserve_write = new
Harald Welte7829d8a2021-04-10 11:28:53 +0200215
Harald Weltec91085e2022-02-10 18:05:45 +0100216 def _onchange_apdu_trace(self, param_name, old, new):
217 if self.card:
218 if new == True:
219 self.card._scc._tp.apdu_tracer = self.Cmd2ApduTracer(self)
220 else:
221 self.card._scc._tp.apdu_tracer = None
Harald Welte7829d8a2021-04-10 11:28:53 +0200222
Harald Weltec91085e2022-02-10 18:05:45 +0100223 class Cmd2ApduTracer(ApduTracer):
224 def __init__(self, cmd2_app):
225 self.cmd2 = app
Harald Welte7829d8a2021-04-10 11:28:53 +0200226
Harald Weltec91085e2022-02-10 18:05:45 +0100227 def trace_response(self, cmd, sw, resp):
228 self.cmd2.poutput("-> %s %s" % (cmd[:10], cmd[10:]))
229 self.cmd2.poutput("<- %s: %s" % (sw, resp))
Harald Welteb2edd142021-01-08 23:29:35 +0100230
Harald Weltec91085e2022-02-10 18:05:45 +0100231 def update_prompt(self):
232 if self.rs:
233 path_list = self.rs.selected_file.fully_qualified_path(
234 not self.numeric_path)
235 self.prompt = 'pySIM-shell (%s)> ' % ('/'.join(path_list))
236 else:
Philipp Maier7226c092022-06-01 17:58:38 +0200237 if self.card:
238 self.prompt = 'pySIM-shell (no card profile)> '
239 else:
240 self.prompt = 'pySIM-shell (no card)> '
Harald Welteb2edd142021-01-08 23:29:35 +0100241
Harald Weltec91085e2022-02-10 18:05:45 +0100242 @cmd2.with_category(CUSTOM_CATEGORY)
243 def do_intro(self, _):
244 """Display the intro banner"""
245 self.poutput(self.intro)
Philipp Maier9764de22021-11-03 10:44:39 +0100246
Harald Weltec91085e2022-02-10 18:05:45 +0100247 def do_eof(self, _: argparse.Namespace) -> bool:
248 self.poutput("")
249 return self.do_quit('')
Philipp Maier5d698e52021-09-16 13:18:01 +0200250
Harald Weltec91085e2022-02-10 18:05:45 +0100251 @cmd2.with_category(CUSTOM_CATEGORY)
252 def do_equip(self, opts):
253 """Equip pySim-shell with card"""
254 rs, card = init_card(sl)
255 self.equip(card, rs)
Philipp Maier76667642021-09-22 16:53:22 +0200256
Philipp Maier7226c092022-06-01 17:58:38 +0200257 apdu_cmd_parser = argparse.ArgumentParser()
258 apdu_cmd_parser.add_argument('APDU', type=str, help='APDU as hex string')
Philipp Maiere7d1b672022-06-01 18:05:34 +0200259 apdu_cmd_parser.add_argument('--expect-sw', help='expect a specified status word', type=str, default=None)
Philipp Maier7226c092022-06-01 17:58:38 +0200260
261 @cmd2.with_argparser(apdu_cmd_parser)
262 def do_apdu(self, opts):
263 """Send a raw APDU to the card, and print SW + Response.
264 DANGEROUS: pySim-shell will not know any card state changes, and
265 not continue to work as expected if you e.g. select a different
266 file."""
267 data, sw = self.card._scc._tp.send_apdu(opts.APDU)
268 if data:
269 self.poutput("SW: %s, RESP: %s" % (sw, data))
270 else:
271 self.poutput("SW: %s" % sw)
Philipp Maiere7d1b672022-06-01 18:05:34 +0200272 if opts.expect_sw:
273 if not sw_match(sw, opts.expect_sw):
274 raise SwMatchError(sw, opts.expect_sw)
Philipp Maier7226c092022-06-01 17:58:38 +0200275
Harald Weltec91085e2022-02-10 18:05:45 +0100276 class InterceptStderr(list):
277 def __init__(self):
278 self._stderr_backup = sys.stderr
Philipp Maier76667642021-09-22 16:53:22 +0200279
Harald Weltec91085e2022-02-10 18:05:45 +0100280 def __enter__(self):
281 self._stringio_stderr = StringIO()
282 sys.stderr = self._stringio_stderr
283 return self
Philipp Maier76667642021-09-22 16:53:22 +0200284
Harald Weltec91085e2022-02-10 18:05:45 +0100285 def __exit__(self, *args):
286 self.stderr = self._stringio_stderr.getvalue().strip()
287 del self._stringio_stderr
288 sys.stderr = self._stderr_backup
Philipp Maier76667642021-09-22 16:53:22 +0200289
Harald Weltec91085e2022-02-10 18:05:45 +0100290 def _show_failure_sign(self):
291 self.poutput(style(" +-------------+", fg=fg.bright_red))
292 self.poutput(style(" + ## ## +", fg=fg.bright_red))
293 self.poutput(style(" + ## ## +", fg=fg.bright_red))
294 self.poutput(style(" + ### +", fg=fg.bright_red))
295 self.poutput(style(" + ## ## +", fg=fg.bright_red))
296 self.poutput(style(" + ## ## +", fg=fg.bright_red))
297 self.poutput(style(" +-------------+", fg=fg.bright_red))
298 self.poutput("")
Philipp Maier76667642021-09-22 16:53:22 +0200299
Harald Weltec91085e2022-02-10 18:05:45 +0100300 def _show_success_sign(self):
301 self.poutput(style(" +-------------+", fg=fg.bright_green))
302 self.poutput(style(" + ## +", fg=fg.bright_green))
303 self.poutput(style(" + ## +", fg=fg.bright_green))
304 self.poutput(style(" + # ## +", fg=fg.bright_green))
305 self.poutput(style(" + ## # +", fg=fg.bright_green))
306 self.poutput(style(" + ## +", fg=fg.bright_green))
307 self.poutput(style(" +-------------+", fg=fg.bright_green))
308 self.poutput("")
Philipp Maier76667642021-09-22 16:53:22 +0200309
Harald Weltec91085e2022-02-10 18:05:45 +0100310 def _process_card(self, first, script_path):
Philipp Maier76667642021-09-22 16:53:22 +0200311
Harald Weltec91085e2022-02-10 18:05:45 +0100312 # Early phase of card initialzation (this part may fail with an exception)
313 try:
314 rs, card = init_card(self.sl)
315 rc = self.equip(card, rs)
316 except:
317 self.poutput("")
318 self.poutput("Card initialization failed with an exception:")
319 self.poutput("---------------------8<---------------------")
320 traceback.print_exc()
321 self.poutput("---------------------8<---------------------")
322 self.poutput("")
323 return -1
Philipp Maier76667642021-09-22 16:53:22 +0200324
Harald Weltec91085e2022-02-10 18:05:45 +0100325 # Actual card processing step. This part should never fail with an exception since the cmd2
326 # do_run_script method will catch any exception that might occur during script execution.
327 if rc:
328 self.poutput("")
329 self.poutput("Transcript stdout:")
330 self.poutput("---------------------8<---------------------")
331 with self.InterceptStderr() as logged:
332 self.do_run_script(script_path)
333 self.poutput("---------------------8<---------------------")
Philipp Maier76667642021-09-22 16:53:22 +0200334
Harald Weltec91085e2022-02-10 18:05:45 +0100335 self.poutput("")
336 self.poutput("Transcript stderr:")
337 if logged.stderr:
338 self.poutput("---------------------8<---------------------")
339 self.poutput(logged.stderr)
340 self.poutput("---------------------8<---------------------")
341 else:
342 self.poutput("(none)")
Philipp Maier76667642021-09-22 16:53:22 +0200343
Harald Weltec91085e2022-02-10 18:05:45 +0100344 # Check for exceptions
345 self.poutput("")
346 if "EXCEPTION of type" not in logged.stderr:
347 return 0
Philipp Maier76667642021-09-22 16:53:22 +0200348
Harald Weltec91085e2022-02-10 18:05:45 +0100349 return -1
Philipp Maier76667642021-09-22 16:53:22 +0200350
Harald Weltec91085e2022-02-10 18:05:45 +0100351 bulk_script_parser = argparse.ArgumentParser()
352 bulk_script_parser.add_argument(
353 'script_path', help="path to the script file")
354 bulk_script_parser.add_argument('--halt_on_error', help='stop card handling if an exeption occurs',
355 action='store_true')
356 bulk_script_parser.add_argument('--tries', type=int, default=2,
357 help='how many tries before trying the next card')
358 bulk_script_parser.add_argument('--on_stop_action', type=str, default=None,
359 help='commandline to execute when card handling has stopped')
360 bulk_script_parser.add_argument('--pre_card_action', type=str, default=None,
361 help='commandline to execute before actually talking to the card')
Philipp Maier76667642021-09-22 16:53:22 +0200362
Harald Weltec91085e2022-02-10 18:05:45 +0100363 @cmd2.with_argparser(bulk_script_parser)
364 @cmd2.with_category(CUSTOM_CATEGORY)
365 def do_bulk_script(self, opts):
366 """Run script on multiple cards (bulk provisioning)"""
Philipp Maier76667642021-09-22 16:53:22 +0200367
Harald Weltec91085e2022-02-10 18:05:45 +0100368 # Make sure that the script file exists and that it is readable.
369 if not os.access(opts.script_path, os.R_OK):
370 self.poutput("Invalid script file!")
371 return
Philipp Maier76667642021-09-22 16:53:22 +0200372
Harald Weltec91085e2022-02-10 18:05:45 +0100373 success_count = 0
374 fail_count = 0
Philipp Maier76667642021-09-22 16:53:22 +0200375
Harald Weltec91085e2022-02-10 18:05:45 +0100376 first = True
377 while 1:
378 # TODO: Count consecutive failures, if more than N consecutive failures occur, then stop.
379 # The ratinale is: There may be a problem with the device, we do want to prevent that
380 # all remaining cards are fired to the error bin. This is only relevant for situations
381 # with large stacks, probably we do not need this feature right now.
Philipp Maier76667642021-09-22 16:53:22 +0200382
Harald Weltec91085e2022-02-10 18:05:45 +0100383 try:
384 # In case of failure, try multiple times.
385 for i in range(opts.tries):
386 # fetch card into reader bay
387 ch.get(first)
Philipp Maier76667642021-09-22 16:53:22 +0200388
Harald Weltec91085e2022-02-10 18:05:45 +0100389 # if necessary execute an action before we start processing the card
390 if(opts.pre_card_action):
391 os.system(opts.pre_card_action)
Philipp Maier76667642021-09-22 16:53:22 +0200392
Harald Weltec91085e2022-02-10 18:05:45 +0100393 # process the card
394 rc = self._process_card(first, opts.script_path)
395 if rc == 0:
396 success_count = success_count + 1
397 self._show_success_sign()
398 self.poutput("Statistics: success :%i, failure: %i" % (
399 success_count, fail_count))
400 break
401 else:
402 fail_count = fail_count + 1
403 self._show_failure_sign()
404 self.poutput("Statistics: success :%i, failure: %i" % (
405 success_count, fail_count))
Philipp Maier76667642021-09-22 16:53:22 +0200406
Harald Weltec91085e2022-02-10 18:05:45 +0100407 # Depending on success or failure, the card goes either in the "error" bin or in the
408 # "done" bin.
409 if rc < 0:
410 ch.error()
411 else:
412 ch.done()
Philipp Maier76667642021-09-22 16:53:22 +0200413
Harald Weltec91085e2022-02-10 18:05:45 +0100414 # In most cases it is possible to proceed with the next card, but the
415 # user may decide to halt immediately when an error occurs
416 if opts.halt_on_error and rc < 0:
417 return
Philipp Maier76667642021-09-22 16:53:22 +0200418
Harald Weltec91085e2022-02-10 18:05:45 +0100419 except (KeyboardInterrupt):
420 self.poutput("")
421 self.poutput("Terminated by user!")
422 return
423 except (SystemExit):
424 # When all cards are processed the card handler device will throw a SystemExit
425 # exception. Also Errors that are not recoverable (cards stuck etc.) will end up here.
426 # The user has the option to execute some action to make aware that the card handler
427 # needs service.
428 if(opts.on_stop_action):
429 os.system(opts.on_stop_action)
430 return
431 except:
432 self.poutput("")
433 self.poutput("Card handling failed with an exception:")
434 self.poutput("---------------------8<---------------------")
435 traceback.print_exc()
436 self.poutput("---------------------8<---------------------")
437 self.poutput("")
438 fail_count = fail_count + 1
439 self._show_failure_sign()
440 self.poutput("Statistics: success :%i, failure: %i" %
441 (success_count, fail_count))
Philipp Maierb52feed2021-09-22 16:43:13 +0200442
Harald Weltec91085e2022-02-10 18:05:45 +0100443 first = False
444
445 echo_parser = argparse.ArgumentParser()
446 echo_parser.add_argument('string', help="string to echo on the shell")
447
448 @cmd2.with_argparser(echo_parser)
449 @cmd2.with_category(CUSTOM_CATEGORY)
450 def do_echo(self, opts):
451 """Echo (print) a string on the console"""
452 self.poutput(opts.string)
453
Harald Welteb2edd142021-01-08 23:29:35 +0100454
Harald Welte31d2cf02021-04-03 10:47:29 +0200455@with_default_category('pySim Commands')
456class PySimCommands(CommandSet):
Harald Weltec91085e2022-02-10 18:05:45 +0100457 def __init__(self):
458 super().__init__()
Harald Welteb2edd142021-01-08 23:29:35 +0100459
Harald Weltec91085e2022-02-10 18:05:45 +0100460 dir_parser = argparse.ArgumentParser()
461 dir_parser.add_argument(
462 '--fids', help='Show file identifiers', action='store_true')
463 dir_parser.add_argument(
464 '--names', help='Show file names', action='store_true')
465 dir_parser.add_argument(
466 '--apps', help='Show applications', action='store_true')
467 dir_parser.add_argument(
468 '--all', help='Show all selectable identifiers and names', action='store_true')
Philipp Maier5d3e2592021-02-22 17:22:16 +0100469
Harald Weltec91085e2022-02-10 18:05:45 +0100470 @cmd2.with_argparser(dir_parser)
471 def do_dir(self, opts):
472 """Show a listing of files available in currently selected DF or MF"""
473 if opts.all:
474 flags = []
475 elif opts.fids or opts.names or opts.apps:
476 flags = ['PARENT', 'SELF']
477 if opts.fids:
478 flags += ['FIDS', 'AIDS']
479 if opts.names:
480 flags += ['FNAMES', 'ANAMES']
481 if opts.apps:
482 flags += ['ANAMES', 'AIDS']
483 else:
484 flags = ['PARENT', 'SELF', 'FNAMES', 'ANAMES']
485 selectables = list(
486 self._cmd.rs.selected_file.get_selectable_names(flags=flags))
487 directory_str = tabulate_str_list(
488 selectables, width=79, hspace=2, lspace=1, align_left=True)
489 path_list = self._cmd.rs.selected_file.fully_qualified_path(True)
490 self._cmd.poutput('/'.join(path_list))
491 path_list = self._cmd.rs.selected_file.fully_qualified_path(False)
492 self._cmd.poutput('/'.join(path_list))
493 self._cmd.poutput(directory_str)
494 self._cmd.poutput("%d files" % len(selectables))
Harald Welteb2edd142021-01-08 23:29:35 +0100495
Philipp Maier7b138b02022-05-31 13:42:56 +0200496 def walk(self, indent=0, action_ef=None, action_df=None, context=None, **kwargs):
Harald Weltec91085e2022-02-10 18:05:45 +0100497 """Recursively walk through the file system, starting at the currently selected DF"""
Philipp Maier7b138b02022-05-31 13:42:56 +0200498
499 if isinstance(self._cmd.rs.selected_file, CardDF):
500 if action_df:
501 action_df(context, opts)
502
Harald Weltec91085e2022-02-10 18:05:45 +0100503 files = self._cmd.rs.selected_file.get_selectables(
504 flags=['FNAMES', 'ANAMES'])
505 for f in files:
Philipp Maier7b138b02022-05-31 13:42:56 +0200506 # special case: When no action is performed, just output a directory
507 if not action_ef and not action_df:
Harald Weltec91085e2022-02-10 18:05:45 +0100508 output_str = " " * indent + str(f) + (" " * 250)
509 output_str = output_str[0:25]
510 if isinstance(files[f], CardADF):
511 output_str += " " + str(files[f].aid)
512 else:
513 output_str += " " + str(files[f].fid)
514 output_str += " " + str(files[f].desc)
515 self._cmd.poutput(output_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200516
Harald Weltec91085e2022-02-10 18:05:45 +0100517 if isinstance(files[f], CardDF):
518 skip_df = False
519 try:
520 fcp_dec = self._cmd.rs.select(f, self._cmd)
521 except Exception as e:
522 skip_df = True
523 df = self._cmd.rs.selected_file
524 df_path_list = df.fully_qualified_path(True)
525 df_skip_reason_str = '/'.join(df_path_list) + \
526 "/" + str(f) + ", " + str(e)
527 if context:
528 context['DF_SKIP'] += 1
529 context['DF_SKIP_REASON'].append(df_skip_reason_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200530
Harald Weltec91085e2022-02-10 18:05:45 +0100531 # If the DF was skipped, we never have entered the directory
532 # below, so we must not move up.
533 if skip_df == False:
Philipp Maier7b138b02022-05-31 13:42:56 +0200534 self.walk(indent + 1, action_ef, action_df, context, **kwargs)
Harald Weltec91085e2022-02-10 18:05:45 +0100535 fcp_dec = self._cmd.rs.select("..", self._cmd)
Philipp Maierf408a402021-04-09 21:16:12 +0200536
Philipp Maier7b138b02022-05-31 13:42:56 +0200537 elif action_ef:
Harald Weltec91085e2022-02-10 18:05:45 +0100538 df_before_action = self._cmd.rs.selected_file
Philipp Maier7b138b02022-05-31 13:42:56 +0200539 action_ef(f, context, **kwargs)
Harald Weltec91085e2022-02-10 18:05:45 +0100540 # When walking through the file system tree the action must not
541 # always restore the currently selected file to the file that
542 # was selected before executing the action() callback.
543 if df_before_action != self._cmd.rs.selected_file:
544 raise RuntimeError("inconsistent walk, %s is currently selected but expecting %s to be selected"
545 % (str(self._cmd.rs.selected_file), str(df_before_action)))
Philipp Maierff9dae22021-02-25 17:03:21 +0100546
Harald Weltec91085e2022-02-10 18:05:45 +0100547 def do_tree(self, opts):
548 """Display a filesystem-tree with all selectable files"""
549 self.walk()
Philipp Maierff9dae22021-02-25 17:03:21 +0100550
Philipp Maier7b138b02022-05-31 13:42:56 +0200551 def export_ef(self, filename, context, as_json):
552 """ Select and export a single elementary file (EF) """
Harald Weltec91085e2022-02-10 18:05:45 +0100553 context['COUNT'] += 1
554 df = self._cmd.rs.selected_file
Philipp Maierac34dcc2021-04-01 17:19:05 +0200555
Philipp Maierea81f752022-05-19 10:13:30 +0200556 # The currently selected file (not the file we are going to export)
557 # must always be an ADF or DF. From this starting point we select
558 # the EF we want to export. To maintain consistency we will then
559 # select the current DF again (see comment below).
Harald Weltec91085e2022-02-10 18:05:45 +0100560 if not isinstance(df, CardDF):
561 raise RuntimeError(
562 "currently selected file %s is not a DF or ADF" % str(df))
Philipp Maierac34dcc2021-04-01 17:19:05 +0200563
Harald Weltec91085e2022-02-10 18:05:45 +0100564 df_path_list = df.fully_qualified_path(True)
565 df_path_list_fid = df.fully_qualified_path(False)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100566
Harald Weltec91085e2022-02-10 18:05:45 +0100567 file_str = '/'.join(df_path_list) + "/" + str(filename)
568 self._cmd.poutput(boxed_heading_str(file_str))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100569
Harald Weltec91085e2022-02-10 18:05:45 +0100570 self._cmd.poutput("# directory: %s (%s)" %
571 ('/'.join(df_path_list), '/'.join(df_path_list_fid)))
572 try:
573 fcp_dec = self._cmd.rs.select(filename, self._cmd)
574 self._cmd.poutput("# file: %s (%s)" % (
575 self._cmd.rs.selected_file.name, self._cmd.rs.selected_file.fid))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100576
Harald Welte747a9782022-02-13 17:52:28 +0100577 structure = self._cmd.rs.selected_file_structure()
Harald Weltec91085e2022-02-10 18:05:45 +0100578 self._cmd.poutput("# structure: %s" % str(structure))
Harald Welte2bb17f32022-02-15 15:41:55 +0100579 self._cmd.poutput("# RAW FCP Template: %s" % str(self._cmd.rs.selected_file_fcp_hex))
580 self._cmd.poutput("# Decoded FCP Template: %s" % str(self._cmd.rs.selected_file_fcp))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100581
Harald Weltec91085e2022-02-10 18:05:45 +0100582 for f in df_path_list:
583 self._cmd.poutput("select " + str(f))
584 self._cmd.poutput("select " + self._cmd.rs.selected_file.name)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100585
Harald Weltec91085e2022-02-10 18:05:45 +0100586 if structure == 'transparent':
Harald Welte08b11ab2022-02-10 18:56:41 +0100587 if as_json:
588 result = self._cmd.rs.read_binary_dec()
589 self._cmd.poutput("update_binary_decoded '%s'" % json.dumps(result[0], cls=JsonEncoder))
590 else:
591 result = self._cmd.rs.read_binary()
592 self._cmd.poutput("update_binary " + str(result[0]))
Harald Weltec91085e2022-02-10 18:05:45 +0100593 elif structure == 'cyclic' or structure == 'linear_fixed':
594 # Use number of records specified in select response
Harald Welte747a9782022-02-13 17:52:28 +0100595 num_of_rec = self._cmd.rs.selected_file_num_of_rec()
596 if num_of_rec:
Harald Weltec91085e2022-02-10 18:05:45 +0100597 for r in range(1, num_of_rec + 1):
Harald Welte08b11ab2022-02-10 18:56:41 +0100598 if as_json:
599 result = self._cmd.rs.read_record_dec(r)
600 self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
601 else:
602 result = self._cmd.rs.read_record(r)
603 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
604
Harald Weltec91085e2022-02-10 18:05:45 +0100605 # When the select response does not return the number of records, read until we hit the
606 # first record that cannot be read.
607 else:
608 r = 1
609 while True:
610 try:
Harald Welte08b11ab2022-02-10 18:56:41 +0100611 if as_json:
612 result = self._cmd.rs.read_record_dec(r)
613 self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
614 else:
615 result = self._cmd.rs.read_record(r)
616 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
Harald Weltec91085e2022-02-10 18:05:45 +0100617 except SwMatchError as e:
618 # We are past the last valid record - stop
619 if e.sw_actual == "9402":
620 break
621 # Some other problem occurred
622 else:
623 raise e
Harald Weltec91085e2022-02-10 18:05:45 +0100624 r = r + 1
625 elif structure == 'ber_tlv':
626 tags = self._cmd.rs.retrieve_tags()
627 for t in tags:
628 result = self._cmd.rs.retrieve_data(t)
629 (tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
630 self._cmd.poutput("set_data 0x%02x %s" % (t, b2h(val)))
631 else:
632 raise RuntimeError(
633 'Unsupported structure "%s" of file "%s"' % (structure, filename))
634 except Exception as e:
635 bad_file_str = '/'.join(df_path_list) + \
636 "/" + str(filename) + ", " + str(e)
637 self._cmd.poutput("# bad file: %s" % bad_file_str)
638 context['ERR'] += 1
639 context['BAD'].append(bad_file_str)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100640
Harald Weltec91085e2022-02-10 18:05:45 +0100641 # When reading the file is done, make sure the parent file is
642 # selected again. This will be the usual case, however we need
643 # to check before since we must not select the same DF twice
644 if df != self._cmd.rs.selected_file:
645 self._cmd.rs.select(df.fid or df.aid, self._cmd)
Philipp Maierac34dcc2021-04-01 17:19:05 +0200646
Harald Weltec91085e2022-02-10 18:05:45 +0100647 self._cmd.poutput("#")
Philipp Maier24f7bd32021-02-25 17:06:18 +0100648
Harald Weltec91085e2022-02-10 18:05:45 +0100649 export_parser = argparse.ArgumentParser()
650 export_parser.add_argument(
651 '--filename', type=str, default=None, help='only export specific file')
Harald Welte08b11ab2022-02-10 18:56:41 +0100652 export_parser.add_argument(
653 '--json', action='store_true', help='export as JSON (less reliable)')
Philipp Maier24f7bd32021-02-25 17:06:18 +0100654
Harald Weltec91085e2022-02-10 18:05:45 +0100655 @cmd2.with_argparser(export_parser)
656 def do_export(self, opts):
657 """Export files to script that can be imported back later"""
658 context = {'ERR': 0, 'COUNT': 0, 'BAD': [],
659 'DF_SKIP': 0, 'DF_SKIP_REASON': []}
Philipp Maier9a4091d2022-05-19 10:20:30 +0200660 kwargs_export = {'as_json': opts.json}
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200661 exception_str_add = ""
662
Harald Weltec91085e2022-02-10 18:05:45 +0100663 if opts.filename:
Philipp Maier7b138b02022-05-31 13:42:56 +0200664 self.export_ef(opts.filename, context, **kwargs_export)
Harald Weltec91085e2022-02-10 18:05:45 +0100665 else:
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200666 try:
667 self.walk(0, self.export_ef, None, context, **kwargs_export)
668 except Exception as e:
669 print("# Stopping early here due to exception: " + str(e))
670 print("#")
671 exception_str_add = ", also had to stop early due to exception:" + str(e)
Philipp Maier80ce71f2021-04-19 21:24:23 +0200672
Harald Weltec91085e2022-02-10 18:05:45 +0100673 self._cmd.poutput(boxed_heading_str("Export summary"))
Philipp Maier80ce71f2021-04-19 21:24:23 +0200674
Harald Weltec91085e2022-02-10 18:05:45 +0100675 self._cmd.poutput("# total files visited: %u" % context['COUNT'])
676 self._cmd.poutput("# bad files: %u" % context['ERR'])
677 for b in context['BAD']:
678 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200679
Harald Weltec91085e2022-02-10 18:05:45 +0100680 self._cmd.poutput("# skipped dedicated files(s): %u" %
681 context['DF_SKIP'])
682 for b in context['DF_SKIP_REASON']:
683 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200684
Harald Weltec91085e2022-02-10 18:05:45 +0100685 if context['ERR'] and context['DF_SKIP']:
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200686 raise RuntimeError("unable to export %i elementary file(s) and %i dedicated file(s)%s" % (
687 context['ERR'], context['DF_SKIP'], exception_str_add))
Harald Weltec91085e2022-02-10 18:05:45 +0100688 elif context['ERR']:
689 raise RuntimeError(
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200690 "unable to export %i elementary file(s)%s" % (context['ERR'], exception_str_add))
Harald Weltec91085e2022-02-10 18:05:45 +0100691 elif context['DF_SKIP']:
692 raise RuntimeError(
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200693 "unable to export %i dedicated files(s)%s" % (context['ERR'], exception_str_add))
Harald Welteb2edd142021-01-08 23:29:35 +0100694
Harald Weltec91085e2022-02-10 18:05:45 +0100695 def do_reset(self, opts):
696 """Reset the Card."""
697 atr = self._cmd.rs.reset(self._cmd)
698 self._cmd.poutput('Card ATR: %s' % atr)
699 self._cmd.update_prompt()
Harald Weltedaf2b392021-05-03 23:17:29 +0200700
Harald Weltec91085e2022-02-10 18:05:45 +0100701 def do_desc(self, opts):
702 """Display human readable file description for the currently selected file"""
703 desc = self._cmd.rs.selected_file.desc
704 if desc:
705 self._cmd.poutput(desc)
706 else:
707 self._cmd.poutput("no description available")
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200708
Harald Weltec91085e2022-02-10 18:05:45 +0100709 def do_verify_adm(self, arg):
710 """VERIFY the ADM1 PIN"""
711 if arg:
712 # use specified ADM-PIN
713 pin_adm = sanitize_pin_adm(arg)
714 else:
715 # try to find an ADM-PIN if none is specified
716 result = card_key_provider_get_field(
717 'ADM1', key='ICCID', value=self._cmd.iccid)
718 pin_adm = sanitize_pin_adm(result)
719 if pin_adm:
720 self._cmd.poutput(
721 "found ADM-PIN '%s' for ICCID '%s'" % (result, self._cmd.iccid))
722 else:
723 raise ValueError(
724 "cannot find ADM-PIN for ICCID '%s'" % (self._cmd.iccid))
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200725
Harald Weltec91085e2022-02-10 18:05:45 +0100726 if pin_adm:
727 self._cmd.card.verify_adm(h2b(pin_adm))
728 else:
729 raise ValueError("error: cannot authenticate, no adm-pin!")
730
Harald Welteb2edd142021-01-08 23:29:35 +0100731
Harald Welte31d2cf02021-04-03 10:47:29 +0200732@with_default_category('ISO7816 Commands')
733class Iso7816Commands(CommandSet):
Harald Weltec91085e2022-02-10 18:05:45 +0100734 def __init__(self):
735 super().__init__()
Harald Welte31d2cf02021-04-03 10:47:29 +0200736
Harald Weltec91085e2022-02-10 18:05:45 +0100737 def do_select(self, opts):
738 """SELECT a File (ADF/DF/EF)"""
739 if len(opts.arg_list) == 0:
740 path_list = self._cmd.rs.selected_file.fully_qualified_path(True)
741 path_list_fid = self._cmd.rs.selected_file.fully_qualified_path(
742 False)
743 self._cmd.poutput("currently selected file: " +
744 '/'.join(path_list) + " (" + '/'.join(path_list_fid) + ")")
745 return
Harald Welte31d2cf02021-04-03 10:47:29 +0200746
Harald Weltec91085e2022-02-10 18:05:45 +0100747 path = opts.arg_list[0]
748 fcp_dec = self._cmd.rs.select(path, self._cmd)
749 self._cmd.update_prompt()
750 self._cmd.poutput_json(fcp_dec)
Harald Welte31d2cf02021-04-03 10:47:29 +0200751
Harald Weltec91085e2022-02-10 18:05:45 +0100752 def complete_select(self, text, line, begidx, endidx) -> List[str]:
753 """Command Line tab completion for SELECT"""
754 index_dict = {1: self._cmd.rs.selected_file.get_selectable_names()}
755 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200756
Harald Weltec91085e2022-02-10 18:05:45 +0100757 def get_code(self, code):
758 """Use code either directly or try to get it from external data source"""
759 auto = ('PIN1', 'PIN2', 'PUK1', 'PUK2')
Harald Welte31d2cf02021-04-03 10:47:29 +0200760
Harald Weltec91085e2022-02-10 18:05:45 +0100761 if str(code).upper() not in auto:
762 return sanitize_pin_adm(code)
Harald Welte31d2cf02021-04-03 10:47:29 +0200763
Harald Weltec91085e2022-02-10 18:05:45 +0100764 result = card_key_provider_get_field(
765 str(code), key='ICCID', value=self._cmd.iccid)
766 result = sanitize_pin_adm(result)
767 if result:
768 self._cmd.poutput("found %s '%s' for ICCID '%s'" %
769 (code.upper(), result, self._cmd.iccid))
770 else:
771 self._cmd.poutput("cannot find %s for ICCID '%s'" %
772 (code.upper(), self._cmd.iccid))
773 return result
Harald Welte31d2cf02021-04-03 10:47:29 +0200774
Harald Weltec91085e2022-02-10 18:05:45 +0100775 verify_chv_parser = argparse.ArgumentParser()
776 verify_chv_parser.add_argument(
777 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
778 verify_chv_parser.add_argument(
779 '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 +0200780
Harald Weltec91085e2022-02-10 18:05:45 +0100781 @cmd2.with_argparser(verify_chv_parser)
782 def do_verify_chv(self, opts):
Harald Welte12af7932022-02-15 16:39:08 +0100783 """Verify (authenticate) using specified CHV (PIN) code, which is how the specifications
784 call it if you authenticate yourself using the specified PIN. There usually is at least PIN1 and
785 PIN2."""
Harald Weltec91085e2022-02-10 18:05:45 +0100786 pin = self.get_code(opts.pin_code)
787 (data, sw) = self._cmd.card._scc.verify_chv(opts.pin_nr, h2b(pin))
788 self._cmd.poutput("CHV verification successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200789
Harald Weltec91085e2022-02-10 18:05:45 +0100790 unblock_chv_parser = argparse.ArgumentParser()
791 unblock_chv_parser.add_argument(
792 '--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
793 unblock_chv_parser.add_argument(
794 'puk_code', type=str, help='PUK code digits \"PUK1\" or \"PUK2\" to get PUK code from external data source')
795 unblock_chv_parser.add_argument(
796 '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 +0200797
Harald Weltec91085e2022-02-10 18:05:45 +0100798 @cmd2.with_argparser(unblock_chv_parser)
799 def do_unblock_chv(self, opts):
800 """Unblock PIN code using specified PUK code"""
801 new_pin = self.get_code(opts.new_pin_code)
802 puk = self.get_code(opts.puk_code)
803 (data, sw) = self._cmd.card._scc.unblock_chv(
804 opts.pin_nr, h2b(puk), h2b(new_pin))
805 self._cmd.poutput("CHV unblock successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200806
Harald Weltec91085e2022-02-10 18:05:45 +0100807 change_chv_parser = argparse.ArgumentParser()
808 change_chv_parser.add_argument(
809 '--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
810 change_chv_parser.add_argument(
811 'pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
812 change_chv_parser.add_argument(
813 '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 +0200814
Harald Weltec91085e2022-02-10 18:05:45 +0100815 @cmd2.with_argparser(change_chv_parser)
816 def do_change_chv(self, opts):
817 """Change PIN code to a new PIN code"""
818 new_pin = self.get_code(opts.new_pin_code)
819 pin = self.get_code(opts.pin_code)
820 (data, sw) = self._cmd.card._scc.change_chv(
821 opts.pin_nr, h2b(pin), h2b(new_pin))
822 self._cmd.poutput("CHV change successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200823
Harald Weltec91085e2022-02-10 18:05:45 +0100824 disable_chv_parser = argparse.ArgumentParser()
825 disable_chv_parser.add_argument(
826 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
827 disable_chv_parser.add_argument(
828 '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 +0200829
Harald Weltec91085e2022-02-10 18:05:45 +0100830 @cmd2.with_argparser(disable_chv_parser)
831 def do_disable_chv(self, opts):
832 """Disable PIN code using specified PIN code"""
833 pin = self.get_code(opts.pin_code)
834 (data, sw) = self._cmd.card._scc.disable_chv(opts.pin_nr, h2b(pin))
835 self._cmd.poutput("CHV disable successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200836
Harald Weltec91085e2022-02-10 18:05:45 +0100837 enable_chv_parser = argparse.ArgumentParser()
838 enable_chv_parser.add_argument(
839 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
840 enable_chv_parser.add_argument(
841 '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 +0200842
Harald Weltec91085e2022-02-10 18:05:45 +0100843 @cmd2.with_argparser(enable_chv_parser)
844 def do_enable_chv(self, opts):
845 """Enable PIN code using specified PIN code"""
846 pin = self.get_code(opts.pin_code)
847 (data, sw) = self._cmd.card._scc.enable_chv(opts.pin_nr, h2b(pin))
848 self._cmd.poutput("CHV enable successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200849
Harald Weltec91085e2022-02-10 18:05:45 +0100850 def do_deactivate_file(self, opts):
Harald Welte799c3542022-02-15 15:56:28 +0100851 """Deactivate the currently selected EF"""
Harald Weltec91085e2022-02-10 18:05:45 +0100852 (data, sw) = self._cmd.card._scc.deactivate_file()
Harald Weltea4631612021-04-10 18:17:55 +0200853
Harald Welte799c3542022-02-15 15:56:28 +0100854 activate_file_parser = argparse.ArgumentParser()
855 activate_file_parser.add_argument('NAME', type=str, help='File name or FID of file to activate')
856 @cmd2.with_argparser(activate_file_parser)
Harald Weltec91085e2022-02-10 18:05:45 +0100857 def do_activate_file(self, opts):
Harald Welte12af7932022-02-15 16:39:08 +0100858 """Activate the specified EF. This used to be called REHABILITATE in TS 11.11 for classic
859 SIM. You need to specify the name or FID of the file to activate."""
Harald Welte799c3542022-02-15 15:56:28 +0100860 (data, sw) = self._cmd.rs.activate_file(opts.NAME)
Harald Welte485692b2021-05-25 22:21:44 +0200861
Harald Weltec91085e2022-02-10 18:05:45 +0100862 def complete_activate_file(self, text, line, begidx, endidx) -> List[str]:
863 """Command Line tab completion for ACTIVATE FILE"""
864 index_dict = {1: self._cmd.rs.selected_file.get_selectable_names()}
865 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200866
Harald Weltec91085e2022-02-10 18:05:45 +0100867 open_chan_parser = argparse.ArgumentParser()
868 open_chan_parser.add_argument(
869 'chan_nr', type=int, default=0, help='Channel Number')
Harald Welte703f9332021-04-10 18:39:32 +0200870
Harald Weltec91085e2022-02-10 18:05:45 +0100871 @cmd2.with_argparser(open_chan_parser)
872 def do_open_channel(self, opts):
873 """Open a logical channel."""
874 (data, sw) = self._cmd.card._scc.manage_channel(
875 mode='open', lchan_nr=opts.chan_nr)
Harald Welte703f9332021-04-10 18:39:32 +0200876
Harald Weltec91085e2022-02-10 18:05:45 +0100877 close_chan_parser = argparse.ArgumentParser()
878 close_chan_parser.add_argument(
879 'chan_nr', type=int, default=0, help='Channel Number')
Harald Welte703f9332021-04-10 18:39:32 +0200880
Harald Weltec91085e2022-02-10 18:05:45 +0100881 @cmd2.with_argparser(close_chan_parser)
882 def do_close_channel(self, opts):
883 """Close a logical channel."""
884 (data, sw) = self._cmd.card._scc.manage_channel(
885 mode='close', lchan_nr=opts.chan_nr)
Harald Welte703f9332021-04-10 18:39:32 +0200886
Harald Weltec91085e2022-02-10 18:05:45 +0100887 def do_status(self, opts):
888 """Perform the STATUS command."""
889 fcp_dec = self._cmd.rs.status()
890 self._cmd.poutput_json(fcp_dec)
Harald Welte34b05d32021-05-25 22:03:13 +0200891
Harald Weltec91085e2022-02-10 18:05:45 +0100892 suspend_uicc_parser = argparse.ArgumentParser()
893 suspend_uicc_parser.add_argument('--min-duration-secs', type=int, default=60,
894 help='Proposed minimum duration of suspension')
895 suspend_uicc_parser.add_argument('--max-duration-secs', type=int, default=24*60*60,
896 help='Proposed maximum duration of suspension')
Harald Welteec950532021-10-20 13:09:00 +0200897
Harald Weltec91085e2022-02-10 18:05:45 +0100898 # not ISO7816-4 but TS 102 221
899 @cmd2.with_argparser(suspend_uicc_parser)
900 def do_suspend_uicc(self, opts):
901 """Perform the SUSPEND UICC command. Only supported on some UICC."""
902 (duration, token, sw) = self._cmd.card._scc.suspend_uicc(min_len_secs=opts.min_duration_secs,
903 max_len_secs=opts.max_duration_secs)
904 self._cmd.poutput(
905 'Negotiated Duration: %u secs, Token: %s, SW: %s' % (duration, token, sw))
Harald Welteec950532021-10-20 13:09:00 +0200906
Harald Welte703f9332021-04-10 18:39:32 +0200907
Harald Weltef2e761c2021-04-11 11:56:44 +0200908option_parser = argparse.ArgumentParser(prog='pySim-shell', description='interactive SIM card shell',
909 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
Harald Welte28c24312021-04-11 12:19:36 +0200910argparse_add_reader_args(option_parser)
Harald Weltec8ff0262021-04-11 12:06:13 +0200911
912global_group = option_parser.add_argument_group('General Options')
913global_group.add_argument('--script', metavar='PATH', default=None,
Harald Welte28c24312021-04-11 12:19:36 +0200914 help='script with pySim-shell commands to be executed automatically at start-up')
Harald Weltec91085e2022-02-10 18:05:45 +0100915global_group.add_argument('--csv', metavar='FILE',
916 default=None, help='Read card data from CSV file')
Philipp Maier76667642021-09-22 16:53:22 +0200917global_group.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
Harald Weltec91085e2022-02-10 18:05:45 +0100918 help="Use automatic card handling machine")
Harald Weltec8ff0262021-04-11 12:06:13 +0200919
920adm_group = global_group.add_mutually_exclusive_group()
921adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', default=None,
922 help='ADM PIN used for provisioning (overwrites default)')
923adm_group.add_argument('-A', '--pin-adm-hex', metavar='PIN_ADM1_HEX', dest='pin_adm_hex', default=None,
924 help='ADM PIN used for provisioning, as hex string (16 characters long)')
Harald Welteb2edd142021-01-08 23:29:35 +0100925
926
927if __name__ == '__main__':
928
Harald Weltec91085e2022-02-10 18:05:45 +0100929 # Parse options
930 opts = option_parser.parse_args()
Harald Welteb2edd142021-01-08 23:29:35 +0100931
Harald Weltec91085e2022-02-10 18:05:45 +0100932 # If a script file is specified, be sure that it actually exists
933 if opts.script:
934 if not os.access(opts.script, os.R_OK):
935 print("Invalid script file!")
936 sys.exit(2)
Philipp Maier13e258d2021-04-08 17:48:49 +0200937
Harald Weltec91085e2022-02-10 18:05:45 +0100938 # Register csv-file as card data provider, either from specified CSV
939 # or from CSV file in home directory
940 csv_default = str(Path.home()) + "/.osmocom/pysim/card_data.csv"
941 if opts.csv:
942 card_key_provider_register(CardKeyProviderCsv(opts.csv))
943 if os.path.isfile(csv_default):
944 card_key_provider_register(CardKeyProviderCsv(csv_default))
Philipp Maier2b11c322021-03-17 12:37:39 +0100945
Harald Weltec91085e2022-02-10 18:05:45 +0100946 # Init card reader driver
947 sl = init_reader(opts)
948 if sl is None:
949 exit(1)
Philipp Maierea95c392021-09-16 13:10:19 +0200950
Harald Weltec91085e2022-02-10 18:05:45 +0100951 # Create command layer
952 scc = SimCardCommands(transport=sl)
Philipp Maierea95c392021-09-16 13:10:19 +0200953
Harald Weltec91085e2022-02-10 18:05:45 +0100954 # Create a card handler (for bulk provisioning)
955 if opts.card_handler_config:
956 ch = CardHandlerAuto(None, opts.card_handler_config)
957 else:
958 ch = CardHandler(sl)
Philipp Maier76667642021-09-22 16:53:22 +0200959
Harald Weltec91085e2022-02-10 18:05:45 +0100960 # Detect and initialize the card in the reader. This may fail when there
961 # is no card in the reader or the card is unresponsive. PysimApp is
962 # able to tolerate and recover from that.
963 try:
964 rs, card = init_card(sl)
965 app = PysimApp(card, rs, sl, ch, opts.script)
966 except:
967 print("Card initialization failed with an exception:")
968 print("---------------------8<---------------------")
969 traceback.print_exc()
970 print("---------------------8<---------------------")
971 print("(you may still try to recover from this manually by using the 'equip' command.)")
972 print(
973 " it should also be noted that some readers may behave strangely when no card")
974 print(" is inserted.)")
975 print("")
Philipp Maier7226c092022-06-01 17:58:38 +0200976 app = PysimApp(card, None, sl, ch, opts.script)
Philipp Maierea95c392021-09-16 13:10:19 +0200977
Harald Weltec91085e2022-02-10 18:05:45 +0100978 # If the user supplies an ADM PIN at via commandline args authenticate
979 # immediately so that the user does not have to use the shell commands
980 pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
981 if pin_adm:
982 if not card:
983 print("Card error, cannot do ADM verification with supplied ADM pin now.")
984 try:
985 card.verify_adm(h2b(pin_adm))
986 except Exception as e:
987 print(e)
Philipp Maier228c98e2021-03-10 20:14:06 +0100988
Harald Weltec91085e2022-02-10 18:05:45 +0100989 app.cmdloop()