blob: 3fc58590a266c4c6dfd53967b77d85eaa68e5e86 [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!")
75 return None, None;
76 except:
77 print("Card not readable!")
78 return None, None;
79
80 card = card_detect("auto", scc)
81 if card is None:
82 print("Could not detect card type!")
83 return None, None;
84
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 Maier5d698e52021-09-16 13:18:01 +0200207 @cmd2.with_category(CUSTOM_CATEGORY)
208 def do_equip(self, opts):
209 """Equip pySim-shell with card"""
210 rs, card = init_card(sl);
211 self.equip(card, rs)
212
Philipp Maier76667642021-09-22 16:53:22 +0200213 class InterceptStderr(list):
214 def __init__(self):
215 self._stderr_backup = sys.stderr
216 def __enter__(self):
217 self._stringio_stderr = StringIO()
218 sys.stderr = self._stringio_stderr
219 return self
220 def __exit__(self, *args):
221 self.stderr = self._stringio_stderr.getvalue().strip()
222 del self._stringio_stderr
223 sys.stderr = self._stderr_backup
224
225 def _show_failure_sign(self):
226 self.poutput(style(" +-------------+", fg=fg.bright_red))
227 self.poutput(style(" + ## ## +", fg=fg.bright_red))
228 self.poutput(style(" + ## ## +", fg=fg.bright_red))
229 self.poutput(style(" + ### +", fg=fg.bright_red))
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("")
234
235 def _show_success_sign(self):
236 self.poutput(style(" +-------------+", fg=fg.bright_green))
237 self.poutput(style(" + ## +", fg=fg.bright_green))
238 self.poutput(style(" + ## +", fg=fg.bright_green))
239 self.poutput(style(" + # ## +", fg=fg.bright_green))
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("")
244
245 def _process_card(self, first, script_path):
246
247 # Early phase of card initialzation (this part may fail with an exception)
248 try:
249 rs, card = init_card(self.sl)
250 rc = self.equip(card, rs)
251 except:
252 self.poutput("")
253 self.poutput("Card initialization failed with an exception:")
254 self.poutput("---------------------8<---------------------")
255 traceback.print_exc()
256 self.poutput("---------------------8<---------------------")
257 self.poutput("")
258 return -1
259
260 # Actual card processing step. This part should never fail with an exception since the cmd2
261 # do_run_script method will catch any exception that might occur during script execution.
262 if rc:
263 self.poutput("")
264 self.poutput("Transcript stdout:")
265 self.poutput("---------------------8<---------------------")
266 with self.InterceptStderr() as logged:
267 self.do_run_script(script_path)
268 self.poutput("---------------------8<---------------------")
269
270 self.poutput("")
271 self.poutput("Transcript stderr:")
272 if logged.stderr:
273 self.poutput("---------------------8<---------------------")
274 self.poutput(logged.stderr)
275 self.poutput("---------------------8<---------------------")
276 else:
277 self.poutput("(none)")
278
279 # Check for exceptions
280 self.poutput("")
281 if "EXCEPTION of type" not in logged.stderr:
282 return 0
283
284 return -1
285
286 bulk_script_parser = argparse.ArgumentParser()
287 bulk_script_parser.add_argument('script_path', help="path to the script file")
288 bulk_script_parser.add_argument('--halt_on_error', help='stop card handling if an exeption occurs',
289 action='store_true')
290 bulk_script_parser.add_argument('--tries', type=int, default=2,
291 help='how many tries before trying the next card')
292 bulk_script_parser.add_argument('--on_stop_action', type=str, default=None,
293 help='commandline to execute when card handling has stopped')
294 bulk_script_parser.add_argument('--pre_card_action', type=str, default=None,
295 help='commandline to execute before actually talking to the card')
296
297 @cmd2.with_argparser(bulk_script_parser)
298 @cmd2.with_category(CUSTOM_CATEGORY)
299 def do_bulk_script(self, opts):
300 """Run script on multiple cards (bulk provisioning)"""
301
302 # Make sure that the script file exists and that it is readable.
303 if not os.access(opts.script_path, os.R_OK):
304 self.poutput("Invalid script file!")
305 return
306
307 success_count = 0
308 fail_count = 0
309
310 first = True
311 while 1:
312 # TODO: Count consecutive failures, if more than N consecutive failures occur, then stop.
313 # The ratinale is: There may be a problem with the device, we do want to prevent that
314 # all remaining cards are fired to the error bin. This is only relevant for situations
315 # with large stacks, probably we do not need this feature right now.
316
317 try:
318 # In case of failure, try multiple times.
319 for i in range(opts.tries):
320 # fetch card into reader bay
321 ch.get(first)
322
323 # if necessary execute an action before we start processing the card
324 if(opts.pre_card_action):
325 os.system(opts.pre_card_action)
326
327 # process the card
328 rc = self._process_card(first, opts.script_path)
329 if rc == 0:
330 success_count = success_count + 1
331 self._show_success_sign()
332 self.poutput("Statistics: success :%i, failure: %i" % (success_count, fail_count))
333 break
334 else:
335 fail_count = fail_count + 1
336 self._show_failure_sign()
337 self.poutput("Statistics: success :%i, failure: %i" % (success_count, fail_count))
338
339
340 # Depending on success or failure, the card goes either in the "error" bin or in the
341 # "done" bin.
342 if rc < 0:
343 ch.error()
344 else:
345 ch.done()
346
347 # In most cases it is possible to proceed with the next card, but the
348 # user may decide to halt immediately when an error occurs
349 if opts.halt_on_error and rc < 0:
350 return
351
352 except (KeyboardInterrupt):
353 self.poutput("")
354 self.poutput("Terminated by user!")
355 return;
356 except (SystemExit):
357 # When all cards are processed the card handler device will throw a SystemExit
358 # exception. Also Errors that are not recoverable (cards stuck etc.) will end up here.
359 # The user has the option to execute some action to make aware that the card handler
360 # needs service.
361 if(opts.on_stop_action):
362 os.system(opts.on_stop_action)
363 return
364 except:
365 self.poutput("")
366 self.poutput("Card handling failed with an exception:")
367 self.poutput("---------------------8<---------------------")
368 traceback.print_exc()
369 self.poutput("---------------------8<---------------------")
370 self.poutput("")
371 fail_count = fail_count + 1
372 self._show_failure_sign()
373 self.poutput("Statistics: success :%i, failure: %i" % (success_count, fail_count))
374
375 first = False
376
Philipp Maierb52feed2021-09-22 16:43:13 +0200377 echo_parser = argparse.ArgumentParser()
378 echo_parser.add_argument('string', help="string to echo on the shell")
379
380 @cmd2.with_argparser(echo_parser)
381 @cmd2.with_category(CUSTOM_CATEGORY)
382 def do_echo(self, opts):
Harald Weltebd02f842021-10-21 14:40:39 +0200383 """Echo (print) a string on the console"""
Philipp Maierb52feed2021-09-22 16:43:13 +0200384 self.poutput(opts.string)
Harald Welteb2edd142021-01-08 23:29:35 +0100385
Harald Welte31d2cf02021-04-03 10:47:29 +0200386@with_default_category('pySim Commands')
387class PySimCommands(CommandSet):
Harald Welteb2edd142021-01-08 23:29:35 +0100388 def __init__(self):
389 super().__init__()
390
Philipp Maier5d3e2592021-02-22 17:22:16 +0100391 dir_parser = argparse.ArgumentParser()
392 dir_parser.add_argument('--fids', help='Show file identifiers', action='store_true')
393 dir_parser.add_argument('--names', help='Show file names', action='store_true')
394 dir_parser.add_argument('--apps', help='Show applications', action='store_true')
395 dir_parser.add_argument('--all', help='Show all selectable identifiers and names', action='store_true')
396
397 @cmd2.with_argparser(dir_parser)
398 def do_dir(self, opts):
399 """Show a listing of files available in currently selected DF or MF"""
400 if opts.all:
401 flags = []
402 elif opts.fids or opts.names or opts.apps:
403 flags = ['PARENT', 'SELF']
404 if opts.fids:
405 flags += ['FIDS', 'AIDS']
406 if opts.names:
407 flags += ['FNAMES', 'ANAMES']
408 if opts.apps:
409 flags += ['ANAMES', 'AIDS']
410 else:
411 flags = ['PARENT', 'SELF', 'FNAMES', 'ANAMES']
412 selectables = list(self._cmd.rs.selected_file.get_selectable_names(flags = flags))
413 directory_str = tabulate_str_list(selectables, width = 79, hspace = 2, lspace = 1, align_left = True)
414 path_list = self._cmd.rs.selected_file.fully_qualified_path(True)
415 self._cmd.poutput('/'.join(path_list))
416 path_list = self._cmd.rs.selected_file.fully_qualified_path(False)
417 self._cmd.poutput('/'.join(path_list))
418 self._cmd.poutput(directory_str)
419 self._cmd.poutput("%d files" % len(selectables))
Harald Welteb2edd142021-01-08 23:29:35 +0100420
Philipp Maierff9dae22021-02-25 17:03:21 +0100421 def walk(self, indent = 0, action = None, context = None):
422 """Recursively walk through the file system, starting at the currently selected DF"""
423 files = self._cmd.rs.selected_file.get_selectables(flags = ['FNAMES', 'ANAMES'])
424 for f in files:
425 if not action:
426 output_str = " " * indent + str(f) + (" " * 250)
427 output_str = output_str[0:25]
428 if isinstance(files[f], CardADF):
429 output_str += " " + str(files[f].aid)
430 else:
431 output_str += " " + str(files[f].fid)
432 output_str += " " + str(files[f].desc)
433 self._cmd.poutput(output_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200434
Philipp Maierff9dae22021-02-25 17:03:21 +0100435 if isinstance(files[f], CardDF):
Philipp Maierf408a402021-04-09 21:16:12 +0200436 skip_df=False
437 try:
438 fcp_dec = self._cmd.rs.select(f, self._cmd)
439 except Exception as e:
440 skip_df=True
441 df = self._cmd.rs.selected_file
442 df_path_list = df.fully_qualified_path(True)
443 df_skip_reason_str = '/'.join(df_path_list) + "/" + str(f) + ", " + str(e)
444 if context:
445 context['DF_SKIP'] += 1
446 context['DF_SKIP_REASON'].append(df_skip_reason_str)
447
448 # If the DF was skipped, we never have entered the directory
449 # below, so we must not move up.
450 if skip_df == False:
451 self.walk(indent + 1, action, context)
452 fcp_dec = self._cmd.rs.select("..", self._cmd)
453
Philipp Maierff9dae22021-02-25 17:03:21 +0100454 elif action:
Philipp Maierb152a9e2021-04-01 17:13:03 +0200455 df_before_action = self._cmd.rs.selected_file
Philipp Maierff9dae22021-02-25 17:03:21 +0100456 action(f, context)
Philipp Maierb152a9e2021-04-01 17:13:03 +0200457 # When walking through the file system tree the action must not
458 # always restore the currently selected file to the file that
459 # was selected before executing the action() callback.
460 if df_before_action != self._cmd.rs.selected_file:
Harald Weltec9cdce32021-04-11 10:28:28 +0200461 raise RuntimeError("inconsistent walk, %s is currently selected but expecting %s to be selected"
Philipp Maierb152a9e2021-04-01 17:13:03 +0200462 % (str(self._cmd.rs.selected_file), str(df_before_action)))
Philipp Maierff9dae22021-02-25 17:03:21 +0100463
464 def do_tree(self, opts):
465 """Display a filesystem-tree with all selectable files"""
466 self.walk()
467
Philipp Maier24f7bd32021-02-25 17:06:18 +0100468 def export(self, filename, context):
Philipp Maierac34dcc2021-04-01 17:19:05 +0200469 """ Select and export a single file """
Philipp Maier24f7bd32021-02-25 17:06:18 +0100470 context['COUNT'] += 1
Philipp Maierac34dcc2021-04-01 17:19:05 +0200471 df = self._cmd.rs.selected_file
472
473 if not isinstance(df, CardDF):
474 raise RuntimeError("currently selected file %s is not a DF or ADF" % str(df))
475
476 df_path_list = df.fully_qualified_path(True)
477 df_path_list_fid = df.fully_qualified_path(False)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100478
Philipp Maier80ce71f2021-04-19 21:24:23 +0200479 file_str = '/'.join(df_path_list) + "/" + str(filename)
480 self._cmd.poutput(boxed_heading_str(file_str))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100481
Philipp Maierac34dcc2021-04-01 17:19:05 +0200482 self._cmd.poutput("# directory: %s (%s)" % ('/'.join(df_path_list), '/'.join(df_path_list_fid)))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100483 try:
484 fcp_dec = self._cmd.rs.select(filename, self._cmd)
Philipp Maierac34dcc2021-04-01 17:19:05 +0200485 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 +0100486
487 fd = fcp_dec['file_descriptor']
488 structure = fd['structure']
489 self._cmd.poutput("# structure: %s" % str(structure))
490
Philipp Maierac34dcc2021-04-01 17:19:05 +0200491 for f in df_path_list:
Philipp Maier24f7bd32021-02-25 17:06:18 +0100492 self._cmd.poutput("select " + str(f))
Philipp Maierac34dcc2021-04-01 17:19:05 +0200493 self._cmd.poutput("select " + self._cmd.rs.selected_file.name)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100494
495 if structure == 'transparent':
496 result = self._cmd.rs.read_binary()
497 self._cmd.poutput("update_binary " + str(result[0]))
Harald Welte917d98c2021-04-21 11:51:25 +0200498 elif structure == 'cyclic' or structure == 'linear_fixed':
Philipp Maier24f7bd32021-02-25 17:06:18 +0100499 num_of_rec = fd['num_of_rec']
500 for r in range(1, num_of_rec + 1):
501 result = self._cmd.rs.read_record(r)
502 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
Harald Welte917d98c2021-04-21 11:51:25 +0200503 elif structure == 'ber_tlv':
504 tags = self._cmd.rs.retrieve_tags()
505 for t in tags:
506 result = self._cmd.rs.retrieve_data(t)
Harald Weltec1475302021-05-21 21:47:55 +0200507 (tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
Harald Welte917d98c2021-04-21 11:51:25 +0200508 self._cmd.poutput("set_data 0x%02x %s" % (t, b2h(val)))
509 else:
510 raise RuntimeError('Unsupported structure "%s" of file "%s"' % (structure, filename))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100511 except Exception as e:
Philipp Maierac34dcc2021-04-01 17:19:05 +0200512 bad_file_str = '/'.join(df_path_list) + "/" + str(filename) + ", " + str(e)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100513 self._cmd.poutput("# bad file: %s" % bad_file_str)
514 context['ERR'] += 1
515 context['BAD'].append(bad_file_str)
516
Philipp Maierac34dcc2021-04-01 17:19:05 +0200517 # When reading the file is done, make sure the parent file is
518 # selected again. This will be the usual case, however we need
519 # to check before since we must not select the same DF twice
520 if df != self._cmd.rs.selected_file:
521 self._cmd.rs.select(df.fid or df.aid, self._cmd)
522
Philipp Maier24f7bd32021-02-25 17:06:18 +0100523 self._cmd.poutput("#")
524
525 export_parser = argparse.ArgumentParser()
526 export_parser.add_argument('--filename', type=str, default=None, help='only export specific file')
527
528 @cmd2.with_argparser(export_parser)
529 def do_export(self, opts):
530 """Export files to script that can be imported back later"""
Philipp Maierf408a402021-04-09 21:16:12 +0200531 context = {'ERR':0, 'COUNT':0, 'BAD':[], 'DF_SKIP':0, 'DF_SKIP_REASON':[]}
Philipp Maier24f7bd32021-02-25 17:06:18 +0100532 if opts.filename:
533 self.export(opts.filename, context)
534 else:
535 self.walk(0, self.export, context)
Philipp Maier80ce71f2021-04-19 21:24:23 +0200536
537 self._cmd.poutput(boxed_heading_str("Export summary"))
538
Philipp Maier24f7bd32021-02-25 17:06:18 +0100539 self._cmd.poutput("# total files visited: %u" % context['COUNT'])
540 self._cmd.poutput("# bad files: %u" % context['ERR'])
541 for b in context['BAD']:
542 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200543
544 self._cmd.poutput("# skipped dedicated files(s): %u" % context['DF_SKIP'])
545 for b in context['DF_SKIP_REASON']:
546 self._cmd.poutput("# " + b)
547
548 if context['ERR'] and context['DF_SKIP']:
Harald Weltec9cdce32021-04-11 10:28:28 +0200549 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 +0200550 elif context['ERR']:
Harald Weltec9cdce32021-04-11 10:28:28 +0200551 raise RuntimeError("unable to export %i elementary file(s)" % context['ERR'])
Philipp Maierf408a402021-04-09 21:16:12 +0200552 elif context['DF_SKIP']:
553 raise RuntimeError("unable to export %i dedicated files(s)" % context['ERR'])
Harald Welteb2edd142021-01-08 23:29:35 +0100554
Harald Weltedaf2b392021-05-03 23:17:29 +0200555 def do_reset(self, opts):
556 """Reset the Card."""
557 atr = self._cmd.rs.reset(self._cmd)
558 self._cmd.poutput('Card ATR: %s' % atr)
559 self._cmd.update_prompt()
560
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200561 def do_desc(self, opts):
562 """Display human readable file description for the currently selected file"""
563 desc = self._cmd.rs.selected_file.desc
564 if desc:
565 self._cmd.poutput(desc)
566 else:
567 self._cmd.poutput("no description available")
568
569 def do_verify_adm(self, arg):
570 """VERIFY the ADM1 PIN"""
571 if arg:
572 # use specified ADM-PIN
573 pin_adm = sanitize_pin_adm(arg)
574 else:
575 # try to find an ADM-PIN if none is specified
576 result = card_key_provider_get_field('ADM1', key='ICCID', value=self._cmd.iccid)
577 pin_adm = sanitize_pin_adm(result)
578 if pin_adm:
579 self._cmd.poutput("found ADM-PIN '%s' for ICCID '%s'" % (result, self._cmd.iccid))
580 else:
Philipp Maierf0241452021-09-22 16:35:55 +0200581 raise ValueError("cannot find ADM-PIN for ICCID '%s'" % (self._cmd.iccid))
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200582
583 if pin_adm:
584 self._cmd.card.verify_adm(h2b(pin_adm))
585 else:
Philipp Maierf0241452021-09-22 16:35:55 +0200586 raise ValueError("error: cannot authenticate, no adm-pin!")
Harald Welteb2edd142021-01-08 23:29:35 +0100587
Harald Welte31d2cf02021-04-03 10:47:29 +0200588@with_default_category('ISO7816 Commands')
589class Iso7816Commands(CommandSet):
590 def __init__(self):
591 super().__init__()
592
593 def do_select(self, opts):
594 """SELECT a File (ADF/DF/EF)"""
595 if len(opts.arg_list) == 0:
596 path_list = self._cmd.rs.selected_file.fully_qualified_path(True)
597 path_list_fid = self._cmd.rs.selected_file.fully_qualified_path(False)
598 self._cmd.poutput("currently selected file: " + '/'.join(path_list) + " (" + '/'.join(path_list_fid) + ")")
599 return
600
601 path = opts.arg_list[0]
602 fcp_dec = self._cmd.rs.select(path, self._cmd)
603 self._cmd.update_prompt()
Harald Welteb00e8932021-04-10 17:19:13 +0200604 self._cmd.poutput_json(fcp_dec)
Harald Welte31d2cf02021-04-03 10:47:29 +0200605
606 def complete_select(self, text, line, begidx, endidx) -> List[str]:
607 """Command Line tab completion for SELECT"""
608 index_dict = { 1: self._cmd.rs.selected_file.get_selectable_names() }
609 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
610
611 def get_code(self, code):
612 """Use code either directly or try to get it from external data source"""
613 auto = ('PIN1', 'PIN2', 'PUK1', 'PUK2')
614
615 if str(code).upper() not in auto:
616 return sanitize_pin_adm(code)
617
618 result = card_key_provider_get_field(str(code), key='ICCID', value=self._cmd.iccid)
619 result = sanitize_pin_adm(result)
620 if result:
621 self._cmd.poutput("found %s '%s' for ICCID '%s'" % (code.upper(), result, self._cmd.iccid))
622 else:
623 self._cmd.poutput("cannot find %s for ICCID '%s'" % (code.upper(), self._cmd.iccid))
624 return result
625
626 verify_chv_parser = argparse.ArgumentParser()
627 verify_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
628 verify_chv_parser.add_argument('pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
629
630 @cmd2.with_argparser(verify_chv_parser)
631 def do_verify_chv(self, opts):
632 """Verify (authenticate) using specified PIN code"""
633 pin = self.get_code(opts.pin_code)
634 (data, sw) = self._cmd.card._scc.verify_chv(opts.pin_nr, h2b(pin))
Harald Weltec9cdce32021-04-11 10:28:28 +0200635 self._cmd.poutput("CHV verification successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200636
637 unblock_chv_parser = argparse.ArgumentParser()
638 unblock_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
639 unblock_chv_parser.add_argument('puk_code', type=str, help='PUK code digits \"PUK1\" or \"PUK2\" to get PUK code from external data source')
640 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')
641
642 @cmd2.with_argparser(unblock_chv_parser)
643 def do_unblock_chv(self, opts):
644 """Unblock PIN code using specified PUK code"""
645 new_pin = self.get_code(opts.new_pin_code)
646 puk = self.get_code(opts.puk_code)
647 (data, sw) = self._cmd.card._scc.unblock_chv(opts.pin_nr, h2b(puk), h2b(new_pin))
648 self._cmd.poutput("CHV unblock successful")
649
650 change_chv_parser = argparse.ArgumentParser()
651 change_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
652 change_chv_parser.add_argument('pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
653 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')
654
655 @cmd2.with_argparser(change_chv_parser)
656 def do_change_chv(self, opts):
657 """Change PIN code to a new PIN code"""
658 new_pin = self.get_code(opts.new_pin_code)
659 pin = self.get_code(opts.pin_code)
660 (data, sw) = self._cmd.card._scc.change_chv(opts.pin_nr, h2b(pin), h2b(new_pin))
661 self._cmd.poutput("CHV change successful")
662
663 disable_chv_parser = argparse.ArgumentParser()
664 disable_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
665 disable_chv_parser.add_argument('pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
666
667 @cmd2.with_argparser(disable_chv_parser)
668 def do_disable_chv(self, opts):
669 """Disable PIN code using specified PIN code"""
670 pin = self.get_code(opts.pin_code)
671 (data, sw) = self._cmd.card._scc.disable_chv(opts.pin_nr, h2b(pin))
672 self._cmd.poutput("CHV disable successful")
673
674 enable_chv_parser = argparse.ArgumentParser()
675 enable_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
676 enable_chv_parser.add_argument('pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
677
678 @cmd2.with_argparser(enable_chv_parser)
679 def do_enable_chv(self, opts):
680 """Enable PIN code using specified PIN code"""
681 pin = self.get_code(opts.pin_code)
682 (data, sw) = self._cmd.card._scc.enable_chv(opts.pin_nr, h2b(pin))
683 self._cmd.poutput("CHV enable successful")
684
Harald Weltea4631612021-04-10 18:17:55 +0200685 def do_deactivate_file(self, opts):
686 """Deactivate the current EF"""
Harald Welte485692b2021-05-25 22:21:44 +0200687 (data, sw) = self._cmd.card._scc.deactivate_file()
Harald Weltea4631612021-04-10 18:17:55 +0200688
689 def do_activate_file(self, opts):
Harald Welte485692b2021-05-25 22:21:44 +0200690 """Activate the specified EF"""
691 path = opts.arg_list[0]
692 (data, sw) = self._cmd.rs.activate_file(path)
693
694 def complete_activate_file(self, text, line, begidx, endidx) -> List[str]:
695 """Command Line tab completion for ACTIVATE FILE"""
696 index_dict = { 1: self._cmd.rs.selected_file.get_selectable_names() }
697 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200698
Harald Welte703f9332021-04-10 18:39:32 +0200699 open_chan_parser = argparse.ArgumentParser()
700 open_chan_parser.add_argument('chan_nr', type=int, default=0, help='Channel Number')
701
702 @cmd2.with_argparser(open_chan_parser)
703 def do_open_channel(self, opts):
704 """Open a logical channel."""
705 (data, sw) = self._cmd.card._scc.manage_channel(mode='open', lchan_nr=opts.chan_nr)
706
707 close_chan_parser = argparse.ArgumentParser()
708 close_chan_parser.add_argument('chan_nr', type=int, default=0, help='Channel Number')
709
710 @cmd2.with_argparser(close_chan_parser)
711 def do_close_channel(self, opts):
712 """Close a logical channel."""
713 (data, sw) = self._cmd.card._scc.manage_channel(mode='close', lchan_nr=opts.chan_nr)
714
Harald Welte34b05d32021-05-25 22:03:13 +0200715 def do_status(self, opts):
716 """Perform the STATUS command."""
717 fcp_dec = self._cmd.rs.status()
718 self._cmd.poutput_json(fcp_dec)
719
Harald Welteec950532021-10-20 13:09:00 +0200720 suspend_uicc_parser = argparse.ArgumentParser()
721 suspend_uicc_parser.add_argument('--min-duration-secs', type=int, default=60,
722 help='Proposed minimum duration of suspension')
723 suspend_uicc_parser.add_argument('--max-duration-secs', type=int, default=24*60*60,
724 help='Proposed maximum duration of suspension')
725
726 # not ISO7816-4 but TS 102 221
727 @cmd2.with_argparser(suspend_uicc_parser)
728 def do_suspend_uicc(self, opts):
729 """Perform the SUSPEND UICC command. Only supported on some UICC."""
730 (duration, token, sw) = self._cmd.card._scc.suspend_uicc(min_len_secs=opts.min_duration_secs,
731 max_len_secs=opts.max_duration_secs)
732 self._cmd.poutput('Negotiated Duration: %u secs, Token: %s, SW: %s' % (duration, token, sw))
733
Harald Welte703f9332021-04-10 18:39:32 +0200734
Harald Weltef2e761c2021-04-11 11:56:44 +0200735option_parser = argparse.ArgumentParser(prog='pySim-shell', description='interactive SIM card shell',
736 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
Harald Welte28c24312021-04-11 12:19:36 +0200737argparse_add_reader_args(option_parser)
Harald Weltec8ff0262021-04-11 12:06:13 +0200738
739global_group = option_parser.add_argument_group('General Options')
740global_group.add_argument('--script', metavar='PATH', default=None,
Harald Welte28c24312021-04-11 12:19:36 +0200741 help='script with pySim-shell commands to be executed automatically at start-up')
742global_group.add_argument('--csv', metavar='FILE', default=None, help='Read card data from CSV file')
Philipp Maier76667642021-09-22 16:53:22 +0200743global_group.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
744 help="Use automatic card handling machine")
Harald Weltec8ff0262021-04-11 12:06:13 +0200745
746adm_group = global_group.add_mutually_exclusive_group()
747adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', default=None,
748 help='ADM PIN used for provisioning (overwrites default)')
749adm_group.add_argument('-A', '--pin-adm-hex', metavar='PIN_ADM1_HEX', dest='pin_adm_hex', default=None,
750 help='ADM PIN used for provisioning, as hex string (16 characters long)')
Harald Welteb2edd142021-01-08 23:29:35 +0100751
752
753if __name__ == '__main__':
754
755 # Parse options
Harald Weltef2e761c2021-04-11 11:56:44 +0200756 opts = option_parser.parse_args()
Harald Welteb2edd142021-01-08 23:29:35 +0100757
Philipp Maier13e258d2021-04-08 17:48:49 +0200758 # If a script file is specified, be sure that it actually exists
759 if opts.script:
760 if not os.access(opts.script, os.R_OK):
761 print("Invalid script file!")
762 sys.exit(2)
763
Philipp Maier2b11c322021-03-17 12:37:39 +0100764 # Register csv-file as card data provider, either from specified CSV
765 # or from CSV file in home directory
766 csv_default = str(Path.home()) + "/.osmocom/pysim/card_data.csv"
767 if opts.csv:
Harald Welte4442b3d2021-04-03 09:00:16 +0200768 card_key_provider_register(CardKeyProviderCsv(opts.csv))
Philipp Maier2b11c322021-03-17 12:37:39 +0100769 if os.path.isfile(csv_default):
Harald Welte4442b3d2021-04-03 09:00:16 +0200770 card_key_provider_register(CardKeyProviderCsv(csv_default))
Philipp Maier2b11c322021-03-17 12:37:39 +0100771
Philipp Maierea95c392021-09-16 13:10:19 +0200772 # Init card reader driver
773 sl = init_reader(opts)
774 if sl is None:
775 exit(1)
776
777 # Create command layer
778 scc = SimCardCommands(transport=sl)
779
Philipp Maier76667642021-09-22 16:53:22 +0200780 # Create a card handler (for bulk provisioning)
781 if opts.card_handler_config:
782 ch = CardHandlerAuto(None, opts.card_handler_config)
783 else:
784 ch = CardHandler(sl)
785
Philipp Maier5d698e52021-09-16 13:18:01 +0200786 # Detect and initialize the card in the reader. This may fail when there
787 # is no card in the reader or the card is unresponsive. PysimApp is
788 # able to tolerate and recover from that.
789 try:
790 rs, card = init_card(sl)
Philipp Maier76667642021-09-22 16:53:22 +0200791 app = PysimApp(card, rs, sl, ch, opts.script)
Philipp Maier5d698e52021-09-16 13:18:01 +0200792 except:
793 print("Card initialization failed with an exception:")
794 print("---------------------8<---------------------")
795 traceback.print_exc()
796 print("---------------------8<---------------------")
797 print("(you may still try to recover from this manually by using the 'equip' command.)")
798 print(" it should also be noted that some readers may behave strangely when no card")
799 print(" is inserted.)")
800 print("")
Philipp Maier76667642021-09-22 16:53:22 +0200801 app = PysimApp(None, None, sl, ch, opts.script)
Philipp Maierea95c392021-09-16 13:10:19 +0200802
Philipp Maier228c98e2021-03-10 20:14:06 +0100803 # If the user supplies an ADM PIN at via commandline args authenticate
Harald Weltec9cdce32021-04-11 10:28:28 +0200804 # immediately so that the user does not have to use the shell commands
Philipp Maier228c98e2021-03-10 20:14:06 +0100805 pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
806 if pin_adm:
Philipp Maier5d698e52021-09-16 13:18:01 +0200807 if not card:
808 print("Card error, cannot do ADM verification with supplied ADM pin now.")
Philipp Maier228c98e2021-03-10 20:14:06 +0100809 try:
810 card.verify_adm(h2b(pin_adm))
811 except Exception as e:
812 print(e)
813
Harald Welteb2edd142021-01-08 23:29:35 +0100814 app.cmdloop()