blob: ff1239646fc381e21554f4fea8ed0a5188f6c407 [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)
141 cmd_set = self.find_commandsets(Iso7816Commands)
142 if cmd_set:
143 self.unregister_command_set(cmd_set[0])
144 cmd_set = self.find_commandsets(PySimCommands)
145 if cmd_set:
146 self.unregister_command_set(cmd_set[0])
147
148 self.card = card
149 self.rs = rs
150
151 # When a card object and a runtime state is present, (re)equip pySim-shell with everything that is
152 # needed to operate on cards.
153 if self.card and self.rs:
154 self._onchange_conserve_write('conserve_write', False, self.conserve_write)
155 self._onchange_apdu_trace('apdu_trace', False, self.apdu_trace)
156 self.register_command_set(Iso7816Commands())
157 self.register_command_set(PySimCommands())
158 self.iccid, sw = self.card.read_iccid()
159 rs.select('MF', self)
Philipp Maier76667642021-09-22 16:53:22 +0200160 rc = True
Philipp Maier5d698e52021-09-16 13:18:01 +0200161 else:
162 self.poutput("pySim-shell not equipped!")
163
164 self.update_prompt()
Philipp Maier76667642021-09-22 16:53:22 +0200165 return rc
Philipp Maier5d698e52021-09-16 13:18:01 +0200166
Harald Welte1748b932021-04-06 21:12:25 +0200167 def poutput_json(self, data, force_no_pretty = False):
Harald Weltec9cdce32021-04-11 10:28:28 +0200168 """like cmd2.poutput() but for a JSON serializable dict."""
Harald Welte1748b932021-04-06 21:12:25 +0200169 if force_no_pretty or self.json_pretty_print == False:
Harald Welte5e749a72021-04-10 17:18:17 +0200170 output = json.dumps(data, cls=JsonEncoder)
Harald Welte1748b932021-04-06 21:12:25 +0200171 else:
Harald Welte5e749a72021-04-10 17:18:17 +0200172 output = json.dumps(data, cls=JsonEncoder, indent=4)
Harald Welte1748b932021-04-06 21:12:25 +0200173 self.poutput(output)
Harald Welteb2edd142021-01-08 23:29:35 +0100174
175 def _onchange_numeric_path(self, param_name, old, new):
176 self.update_prompt()
177
Philipp Maier38c74f62021-03-17 17:19:52 +0100178 def _onchange_conserve_write(self, param_name, old, new):
Philipp Maier5d698e52021-09-16 13:18:01 +0200179 if self.rs:
180 self.rs.conserve_write = new
Philipp Maier38c74f62021-03-17 17:19:52 +0100181
Harald Welte7829d8a2021-04-10 11:28:53 +0200182 def _onchange_apdu_trace(self, param_name, old, new):
Philipp Maier5d698e52021-09-16 13:18:01 +0200183 if self.card:
184 if new == True:
185 self.card._scc._tp.apdu_tracer = self.Cmd2ApduTracer(self)
186 else:
187 self.card._scc._tp.apdu_tracer = None
Harald Welte7829d8a2021-04-10 11:28:53 +0200188
189 class Cmd2ApduTracer(ApduTracer):
190 def __init__(self, cmd2_app):
191 self.cmd2 = app
192
193 def trace_response(self, cmd, sw, resp):
194 self.cmd2.poutput("-> %s %s" % (cmd[:10], cmd[10:]))
195 self.cmd2.poutput("<- %s: %s" % (sw, resp))
196
Harald Welteb2edd142021-01-08 23:29:35 +0100197 def update_prompt(self):
Philipp Maier5d698e52021-09-16 13:18:01 +0200198 if self.rs:
199 path_list = self.rs.selected_file.fully_qualified_path(not self.numeric_path)
200 self.prompt = 'pySIM-shell (%s)> ' % ('/'.join(path_list))
201 else:
202 self.prompt = 'pySIM-shell (no card)> '
Harald Welteb2edd142021-01-08 23:29:35 +0100203
204 @cmd2.with_category(CUSTOM_CATEGORY)
205 def do_intro(self, _):
206 """Display the intro banner"""
207 self.poutput(self.intro)
208
Philipp Maier5d698e52021-09-16 13:18:01 +0200209 @cmd2.with_category(CUSTOM_CATEGORY)
210 def do_equip(self, opts):
211 """Equip pySim-shell with card"""
212 rs, card = init_card(sl);
213 self.equip(card, rs)
214
Philipp Maier76667642021-09-22 16:53:22 +0200215 class InterceptStderr(list):
216 def __init__(self):
217 self._stderr_backup = sys.stderr
218 def __enter__(self):
219 self._stringio_stderr = StringIO()
220 sys.stderr = self._stringio_stderr
221 return self
222 def __exit__(self, *args):
223 self.stderr = self._stringio_stderr.getvalue().strip()
224 del self._stringio_stderr
225 sys.stderr = self._stderr_backup
226
227 def _show_failure_sign(self):
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(style(" + ## ## +", fg=fg.bright_red))
234 self.poutput(style(" +-------------+", fg=fg.bright_red))
235 self.poutput("")
236
237 def _show_success_sign(self):
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(style(" + ## +", fg=fg.bright_green))
244 self.poutput(style(" +-------------+", fg=fg.bright_green))
245 self.poutput("")
246
247 def _process_card(self, first, script_path):
248
249 # Early phase of card initialzation (this part may fail with an exception)
250 try:
251 rs, card = init_card(self.sl)
252 rc = self.equip(card, rs)
253 except:
254 self.poutput("")
255 self.poutput("Card initialization failed with an exception:")
256 self.poutput("---------------------8<---------------------")
257 traceback.print_exc()
258 self.poutput("---------------------8<---------------------")
259 self.poutput("")
260 return -1
261
262 # Actual card processing step. This part should never fail with an exception since the cmd2
263 # do_run_script method will catch any exception that might occur during script execution.
264 if rc:
265 self.poutput("")
266 self.poutput("Transcript stdout:")
267 self.poutput("---------------------8<---------------------")
268 with self.InterceptStderr() as logged:
269 self.do_run_script(script_path)
270 self.poutput("---------------------8<---------------------")
271
272 self.poutput("")
273 self.poutput("Transcript stderr:")
274 if logged.stderr:
275 self.poutput("---------------------8<---------------------")
276 self.poutput(logged.stderr)
277 self.poutput("---------------------8<---------------------")
278 else:
279 self.poutput("(none)")
280
281 # Check for exceptions
282 self.poutput("")
283 if "EXCEPTION of type" not in logged.stderr:
284 return 0
285
286 return -1
287
288 bulk_script_parser = argparse.ArgumentParser()
289 bulk_script_parser.add_argument('script_path', help="path to the script file")
290 bulk_script_parser.add_argument('--halt_on_error', help='stop card handling if an exeption occurs',
291 action='store_true')
292 bulk_script_parser.add_argument('--tries', type=int, default=2,
293 help='how many tries before trying the next card')
294 bulk_script_parser.add_argument('--on_stop_action', type=str, default=None,
295 help='commandline to execute when card handling has stopped')
296 bulk_script_parser.add_argument('--pre_card_action', type=str, default=None,
297 help='commandline to execute before actually talking to the card')
298
299 @cmd2.with_argparser(bulk_script_parser)
300 @cmd2.with_category(CUSTOM_CATEGORY)
301 def do_bulk_script(self, opts):
302 """Run script on multiple cards (bulk provisioning)"""
303
304 # Make sure that the script file exists and that it is readable.
305 if not os.access(opts.script_path, os.R_OK):
306 self.poutput("Invalid script file!")
307 return
308
309 success_count = 0
310 fail_count = 0
311
312 first = True
313 while 1:
314 # TODO: Count consecutive failures, if more than N consecutive failures occur, then stop.
315 # The ratinale is: There may be a problem with the device, we do want to prevent that
316 # all remaining cards are fired to the error bin. This is only relevant for situations
317 # with large stacks, probably we do not need this feature right now.
318
319 try:
320 # In case of failure, try multiple times.
321 for i in range(opts.tries):
322 # fetch card into reader bay
323 ch.get(first)
324
325 # if necessary execute an action before we start processing the card
326 if(opts.pre_card_action):
327 os.system(opts.pre_card_action)
328
329 # process the card
330 rc = self._process_card(first, opts.script_path)
331 if rc == 0:
332 success_count = success_count + 1
333 self._show_success_sign()
334 self.poutput("Statistics: success :%i, failure: %i" % (success_count, fail_count))
335 break
336 else:
337 fail_count = fail_count + 1
338 self._show_failure_sign()
339 self.poutput("Statistics: success :%i, failure: %i" % (success_count, fail_count))
340
341
342 # Depending on success or failure, the card goes either in the "error" bin or in the
343 # "done" bin.
344 if rc < 0:
345 ch.error()
346 else:
347 ch.done()
348
349 # In most cases it is possible to proceed with the next card, but the
350 # user may decide to halt immediately when an error occurs
351 if opts.halt_on_error and rc < 0:
352 return
353
354 except (KeyboardInterrupt):
355 self.poutput("")
356 self.poutput("Terminated by user!")
357 return;
358 except (SystemExit):
359 # When all cards are processed the card handler device will throw a SystemExit
360 # exception. Also Errors that are not recoverable (cards stuck etc.) will end up here.
361 # The user has the option to execute some action to make aware that the card handler
362 # needs service.
363 if(opts.on_stop_action):
364 os.system(opts.on_stop_action)
365 return
366 except:
367 self.poutput("")
368 self.poutput("Card handling failed with an exception:")
369 self.poutput("---------------------8<---------------------")
370 traceback.print_exc()
371 self.poutput("---------------------8<---------------------")
372 self.poutput("")
373 fail_count = fail_count + 1
374 self._show_failure_sign()
375 self.poutput("Statistics: success :%i, failure: %i" % (success_count, fail_count))
376
377 first = False
378
Philipp Maierb52feed2021-09-22 16:43:13 +0200379 echo_parser = argparse.ArgumentParser()
380 echo_parser.add_argument('string', help="string to echo on the shell")
381
382 @cmd2.with_argparser(echo_parser)
383 @cmd2.with_category(CUSTOM_CATEGORY)
384 def do_echo(self, opts):
385 self.poutput(opts.string)
Harald Welteb2edd142021-01-08 23:29:35 +0100386
Harald Welte31d2cf02021-04-03 10:47:29 +0200387@with_default_category('pySim Commands')
388class PySimCommands(CommandSet):
Harald Welteb2edd142021-01-08 23:29:35 +0100389 def __init__(self):
390 super().__init__()
391
Philipp Maier5d3e2592021-02-22 17:22:16 +0100392 dir_parser = argparse.ArgumentParser()
393 dir_parser.add_argument('--fids', help='Show file identifiers', action='store_true')
394 dir_parser.add_argument('--names', help='Show file names', action='store_true')
395 dir_parser.add_argument('--apps', help='Show applications', action='store_true')
396 dir_parser.add_argument('--all', help='Show all selectable identifiers and names', action='store_true')
397
398 @cmd2.with_argparser(dir_parser)
399 def do_dir(self, opts):
400 """Show a listing of files available in currently selected DF or MF"""
401 if opts.all:
402 flags = []
403 elif opts.fids or opts.names or opts.apps:
404 flags = ['PARENT', 'SELF']
405 if opts.fids:
406 flags += ['FIDS', 'AIDS']
407 if opts.names:
408 flags += ['FNAMES', 'ANAMES']
409 if opts.apps:
410 flags += ['ANAMES', 'AIDS']
411 else:
412 flags = ['PARENT', 'SELF', 'FNAMES', 'ANAMES']
413 selectables = list(self._cmd.rs.selected_file.get_selectable_names(flags = flags))
414 directory_str = tabulate_str_list(selectables, width = 79, hspace = 2, lspace = 1, align_left = True)
415 path_list = self._cmd.rs.selected_file.fully_qualified_path(True)
416 self._cmd.poutput('/'.join(path_list))
417 path_list = self._cmd.rs.selected_file.fully_qualified_path(False)
418 self._cmd.poutput('/'.join(path_list))
419 self._cmd.poutput(directory_str)
420 self._cmd.poutput("%d files" % len(selectables))
Harald Welteb2edd142021-01-08 23:29:35 +0100421
Philipp Maierff9dae22021-02-25 17:03:21 +0100422 def walk(self, indent = 0, action = None, context = None):
423 """Recursively walk through the file system, starting at the currently selected DF"""
424 files = self._cmd.rs.selected_file.get_selectables(flags = ['FNAMES', 'ANAMES'])
425 for f in files:
426 if not action:
427 output_str = " " * indent + str(f) + (" " * 250)
428 output_str = output_str[0:25]
429 if isinstance(files[f], CardADF):
430 output_str += " " + str(files[f].aid)
431 else:
432 output_str += " " + str(files[f].fid)
433 output_str += " " + str(files[f].desc)
434 self._cmd.poutput(output_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200435
Philipp Maierff9dae22021-02-25 17:03:21 +0100436 if isinstance(files[f], CardDF):
Philipp Maierf408a402021-04-09 21:16:12 +0200437 skip_df=False
438 try:
439 fcp_dec = self._cmd.rs.select(f, self._cmd)
440 except Exception as e:
441 skip_df=True
442 df = self._cmd.rs.selected_file
443 df_path_list = df.fully_qualified_path(True)
444 df_skip_reason_str = '/'.join(df_path_list) + "/" + str(f) + ", " + str(e)
445 if context:
446 context['DF_SKIP'] += 1
447 context['DF_SKIP_REASON'].append(df_skip_reason_str)
448
449 # If the DF was skipped, we never have entered the directory
450 # below, so we must not move up.
451 if skip_df == False:
452 self.walk(indent + 1, action, context)
453 fcp_dec = self._cmd.rs.select("..", self._cmd)
454
Philipp Maierff9dae22021-02-25 17:03:21 +0100455 elif action:
Philipp Maierb152a9e2021-04-01 17:13:03 +0200456 df_before_action = self._cmd.rs.selected_file
Philipp Maierff9dae22021-02-25 17:03:21 +0100457 action(f, context)
Philipp Maierb152a9e2021-04-01 17:13:03 +0200458 # When walking through the file system tree the action must not
459 # always restore the currently selected file to the file that
460 # was selected before executing the action() callback.
461 if df_before_action != self._cmd.rs.selected_file:
Harald Weltec9cdce32021-04-11 10:28:28 +0200462 raise RuntimeError("inconsistent walk, %s is currently selected but expecting %s to be selected"
Philipp Maierb152a9e2021-04-01 17:13:03 +0200463 % (str(self._cmd.rs.selected_file), str(df_before_action)))
Philipp Maierff9dae22021-02-25 17:03:21 +0100464
465 def do_tree(self, opts):
466 """Display a filesystem-tree with all selectable files"""
467 self.walk()
468
Philipp Maier24f7bd32021-02-25 17:06:18 +0100469 def export(self, filename, context):
Philipp Maierac34dcc2021-04-01 17:19:05 +0200470 """ Select and export a single file """
Philipp Maier24f7bd32021-02-25 17:06:18 +0100471 context['COUNT'] += 1
Philipp Maierac34dcc2021-04-01 17:19:05 +0200472 df = self._cmd.rs.selected_file
473
474 if not isinstance(df, CardDF):
475 raise RuntimeError("currently selected file %s is not a DF or ADF" % str(df))
476
477 df_path_list = df.fully_qualified_path(True)
478 df_path_list_fid = df.fully_qualified_path(False)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100479
Philipp Maier80ce71f2021-04-19 21:24:23 +0200480 file_str = '/'.join(df_path_list) + "/" + str(filename)
481 self._cmd.poutput(boxed_heading_str(file_str))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100482
Philipp Maierac34dcc2021-04-01 17:19:05 +0200483 self._cmd.poutput("# directory: %s (%s)" % ('/'.join(df_path_list), '/'.join(df_path_list_fid)))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100484 try:
485 fcp_dec = self._cmd.rs.select(filename, self._cmd)
Philipp Maierac34dcc2021-04-01 17:19:05 +0200486 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 +0100487
488 fd = fcp_dec['file_descriptor']
489 structure = fd['structure']
490 self._cmd.poutput("# structure: %s" % str(structure))
491
Philipp Maierac34dcc2021-04-01 17:19:05 +0200492 for f in df_path_list:
Philipp Maier24f7bd32021-02-25 17:06:18 +0100493 self._cmd.poutput("select " + str(f))
Philipp Maierac34dcc2021-04-01 17:19:05 +0200494 self._cmd.poutput("select " + self._cmd.rs.selected_file.name)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100495
496 if structure == 'transparent':
497 result = self._cmd.rs.read_binary()
498 self._cmd.poutput("update_binary " + str(result[0]))
Harald Welte917d98c2021-04-21 11:51:25 +0200499 elif structure == 'cyclic' or structure == 'linear_fixed':
Philipp Maier24f7bd32021-02-25 17:06:18 +0100500 num_of_rec = fd['num_of_rec']
501 for r in range(1, num_of_rec + 1):
502 result = self._cmd.rs.read_record(r)
503 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
Harald Welte917d98c2021-04-21 11:51:25 +0200504 elif structure == 'ber_tlv':
505 tags = self._cmd.rs.retrieve_tags()
506 for t in tags:
507 result = self._cmd.rs.retrieve_data(t)
Harald Weltec1475302021-05-21 21:47:55 +0200508 (tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
Harald Welte917d98c2021-04-21 11:51:25 +0200509 self._cmd.poutput("set_data 0x%02x %s" % (t, b2h(val)))
510 else:
511 raise RuntimeError('Unsupported structure "%s" of file "%s"' % (structure, filename))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100512 except Exception as e:
Philipp Maierac34dcc2021-04-01 17:19:05 +0200513 bad_file_str = '/'.join(df_path_list) + "/" + str(filename) + ", " + str(e)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100514 self._cmd.poutput("# bad file: %s" % bad_file_str)
515 context['ERR'] += 1
516 context['BAD'].append(bad_file_str)
517
Philipp Maierac34dcc2021-04-01 17:19:05 +0200518 # When reading the file is done, make sure the parent file is
519 # selected again. This will be the usual case, however we need
520 # to check before since we must not select the same DF twice
521 if df != self._cmd.rs.selected_file:
522 self._cmd.rs.select(df.fid or df.aid, self._cmd)
523
Philipp Maier24f7bd32021-02-25 17:06:18 +0100524 self._cmd.poutput("#")
525
526 export_parser = argparse.ArgumentParser()
527 export_parser.add_argument('--filename', type=str, default=None, help='only export specific file')
528
529 @cmd2.with_argparser(export_parser)
530 def do_export(self, opts):
531 """Export files to script that can be imported back later"""
Philipp Maierf408a402021-04-09 21:16:12 +0200532 context = {'ERR':0, 'COUNT':0, 'BAD':[], 'DF_SKIP':0, 'DF_SKIP_REASON':[]}
Philipp Maier24f7bd32021-02-25 17:06:18 +0100533 if opts.filename:
534 self.export(opts.filename, context)
535 else:
536 self.walk(0, self.export, context)
Philipp Maier80ce71f2021-04-19 21:24:23 +0200537
538 self._cmd.poutput(boxed_heading_str("Export summary"))
539
Philipp Maier24f7bd32021-02-25 17:06:18 +0100540 self._cmd.poutput("# total files visited: %u" % context['COUNT'])
541 self._cmd.poutput("# bad files: %u" % context['ERR'])
542 for b in context['BAD']:
543 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200544
545 self._cmd.poutput("# skipped dedicated files(s): %u" % context['DF_SKIP'])
546 for b in context['DF_SKIP_REASON']:
547 self._cmd.poutput("# " + b)
548
549 if context['ERR'] and context['DF_SKIP']:
Harald Weltec9cdce32021-04-11 10:28:28 +0200550 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 +0200551 elif context['ERR']:
Harald Weltec9cdce32021-04-11 10:28:28 +0200552 raise RuntimeError("unable to export %i elementary file(s)" % context['ERR'])
Philipp Maierf408a402021-04-09 21:16:12 +0200553 elif context['DF_SKIP']:
554 raise RuntimeError("unable to export %i dedicated files(s)" % context['ERR'])
Harald Welteb2edd142021-01-08 23:29:35 +0100555
Harald Weltedaf2b392021-05-03 23:17:29 +0200556 def do_reset(self, opts):
557 """Reset the Card."""
558 atr = self._cmd.rs.reset(self._cmd)
559 self._cmd.poutput('Card ATR: %s' % atr)
560 self._cmd.update_prompt()
561
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200562 def do_desc(self, opts):
563 """Display human readable file description for the currently selected file"""
564 desc = self._cmd.rs.selected_file.desc
565 if desc:
566 self._cmd.poutput(desc)
567 else:
568 self._cmd.poutput("no description available")
569
570 def do_verify_adm(self, arg):
571 """VERIFY the ADM1 PIN"""
572 if arg:
573 # use specified ADM-PIN
574 pin_adm = sanitize_pin_adm(arg)
575 else:
576 # try to find an ADM-PIN if none is specified
577 result = card_key_provider_get_field('ADM1', key='ICCID', value=self._cmd.iccid)
578 pin_adm = sanitize_pin_adm(result)
579 if pin_adm:
580 self._cmd.poutput("found ADM-PIN '%s' for ICCID '%s'" % (result, self._cmd.iccid))
581 else:
Philipp Maierf0241452021-09-22 16:35:55 +0200582 raise ValueError("cannot find ADM-PIN for ICCID '%s'" % (self._cmd.iccid))
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200583
584 if pin_adm:
585 self._cmd.card.verify_adm(h2b(pin_adm))
586 else:
Philipp Maierf0241452021-09-22 16:35:55 +0200587 raise ValueError("error: cannot authenticate, no adm-pin!")
Harald Welteb2edd142021-01-08 23:29:35 +0100588
Harald Welte31d2cf02021-04-03 10:47:29 +0200589@with_default_category('ISO7816 Commands')
590class Iso7816Commands(CommandSet):
591 def __init__(self):
592 super().__init__()
593
594 def do_select(self, opts):
595 """SELECT a File (ADF/DF/EF)"""
596 if len(opts.arg_list) == 0:
597 path_list = self._cmd.rs.selected_file.fully_qualified_path(True)
598 path_list_fid = self._cmd.rs.selected_file.fully_qualified_path(False)
599 self._cmd.poutput("currently selected file: " + '/'.join(path_list) + " (" + '/'.join(path_list_fid) + ")")
600 return
601
602 path = opts.arg_list[0]
603 fcp_dec = self._cmd.rs.select(path, self._cmd)
604 self._cmd.update_prompt()
Harald Welteb00e8932021-04-10 17:19:13 +0200605 self._cmd.poutput_json(fcp_dec)
Harald Welte31d2cf02021-04-03 10:47:29 +0200606
607 def complete_select(self, text, line, begidx, endidx) -> List[str]:
608 """Command Line tab completion for SELECT"""
609 index_dict = { 1: self._cmd.rs.selected_file.get_selectable_names() }
610 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
611
612 def get_code(self, code):
613 """Use code either directly or try to get it from external data source"""
614 auto = ('PIN1', 'PIN2', 'PUK1', 'PUK2')
615
616 if str(code).upper() not in auto:
617 return sanitize_pin_adm(code)
618
619 result = card_key_provider_get_field(str(code), key='ICCID', value=self._cmd.iccid)
620 result = sanitize_pin_adm(result)
621 if result:
622 self._cmd.poutput("found %s '%s' for ICCID '%s'" % (code.upper(), result, self._cmd.iccid))
623 else:
624 self._cmd.poutput("cannot find %s for ICCID '%s'" % (code.upper(), self._cmd.iccid))
625 return result
626
627 verify_chv_parser = argparse.ArgumentParser()
628 verify_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
629 verify_chv_parser.add_argument('pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
630
631 @cmd2.with_argparser(verify_chv_parser)
632 def do_verify_chv(self, opts):
633 """Verify (authenticate) using specified PIN code"""
634 pin = self.get_code(opts.pin_code)
635 (data, sw) = self._cmd.card._scc.verify_chv(opts.pin_nr, h2b(pin))
Harald Weltec9cdce32021-04-11 10:28:28 +0200636 self._cmd.poutput("CHV verification successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200637
638 unblock_chv_parser = argparse.ArgumentParser()
639 unblock_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
640 unblock_chv_parser.add_argument('puk_code', type=str, help='PUK code digits \"PUK1\" or \"PUK2\" to get PUK code from external data source')
641 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')
642
643 @cmd2.with_argparser(unblock_chv_parser)
644 def do_unblock_chv(self, opts):
645 """Unblock PIN code using specified PUK code"""
646 new_pin = self.get_code(opts.new_pin_code)
647 puk = self.get_code(opts.puk_code)
648 (data, sw) = self._cmd.card._scc.unblock_chv(opts.pin_nr, h2b(puk), h2b(new_pin))
649 self._cmd.poutput("CHV unblock successful")
650
651 change_chv_parser = argparse.ArgumentParser()
652 change_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
653 change_chv_parser.add_argument('pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
654 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')
655
656 @cmd2.with_argparser(change_chv_parser)
657 def do_change_chv(self, opts):
658 """Change PIN code to a new PIN code"""
659 new_pin = self.get_code(opts.new_pin_code)
660 pin = self.get_code(opts.pin_code)
661 (data, sw) = self._cmd.card._scc.change_chv(opts.pin_nr, h2b(pin), h2b(new_pin))
662 self._cmd.poutput("CHV change successful")
663
664 disable_chv_parser = argparse.ArgumentParser()
665 disable_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
666 disable_chv_parser.add_argument('pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
667
668 @cmd2.with_argparser(disable_chv_parser)
669 def do_disable_chv(self, opts):
670 """Disable PIN code using specified PIN code"""
671 pin = self.get_code(opts.pin_code)
672 (data, sw) = self._cmd.card._scc.disable_chv(opts.pin_nr, h2b(pin))
673 self._cmd.poutput("CHV disable successful")
674
675 enable_chv_parser = argparse.ArgumentParser()
676 enable_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
677 enable_chv_parser.add_argument('pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
678
679 @cmd2.with_argparser(enable_chv_parser)
680 def do_enable_chv(self, opts):
681 """Enable PIN code using specified PIN code"""
682 pin = self.get_code(opts.pin_code)
683 (data, sw) = self._cmd.card._scc.enable_chv(opts.pin_nr, h2b(pin))
684 self._cmd.poutput("CHV enable successful")
685
Harald Weltea4631612021-04-10 18:17:55 +0200686 def do_deactivate_file(self, opts):
687 """Deactivate the current EF"""
Harald Welte485692b2021-05-25 22:21:44 +0200688 (data, sw) = self._cmd.card._scc.deactivate_file()
Harald Weltea4631612021-04-10 18:17:55 +0200689
690 def do_activate_file(self, opts):
Harald Welte485692b2021-05-25 22:21:44 +0200691 """Activate the specified EF"""
692 path = opts.arg_list[0]
693 (data, sw) = self._cmd.rs.activate_file(path)
694
695 def complete_activate_file(self, text, line, begidx, endidx) -> List[str]:
696 """Command Line tab completion for ACTIVATE FILE"""
697 index_dict = { 1: self._cmd.rs.selected_file.get_selectable_names() }
698 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200699
Harald Welte703f9332021-04-10 18:39:32 +0200700 open_chan_parser = argparse.ArgumentParser()
701 open_chan_parser.add_argument('chan_nr', type=int, default=0, help='Channel Number')
702
703 @cmd2.with_argparser(open_chan_parser)
704 def do_open_channel(self, opts):
705 """Open a logical channel."""
706 (data, sw) = self._cmd.card._scc.manage_channel(mode='open', lchan_nr=opts.chan_nr)
707
708 close_chan_parser = argparse.ArgumentParser()
709 close_chan_parser.add_argument('chan_nr', type=int, default=0, help='Channel Number')
710
711 @cmd2.with_argparser(close_chan_parser)
712 def do_close_channel(self, opts):
713 """Close a logical channel."""
714 (data, sw) = self._cmd.card._scc.manage_channel(mode='close', lchan_nr=opts.chan_nr)
715
Harald Welte34b05d32021-05-25 22:03:13 +0200716 def do_status(self, opts):
717 """Perform the STATUS command."""
718 fcp_dec = self._cmd.rs.status()
719 self._cmd.poutput_json(fcp_dec)
720
Harald Welteec950532021-10-20 13:09:00 +0200721 suspend_uicc_parser = argparse.ArgumentParser()
722 suspend_uicc_parser.add_argument('--min-duration-secs', type=int, default=60,
723 help='Proposed minimum duration of suspension')
724 suspend_uicc_parser.add_argument('--max-duration-secs', type=int, default=24*60*60,
725 help='Proposed maximum duration of suspension')
726
727 # not ISO7816-4 but TS 102 221
728 @cmd2.with_argparser(suspend_uicc_parser)
729 def do_suspend_uicc(self, opts):
730 """Perform the SUSPEND UICC command. Only supported on some UICC."""
731 (duration, token, sw) = self._cmd.card._scc.suspend_uicc(min_len_secs=opts.min_duration_secs,
732 max_len_secs=opts.max_duration_secs)
733 self._cmd.poutput('Negotiated Duration: %u secs, Token: %s, SW: %s' % (duration, token, sw))
734
Harald Welte703f9332021-04-10 18:39:32 +0200735
Harald Weltef2e761c2021-04-11 11:56:44 +0200736option_parser = argparse.ArgumentParser(prog='pySim-shell', description='interactive SIM card shell',
737 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
Harald Welte28c24312021-04-11 12:19:36 +0200738argparse_add_reader_args(option_parser)
Harald Weltec8ff0262021-04-11 12:06:13 +0200739
740global_group = option_parser.add_argument_group('General Options')
741global_group.add_argument('--script', metavar='PATH', default=None,
Harald Welte28c24312021-04-11 12:19:36 +0200742 help='script with pySim-shell commands to be executed automatically at start-up')
743global_group.add_argument('--csv', metavar='FILE', default=None, help='Read card data from CSV file')
Philipp Maier76667642021-09-22 16:53:22 +0200744global_group.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
745 help="Use automatic card handling machine")
Harald Weltec8ff0262021-04-11 12:06:13 +0200746
747adm_group = global_group.add_mutually_exclusive_group()
748adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', default=None,
749 help='ADM PIN used for provisioning (overwrites default)')
750adm_group.add_argument('-A', '--pin-adm-hex', metavar='PIN_ADM1_HEX', dest='pin_adm_hex', default=None,
751 help='ADM PIN used for provisioning, as hex string (16 characters long)')
Harald Welteb2edd142021-01-08 23:29:35 +0100752
753
754if __name__ == '__main__':
755
756 # Parse options
Harald Weltef2e761c2021-04-11 11:56:44 +0200757 opts = option_parser.parse_args()
Harald Welteb2edd142021-01-08 23:29:35 +0100758
Philipp Maier13e258d2021-04-08 17:48:49 +0200759 # If a script file is specified, be sure that it actually exists
760 if opts.script:
761 if not os.access(opts.script, os.R_OK):
762 print("Invalid script file!")
763 sys.exit(2)
764
Philipp Maier2b11c322021-03-17 12:37:39 +0100765 # Register csv-file as card data provider, either from specified CSV
766 # or from CSV file in home directory
767 csv_default = str(Path.home()) + "/.osmocom/pysim/card_data.csv"
768 if opts.csv:
Harald Welte4442b3d2021-04-03 09:00:16 +0200769 card_key_provider_register(CardKeyProviderCsv(opts.csv))
Philipp Maier2b11c322021-03-17 12:37:39 +0100770 if os.path.isfile(csv_default):
Harald Welte4442b3d2021-04-03 09:00:16 +0200771 card_key_provider_register(CardKeyProviderCsv(csv_default))
Philipp Maier2b11c322021-03-17 12:37:39 +0100772
Philipp Maierea95c392021-09-16 13:10:19 +0200773 # Init card reader driver
774 sl = init_reader(opts)
775 if sl is None:
776 exit(1)
777
778 # Create command layer
779 scc = SimCardCommands(transport=sl)
780
Philipp Maier76667642021-09-22 16:53:22 +0200781 # Create a card handler (for bulk provisioning)
782 if opts.card_handler_config:
783 ch = CardHandlerAuto(None, opts.card_handler_config)
784 else:
785 ch = CardHandler(sl)
786
Philipp Maier5d698e52021-09-16 13:18:01 +0200787 # Detect and initialize the card in the reader. This may fail when there
788 # is no card in the reader or the card is unresponsive. PysimApp is
789 # able to tolerate and recover from that.
790 try:
791 rs, card = init_card(sl)
Philipp Maier76667642021-09-22 16:53:22 +0200792 app = PysimApp(card, rs, sl, ch, opts.script)
Philipp Maier5d698e52021-09-16 13:18:01 +0200793 except:
794 print("Card initialization failed with an exception:")
795 print("---------------------8<---------------------")
796 traceback.print_exc()
797 print("---------------------8<---------------------")
798 print("(you may still try to recover from this manually by using the 'equip' command.)")
799 print(" it should also be noted that some readers may behave strangely when no card")
800 print(" is inserted.)")
801 print("")
Philipp Maier76667642021-09-22 16:53:22 +0200802 app = PysimApp(None, None, sl, ch, opts.script)
Philipp Maierea95c392021-09-16 13:10:19 +0200803
Philipp Maier228c98e2021-03-10 20:14:06 +0100804 # If the user supplies an ADM PIN at via commandline args authenticate
Harald Weltec9cdce32021-04-11 10:28:28 +0200805 # immediately so that the user does not have to use the shell commands
Philipp Maier228c98e2021-03-10 20:14:06 +0100806 pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
807 if pin_adm:
Philipp Maier5d698e52021-09-16 13:18:01 +0200808 if not card:
809 print("Card error, cannot do ADM verification with supplied ADM pin now.")
Philipp Maier228c98e2021-03-10 20:14:06 +0100810 try:
811 card.verify_adm(h2b(pin_adm))
812 except Exception as e:
813 print(e)
814
Harald Welteb2edd142021-01-08 23:29:35 +0100815 app.cmdloop()