blob: 037b843cf07bd5ca3da002f5536a63201abae530 [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
26from cmd2 import style, fg, bg
27from 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
Harald Welte917d98c2021-04-21 11:51:25 +020043from pySim.utils import h2b, swap_nibbles, rpad, b2h, h2s, JsonEncoder, bertlv_parse_one
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
Harald Welteb2edd142021-01-08 23:29:35 +010048from pySim.ts_51_011 import CardProfileSIM, DF_TELECOM, DF_GSM
49from pySim.ts_102_221 import CardProfileUICC
Harald Welte5ce35242021-04-02 20:27:05 +020050from pySim.ts_31_102 import CardApplicationUSIM
51from pySim.ts_31_103 import CardApplicationISIM
Harald Welte2a33ad22021-10-20 10:14:18 +020052from pySim.gsm_r import DF_EIRENE
Harald Welteb2edd142021-01-08 23:29:35 +010053
Harald Welte4c1dca02021-10-14 17:48:25 +020054# we need to import this module so that the SysmocomSJA2 sub-class of
55# CardModel is created, which will add the ATR-based matching and
56# calling of SysmocomSJA2.add_files. See CardModel.apply_matching_models
Harald Weltef44256c2021-10-14 15:53:39 +020057import pySim.sysmocom_sja2
58
Harald Welte4442b3d2021-04-03 09:00:16 +020059from pySim.card_key_provider import CardKeyProviderCsv, card_key_provider_register, card_key_provider_get_field
Philipp Maier2b11c322021-03-17 12:37:39 +010060
Philipp Maierea95c392021-09-16 13:10:19 +020061def init_card(sl):
62 """
63 Detect card in reader and setup card profile and runtime state. This
64 function must be called at least once on startup. The card and runtime
65 state object (rs) is required for all pySim-shell commands.
66 """
67
68 # Wait up to three seconds for a card in reader and try to detect
69 # the card type.
70 print("Waiting for card...")
71 try:
72 sl.wait_for_card(3)
73 except NoCardError:
74 print("No card detected!")
Vadim Yanitskiydbd5ed62021-11-05 16:20:52 +030075 return None, None
Philipp Maierea95c392021-09-16 13:10:19 +020076 except:
77 print("Card not readable!")
Vadim Yanitskiydbd5ed62021-11-05 16:20:52 +030078 return None, None
Philipp Maierea95c392021-09-16 13:10:19 +020079
80 card = card_detect("auto", scc)
81 if card is None:
82 print("Could not detect card type!")
Vadim Yanitskiydbd5ed62021-11-05 16:20:52 +030083 return None, None
Philipp Maierea95c392021-09-16 13:10:19 +020084
85 # Create runtime state with card profile
86 profile = CardProfileUICC()
Philipp Maier57f65ee2021-10-18 14:09:02 +020087 profile.add_application(CardApplicationUSIM())
88 profile.add_application(CardApplicationISIM())
Philipp Maierea95c392021-09-16 13:10:19 +020089 rs = RuntimeState(card, profile)
90
91 # FIXME: do this dynamically
92 rs.mf.add_file(DF_TELECOM())
93 rs.mf.add_file(DF_GSM())
Harald Welte2a33ad22021-10-20 10:14:18 +020094 rs.mf.add_file(DF_EIRENE())
Philipp Maierea95c392021-09-16 13:10:19 +020095
Harald Weltef44256c2021-10-14 15:53:39 +020096 CardModel.apply_matching_models(scc, rs)
97
Philipp Maierea95c392021-09-16 13:10:19 +020098 # inform the transport that we can do context-specific SW interpretation
99 sl.set_sw_interpreter(rs)
100
101 return rs, card
Philipp Maier2b11c322021-03-17 12:37:39 +0100102
Harald Welteb2edd142021-01-08 23:29:35 +0100103class PysimApp(cmd2.Cmd):
104 CUSTOM_CATEGORY = 'pySim Commands'
Philipp Maier76667642021-09-22 16:53:22 +0200105 def __init__(self, card, rs, sl, ch, script = None):
Harald Welteb2edd142021-01-08 23:29:35 +0100106 super().__init__(persistent_history_file='~/.pysim_shell_history', allow_cli_args=False,
Philipp Maier5d698e52021-09-16 13:18:01 +0200107 use_ipython=True, auto_load_commands=False, startup_script=script)
Harald Welteb2edd142021-01-08 23:29:35 +0100108 self.intro = style('Welcome to pySim-shell!', fg=fg.red)
109 self.default_category = 'pySim-shell built-in commands'
Philipp Maier5d698e52021-09-16 13:18:01 +0200110 self.card = None
111 self.rs = None
Harald Welteb2edd142021-01-08 23:29:35 +0100112 self.py_locals = { 'card': self.card, 'rs' : self.rs }
Philipp Maier76667642021-09-22 16:53:22 +0200113 self.sl = sl
114 self.ch = ch
115
Harald Welteb2edd142021-01-08 23:29:35 +0100116 self.numeric_path = False
117 self.add_settable(cmd2.Settable('numeric_path', bool, 'Print File IDs instead of names',
118 onchange_cb=self._onchange_numeric_path))
Philipp Maier38c74f62021-03-17 17:19:52 +0100119 self.conserve_write = True
120 self.add_settable(cmd2.Settable('conserve_write', bool, 'Read and compare before write',
121 onchange_cb=self._onchange_conserve_write))
Harald Welte1748b932021-04-06 21:12:25 +0200122 self.json_pretty_print = True
123 self.add_settable(cmd2.Settable('json_pretty_print', bool, 'Pretty-Print JSON output'))
Harald Welte7829d8a2021-04-10 11:28:53 +0200124 self.apdu_trace = False
125 self.add_settable(cmd2.Settable('apdu_trace', bool, 'Trace and display APDUs exchanged with card',
126 onchange_cb=self._onchange_apdu_trace))
Harald Welte1748b932021-04-06 21:12:25 +0200127
Philipp Maier5d698e52021-09-16 13:18:01 +0200128 self.equip(card, rs)
129
130 def equip(self, card, rs):
131 """
132 Equip pySim-shell with the supplied card and runtime state, add (or remove) all required settables and
133 and commands to enable card operations.
134 """
135
Philipp Maier76667642021-09-22 16:53:22 +0200136 rc = False
137
Philipp Maier5d698e52021-09-16 13:18:01 +0200138 # Unequip everything from pySim-shell that would not work in unequipped state
139 if self.rs:
140 self.rs.unregister_cmds(self)
Harald Welte0bc53262021-10-20 14:24:41 +0200141 for cmds in [Iso7816Commands, PySimCommands]:
142 cmd_set = self.find_commandsets(cmds)
143 if cmd_set:
144 self.unregister_command_set(cmd_set[0])
Philipp Maier5d698e52021-09-16 13:18:01 +0200145
146 self.card = card
147 self.rs = rs
148
149 # When a card object and a runtime state is present, (re)equip pySim-shell with everything that is
150 # needed to operate on cards.
151 if self.card and self.rs:
152 self._onchange_conserve_write('conserve_write', False, self.conserve_write)
153 self._onchange_apdu_trace('apdu_trace', False, self.apdu_trace)
154 self.register_command_set(Iso7816Commands())
155 self.register_command_set(PySimCommands())
156 self.iccid, sw = self.card.read_iccid()
157 rs.select('MF', self)
Philipp Maier76667642021-09-22 16:53:22 +0200158 rc = True
Philipp Maier5d698e52021-09-16 13:18:01 +0200159 else:
160 self.poutput("pySim-shell not equipped!")
161
162 self.update_prompt()
Philipp Maier76667642021-09-22 16:53:22 +0200163 return rc
Philipp Maier5d698e52021-09-16 13:18:01 +0200164
Harald Welte1748b932021-04-06 21:12:25 +0200165 def poutput_json(self, data, force_no_pretty = False):
Harald Weltec9cdce32021-04-11 10:28:28 +0200166 """like cmd2.poutput() but for a JSON serializable dict."""
Harald Welte1748b932021-04-06 21:12:25 +0200167 if force_no_pretty or self.json_pretty_print == False:
Harald Welte5e749a72021-04-10 17:18:17 +0200168 output = json.dumps(data, cls=JsonEncoder)
Harald Welte1748b932021-04-06 21:12:25 +0200169 else:
Harald Welte5e749a72021-04-10 17:18:17 +0200170 output = json.dumps(data, cls=JsonEncoder, indent=4)
Harald Welte1748b932021-04-06 21:12:25 +0200171 self.poutput(output)
Harald Welteb2edd142021-01-08 23:29:35 +0100172
173 def _onchange_numeric_path(self, param_name, old, new):
174 self.update_prompt()
175
Philipp Maier38c74f62021-03-17 17:19:52 +0100176 def _onchange_conserve_write(self, param_name, old, new):
Philipp Maier5d698e52021-09-16 13:18:01 +0200177 if self.rs:
178 self.rs.conserve_write = new
Philipp Maier38c74f62021-03-17 17:19:52 +0100179
Harald Welte7829d8a2021-04-10 11:28:53 +0200180 def _onchange_apdu_trace(self, param_name, old, new):
Philipp Maier5d698e52021-09-16 13:18:01 +0200181 if self.card:
182 if new == True:
183 self.card._scc._tp.apdu_tracer = self.Cmd2ApduTracer(self)
184 else:
185 self.card._scc._tp.apdu_tracer = None
Harald Welte7829d8a2021-04-10 11:28:53 +0200186
187 class Cmd2ApduTracer(ApduTracer):
188 def __init__(self, cmd2_app):
189 self.cmd2 = app
190
191 def trace_response(self, cmd, sw, resp):
192 self.cmd2.poutput("-> %s %s" % (cmd[:10], cmd[10:]))
193 self.cmd2.poutput("<- %s: %s" % (sw, resp))
194
Harald Welteb2edd142021-01-08 23:29:35 +0100195 def update_prompt(self):
Philipp Maier5d698e52021-09-16 13:18:01 +0200196 if self.rs:
197 path_list = self.rs.selected_file.fully_qualified_path(not self.numeric_path)
198 self.prompt = 'pySIM-shell (%s)> ' % ('/'.join(path_list))
199 else:
200 self.prompt = 'pySIM-shell (no card)> '
Harald Welteb2edd142021-01-08 23:29:35 +0100201
202 @cmd2.with_category(CUSTOM_CATEGORY)
203 def do_intro(self, _):
204 """Display the intro banner"""
205 self.poutput(self.intro)
206
Philipp Maier9764de22021-11-03 10:44:39 +0100207 def do_eof(self, _: argparse.Namespace) -> bool:
208 self.poutput("")
209 return self.do_quit('')
210
Philipp Maier5d698e52021-09-16 13:18:01 +0200211 @cmd2.with_category(CUSTOM_CATEGORY)
212 def do_equip(self, opts):
213 """Equip pySim-shell with card"""
Vadim Yanitskiydbd5ed62021-11-05 16:20:52 +0300214 rs, card = init_card(sl)
Philipp Maier5d698e52021-09-16 13:18:01 +0200215 self.equip(card, rs)
216
Philipp Maier76667642021-09-22 16:53:22 +0200217 class InterceptStderr(list):
218 def __init__(self):
219 self._stderr_backup = sys.stderr
220 def __enter__(self):
221 self._stringio_stderr = StringIO()
222 sys.stderr = self._stringio_stderr
223 return self
224 def __exit__(self, *args):
225 self.stderr = self._stringio_stderr.getvalue().strip()
226 del self._stringio_stderr
227 sys.stderr = self._stderr_backup
228
229 def _show_failure_sign(self):
230 self.poutput(style(" +-------------+", fg=fg.bright_red))
231 self.poutput(style(" + ## ## +", fg=fg.bright_red))
232 self.poutput(style(" + ## ## +", fg=fg.bright_red))
233 self.poutput(style(" + ### +", fg=fg.bright_red))
234 self.poutput(style(" + ## ## +", fg=fg.bright_red))
235 self.poutput(style(" + ## ## +", fg=fg.bright_red))
236 self.poutput(style(" +-------------+", fg=fg.bright_red))
237 self.poutput("")
238
239 def _show_success_sign(self):
240 self.poutput(style(" +-------------+", fg=fg.bright_green))
241 self.poutput(style(" + ## +", fg=fg.bright_green))
242 self.poutput(style(" + ## +", fg=fg.bright_green))
243 self.poutput(style(" + # ## +", fg=fg.bright_green))
244 self.poutput(style(" + ## # +", fg=fg.bright_green))
245 self.poutput(style(" + ## +", fg=fg.bright_green))
246 self.poutput(style(" +-------------+", fg=fg.bright_green))
247 self.poutput("")
248
249 def _process_card(self, first, script_path):
250
251 # Early phase of card initialzation (this part may fail with an exception)
252 try:
253 rs, card = init_card(self.sl)
254 rc = self.equip(card, rs)
255 except:
256 self.poutput("")
257 self.poutput("Card initialization failed with an exception:")
258 self.poutput("---------------------8<---------------------")
259 traceback.print_exc()
260 self.poutput("---------------------8<---------------------")
261 self.poutput("")
262 return -1
263
264 # Actual card processing step. This part should never fail with an exception since the cmd2
265 # do_run_script method will catch any exception that might occur during script execution.
266 if rc:
267 self.poutput("")
268 self.poutput("Transcript stdout:")
269 self.poutput("---------------------8<---------------------")
270 with self.InterceptStderr() as logged:
271 self.do_run_script(script_path)
272 self.poutput("---------------------8<---------------------")
273
274 self.poutput("")
275 self.poutput("Transcript stderr:")
276 if logged.stderr:
277 self.poutput("---------------------8<---------------------")
278 self.poutput(logged.stderr)
279 self.poutput("---------------------8<---------------------")
280 else:
281 self.poutput("(none)")
282
283 # Check for exceptions
284 self.poutput("")
285 if "EXCEPTION of type" not in logged.stderr:
286 return 0
287
288 return -1
289
290 bulk_script_parser = argparse.ArgumentParser()
291 bulk_script_parser.add_argument('script_path', help="path to the script file")
292 bulk_script_parser.add_argument('--halt_on_error', help='stop card handling if an exeption occurs',
293 action='store_true')
294 bulk_script_parser.add_argument('--tries', type=int, default=2,
295 help='how many tries before trying the next card')
296 bulk_script_parser.add_argument('--on_stop_action', type=str, default=None,
297 help='commandline to execute when card handling has stopped')
298 bulk_script_parser.add_argument('--pre_card_action', type=str, default=None,
299 help='commandline to execute before actually talking to the card')
300
301 @cmd2.with_argparser(bulk_script_parser)
302 @cmd2.with_category(CUSTOM_CATEGORY)
303 def do_bulk_script(self, opts):
304 """Run script on multiple cards (bulk provisioning)"""
305
306 # Make sure that the script file exists and that it is readable.
307 if not os.access(opts.script_path, os.R_OK):
308 self.poutput("Invalid script file!")
309 return
310
311 success_count = 0
312 fail_count = 0
313
314 first = True
315 while 1:
316 # TODO: Count consecutive failures, if more than N consecutive failures occur, then stop.
317 # The ratinale is: There may be a problem with the device, we do want to prevent that
318 # all remaining cards are fired to the error bin. This is only relevant for situations
319 # with large stacks, probably we do not need this feature right now.
320
321 try:
322 # In case of failure, try multiple times.
323 for i in range(opts.tries):
324 # fetch card into reader bay
325 ch.get(first)
326
327 # if necessary execute an action before we start processing the card
328 if(opts.pre_card_action):
329 os.system(opts.pre_card_action)
330
331 # process the card
332 rc = self._process_card(first, opts.script_path)
333 if rc == 0:
334 success_count = success_count + 1
335 self._show_success_sign()
336 self.poutput("Statistics: success :%i, failure: %i" % (success_count, fail_count))
337 break
338 else:
339 fail_count = fail_count + 1
340 self._show_failure_sign()
341 self.poutput("Statistics: success :%i, failure: %i" % (success_count, fail_count))
342
343
344 # Depending on success or failure, the card goes either in the "error" bin or in the
345 # "done" bin.
346 if rc < 0:
347 ch.error()
348 else:
349 ch.done()
350
351 # In most cases it is possible to proceed with the next card, but the
352 # user may decide to halt immediately when an error occurs
353 if opts.halt_on_error and rc < 0:
354 return
355
356 except (KeyboardInterrupt):
357 self.poutput("")
358 self.poutput("Terminated by user!")
Vadim Yanitskiydbd5ed62021-11-05 16:20:52 +0300359 return
Philipp Maier76667642021-09-22 16:53:22 +0200360 except (SystemExit):
361 # When all cards are processed the card handler device will throw a SystemExit
362 # exception. Also Errors that are not recoverable (cards stuck etc.) will end up here.
363 # The user has the option to execute some action to make aware that the card handler
364 # needs service.
365 if(opts.on_stop_action):
366 os.system(opts.on_stop_action)
367 return
368 except:
369 self.poutput("")
370 self.poutput("Card handling failed with an exception:")
371 self.poutput("---------------------8<---------------------")
372 traceback.print_exc()
373 self.poutput("---------------------8<---------------------")
374 self.poutput("")
375 fail_count = fail_count + 1
376 self._show_failure_sign()
377 self.poutput("Statistics: success :%i, failure: %i" % (success_count, fail_count))
378
379 first = False
380
Philipp Maierb52feed2021-09-22 16:43:13 +0200381 echo_parser = argparse.ArgumentParser()
382 echo_parser.add_argument('string', help="string to echo on the shell")
383
384 @cmd2.with_argparser(echo_parser)
385 @cmd2.with_category(CUSTOM_CATEGORY)
386 def do_echo(self, opts):
Harald Weltebd02f842021-10-21 14:40:39 +0200387 """Echo (print) a string on the console"""
Philipp Maierb52feed2021-09-22 16:43:13 +0200388 self.poutput(opts.string)
Harald Welteb2edd142021-01-08 23:29:35 +0100389
Harald Welte31d2cf02021-04-03 10:47:29 +0200390@with_default_category('pySim Commands')
391class PySimCommands(CommandSet):
Harald Welteb2edd142021-01-08 23:29:35 +0100392 def __init__(self):
393 super().__init__()
394
Philipp Maier5d3e2592021-02-22 17:22:16 +0100395 dir_parser = argparse.ArgumentParser()
396 dir_parser.add_argument('--fids', help='Show file identifiers', action='store_true')
397 dir_parser.add_argument('--names', help='Show file names', action='store_true')
398 dir_parser.add_argument('--apps', help='Show applications', action='store_true')
399 dir_parser.add_argument('--all', help='Show all selectable identifiers and names', action='store_true')
400
401 @cmd2.with_argparser(dir_parser)
402 def do_dir(self, opts):
403 """Show a listing of files available in currently selected DF or MF"""
404 if opts.all:
405 flags = []
406 elif opts.fids or opts.names or opts.apps:
407 flags = ['PARENT', 'SELF']
408 if opts.fids:
409 flags += ['FIDS', 'AIDS']
410 if opts.names:
411 flags += ['FNAMES', 'ANAMES']
412 if opts.apps:
413 flags += ['ANAMES', 'AIDS']
414 else:
415 flags = ['PARENT', 'SELF', 'FNAMES', 'ANAMES']
416 selectables = list(self._cmd.rs.selected_file.get_selectable_names(flags = flags))
417 directory_str = tabulate_str_list(selectables, width = 79, hspace = 2, lspace = 1, align_left = True)
418 path_list = self._cmd.rs.selected_file.fully_qualified_path(True)
419 self._cmd.poutput('/'.join(path_list))
420 path_list = self._cmd.rs.selected_file.fully_qualified_path(False)
421 self._cmd.poutput('/'.join(path_list))
422 self._cmd.poutput(directory_str)
423 self._cmd.poutput("%d files" % len(selectables))
Harald Welteb2edd142021-01-08 23:29:35 +0100424
Philipp Maierff9dae22021-02-25 17:03:21 +0100425 def walk(self, indent = 0, action = None, context = None):
426 """Recursively walk through the file system, starting at the currently selected DF"""
427 files = self._cmd.rs.selected_file.get_selectables(flags = ['FNAMES', 'ANAMES'])
428 for f in files:
429 if not action:
430 output_str = " " * indent + str(f) + (" " * 250)
431 output_str = output_str[0:25]
432 if isinstance(files[f], CardADF):
433 output_str += " " + str(files[f].aid)
434 else:
435 output_str += " " + str(files[f].fid)
436 output_str += " " + str(files[f].desc)
437 self._cmd.poutput(output_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200438
Philipp Maierff9dae22021-02-25 17:03:21 +0100439 if isinstance(files[f], CardDF):
Philipp Maierf408a402021-04-09 21:16:12 +0200440 skip_df=False
441 try:
442 fcp_dec = self._cmd.rs.select(f, self._cmd)
443 except Exception as e:
444 skip_df=True
445 df = self._cmd.rs.selected_file
446 df_path_list = df.fully_qualified_path(True)
447 df_skip_reason_str = '/'.join(df_path_list) + "/" + str(f) + ", " + str(e)
448 if context:
449 context['DF_SKIP'] += 1
450 context['DF_SKIP_REASON'].append(df_skip_reason_str)
451
452 # If the DF was skipped, we never have entered the directory
453 # below, so we must not move up.
454 if skip_df == False:
455 self.walk(indent + 1, action, context)
456 fcp_dec = self._cmd.rs.select("..", self._cmd)
457
Philipp Maierff9dae22021-02-25 17:03:21 +0100458 elif action:
Philipp Maierb152a9e2021-04-01 17:13:03 +0200459 df_before_action = self._cmd.rs.selected_file
Philipp Maierff9dae22021-02-25 17:03:21 +0100460 action(f, context)
Philipp Maierb152a9e2021-04-01 17:13:03 +0200461 # When walking through the file system tree the action must not
462 # always restore the currently selected file to the file that
463 # was selected before executing the action() callback.
464 if df_before_action != self._cmd.rs.selected_file:
Harald Weltec9cdce32021-04-11 10:28:28 +0200465 raise RuntimeError("inconsistent walk, %s is currently selected but expecting %s to be selected"
Philipp Maierb152a9e2021-04-01 17:13:03 +0200466 % (str(self._cmd.rs.selected_file), str(df_before_action)))
Philipp Maierff9dae22021-02-25 17:03:21 +0100467
468 def do_tree(self, opts):
469 """Display a filesystem-tree with all selectable files"""
470 self.walk()
471
Philipp Maier24f7bd32021-02-25 17:06:18 +0100472 def export(self, filename, context):
Philipp Maierac34dcc2021-04-01 17:19:05 +0200473 """ Select and export a single file """
Philipp Maier24f7bd32021-02-25 17:06:18 +0100474 context['COUNT'] += 1
Philipp Maierac34dcc2021-04-01 17:19:05 +0200475 df = self._cmd.rs.selected_file
476
477 if not isinstance(df, CardDF):
478 raise RuntimeError("currently selected file %s is not a DF or ADF" % str(df))
479
480 df_path_list = df.fully_qualified_path(True)
481 df_path_list_fid = df.fully_qualified_path(False)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100482
Philipp Maier80ce71f2021-04-19 21:24:23 +0200483 file_str = '/'.join(df_path_list) + "/" + str(filename)
484 self._cmd.poutput(boxed_heading_str(file_str))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100485
Philipp Maierac34dcc2021-04-01 17:19:05 +0200486 self._cmd.poutput("# directory: %s (%s)" % ('/'.join(df_path_list), '/'.join(df_path_list_fid)))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100487 try:
488 fcp_dec = self._cmd.rs.select(filename, self._cmd)
Philipp Maierac34dcc2021-04-01 17:19:05 +0200489 self._cmd.poutput("# file: %s (%s)" % (self._cmd.rs.selected_file.name, self._cmd.rs.selected_file.fid))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100490
491 fd = fcp_dec['file_descriptor']
492 structure = fd['structure']
493 self._cmd.poutput("# structure: %s" % str(structure))
494
Philipp Maierac34dcc2021-04-01 17:19:05 +0200495 for f in df_path_list:
Philipp Maier24f7bd32021-02-25 17:06:18 +0100496 self._cmd.poutput("select " + str(f))
Philipp Maierac34dcc2021-04-01 17:19:05 +0200497 self._cmd.poutput("select " + self._cmd.rs.selected_file.name)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100498
499 if structure == 'transparent':
500 result = self._cmd.rs.read_binary()
501 self._cmd.poutput("update_binary " + str(result[0]))
Harald Welte917d98c2021-04-21 11:51:25 +0200502 elif structure == 'cyclic' or structure == 'linear_fixed':
Philipp Maier24f7bd32021-02-25 17:06:18 +0100503 num_of_rec = fd['num_of_rec']
504 for r in range(1, num_of_rec + 1):
505 result = self._cmd.rs.read_record(r)
506 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
Harald Welte917d98c2021-04-21 11:51:25 +0200507 elif structure == 'ber_tlv':
508 tags = self._cmd.rs.retrieve_tags()
509 for t in tags:
510 result = self._cmd.rs.retrieve_data(t)
Harald Weltec1475302021-05-21 21:47:55 +0200511 (tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
Harald Welte917d98c2021-04-21 11:51:25 +0200512 self._cmd.poutput("set_data 0x%02x %s" % (t, b2h(val)))
513 else:
514 raise RuntimeError('Unsupported structure "%s" of file "%s"' % (structure, filename))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100515 except Exception as e:
Philipp Maierac34dcc2021-04-01 17:19:05 +0200516 bad_file_str = '/'.join(df_path_list) + "/" + str(filename) + ", " + str(e)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100517 self._cmd.poutput("# bad file: %s" % bad_file_str)
518 context['ERR'] += 1
519 context['BAD'].append(bad_file_str)
520
Philipp Maierac34dcc2021-04-01 17:19:05 +0200521 # When reading the file is done, make sure the parent file is
522 # selected again. This will be the usual case, however we need
523 # to check before since we must not select the same DF twice
524 if df != self._cmd.rs.selected_file:
525 self._cmd.rs.select(df.fid or df.aid, self._cmd)
526
Philipp Maier24f7bd32021-02-25 17:06:18 +0100527 self._cmd.poutput("#")
528
529 export_parser = argparse.ArgumentParser()
530 export_parser.add_argument('--filename', type=str, default=None, help='only export specific file')
531
532 @cmd2.with_argparser(export_parser)
533 def do_export(self, opts):
534 """Export files to script that can be imported back later"""
Philipp Maierf408a402021-04-09 21:16:12 +0200535 context = {'ERR':0, 'COUNT':0, 'BAD':[], 'DF_SKIP':0, 'DF_SKIP_REASON':[]}
Philipp Maier24f7bd32021-02-25 17:06:18 +0100536 if opts.filename:
537 self.export(opts.filename, context)
538 else:
539 self.walk(0, self.export, context)
Philipp Maier80ce71f2021-04-19 21:24:23 +0200540
541 self._cmd.poutput(boxed_heading_str("Export summary"))
542
Philipp Maier24f7bd32021-02-25 17:06:18 +0100543 self._cmd.poutput("# total files visited: %u" % context['COUNT'])
544 self._cmd.poutput("# bad files: %u" % context['ERR'])
545 for b in context['BAD']:
546 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200547
548 self._cmd.poutput("# skipped dedicated files(s): %u" % context['DF_SKIP'])
549 for b in context['DF_SKIP_REASON']:
550 self._cmd.poutput("# " + b)
551
552 if context['ERR'] and context['DF_SKIP']:
Harald Weltec9cdce32021-04-11 10:28:28 +0200553 raise RuntimeError("unable to export %i elementary file(s) and %i dedicated file(s)" % (context['ERR'], context['DF_SKIP']))
Philipp Maierf408a402021-04-09 21:16:12 +0200554 elif context['ERR']:
Harald Weltec9cdce32021-04-11 10:28:28 +0200555 raise RuntimeError("unable to export %i elementary file(s)" % context['ERR'])
Philipp Maierf408a402021-04-09 21:16:12 +0200556 elif context['DF_SKIP']:
557 raise RuntimeError("unable to export %i dedicated files(s)" % context['ERR'])
Harald Welteb2edd142021-01-08 23:29:35 +0100558
Harald Weltedaf2b392021-05-03 23:17:29 +0200559 def do_reset(self, opts):
560 """Reset the Card."""
561 atr = self._cmd.rs.reset(self._cmd)
562 self._cmd.poutput('Card ATR: %s' % atr)
563 self._cmd.update_prompt()
564
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200565 def do_desc(self, opts):
566 """Display human readable file description for the currently selected file"""
567 desc = self._cmd.rs.selected_file.desc
568 if desc:
569 self._cmd.poutput(desc)
570 else:
571 self._cmd.poutput("no description available")
572
573 def do_verify_adm(self, arg):
574 """VERIFY the ADM1 PIN"""
575 if arg:
576 # use specified ADM-PIN
577 pin_adm = sanitize_pin_adm(arg)
578 else:
579 # try to find an ADM-PIN if none is specified
580 result = card_key_provider_get_field('ADM1', key='ICCID', value=self._cmd.iccid)
581 pin_adm = sanitize_pin_adm(result)
582 if pin_adm:
583 self._cmd.poutput("found ADM-PIN '%s' for ICCID '%s'" % (result, self._cmd.iccid))
584 else:
Philipp Maierf0241452021-09-22 16:35:55 +0200585 raise ValueError("cannot find ADM-PIN for ICCID '%s'" % (self._cmd.iccid))
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200586
587 if pin_adm:
588 self._cmd.card.verify_adm(h2b(pin_adm))
589 else:
Philipp Maierf0241452021-09-22 16:35:55 +0200590 raise ValueError("error: cannot authenticate, no adm-pin!")
Harald Welteb2edd142021-01-08 23:29:35 +0100591
Harald Welte31d2cf02021-04-03 10:47:29 +0200592@with_default_category('ISO7816 Commands')
593class Iso7816Commands(CommandSet):
594 def __init__(self):
595 super().__init__()
596
597 def do_select(self, opts):
598 """SELECT a File (ADF/DF/EF)"""
599 if len(opts.arg_list) == 0:
600 path_list = self._cmd.rs.selected_file.fully_qualified_path(True)
601 path_list_fid = self._cmd.rs.selected_file.fully_qualified_path(False)
602 self._cmd.poutput("currently selected file: " + '/'.join(path_list) + " (" + '/'.join(path_list_fid) + ")")
603 return
604
605 path = opts.arg_list[0]
606 fcp_dec = self._cmd.rs.select(path, self._cmd)
607 self._cmd.update_prompt()
Harald Welteb00e8932021-04-10 17:19:13 +0200608 self._cmd.poutput_json(fcp_dec)
Harald Welte31d2cf02021-04-03 10:47:29 +0200609
610 def complete_select(self, text, line, begidx, endidx) -> List[str]:
611 """Command Line tab completion for SELECT"""
612 index_dict = { 1: self._cmd.rs.selected_file.get_selectable_names() }
613 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
614
615 def get_code(self, code):
616 """Use code either directly or try to get it from external data source"""
617 auto = ('PIN1', 'PIN2', 'PUK1', 'PUK2')
618
619 if str(code).upper() not in auto:
620 return sanitize_pin_adm(code)
621
622 result = card_key_provider_get_field(str(code), key='ICCID', value=self._cmd.iccid)
623 result = sanitize_pin_adm(result)
624 if result:
625 self._cmd.poutput("found %s '%s' for ICCID '%s'" % (code.upper(), result, self._cmd.iccid))
626 else:
627 self._cmd.poutput("cannot find %s for ICCID '%s'" % (code.upper(), self._cmd.iccid))
628 return result
629
630 verify_chv_parser = argparse.ArgumentParser()
631 verify_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
632 verify_chv_parser.add_argument('pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
633
634 @cmd2.with_argparser(verify_chv_parser)
635 def do_verify_chv(self, opts):
636 """Verify (authenticate) using specified PIN code"""
637 pin = self.get_code(opts.pin_code)
638 (data, sw) = self._cmd.card._scc.verify_chv(opts.pin_nr, h2b(pin))
Harald Weltec9cdce32021-04-11 10:28:28 +0200639 self._cmd.poutput("CHV verification successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200640
641 unblock_chv_parser = argparse.ArgumentParser()
642 unblock_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
643 unblock_chv_parser.add_argument('puk_code', type=str, help='PUK code digits \"PUK1\" or \"PUK2\" to get PUK code from external data source')
644 unblock_chv_parser.add_argument('new_pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
645
646 @cmd2.with_argparser(unblock_chv_parser)
647 def do_unblock_chv(self, opts):
648 """Unblock PIN code using specified PUK code"""
649 new_pin = self.get_code(opts.new_pin_code)
650 puk = self.get_code(opts.puk_code)
651 (data, sw) = self._cmd.card._scc.unblock_chv(opts.pin_nr, h2b(puk), h2b(new_pin))
652 self._cmd.poutput("CHV unblock successful")
653
654 change_chv_parser = argparse.ArgumentParser()
655 change_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
656 change_chv_parser.add_argument('pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
657 change_chv_parser.add_argument('new_pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
658
659 @cmd2.with_argparser(change_chv_parser)
660 def do_change_chv(self, opts):
661 """Change PIN code to a new PIN code"""
662 new_pin = self.get_code(opts.new_pin_code)
663 pin = self.get_code(opts.pin_code)
664 (data, sw) = self._cmd.card._scc.change_chv(opts.pin_nr, h2b(pin), h2b(new_pin))
665 self._cmd.poutput("CHV change successful")
666
667 disable_chv_parser = argparse.ArgumentParser()
668 disable_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
669 disable_chv_parser.add_argument('pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
670
671 @cmd2.with_argparser(disable_chv_parser)
672 def do_disable_chv(self, opts):
673 """Disable PIN code using specified PIN code"""
674 pin = self.get_code(opts.pin_code)
675 (data, sw) = self._cmd.card._scc.disable_chv(opts.pin_nr, h2b(pin))
676 self._cmd.poutput("CHV disable successful")
677
678 enable_chv_parser = argparse.ArgumentParser()
679 enable_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
680 enable_chv_parser.add_argument('pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
681
682 @cmd2.with_argparser(enable_chv_parser)
683 def do_enable_chv(self, opts):
684 """Enable PIN code using specified PIN code"""
685 pin = self.get_code(opts.pin_code)
686 (data, sw) = self._cmd.card._scc.enable_chv(opts.pin_nr, h2b(pin))
687 self._cmd.poutput("CHV enable successful")
688
Harald Weltea4631612021-04-10 18:17:55 +0200689 def do_deactivate_file(self, opts):
690 """Deactivate the current EF"""
Harald Welte485692b2021-05-25 22:21:44 +0200691 (data, sw) = self._cmd.card._scc.deactivate_file()
Harald Weltea4631612021-04-10 18:17:55 +0200692
693 def do_activate_file(self, opts):
Harald Welte485692b2021-05-25 22:21:44 +0200694 """Activate the specified EF"""
695 path = opts.arg_list[0]
696 (data, sw) = self._cmd.rs.activate_file(path)
697
698 def complete_activate_file(self, text, line, begidx, endidx) -> List[str]:
699 """Command Line tab completion for ACTIVATE FILE"""
700 index_dict = { 1: self._cmd.rs.selected_file.get_selectable_names() }
701 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200702
Harald Welte703f9332021-04-10 18:39:32 +0200703 open_chan_parser = argparse.ArgumentParser()
704 open_chan_parser.add_argument('chan_nr', type=int, default=0, help='Channel Number')
705
706 @cmd2.with_argparser(open_chan_parser)
707 def do_open_channel(self, opts):
708 """Open a logical channel."""
709 (data, sw) = self._cmd.card._scc.manage_channel(mode='open', lchan_nr=opts.chan_nr)
710
711 close_chan_parser = argparse.ArgumentParser()
712 close_chan_parser.add_argument('chan_nr', type=int, default=0, help='Channel Number')
713
714 @cmd2.with_argparser(close_chan_parser)
715 def do_close_channel(self, opts):
716 """Close a logical channel."""
717 (data, sw) = self._cmd.card._scc.manage_channel(mode='close', lchan_nr=opts.chan_nr)
718
Harald Welte34b05d32021-05-25 22:03:13 +0200719 def do_status(self, opts):
720 """Perform the STATUS command."""
721 fcp_dec = self._cmd.rs.status()
722 self._cmd.poutput_json(fcp_dec)
723
Harald Welteec950532021-10-20 13:09:00 +0200724 suspend_uicc_parser = argparse.ArgumentParser()
725 suspend_uicc_parser.add_argument('--min-duration-secs', type=int, default=60,
726 help='Proposed minimum duration of suspension')
727 suspend_uicc_parser.add_argument('--max-duration-secs', type=int, default=24*60*60,
728 help='Proposed maximum duration of suspension')
729
730 # not ISO7816-4 but TS 102 221
731 @cmd2.with_argparser(suspend_uicc_parser)
732 def do_suspend_uicc(self, opts):
733 """Perform the SUSPEND UICC command. Only supported on some UICC."""
734 (duration, token, sw) = self._cmd.card._scc.suspend_uicc(min_len_secs=opts.min_duration_secs,
735 max_len_secs=opts.max_duration_secs)
736 self._cmd.poutput('Negotiated Duration: %u secs, Token: %s, SW: %s' % (duration, token, sw))
737
Harald Welte703f9332021-04-10 18:39:32 +0200738
Harald Weltef2e761c2021-04-11 11:56:44 +0200739option_parser = argparse.ArgumentParser(prog='pySim-shell', description='interactive SIM card shell',
740 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
Harald Welte28c24312021-04-11 12:19:36 +0200741argparse_add_reader_args(option_parser)
Harald Weltec8ff0262021-04-11 12:06:13 +0200742
743global_group = option_parser.add_argument_group('General Options')
744global_group.add_argument('--script', metavar='PATH', default=None,
Harald Welte28c24312021-04-11 12:19:36 +0200745 help='script with pySim-shell commands to be executed automatically at start-up')
746global_group.add_argument('--csv', metavar='FILE', default=None, help='Read card data from CSV file')
Philipp Maier76667642021-09-22 16:53:22 +0200747global_group.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
748 help="Use automatic card handling machine")
Harald Weltec8ff0262021-04-11 12:06:13 +0200749
750adm_group = global_group.add_mutually_exclusive_group()
751adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', default=None,
752 help='ADM PIN used for provisioning (overwrites default)')
753adm_group.add_argument('-A', '--pin-adm-hex', metavar='PIN_ADM1_HEX', dest='pin_adm_hex', default=None,
754 help='ADM PIN used for provisioning, as hex string (16 characters long)')
Harald Welteb2edd142021-01-08 23:29:35 +0100755
756
757if __name__ == '__main__':
758
759 # Parse options
Harald Weltef2e761c2021-04-11 11:56:44 +0200760 opts = option_parser.parse_args()
Harald Welteb2edd142021-01-08 23:29:35 +0100761
Philipp Maier13e258d2021-04-08 17:48:49 +0200762 # If a script file is specified, be sure that it actually exists
763 if opts.script:
764 if not os.access(opts.script, os.R_OK):
765 print("Invalid script file!")
766 sys.exit(2)
767
Philipp Maier2b11c322021-03-17 12:37:39 +0100768 # Register csv-file as card data provider, either from specified CSV
769 # or from CSV file in home directory
770 csv_default = str(Path.home()) + "/.osmocom/pysim/card_data.csv"
771 if opts.csv:
Harald Welte4442b3d2021-04-03 09:00:16 +0200772 card_key_provider_register(CardKeyProviderCsv(opts.csv))
Philipp Maier2b11c322021-03-17 12:37:39 +0100773 if os.path.isfile(csv_default):
Harald Welte4442b3d2021-04-03 09:00:16 +0200774 card_key_provider_register(CardKeyProviderCsv(csv_default))
Philipp Maier2b11c322021-03-17 12:37:39 +0100775
Philipp Maierea95c392021-09-16 13:10:19 +0200776 # Init card reader driver
777 sl = init_reader(opts)
778 if sl is None:
779 exit(1)
780
781 # Create command layer
782 scc = SimCardCommands(transport=sl)
783
Philipp Maier76667642021-09-22 16:53:22 +0200784 # Create a card handler (for bulk provisioning)
785 if opts.card_handler_config:
786 ch = CardHandlerAuto(None, opts.card_handler_config)
787 else:
788 ch = CardHandler(sl)
789
Philipp Maier5d698e52021-09-16 13:18:01 +0200790 # Detect and initialize the card in the reader. This may fail when there
791 # is no card in the reader or the card is unresponsive. PysimApp is
792 # able to tolerate and recover from that.
793 try:
794 rs, card = init_card(sl)
Philipp Maier76667642021-09-22 16:53:22 +0200795 app = PysimApp(card, rs, sl, ch, opts.script)
Philipp Maier5d698e52021-09-16 13:18:01 +0200796 except:
797 print("Card initialization failed with an exception:")
798 print("---------------------8<---------------------")
799 traceback.print_exc()
800 print("---------------------8<---------------------")
801 print("(you may still try to recover from this manually by using the 'equip' command.)")
802 print(" it should also be noted that some readers may behave strangely when no card")
803 print(" is inserted.)")
804 print("")
Philipp Maier76667642021-09-22 16:53:22 +0200805 app = PysimApp(None, None, sl, ch, opts.script)
Philipp Maierea95c392021-09-16 13:10:19 +0200806
Philipp Maier228c98e2021-03-10 20:14:06 +0100807 # If the user supplies an ADM PIN at via commandline args authenticate
Harald Weltec9cdce32021-04-11 10:28:28 +0200808 # immediately so that the user does not have to use the shell commands
Philipp Maier228c98e2021-03-10 20:14:06 +0100809 pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
810 if pin_adm:
Philipp Maier5d698e52021-09-16 13:18:01 +0200811 if not card:
812 print("Card error, cannot do ADM verification with supplied ADM pin now.")
Philipp Maier228c98e2021-03-10 20:14:06 +0100813 try:
814 card.verify_adm(h2b(pin_adm))
815 except Exception as e:
816 print(e)
817
Harald Welteb2edd142021-01-08 23:29:35 +0100818 app.cmdloop()